Skip to content
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
6 changes: 6 additions & 0 deletions build.assets/tooling/cmd/resource-ref-generator/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ resources:
yaml_kind: saml
yaml_version: v2

camel_case_exceptions:
- ID
- MFA
- OIDC
- SAML
- SSO
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ""
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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"
}

Expand All @@ -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"
}

Expand All @@ -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"
}

Expand All @@ -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"
}

Expand Down Expand Up @@ -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), " ", "-"),
)
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ func replaceBackticks(source string) string {
}

func TestReferenceDataFromDeclaration(t *testing.T) {
camelCaseExceptions := []string{
"IdP",
}

cases := []struct {
description string
source string
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1694,6 +1698,10 @@ import alias "my/multi/segment/package"
}

func TestMakeFieldTableInfo(t *testing.T) {
camelCaseExceptions := []string{
"IdP",
}

cases := []struct {
description string
input []rawField
Expand Down Expand Up @@ -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)
})
Expand Down Expand Up @@ -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
Expand All @@ -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))
})
}
}
Loading