diff --git a/build.assets/tooling/cmd/resource-ref-generator/config.yaml b/build.assets/tooling/cmd/resource-ref-generator/config.yaml index 9a97cc4b3d4fd..5e14d4b648478 100644 --- a/build.assets/tooling/cmd/resource-ref-generator/config.yaml +++ b/build.assets/tooling/cmd/resource-ref-generator/config.yaml @@ -24,3 +24,9 @@ resources: yaml_kind: saml yaml_version: v2 +camel_case_exceptions: + - ID + - MFA + - OIDC + - SAML + - SSO diff --git a/build.assets/tooling/cmd/resource-ref-generator/reference/reference.go b/build.assets/tooling/cmd/resource-ref-generator/reference/reference.go index dbe4c757f6802..7a1f312a2eda7 100644 --- a/build.assets/tooling/cmd/resource-ref-generator/reference/reference.go +++ b/build.assets/tooling/cmd/resource-ref-generator/reference/reference.go @@ -75,7 +75,8 @@ type GeneratorConfig struct { // Path to the root of the Go project directory. SourcePath string `yaml:"source"` // Directory where the generator writes reference pages. - DestinationDirectory string `yaml:"destination"` + DestinationDirectory string `yaml:"destination"` + CamelCaseExceptions []string `yaml:"camel_case_exceptions"` } type GenerationError struct { @@ -117,7 +118,7 @@ func Generate(conf GeneratorConfig, tmpl *template.Template) error { // decl is a dynamic resource type, so get data for the type and // its dependencies. - entries, err := resource.ReferenceDataFromDeclaration(decl, sourceData.TypeDecls) + entries, err := resource.ReferenceDataFromDeclaration(decl, sourceData.TypeDecls, conf.CamelCaseExceptions) if errors.As(err, &resource.NotAGenDeclError{}) { continue } diff --git a/build.assets/tooling/cmd/resource-ref-generator/resource/resource.go b/build.assets/tooling/cmd/resource-ref-generator/resource/resource.go index 491575c455f84..5867b492d6d5d 100644 --- a/build.assets/tooling/cmd/resource-ref-generator/resource/resource.go +++ b/build.assets/tooling/cmd/resource-ref-generator/resource/resource.go @@ -190,13 +190,21 @@ type rawType struct { fields []rawField } +// kindTableFormatOptions configures the way the generator formats YAML kinds +// for the field table in a reference page. +type kindTableFormatOptions struct { + // camelCaseExceptions is a list of strings to exempt when splitting + // camel-case words. + camelCaseExceptions []string +} + // yamlKindNode represents a node in a potentially recursive YAML type, such as // an integer, a map of integers to strings, a sequence of maps of strings to // strings, etc. Used for printing example YAML documents and tables of fields. // This is not intended to be a comprehensive YAML AST. type yamlKindNode interface { // Generate a string representation to include in a table of fields. - formatForTable() string + formatForTable(kindTableFormatOptions) string // Generate an example YAML value for the type with the provided number // of indendations. formatForExampleYAML(indents int) string @@ -210,7 +218,7 @@ type yamlKindNode interface { // for this kind. type nonYAMLKind struct{} -func (n nonYAMLKind) formatForTable() string { +func (n nonYAMLKind) formatForTable(opts kindTableFormatOptions) string { return "" } @@ -227,8 +235,8 @@ type yamlSequence struct { elementKind yamlKindNode } -func (y yamlSequence) formatForTable() string { - return `[]` + y.elementKind.formatForTable() +func (y yamlSequence) formatForTable(opts kindTableFormatOptions) string { + return `[]` + y.elementKind.formatForTable(opts) } func (y yamlSequence) formatForExampleYAML(indents int) string { @@ -279,8 +287,8 @@ func (y yamlMapping) formatForExampleYAML(indents int) string { return fmt.Sprintf("\n%v\n%v\n%v", kv, kv, kv) } -func (y yamlMapping) formatForTable() string { - return fmt.Sprintf("map[%v]%v", y.keyKind.formatForTable(), y.valueKind.formatForTable()) +func (y yamlMapping) formatForTable(opts kindTableFormatOptions) string { + return fmt.Sprintf("map[%v]%v", y.keyKind.formatForTable(opts), y.valueKind.formatForTable(opts)) } func (y yamlMapping) customFieldData() []PackageInfo { @@ -291,7 +299,7 @@ func (y yamlMapping) customFieldData() []PackageInfo { type yamlString struct{} -func (y yamlString) formatForTable() string { +func (y yamlString) formatForTable(opts kindTableFormatOptions) string { return "string" } @@ -305,7 +313,7 @@ func (y yamlString) customFieldData() []PackageInfo { type yamlBase64 struct{} -func (y yamlBase64) formatForTable() string { +func (y yamlBase64) formatForTable(opts kindTableFormatOptions) string { return "base64-encoded string" } @@ -319,7 +327,7 @@ func (y yamlBase64) customFieldData() []PackageInfo { type yamlNumber struct{} -func (y yamlNumber) formatForTable() string { +func (y yamlNumber) formatForTable(opts kindTableFormatOptions) string { return "number" } @@ -333,7 +341,7 @@ func (y yamlNumber) customFieldData() []PackageInfo { type yamlBool struct{} -func (y yamlBool) formatForTable() string { +func (y yamlBool) formatForTable(opts kindTableFormatOptions) string { return "Boolean" } @@ -368,11 +376,12 @@ func (y yamlCustomType) formatForExampleYAML(indents int) string { return leading + "# [...]" } -func (y yamlCustomType) formatForTable() string { +func (y yamlCustomType) formatForTable(opts kindTableFormatOptions) string { + name := splitCamelCase(y.name, opts.camelCaseExceptions) return fmt.Sprintf( "[%v](#%v)", - y.name, - strings.ReplaceAll(strings.ToLower(y.name), " ", "-"), + name, + strings.ReplaceAll(strings.ToLower(name), " ", "-"), ) } @@ -490,20 +499,23 @@ func getJSONTag(tags string) string { return strings.TrimSuffix(kv[1], ",omitempty") } -var camelCaseExceptions = []string{ - "IdP", -} -var abbreviationWordBoundary *regexp.Regexp = regexp.MustCompile(`([A-Z]{2,})([A-Z][a-z0-9])`) -var camelCaseWordBoundary *regexp.Regexp = regexp.MustCompile( - fmt.Sprintf(`(%v|[a-z]+)([A-Z])`, strings.Join(camelCaseExceptions, "|")), -) - -// makeSectionName edits the original name of a declaration to make it more +// splitCamelCase edits the original name of a declaration to make it more // suitable as a section within the resource reference. -func makeSectionName(original string) string { - s := abbreviationWordBoundary.ReplaceAllString(original, "$1 $2") - s = camelCaseWordBoundary.ReplaceAllString(s, "$1 $2") - return s +func splitCamelCase(original string, camelCaseExceptions []string) string { + var exceptions string + if len(camelCaseExceptions) > 0 { + exceptions = strings.Join(camelCaseExceptions, "|") + "|" + } + camelCaseWordBoundary := regexp.MustCompile(fmt.Sprintf(`(%[1]v[a-z0-9])(%[1]v[A-Z][a-z0-9])`, exceptions)) + + // Matches can be overlapping, and ReplaceAllString only replaces + // non-overlapping matches, so repeat the ReplaceAllString call until + // there are no more matches. + result := original + for camelCaseWordBoundary.MatchString(result) { + result = camelCaseWordBoundary.ReplaceAllString(result, "$1 $2") + } + return result } // isByteSlice returns whether t is a []byte. @@ -542,7 +554,7 @@ func getYAMLTypeForExpr(exp ast.Expr, pkg string, allDecls map[PackageInfo]Decla } return yamlCustomType{ - name: makeSectionName(t.Name), + name: t.Name, declarationInfo: info, }, nil } @@ -590,7 +602,7 @@ func getYAMLTypeForExpr(exp ast.Expr, pkg string, allDecls map[PackageInfo]Decla } return yamlCustomType{ - name: makeSectionName(t.Sel.Name), + name: t.Sel.Name, declarationInfo: info, }, nil default: @@ -652,14 +664,16 @@ func makeRawField(field *ast.Field, packageName string, allDecls map[PackageInfo // makeFieldTableInfo assembles a slice of human-readable information about // fields within a Go struct to include within the resource reference. -func makeFieldTableInfo(fields []rawField) ([]Field, error) { +func makeFieldTableInfo(fields []rawField, camelCaseExceptions []string) ([]Field, error) { var result []Field for _, field := range fields { var desc string var typ string desc = field.doc - typ = field.kind.formatForTable() + typ = field.kind.formatForTable(kindTableFormatOptions{ + camelCaseExceptions: camelCaseExceptions, + }) // Escape pipes so they do not affect table rendering. desc = strings.ReplaceAll(desc, "|", `\|`) // Remove surrounding spaces and inner line breaks. @@ -818,7 +832,11 @@ func NamedImports(file *ast.File) map[string]string { // ReferenceDataFromDeclaration gets data for the reference by examining decl. // Looks up decl's fields in allDecls and methods in allMethods. -func ReferenceDataFromDeclaration(decl DeclarationInfo, allDecls map[PackageInfo]DeclarationInfo) (map[PackageInfo]ReferenceEntry, error) { +func ReferenceDataFromDeclaration( + decl DeclarationInfo, + allDecls map[PackageInfo]DeclarationInfo, + camelCaseExceptions []string, +) (map[PackageInfo]ReferenceEntry, error) { rs, err := typeForDecl(decl, allDecls) if err != nil { return nil, err @@ -842,7 +860,7 @@ func ReferenceDataFromDeclaration(decl DeclarationInfo, allDecls map[PackageInfo refs := make(map[PackageInfo]ReferenceEntry) description = strings.Trim(strings.ReplaceAll(description, "\n", " "), " ") entry := ReferenceEntry{ - SectionName: makeSectionName(rs.name), + SectionName: splitCamelCase(rs.name, camelCaseExceptions), Description: printableDescription(description, rs.name), SourcePath: decl.FilePath, YAMLExample: example, @@ -853,7 +871,7 @@ func ReferenceDataFromDeclaration(decl DeclarationInfo, allDecls map[PackageInfo PackageName: decl.PackageName, } - fld, err := makeFieldTableInfo(fieldsToProcess) + fld, err := makeFieldTableInfo(fieldsToProcess, camelCaseExceptions) if err != nil { return nil, err } @@ -895,7 +913,7 @@ func ReferenceDataFromDeclaration(decl DeclarationInfo, allDecls map[PackageInfo if !ok { continue } - r, err := ReferenceDataFromDeclaration(gd, allDecls) + r, err := ReferenceDataFromDeclaration(gd, allDecls, camelCaseExceptions) if errors.As(err, &NotAGenDeclError{}) { continue } diff --git a/build.assets/tooling/cmd/resource-ref-generator/resource/resource_test.go b/build.assets/tooling/cmd/resource-ref-generator/resource/resource_test.go index 21b41d8949a3a..a5c35850eb26a 100644 --- a/build.assets/tooling/cmd/resource-ref-generator/resource/resource_test.go +++ b/build.assets/tooling/cmd/resource-ref-generator/resource/resource_test.go @@ -35,6 +35,10 @@ func replaceBackticks(source string) string { } func TestReferenceDataFromDeclaration(t *testing.T) { + camelCaseExceptions := []string{ + "IdP", + } + cases := []struct { description string source string @@ -1628,7 +1632,7 @@ type Metadata struct { t.Fatalf("expected data for %v.%v not found in the source", tc.declInfo.PackageName, tc.declInfo.DeclName) } - r, err := ReferenceDataFromDeclaration(di, sourceData.TypeDecls) + r, err := ReferenceDataFromDeclaration(di, sourceData.TypeDecls, camelCaseExceptions) if tc.errorSubstring == "" { assert.NoError(t, err) } else { @@ -1694,6 +1698,10 @@ import alias "my/multi/segment/package" } func TestMakeFieldTableInfo(t *testing.T) { + camelCaseExceptions := []string{ + "IdP", + } + cases := []struct { description string input []rawField @@ -1741,14 +1749,14 @@ func TestMakeFieldTableInfo(t *testing.T) { { Name: "locking_mode", Description: `Specifies the locking mode (strict\|best_effort) to be applied with the role.`, - Type: "[LockingMode](#lockingmode)", + Type: "[Locking Mode](#locking-mode)", }, }, }, } for _, c := range cases { t.Run(c.description, func(t *testing.T) { - f, err := makeFieldTableInfo(c.input) + f, err := makeFieldTableInfo(c.input, camelCaseExceptions) assert.NoError(t, err) assert.Equal(t, c.expected, f) }) @@ -2058,7 +2066,13 @@ my_string: "string" } } -func TestMakeSectionName(t *testing.T) { +func TestSplitCamelCase(t *testing.T) { + camelCaseExceptions := []string{ + "IdP", + "MySQL", + "SAML", + } + cases := []struct { description string original string @@ -2085,15 +2099,25 @@ func TestMakeSectionName(t *testing.T) { expected: "SAML Connector", }, { - description: "IdP", + description: "idp", original: "IdPSAMLOptions", expected: "IdP SAML Options", }, + { + description: "excepted word with abbreviation", + original: "MySQLOptions", + expected: "MySQL Options", + }, + { + description: "one abbreviation", + original: "AWS", + expected: "AWS", + }, } for _, c := range cases { t.Run(c.description, func(t *testing.T) { - assert.Equal(t, c.expected, makeSectionName(c.original)) + assert.Equal(t, c.expected, splitCamelCase(c.original, camelCaseExceptions)) }) } }