diff --git a/rules/aip0123/resource_type_name.go b/rules/aip0123/resource_type_name.go index 41971c6cd..42c960c6f 100644 --- a/rules/aip0123/resource_type_name.go +++ b/rules/aip0123/resource_type_name.go @@ -15,7 +15,10 @@ package aip0123 import ( + "fmt" + "regexp" "strings" + "unicode" "github.com/googleapis/api-linter/lint" "github.com/googleapis/api-linter/locations" @@ -23,6 +26,10 @@ import ( "github.com/jhump/protoreflect/desc" ) +var ( + resourceNameRegex = regexp.MustCompile(`^[\p{L}A-Za-z0-9]+$`) +) + var resourceTypeName = &lint.MessageRule{ Name: lint.NewRuleName(123, "resource-type-name"), OnlyIf: func(m *desc.MessageDescriptor) bool { @@ -30,14 +37,31 @@ var resourceTypeName = &lint.MessageRule{ }, LintMessage: func(m *desc.MessageDescriptor) []lint.Problem { resource := utils.GetResource(m) - if strings.Count(resource.GetType(), "/") != 1 { + resourceType := resource.GetType() + if strings.Count(resourceType, "/") != 1 { return []lint.Problem{{ Message: "Resource type names must be of the form {Service Name}/{Type}.", Descriptor: m, Location: locations.MessageResource(m), }} } - + typeName := strings.Split(resourceType, "/")[1] + if !unicode.IsUpper(rune(typeName[0])) { + return []lint.Problem{{ + Message: fmt.Sprintf("Type %q must be UpperCamelCase", typeName), + Descriptor: m, + Location: locations.MessageResource(m), + }} + } + if !resourceNameRegex.MatchString(typeName) { + return []lint.Problem{{ + Message: fmt.Sprintf( + "Type %q must only contain alphanumeric characters", typeName, + ), + Descriptor: m, + Location: locations.MessageResource(m), + }} + } return nil }, } diff --git a/rules/aip0123/resource_type_name_test.go b/rules/aip0123/resource_type_name_test.go index 45670e584..f6038ba8f 100644 --- a/rules/aip0123/resource_type_name_test.go +++ b/rules/aip0123/resource_type_name_test.go @@ -27,8 +27,12 @@ func TestResourceTypeName(t *testing.T) { problems testutils.Problems }{ {"Valid", "library.googleapis.com/Book", testutils.Problems{}}, + {"ValidWithUnicode", "library.googleapis.com/BoØkLibre", 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}"}}}, + {"InvalidLowerCamelCase", "library.googleapis.com/bookLoan", testutils.Problems{{Message: "Type \"bookLoan\" must be UpperCamelCase"}}}, + {"InvalidTypeNotAlphaNumeric", "library.googleapis.com/Book.:3", testutils.Problems{{Message: "Type \"Book.:3\" must only contain alphanumeric characters"}}}, + {"InvalidTypeContainsEmoji", "library.googleapis.com/Book♥️", testutils.Problems{{Message: "Type \"Book♥️\" must only contain alphanumeric characters"}}}, } { t.Run(test.name, func(t *testing.T) { f := testutils.ParseProto3Tmpl(t, `