Skip to content

Commit 4cec94c

Browse files
*: allow users to ask for minimal names (#324)
* *: allow users to ask for minimal names The new --minimal-names flag allows users to ask the generator to come up with the most minimal name possible, meaning that redundant context prefixes are omitted and collection type names are inferred from field names where possible. A real-world use-case JSON Schema is imported as a test case; some examples of names before and after: RolloutSpecificationOrchestratedStepsElem -> OrchestratedStep RolloutSpecificationOrchestratedStepsElemApplicationsApplyAcrossServiceResources -> ApplyAcrossServiceResources Signed-off-by: Steve Kuznetsov <[email protected]> * chore: tidy up go modules * fix: resolve issues with new nameScope implementation * tidy up go modules --------- Signed-off-by: Steve Kuznetsov <[email protected]> Co-authored-by: Claudio Beatrice <[email protected]>
1 parent b662ae3 commit 4cec94c

File tree

15 files changed

+1200
-87
lines changed

15 files changed

+1200
-87
lines changed

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ require (
1414
github.com/sanity-io/litter v1.5.8
1515
github.com/spf13/cobra v1.9.1
1616
github.com/stretchr/testify v1.10.0
17-
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
1817
)
1918

2019
require gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
2-
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
31
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
42
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
53
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -31,7 +29,6 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
3129
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
3230
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
3331
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
34-
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
3532
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
3633
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3734
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

go.work.sum

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
12
github.com/atombender/go-jsonschema/tests/data v0.0.0-20231003003002-2b73c089a581/go.mod h1:kLoRQLRVy+GT9/PG2e3u31DPvDmtFEn7pX6FItvbqlA=
23
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
34
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
45
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
56
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
67
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
8+
github.com/goccy/go-yaml v1.15.23/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
79
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
10+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
811
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
912
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
1013
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
@@ -32,13 +35,15 @@ golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
3235
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
3336
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
3437
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
38+
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
3539
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
3640
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
3741
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
3842
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
3943
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
4044
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
4145
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
46+
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
4247
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
4348
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
4449
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -49,9 +54,11 @@ golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
4954
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
5055
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
5156
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
57+
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
5258
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
5359
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
5460
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
61+
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
5562
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
5663
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
5764
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -70,5 +77,7 @@ golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q
7077
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
7178
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
7279
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
80+
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
7381
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
82+
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
7483
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ var (
3232
tags []string
3333
structNameFromTitle bool
3434
minSizedInts bool
35+
minimalNames bool
3536
disableReadOnlyValidation bool
3637

3738
errFlagFormat = errors.New("flag must be in the format URI=PACKAGE")
@@ -78,6 +79,7 @@ var (
7879
Tags: tags,
7980
OnlyModels: onlyModels,
8081
MinSizedInts: minSizedInts,
82+
MinimalNames: minimalNames,
8183
DisableReadOnlyValidation: disableReadOnlyValidation,
8284
}
8385
for _, id := range allKeys(schemaPackageMap, schemaOutputMap, schemaRootTypeMap) {
@@ -181,6 +183,8 @@ also look for foo.json if --resolve-extension json is provided.`)
181183
"Uses sized int and uint values based on the min and max values for the field")
182184
rootCmd.PersistentFlags().BoolVar(&disableReadOnlyValidation, "disable-readonly-validation", false,
183185
"Do not include validation of readonly fields")
186+
rootCmd.PersistentFlags().BoolVar(&minimalNames, "minimal-names", false,
187+
"Uses the shortest possible names")
184188

185189
abortWithErr(rootCmd.Execute())
186190
}

pkg/generator/config.go

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,33 @@ package generator
33
import "github.com/atombender/go-jsonschema/pkg/schemas"
44

55
type Config struct {
6-
SchemaMappings []SchemaMapping
7-
ExtraImports bool
8-
Capitalizations []string
9-
ResolveExtensions []string
10-
YAMLExtensions []string
11-
DefaultPackageName string
12-
DefaultOutputName string
6+
SchemaMappings []SchemaMapping
7+
// ExtraImports allows the generator to pull imports from outside the standard library.
8+
ExtraImports bool
9+
// Capitalizations configures capitalized forms for identifiers which take precedence over the default algorithm.
10+
Capitalizations []string
11+
// ResolveExtensions configures file extensions to use when resolving schema names.
12+
ResolveExtensions []string
13+
// YAMLExtensions configures the file extensions that are recognized as YAML files.
14+
YAMLExtensions []string
15+
// DefaultPackageName configures the package to declare files under.
16+
DefaultPackageName string
17+
// DefaultOutputName configures the file to write.
18+
DefaultOutputName string
19+
// StructNameFromTitle configures the generator to use the schema title as the generated struct name.
1320
StructNameFromTitle bool
14-
Warner func(string)
15-
Tags []string
16-
OnlyModels bool
17-
MinSizedInts bool
18-
Loader schemas.Loader
21+
// Warner provides a handler for warning messages.
22+
Warner func(string)
23+
// Tags specifies which struct tags should be generated.
24+
Tags []string
25+
// OnlyModels configures the generator to omit unmarshal methods, validations, anything but models.
26+
OnlyModels bool
27+
// MinSizedInts configures the generator to use the smallest int and uint types based on schema maximum values.
28+
MinSizedInts bool
29+
// MinimalNames configures the generator to use the shortest identifier names possible.
30+
MinimalNames bool
31+
// Loader provides a schema loader for the generator.
32+
Loader schemas.Loader
1933
// When DisableOmitempty is set to true,
2034
// an "omitempty" tag will never be present in generated struct fields.
2135
// When DisableOmitempty is set to false,

pkg/generator/generate.go

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,14 @@ var (
3434
)
3535

3636
type Generator struct {
37-
caser *text.Caser
38-
config Config
39-
inScope map[qualifiedDefinition]struct{}
40-
outputs map[string]*output
41-
warner func(string)
42-
formatters []formatter
43-
loader schemas.Loader
37+
caser *text.Caser
38+
config Config
39+
inScope map[qualifiedDefinition]struct{}
40+
outputs map[string]*output
41+
warner func(string)
42+
formatters []formatter
43+
loader schemas.Loader
44+
minimalNames bool
4445
}
4546

4647
type qualifiedDefinition struct {
@@ -59,13 +60,14 @@ func New(config Config) (*Generator, error) {
5960
}
6061

6162
generator := &Generator{
62-
caser: text.NewCaser(config.Capitalizations, config.ResolveExtensions),
63-
config: config,
64-
inScope: map[qualifiedDefinition]struct{}{},
65-
outputs: map[string]*output{},
66-
warner: config.Warner,
67-
formatters: formatters,
68-
loader: config.Loader,
63+
caser: text.NewCaser(config.Capitalizations, config.ResolveExtensions),
64+
config: config,
65+
inScope: map[qualifiedDefinition]struct{}{},
66+
outputs: map[string]*output{},
67+
warner: config.Warner,
68+
formatters: formatters,
69+
loader: config.Loader,
70+
minimalNames: config.MinimalNames,
6971
}
7072

7173
if config.Loader == nil {
@@ -202,7 +204,8 @@ func (g *Generator) beginOutput(
202204
}
203205

204206
output := &output{
205-
warner: g.warner,
207+
minimalNames: g.minimalNames,
208+
warner: g.warner,
206209
file: &codegen.File{
207210
FileName: outputName,
208211
Package: pkg,

pkg/generator/name_scope.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ func (ns nameScope) string() string {
1616
return strings.Join(ns.stack, "")
1717
}
1818

19+
func (ns nameScope) stringFrom(start int) string {
20+
if start >= len(ns.stack) {
21+
return ""
22+
}
23+
24+
return strings.Join(ns.stack[start:], "")
25+
}
26+
1927
func (ns nameScope) add(s string) nameScope {
2028
result := make([]string, len(ns.stack)+1)
2129
copy(result, ns.stack)
@@ -25,3 +33,15 @@ func (ns nameScope) add(s string) nameScope {
2533

2634
return ns
2735
}
36+
37+
func (ns nameScope) last() (string, bool) {
38+
if len(ns.stack) == 0 {
39+
return "", false
40+
}
41+
42+
return ns.stack[len(ns.stack)-1], true
43+
}
44+
45+
func (ns nameScope) len() int {
46+
return len(ns.stack)
47+
}

pkg/generator/output.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
)
1212

1313
type output struct {
14+
minimalNames bool
1415
file *codegen.File
1516
declsByName map[string]*codegen.TypeDecl
1617
declsBySchema map[*schemas.Type]*codegen.TypeDecl
@@ -49,15 +50,32 @@ func (o *output) isUniqueTypeName(name string) bool {
4950
return !ok || (ok && v.Type == nil)
5051
}
5152

52-
func (o *output) uniqueTypeName(name string) string {
53-
v, ok := o.declsByName[name]
53+
// uniqueTypeName finds the shortest identifier in a name scope that yields a unique type name.
54+
// If a given suffix on the name scope is not unique, more context from the scope is added. If the
55+
// entire context does not yield a unique name, a numeric suffix is used.
56+
// TODO: we should check for schema equality on name collisions here to deduplicate identifiers.
57+
func (o *output) uniqueTypeName(scope nameScope) string {
58+
if o.minimalNames {
59+
for i := scope.len() - 1; i >= 0; i-- {
60+
name := scope.stringFrom(i)
61+
62+
v, ok := o.declsByName[name]
63+
if !ok || (ok && v.Type == nil) {
64+
// An identifier using the current amount of name context is unique, use it.
65+
return name
66+
}
67+
}
68+
}
69+
70+
// If we can't make a unique name with the entire context, attempt a numeric suffix.
71+
count := 1
72+
name := scope.string()
5473

74+
v, ok := o.declsByName[name]
5575
if !ok || (ok && v.Type == nil) {
5676
return name
5777
}
5878

59-
count := 1
60-
6179
for {
6280
suffixed := fmt.Sprintf("%s_%d", name, count)
6381
if _, ok := o.declsByName[suffixed]; !ok {

pkg/generator/schema_generator.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ func (g *schemaGenerator) generateDeclaredType(t *schemas.Type, scope nameScope)
276276
}
277277

278278
decl := codegen.TypeDecl{
279-
Name: g.output.uniqueTypeName(scope.string()),
279+
Name: g.output.uniqueTypeName(scope),
280280
Comment: t.Description,
281281
SchemaType: t,
282282
}
@@ -523,7 +523,7 @@ func (g *schemaGenerator) generateType(t *schemas.Type, scope nameScope) (codege
523523
return nil, errArrayPropertyItems
524524
}
525525

526-
elemType, err := g.generateType(t.Items, scope.add("Elem"))
526+
elemType, err := g.generateType(t.Items, g.singularScope(scope))
527527
if err != nil {
528528
return nil, err
529529
}
@@ -1004,7 +1004,7 @@ func (g *schemaGenerator) generateTypeInline(t *schemas.Type, scope nameScope) (
10041004
} else {
10051005
var err error
10061006

1007-
theType, err = g.generateTypeInline(t.Items, scope.add("Elem"))
1007+
theType, err = g.generateTypeInline(t.Items, g.singularScope(scope))
10081008
if err != nil {
10091009
return nil, err
10101010
}
@@ -1026,7 +1026,21 @@ func (g *schemaGenerator) generateTypeInline(t *schemas.Type, scope nameScope) (
10261026
return dt, nil
10271027
}
10281028

1029-
func (g *schemaGenerator) generateEnumType(t *schemas.Type, scope nameScope) (codegen.Type, error) {
1029+
// singularScope attempts to create a name scope for an element of a collection. If the parent collection
1030+
// has a plural name like "Actions", then the singular name for the element will be "Action". If the collection
1031+
// is not plural, like "WhateverElse", then the element's name will be "WhateverElseElem".
1032+
func (g *Generator) singularScope(scope nameScope) nameScope {
1033+
last, ok := scope.last()
1034+
if g.minimalNames && ok && strings.HasSuffix(last, "s") {
1035+
return scope.add(strings.TrimSuffix(last, "s"))
1036+
}
1037+
1038+
return scope.add("Elem")
1039+
}
1040+
1041+
func (g *schemaGenerator) generateEnumType(
1042+
t *schemas.Type, scope nameScope,
1043+
) (codegen.Type, error) {
10301044
if len(t.Enum) == 0 {
10311045
return nil, errEnumArrCannotBeEmpty
10321046
}
@@ -1123,7 +1137,7 @@ func (g *schemaGenerator) generateEnumType(t *schemas.Type, scope nameScope) (co
11231137
}
11241138

11251139
enumDecl := codegen.TypeDecl{
1126-
Name: g.output.uniqueTypeName(scope.string()),
1140+
Name: g.output.uniqueTypeName(scope),
11271141
Type: enumType,
11281142
SchemaType: t,
11291143
}

tests/data/deeplyNested/Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
standalone/RolloutSpecification.json:
2+
wget --output-document=$@ --quiet https://ev2schema.azure.net/schemas/2020-01-01/$(notdir $@)

0 commit comments

Comments
 (0)