Skip to content

Commit

Permalink
*: allow users to ask for minimal names
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
stevekuznetsov committed Nov 19, 2024
1 parent 6c2b600 commit 946888b
Show file tree
Hide file tree
Showing 13 changed files with 880 additions and 145 deletions.
5 changes: 0 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ go 1.22.0

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/goccy/go-yaml v1.14.3
github.com/mitchellh/go-wordwrap v1.0.1
github.com/pkg/errors v0.9.1
Expand All @@ -16,11 +15,7 @@ require (

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
63 changes: 0 additions & 63 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM=
github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
github.com/goccy/go-yaml v1.13.0 h1:0Wtp0FZLd7Sm8gERmR9S6Iczzb3vItJj7NaHmFg8pTs=
github.com/goccy/go-yaml v1.13.0/go.mod h1:IjYwxUiJDoqpx2RmbdjMUceGHZwYLon3sfOGl5Hi9lc=
github.com/goccy/go-yaml v1.13.1 h1:xZqDO9euwefeRx5am/ca9DPSCbV3fMNPkrl+Tivmz8A=
github.com/goccy/go-yaml v1.13.1/go.mod h1:IjYwxUiJDoqpx2RmbdjMUceGHZwYLon3sfOGl5Hi9lc=
github.com/goccy/go-yaml v1.13.2 h1:jApcuETDAB6R10spnUfQDTVu090oUwGo+p3GakYTJKw=
github.com/goccy/go-yaml v1.13.2/go.mod h1:IjYwxUiJDoqpx2RmbdjMUceGHZwYLon3sfOGl5Hi9lc=
github.com/goccy/go-yaml v1.13.3 h1:IXRULR8mAa0MXQobzzp0VOfMUJ8EnaQ4x3jhf7S0/nI=
github.com/goccy/go-yaml v1.13.3/go.mod h1:IjYwxUiJDoqpx2RmbdjMUceGHZwYLon3sfOGl5Hi9lc=
github.com/goccy/go-yaml v1.13.4 h1:XOnLX9GqT+kH/gB7YzCMUiDBFU9B7pm3HZz6kyeDPkk=
github.com/goccy/go-yaml v1.13.4/go.mod h1:IjYwxUiJDoqpx2RmbdjMUceGHZwYLon3sfOGl5Hi9lc=
github.com/goccy/go-yaml v1.13.5 h1:/Hh9Q3d1Q2T7E8ECUUS7Uh53FBGKLfetvQoSV7kLViU=
github.com/goccy/go-yaml v1.13.5/go.mod h1:IjYwxUiJDoqpx2RmbdjMUceGHZwYLon3sfOGl5Hi9lc=
github.com/goccy/go-yaml v1.13.6 h1:pa3JkBPBseTtfqpG9DiSFhyxNPSpJ0BFa39BlMZE16E=
github.com/goccy/go-yaml v1.13.6/go.mod h1:IjYwxUiJDoqpx2RmbdjMUceGHZwYLon3sfOGl5Hi9lc=
github.com/goccy/go-yaml v1.13.7 h1:5k2i973KptPV1mur30XMXwGepDmskip4gA2zHWzWmOY=
github.com/goccy/go-yaml v1.13.7/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.13.8 h1:ftugzaplJyFaFwfyVNeq1XQOBxmlp8zazmuiobaCXbk=
github.com/goccy/go-yaml v1.13.8/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.13.9 h1:D/LhDa7E5HS/iYxSZzikUSHt1U9q/TeymVBJwodaglc=
github.com/goccy/go-yaml v1.13.9/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.14.0 h1:G/NDXJvf1CX0FshjxKn2AOL0MnrxsSJNpY9FpvMRblw=
github.com/goccy/go-yaml v1.14.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.14.1 h1:NJ9Ch49K/WichY7pAtSvIJbvGjmBDjHVQxuWBbPSOPg=
github.com/goccy/go-yaml v1.14.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.14.2 h1:MzONUP3PM6jnePSNWb2A9fI/xEx1OduPaK/hMC9L9fQ=
github.com/goccy/go-yaml v1.14.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.14.3 h1:8tVD+aqqPLWisSEhM+6wWoiURWXCx6BwaTKS6ZeITgM=
github.com/goccy/go-yaml v1.14.3/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
Expand All @@ -70,24 +23,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 h1:1wqE9dj9NpSm04INVsJhhEUzhuDVjbcyKH91sVyPATw=
golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var (
tags []string
structNameFromTitle bool
minSizedInts bool
minimalNames bool

errFlagFormat = errors.New("flag must be in the format URI=PACKAGE")

Expand Down Expand Up @@ -77,6 +78,7 @@ var (
Tags: tags,
OnlyModels: onlyModels,
MinSizedInts: minSizedInts,
MinimalNames: minimalNames,
}
for _, id := range allKeys(schemaPackageMap, schemaOutputMap, schemaRootTypeMap) {
mapping := generator.SchemaMapping{SchemaID: id}
Expand Down Expand Up @@ -173,6 +175,8 @@ also look for foo.json if --resolve-extension json is provided.`)
"min-sized-ints",
false,
"Uses sized int and uint values based on the min and max values for the field")
rootCmd.PersistentFlags().BoolVar(&minimalNames, "minimal-names", false,
"Uses the shortest possible names")

abortWithErr(rootCmd.Execute())
}
Expand Down
38 changes: 26 additions & 12 deletions pkg/generator/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,33 @@ package generator
import "github.com/atombender/go-jsonschema/pkg/schemas"

type Config struct {
SchemaMappings []SchemaMapping
ExtraImports bool
Capitalizations []string
ResolveExtensions []string
YAMLExtensions []string
DefaultPackageName string
DefaultOutputName string
SchemaMappings []SchemaMapping
// ExtraImports allows the generator to pull imports from outside the standard library.
ExtraImports bool
// Capitalizations configures capitalized forms for identifiers which take precedence over the default algorithm.
Capitalizations []string
// ResolveExtensions configures file extensions to use when resolving schema names.
ResolveExtensions []string
// YAMLExtensions configures the file extensions that are recognized as YAML files.
YAMLExtensions []string
// DefaultPackageName configures the package to declare files under.
DefaultPackageName string
// DefaultOutputName configures the file to write.
DefaultOutputName string
// StructNameFromTitle configures the generator to use the schema title as the generated struct name.
StructNameFromTitle bool
Warner func(string)
Tags []string
OnlyModels bool
MinSizedInts bool
Loader schemas.Loader
// Warner provides a handler for warning messages.
Warner func(string)
// Tags specifies which struct tags should be generated.
Tags []string
// OnlyModels configures the generator to omit unmarshal methods, validations, anything but models.
OnlyModels bool
// MinSizedInts configures the generator to use the smallest int and uint types based on schema maximum values.
MinSizedInts bool
// MinimalNames configures the generator to use the shortest identifier names possible.
MinimalNames bool
// Loader provides a schema loader for the generator.
Loader schemas.Loader
}

type SchemaMapping struct {
Expand Down
33 changes: 18 additions & 15 deletions pkg/generator/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,14 @@ var (
)

type Generator struct {
caser *text.Caser
config Config
inScope map[qualifiedDefinition]struct{}
outputs map[string]*output
warner func(string)
formatters []formatter
loader schemas.Loader
caser *text.Caser
config Config
inScope map[qualifiedDefinition]struct{}
outputs map[string]*output
warner func(string)
formatters []formatter
loader schemas.Loader
minimalNames bool
}

type qualifiedDefinition struct {
Expand All @@ -54,13 +55,14 @@ func New(config Config) (*Generator, error) {
}

generator := &Generator{
caser: text.NewCaser(config.Capitalizations, config.ResolveExtensions),
config: config,
inScope: map[qualifiedDefinition]struct{}{},
outputs: map[string]*output{},
warner: config.Warner,
formatters: formatters,
loader: config.Loader,
caser: text.NewCaser(config.Capitalizations, config.ResolveExtensions),
config: config,
inScope: map[qualifiedDefinition]struct{}{},
outputs: map[string]*output{},
warner: config.Warner,
formatters: formatters,
loader: config.Loader,
minimalNames: config.MinimalNames,
}

if config.Loader == nil {
Expand Down Expand Up @@ -199,7 +201,8 @@ func (g *Generator) beginOutput(
}

output := &output{
warner: g.warner,
minimalNames: g.minimalNames,
warner: g.warner,
file: &codegen.File{
FileName: outputName,
Package: pkg,
Expand Down
26 changes: 22 additions & 4 deletions pkg/generator/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,39 @@ import (
)

type output struct {
minimalNames bool
file *codegen.File
declsByName map[string]*codegen.TypeDecl
declsBySchema map[*schemas.Type]*codegen.TypeDecl
warner func(string)
}

func (o *output) uniqueTypeName(name string) string {
v, ok := o.declsByName[name]
// uniqueTypeName finds the shortest identifier in a name scope that yields a unique type name.
// If a given suffix on the name scope is not unique, more context from the scope is added. If the
// entire context does not yield a unique name, a numeric suffix is used.
// TODO: we should check for schema equality on name collisions here to deduplicate identifiers.
func (o *output) uniqueTypeName(scope nameScope) string {
if o.minimalNames {
for i := len(scope) - 1; i >= 0; i-- {
name := scope[i:].string()

v, ok := o.declsByName[name]
if !ok || (ok && v.Type == nil) {
// An identifier using the current amount of name context is unique, use it.
return name
}
}
}

// If we can't make a unique name with the entire context, attempt a numeric suffix.
count := 1
name := scope.string()

v, ok := o.declsByName[name]
if !ok || (ok && v.Type == nil) {
return name
}

count := 1

for {
suffixed := fmt.Sprintf("%s_%d", name, count)
if _, ok := o.declsByName[suffixed]; !ok {
Expand Down
19 changes: 15 additions & 4 deletions pkg/generator/schema_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ func (g *schemaGenerator) generateDeclaredType(
}

decl := codegen.TypeDecl{
Name: g.output.uniqueTypeName(scope.string()),
Name: g.output.uniqueTypeName(scope),
Comment: t.Description,
}
g.output.declsBySchema[t] = &decl
Expand Down Expand Up @@ -432,7 +432,7 @@ func (g *schemaGenerator) generateType(
return nil, errArrayPropertyItems
}

elemType, err := g.generateType(t.Items, scope.add("Elem"))
elemType, err := g.generateType(t.Items, g.singularScope(scope))
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -764,7 +764,7 @@ func (g *schemaGenerator) generateTypeInline(
} else {
var err error

theType, err = g.generateTypeInline(t.Items, scope.add("Elem"))
theType, err = g.generateTypeInline(t.Items, g.singularScope(scope))
if err != nil {
return nil, err
}
Expand All @@ -777,6 +777,17 @@ func (g *schemaGenerator) generateTypeInline(
return g.generateDeclaredType(t, scope)
}

// singularScope attempts to create a name scope for an element of a collection. If the parent collection
// has a plural name like "Actions", then the singular name for the element will be "Action". If the collection
// is not plural, like "WhateverElse", then the element's name will be "WhateverElseElem".
func (g *Generator) singularScope(scope nameScope) nameScope {
if g.minimalNames && len(scope) > 0 && strings.HasSuffix(scope[len(scope)-1], "s") {
return scope[:len(scope)-1].add(strings.TrimSuffix(scope[len(scope)-1], "s"))
}

return scope.add("Elem")
}

func (g *schemaGenerator) generateEnumType(
t *schemas.Type, scope nameScope,
) (codegen.Type, error) {
Expand Down Expand Up @@ -876,7 +887,7 @@ func (g *schemaGenerator) generateEnumType(
}

enumDecl := codegen.TypeDecl{
Name: g.output.uniqueTypeName(scope.string()),
Name: g.output.uniqueTypeName(scope),
Type: enumType,
}
g.output.file.Package.AddDecl(&enumDecl)
Expand Down
2 changes: 2 additions & 0 deletions tests/data/deeplyNested/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
standalone/RolloutSpecification.json:
wget --output-document=$@ --quiet https://ev2schema.azure.net/schemas/2020-01-01/$(notdir $@)
Loading

0 comments on commit 946888b

Please sign in to comment.