Skip to content

Commit

Permalink
Derive profile name from display name in ProfileService.CreateProfile
Browse files Browse the repository at this point in the history
Currently, clients of the API are required to derive the profile name from the display name using various transformations.
This results in duplicated logic across the ecosystem, increasing complexity and potential inconsistencies.

This commit scopes the logic to derive the profile name from the display name to the ProfileService.CreateProfile endpoint.
By centralizing this logic, we reduce redundancy and simplify client code.
Clients of the API are no longer need to implement logic to deriive the profile name when creating a new profile.
  • Loading branch information
gajananan committed Sep 1, 2024
1 parent 55cc7c4 commit 2b95134
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 1 deletion.
7 changes: 6 additions & 1 deletion internal/profiles/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"encoding/json"
"errors"
"fmt"

// ignore this linter warning - this is pre-existing code, and I do not
// want to change the logging library it uses at this time.
// nolint:depguard
Expand Down Expand Up @@ -129,14 +130,18 @@ func (p *profileService) CreateProfile(
PopulateRuleNames(profile)

displayName := profile.GetDisplayName()

// Derive the profile name from the profile display name
name := DeriveProfileNameFromDisplayName(profile)

// if empty use the name
if displayName == "" {
displayName = profile.GetName()
}

params := db.CreateProfileParams{
ProjectID: projectID,
Name: profile.GetName(),
Name: name,
DisplayName: displayName,
Labels: profile.GetLabels(),
Remediate: db.ValidateRemediateType(profile.GetRemediate()),
Expand Down
55 changes: 55 additions & 0 deletions internal/profiles/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"io"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/rs/zerolog/log"
"github.com/sqlc-dev/pqtype"
Expand Down Expand Up @@ -299,6 +301,59 @@ func MergeDatabaseGetByNameIntoProfiles(ppl []db.GetProfileByProjectAndNameRow)
return profiles
}

// Derive the profile name from the profile display name
func DeriveProfileNameFromDisplayName(
profile *pb.Profile,
) (profileName string) {

displayName := profile.GetDisplayName()
name := profile.GetName()

if displayName != "" && name != "" {
// when both a display name and a profile name are provided
// then the profile name from the incoming request is used as the profile name
profileName = name

// when both a display name and a profile name are provided, but the project already has a profile with that name

} else if displayName != "" && name == "" {
// when a display name is provided, but no name
// then the profile name is created and saved based on the profile display name

profileName = CleanDisplayName(displayName)

}

return profileName
}

// The profile name should be derived from the profile display name given the following logic
func CleanDisplayName(displayName string) string {

// Trim leading and trailing whitespace
displayName = strings.TrimSpace(displayName)

// Remove non-alphanumeric characters
re := regexp.MustCompile(`[^a-zA-Z0-9\s]`)
displayName = re.ReplaceAllString(displayName, "")

// Replace multiple spaces with a single space
displayName = regexp.MustCompile(`\s{2,}`).ReplaceAllString(displayName, " ")

// Replace all whitespace with underscores
displayName = strings.ReplaceAll(displayName, " ", "_")

// Convert to lower-case
displayName = strings.ToLower(displayName)

// Trim to a maximum length of 63 characters
if len(displayName) > 63 {
displayName = displayName[:63]
}

return displayName
}

func dbProfileToPB(p db.Profile) *pb.Profile {
profileID := p.ID.String()
project := p.ProjectID.String()
Expand Down
54 changes: 54 additions & 0 deletions internal/profiles/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -634,3 +634,57 @@ func TestFilterRulesForType(t *testing.T) {
})
}
}

func TestCleanDisplayName(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "A short DisplayName with whitespace",
input: "My custom profile",
expected: "my_custom_profile",
},
{
name: "A very long DisplayName with whitespaces and more than 63 characters",
input: "A very long profile name that is longer than sixty three characters and will be trimmed",
expected: "a_very_long_profile_name_that_is_longer_than_sixty_three_charac",
},
{
name: "A DisplayName with special characters",
input: "Profile with !#$() characters",
expected: "profile_with_characters",
},
{
name: "A DisplayName with alphanumeric values",
input: "My 1st Profile",
expected: "my_1st_profile",
},
{
name: "A DisplayName with non-alphanumeric characters and leadning & trailing whitespaces",
input: " New, Profile! 123. This is a Test Display Name with Special Characters! ",
expected: "new_profile_123_this_is_a_test_display_name_with_special_charac",
},
{
name: "A DisplayName with Leading and trailing white spaces",
input: " Leading and trailing spaces ",
expected: "leading_and_trailing_spaces",
},
{
name: "A DisplayName with mix of upper and low case",
input: "UPPER CASE to lower case",
expected: "upper_case_to_lower_case",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := profiles.CleanDisplayName(tt.input)
if result != tt.expected {
t.Errorf("CleanDisplayName(%q) = %q; want %q", tt.input, result, tt.expected)
}
})
}
}

0 comments on commit 2b95134

Please sign in to comment.