diff --git a/rules/aep0004/resource_name_components_alternate.go b/rules/aep0004/resource_name_components_alternate.go index d0eae9c..8dfb122 100644 --- a/rules/aep0004/resource_name_components_alternate.go +++ b/rules/aep0004/resource_name_components_alternate.go @@ -25,7 +25,7 @@ import ( "github.com/jhump/protoreflect/desc" ) -var identifierRegexp = regexp.MustCompile("^{[a-z][_a-z0-9]*[a-z0-9]}$") +var identifierRegexp = regexp.MustCompile("^{[a-z][-a-z0-9]*[a-z0-9]}$") var resourceNameComponentsAlternate = &lint.MessageRule{ Name: lint.NewRuleName(4, "resource-name-components-alternate"), diff --git a/rules/aep0004/resource_name_components_alternate_test.go b/rules/aep0004/resource_name_components_alternate_test.go index eee46fe..e2c3e83 100644 --- a/rules/aep0004/resource_name_components_alternate_test.go +++ b/rules/aep0004/resource_name_components_alternate_test.go @@ -27,6 +27,7 @@ func TestResourceNameComponentsAlternate(t *testing.T) { problems testutils.Problems }{ {"Valid", "author/{author}/books/{book}", testutils.Problems{}}, + {"Valid", "publishers/{publisher}/books/{book}/editions/{book-edition}", testutils.Problems{}}, {"ValidSingleton", "user/{user}/config", testutils.Problems{}}, {"InvalidDoubleCollection", "author/books/{book}", testutils.Problems{{Message: "must alternate"}}}, {"InvalidDoubleIdentifier", "books/{author}/{book}", testutils.Problems{{Message: "must alternate"}}}, diff --git a/rules/aep0004/resource_plural.go b/rules/aep0004/resource_plural.go index 28b723b..9d701c7 100644 --- a/rules/aep0004/resource_plural.go +++ b/rules/aep0004/resource_plural.go @@ -31,7 +31,7 @@ var resourcePlural = &lint.MessageRule{ r := utils.GetResource(m) l := locations.MessageResource(m) p := r.GetPlural() - pLower := utils.ToLowerCamelCase(p) + pLower := utils.ToKebabCase(p) if p == "" { return []lint.Problem{{ Message: "Resources should declare plural.", diff --git a/rules/aep0004/resource_plural_test.go b/rules/aep0004/resource_plural_test.go index 9292ddd..6b43c01 100644 --- a/rules/aep0004/resource_plural_test.go +++ b/rules/aep0004/resource_plural_test.go @@ -27,7 +27,7 @@ func TestResourcePlural(t *testing.T) { }{ { "Valid", - `plural: "bookShelves"`, + `plural: "book-shelves"`, nil, }, { diff --git a/rules/aep0004/resource_singular.go b/rules/aep0004/resource_singular.go index fdd9611..2290a72 100644 --- a/rules/aep0004/resource_singular.go +++ b/rules/aep0004/resource_singular.go @@ -32,7 +32,7 @@ var resourceSingular = &lint.MessageRule{ l := locations.MessageResource(m) s := r.GetSingular() _, typeName, ok := utils.SplitResourceTypeName(r.GetType()) - lowerTypeName := utils.ToLowerCamelCase(typeName) + lowerTypeName := utils.ToKebabCase(typeName) if s == "" { return []lint.Problem{{ Message: fmt.Sprintf("Resources should declare singular: %q", lowerTypeName), diff --git a/rules/aep0004/resource_type_name.go b/rules/aep0004/resource_type_name.go index 0f430f9..d71a29f 100644 --- a/rules/aep0004/resource_type_name.go +++ b/rules/aep0004/resource_type_name.go @@ -32,7 +32,7 @@ var resourceTypeName = &lint.MessageRule{ LintMessage: func(m *desc.MessageDescriptor) []lint.Problem { resource := utils.GetResource(m) _, typeName, ok := utils.SplitResourceTypeName(resource.GetType()) - upperTypeName := utils.ToUpperCamelCase(typeName) + kebabCase := utils.ToKebabCase(typeName) if !ok { return []lint.Problem{{ Message: "Resource type names must be of the form {Service Name}/{Type}.", @@ -40,9 +40,9 @@ var resourceTypeName = &lint.MessageRule{ Location: locations.MessageResource(m), }} } - if upperTypeName != typeName { + if kebabCase != typeName { return []lint.Problem{{ - Message: fmt.Sprintf("Type must be UpperCamelCase with alphanumeric characters: %q", upperTypeName), + Message: fmt.Sprintf("Type must be kebob-case with alphanumeric characters: %q", kebabCase), Descriptor: m, Location: locations.MessageResource(m), }} diff --git a/rules/aep0004/resource_type_name_test.go b/rules/aep0004/resource_type_name_test.go index f34df58..f7c2512 100644 --- a/rules/aep0004/resource_type_name_test.go +++ b/rules/aep0004/resource_type_name_test.go @@ -26,15 +26,16 @@ func TestResourceTypeName(t *testing.T) { TypeName string problems testutils.Problems }{ - {"Valid", "library.googleapis.com/Book", testutils.Problems{}}, + {"Valid", "library.googleapis.com/book", testutils.Problems{}}, {"InvalidTooMany", "library.googleapis.com/shelf/Book", testutils.Problems{{Message: "{Service Name}/{Type}"}}}, {"InvalidNotEnough", "library.googleapis.com~Book", testutils.Problems{{Message: "{Service Name}/{Type}"}}}, - {"InvalidWithUnicode", "library.googleapis.com/BoØkLibre", testutils.Problems{{Message: `Type must be UpperCamelCase`}}}, - {"InvalidLowerCamelCase", "library.googleapis.com/bookLoan", testutils.Problems{{Message: `Type must be UpperCamelCase with alphanumeric characters: "BookLoan"`}}}, - {"InvalidTypeNotAlphaNumeric", "library.googleapis.com/Book.:3", testutils.Problems{{Message: `Type must be UpperCamelCase with alphanumeric characters: "Book3"`}}}, - {"InvalidTypeContainsEmoji", "library.googleapis.com/Book♥️", testutils.Problems{{Message: `Type must be UpperCamelCase with alphanumeric characters: "Book"`}}}, - {"InvalidTypeContainsDashes", "library.googleapis.com/Book-Shelf️", testutils.Problems{{Message: `Type must be UpperCamelCase with alphanumeric characters: "BookShelf"`}}}, - {"InvalidTypeContainsUnderscore", "library.googleapis.com/Book_Shelf️", testutils.Problems{{Message: `Type must be UpperCamelCase with alphanumeric characters: "BookShelf"`}}}, + {"InvalidWithUnicode", "library.googleapis.com/BoØkLibre", testutils.Problems{{Message: `Type must be kebob-case`}}}, + {"InvalidLowerCamelCase", "library.googleapis.com/bookLoan", testutils.Problems{{Message: `Type must be kebob-case with alphanumeric characters: "book-loan"`}}}, + {"ValidLowerCamelCase", "library.googleapis.com/book-loan", testutils.Problems{}}, + {"InvalidTypeNotAlphaNumeric", "library.googleapis.com/Book.:3", testutils.Problems{{Message: `Type must be kebob-case with alphanumeric characters: "book-:3"`}}}, + {"InvalidTypeContainsEmoji", "library.googleapis.com/Book♥️", testutils.Problems{{Message: `Type must be kebob-case with alphanumeric characters: "book♥️"`}}}, + {"InvalidTypeContainsDashes", "library.googleapis.com/Book-Shelf️", testutils.Problems{{Message: `Type must be kebob-case with alphanumeric characters: "book--she`}}}, + {"InvalidTypeContainsUnderscore", "library.googleapis.com/Book_Shelf️", testutils.Problems{{Message: `Type must be kebob-case with alphanumeric characters: "book--she`}}}, } { t.Run(test.name, func(t *testing.T) { f := testutils.ParseProto3Tmpl(t, ` diff --git a/rules/internal/utils/casing.go b/rules/internal/utils/casing.go index 8d24a12..6063f2a 100644 --- a/rules/internal/utils/casing.go +++ b/rules/internal/utils/casing.go @@ -14,42 +14,23 @@ package utils -// ToUpperCamelCase returns the UpperCamelCase of a string, including removing -// delimiters (_,-,., ) and using them to denote a new word. -func ToUpperCamelCase(s string) string { - return toCamelCase(s, true, false) -} - -// ToLowerCamelCase returns the lowerCamelCase of a string, including removing -// delimiters (_,-,., ) and using them to denote a new word. -func ToLowerCamelCase(s string) string { - return toCamelCase(s, false, true) -} - -func toCamelCase(s string, makeNextUpper bool, makeNextLower bool) string { +// ToKebabCase returns the kebob-case of a word (book-edition). +func ToKebabCase(s string) string { asLower := make([]rune, 0, len(s)) - for _, r := range s { - if isLower(r) { - if makeNextUpper { - r = r & '_' // make uppercase - } - asLower = append(asLower, r) - } else if isUpper(r) { - if makeNextLower { - r = r | ' ' // make lowercase + for i, r := range s { + if isUpper(r) { + r = r | ' ' // make lowercase + + // Only insert hypen after first word. + if i != 0 { + asLower = append(asLower, '-') } asLower = append(asLower, r) - } else if isNumber(r) { + } else if r == '-' || r == '_' || r == ' ' || r == '.' { + asLower = append(asLower, '-') + } else { asLower = append(asLower, r) } - makeNextUpper = false - makeNextLower = false - - if r == '-' || r == '_' || r == ' ' || r == '.' { - // handle snake case scenarios, which generally indicates - // a delimited word. - makeNextUpper = true - } } return string(asLower) } @@ -57,11 +38,3 @@ func toCamelCase(s string, makeNextUpper bool, makeNextLower bool) string { func isUpper(r rune) bool { return ('A' <= r && r <= 'Z') } - -func isNumber(r rune) bool { - return ('0' <= r && r <= '9') -} - -func isLower(r rune) bool { - return ('a' <= r && r <= 'z') -} diff --git a/rules/internal/utils/casing_test.go b/rules/internal/utils/casing_test.go index 1cdbff8..c600396 100644 --- a/rules/internal/utils/casing_test.go +++ b/rules/internal/utils/casing_test.go @@ -16,67 +16,6 @@ package utils import "testing" -func TestToLowerCamelCase(t *testing.T) { - for _, test := range []struct { - name string - input string - want string - }{ - { - name: "OneWord", - input: "Foo", - want: "foo", - }, - { - name: "OneWordNoop", - input: "foo", - want: "foo", - }, - { - name: "TwoWords", - input: "bookShelf", - want: "bookShelf", - }, - { - name: "WithDash", - input: "book-shelf", - want: "bookShelf", - }, - { - name: "WithNumbers", - input: "universe42love", - want: "universe42love", - }, - { - name: "WithUnderscore", - input: "book_shelf", - want: "bookShelf", - }, - { - name: "WithUnderscore", - input: "book_shelf", - want: "bookShelf", - }, - { - name: "WithSpaces", - input: "book shelf", - want: "bookShelf", - }, - { - name: "WithPeriods", - input: "book.shelf", - want: "bookShelf", - }, - } { - t.Run(test.name, func(t *testing.T) { - got := ToLowerCamelCase(test.input) - if got != test.want { - t.Errorf("ToLowerCamelCase(%q) = %q, got %q", test.input, test.want, got) - } - }) - } -} - func TestToUpperCamelCase(t *testing.T) { for _, test := range []struct { name string @@ -86,53 +25,53 @@ func TestToUpperCamelCase(t *testing.T) { { name: "OneWord", input: "foo", - want: "Foo", + want: "foo", }, { name: "OneWordNoop", input: "Foo", - want: "Foo", + want: "foo", }, { name: "TwoWords", input: "bookShelf", - want: "BookShelf", + want: "book-shelf", }, { name: "WithDash", input: "book-shelf", - want: "BookShelf", + want: "book-shelf", }, { name: "WithNumbers", input: "universe42love", - want: "Universe42love", + want: "universe42love", }, { name: "WithUnderscore", input: "Book_shelf", - want: "BookShelf", + want: "book-shelf", }, { name: "WithUnderscore", input: "Book_shelf", - want: "BookShelf", + want: "book-shelf", }, { name: "WithSpaces", input: "Book shelf", - want: "BookShelf", + want: "book-shelf", }, { name: "WithPeriods", input: "book.shelf", - want: "BookShelf", + want: "book-shelf", }, } { t.Run(test.name, func(t *testing.T) { - got := ToUpperCamelCase(test.input) + got := ToKebabCase(test.input) if got != test.want { - t.Errorf("ToLowerCamelCase(%q) = %q, got %q", test.input, test.want, got) + t.Errorf("ToKebabCase(%q) = %q, got %q", test.input, test.want, got) } }) }