Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

app-init: provide better generated names #4403

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cli/azd/internal/repository/app_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ func prjConfigFromDetect(
root string,
detect detectConfirm) (project.ProjectConfig, error) {
config := project.ProjectConfig{
Name: filepath.Base(root),
Name: LabelName(filepath.Base(root)),
Metadata: &project.ProjectMetadata{
Template: fmt.Sprintf("%s@%s", InitGenTemplateId, internal.VersionInfo().Version),
},
Expand Down Expand Up @@ -410,6 +410,7 @@ func prjConfigFromDetect(
if name == "." {
name = config.Name
}
name = LabelName(name)
config.Services[name] = &svc
}

Expand Down
2 changes: 1 addition & 1 deletion cli/azd/internal/repository/infra_confirm.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (i *Initializer) infraSpecFromDetect(
}

for _, svc := range detect.Services {
name := filepath.Base(svc.Path)
name := LabelName(filepath.Base(svc.Path))
serviceSpec := scaffold.ServiceSpec{
Name: name,
Port: -1,
Expand Down
106 changes: 106 additions & 0 deletions cli/azd/internal/repository/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package repository

import "strings"

//cspell:disable

// LabelName cleans up a string to be used as a RFC 1123 Label name.
// It does not enforce the 63 character limit.
//
// RFC 1123 Label name:
// - contain only lowercase alphanumeric characters or '-'
// - start with an alphanumeric character
// - end with an alphanumeric character
//
// Examples:
// - myproject, MYPROJECT -> myproject
// - myProject, myProjecT, MyProject, MyProjecT -> my-project
// - my.project, My.Project, my-project, My-Project -> my-project
func LabelName(name string) string {
hasSeparator, n := cleanAlphaNumeric(name)
if hasSeparator {
return labelNameFromSeparators(n)
}

return labelNameFromCasing(name)
}

//cspell:enable

// cleanAlphaNumeric removes non-alphanumeric characters from the name.
//
// It also returns whether the name uses word separators.
func cleanAlphaNumeric(name string) (hasSeparator bool, cleaned string) {
sb := strings.Builder{}
hasSeparator = false
for _, c := range name {
if isAsciiAlphaNumeric(c) {
sb.WriteRune(c)
} else if isSeparator(c) {
hasSeparator = true
sb.WriteRune(c)
}
}

return hasSeparator, sb.String()
}

func isAsciiAlphaNumeric(r rune) bool {
return ('0' <= r && r <= '9') || ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z')
}

func isSeparator(r rune) bool {
return r == '-' || r == '_' || r == '.'
}

func lowerCase(r rune) rune {
if 'A' <= r && r <= 'Z' {
r += 'a' - 'A'
}
return r
}

// Converts camel-cased or Pascal-cased names into lower-cased dash-separated names.
// Example: MyProject, myProject -> my-project
func labelNameFromCasing(name string) string {
result := strings.Builder{}
// previously seen upper-case character
prevUpperCase := -2 // -2 to avoid matching the first character

for i, c := range name {
if 'A' <= c && c <= 'Z' {
if prevUpperCase == i-1 { // handle runs of upper-case word
prevUpperCase = i
result.WriteRune(lowerCase(c))
continue
}

if i > 0 && i != len(name)-1 {
result.WriteRune('-')
}

prevUpperCase = i
}

if isAsciiAlphaNumeric(c) {
result.WriteRune(lowerCase(c))
}
}

return result.String()
}

// Converts all word-separated names into lower-cased dash-separated names.
// Examples: my.project, my_project, My-Project -> my-project
func labelNameFromSeparators(name string) string {
result := strings.Builder{}
for i, c := range name {
if isAsciiAlphaNumeric(c) {
result.WriteRune(lowerCase(c))
} else if i > 0 && i != len(name)-1 && isSeparator(c) {
result.WriteRune('-')
}
}

return result.String()
}
67 changes: 67 additions & 0 deletions cli/azd/internal/repository/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package repository

import (
"testing"
)

//cspell:disable

func TestLabelName(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"Lowercase", "myproject", "myproject"},
{"Uppercase", "MYPROJECT", "myproject"},
{"MixedCase", "myProject", "my-project"},
{"MixedCaseEnd", "myProjecT", "my-project"},
{"TitleCase", "MyProject", "my-project"},
{"TitleCaseEnd", "MyProjecT", "my-project"},
{"WithDot", "my.project", "my-project"},
{"WithDotTitleCase", "My.Project", "my-project"},
{"WithHyphen", "my-project", "my-project"},
{"WithHyphenTitleCase", "My-Project", "my-project"},
{"StartWithNumber", "1myproject", "1myproject"},
{"EndWithNumber", "myproject2", "myproject2"},
{"MixedWithNumbers", "my2Project3", "my2-project3"},
{"SpecialCharacters", "my_project!@#", "my-project"},
{"EmptyString", "", ""},
{"OnlySpecialCharacters", "@#$%^&*", ""},
}

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

func TestLabelNameEdgeCases(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"SingleCharacter", "A", "a"},
{"TwoCharacters", "Ab", "ab"},
{"StartEndHyphens", "-abc-", "abc"},
{"LongString",
"ThisIsOneVeryLongStringThatExceedsTheSixtyThreeCharacterLimitForRFC1123LabelNames",
"this-is-one-very-long-string-that-exceeds-the-sixty-three-character-limit-for-rfc1123-label-names"},
}

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

//cspell:enable
Loading