github.com/googleapis/api-linter@v1.65.2/rules/aip0123/aip0123.go (about) 1 // Copyright 2019 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package aip0123 contains rules defined in https://aip.dev/123. 16 package aip0123 17 18 import ( 19 "fmt" 20 "regexp" 21 "strings" 22 23 "github.com/googleapis/api-linter/lint" 24 "github.com/googleapis/api-linter/rules/internal/utils" 25 "github.com/jhump/protoreflect/desc" 26 "github.com/stoewer/go-strcase" 27 ) 28 29 // AddRules accepts a register function and registers each of 30 // this AIP's rules to it. 31 func AddRules(r lint.RuleRegistry) error { 32 return r.Register( 33 123, 34 duplicateResource, 35 resourceAnnotation, 36 resourceNameComponentsAlternate, 37 resourceNameField, 38 resourcePattern, 39 resourcePlural, 40 resourceReferenceType, 41 resourceSingular, 42 resourceTypeName, 43 resourceVariables, 44 resourceDefinitionVariables, 45 resourceDefinitionPatterns, 46 resourceDefinitionTypeName, 47 nameNeverOptional, 48 ) 49 } 50 51 func isResourceMessage(m *desc.MessageDescriptor) bool { 52 // If the parent of this message is a message, it is nested and shoudn't 53 // be considered a resource, even if it has a name field. 54 _, nested := m.GetParent().(*desc.MessageDescriptor) 55 return m.FindFieldByName("name") != nil && !strings.HasSuffix(m.GetName(), "Request") && 56 !strings.HasSuffix(m.GetName(), "Response") && !nested 57 } 58 59 func hasResourceAnnotation(m *desc.MessageDescriptor) bool { 60 return utils.GetResource(m) != nil 61 } 62 63 func hasResourceDefinitionAnnotation(f *desc.FileDescriptor) bool { 64 return len(utils.GetResourceDefinitions(f)) > 0 65 } 66 67 // getVariables returns a slice of variables declared in the pattern. 68 // 69 // For example, a pattern of "publishers/{publisher}/books/{book}" would 70 // return []string{"publisher", "book"}. 71 func getVariables(pattern string) []string { 72 answer := []string{} 73 for _, match := range varRegexp.FindAllStringSubmatch(pattern, -1) { 74 answer = append(answer, match[1]) 75 } 76 return answer 77 } 78 79 // getPlainPattern returns the pattern with all variables replaced with "*". 80 // 81 // For example, a pattern of "publishers/{publisher}/books/{book}" would 82 // return "publishers/*/books/*". 83 func getPlainPattern(pattern string) string { 84 return varRegexp.ReplaceAllLiteralString(pattern, "*") 85 } 86 87 // getDesiredPattern returns the expected desired pattern, with errors we 88 // lint for corrected. 89 func getDesiredPattern(pattern string) string { 90 want := []string{} 91 for _, token := range strings.Split(pattern, "/") { 92 if strings.HasPrefix(token, "{") && strings.HasSuffix(token, "}") { 93 varname := token[1 : len(token)-1] 94 want = append(want, fmt.Sprintf("{%s}", strings.TrimSuffix(strcase.SnakeCase(varname), "_id"))) 95 } else { 96 want = append(want, strcase.LowerCamelCase(token)) 97 } 98 } 99 return strings.Join(want, "/") 100 } 101 102 var varRegexp = regexp.MustCompile(`\{([^}=]+)}`)