Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pattern, support reference constraints on primitives, and add number/integer constraints #264

Merged
merged 14 commits into from
Sep 16, 2024
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,15 @@ only specific validations remain to be fully implemented.
* [x] `type` (multiple; **note**: partial support, limited validation)
* [ ] `const`
* [ ] Numeric validation (§6.2)
nolag marked this conversation as resolved.
Show resolved Hide resolved
* [ ] `multipleOf`
* [ ] `maximum`
* [ ] `exclusiveMaximum`
* [ ] `minimum`
* [ ] `exclusiveMinimum`
* [ ] String validation (§6.3)
* [X] `multipleOf`
* [X] `maximum`
* [X] `exclusiveMaximum`
* [X] `minimum`
* [X] `exclusiveMinimum`
* [X] String validation (§6.3)
* [X] `maxLength`
* [X] `minLength`
* [ ] `pattern`
* [X] `pattern`
* [ ] Array validation (§6.4)
* [X] `items`
* [x] `maxItems`
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ require (
github.com/pkg/errors v0.9.1
github.com/sanity-io/litter v1.5.5
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e
)

require (
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b // indirect
github.com/fatih/color v1.16.0 // indirect
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 v0.0.0-20151028094244-d8ed2627bdf0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
Expand Down
37 changes: 2 additions & 35 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b h1:XxMZvQZtTXpWMNWK82vdjCLCe7uGMFXdTsJH0v3Hkvw=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand All @@ -10,12 +9,10 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
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.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I=
github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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=
Expand All @@ -34,8 +31,6 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
Expand All @@ -44,34 +39,6 @@ github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312 h1:UsFdQ3ZmlzS0Bq
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4=
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 h1:vpzMC/iZhYFAjJzHU0Cfuq+w1vLLsF2vLkDrPjzKYck=
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/exp v0.0.0-20240530194437-404ba88c7ed0 h1:Mi0bCswbz+9cXmwFAdxoo5GPFMKONUpua6iUdtQS7lk=
golang.org/x/exp v0.0.0-20240530194437-404ba88c7ed0/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM=
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA=
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/exp v0.0.0-20240822175202-778ce7bba035 h1:VkSUcpKXdGwUpn/JsiWXwSNnIJVXRfMA4ThL5vwljWg=
golang.org/x/exp v0.0.0-20240822175202-778ce7bba035/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
7 changes: 7 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var (
yamlExtensions []string
tags []string
structNameFromTitle bool
minSizedInts bool

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

Expand Down Expand Up @@ -75,6 +76,7 @@ var (
StructNameFromTitle: structNameFromTitle,
Tags: tags,
OnlyModels: onlyModels,
MinSizedInts: minSizedInts,
}
for _, id := range allKeys(schemaPackageMap, schemaOutputMap, schemaRootTypeMap) {
mapping := generator.SchemaMapping{SchemaID: id}
Expand Down Expand Up @@ -166,6 +168,11 @@ also look for foo.json if --resolve-extension json is provided.`)
"Use the schema title as the generated struct name")
rootCmd.PersistentFlags().StringSliceVar(&tags, "tags", []string{"json", "yaml", "mapstructure"},
`Specify which struct tags to generate. Defaults are json, yaml, mapstructure`)
rootCmd.PersistentFlags().BoolVar(
&minSizedInts,
"min-sized-ints",
false,
"Uses sized int and unit values based on the min and max values for the field")
nolag marked this conversation as resolved.
Show resolved Hide resolved

abortWithErr(rootCmd.Execute())
}
Expand Down
8 changes: 4 additions & 4 deletions pkg/codegen/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,11 @@ func (t NamedType) IsNillable() bool {

func (t NamedType) Generate(out *Emitter) {
if t.Package != nil {
out.Printf(t.Package.Name())
out.Printf("%s", t.Package.Name())
out.Printf(".")
}

out.Printf(t.Decl.Name)
out.Printf("%s", t.Decl.Name)
}

type PrimitiveType struct {
Expand All @@ -269,7 +269,7 @@ type PrimitiveType struct {
func (PrimitiveType) IsNillable() bool { return false }

func (p PrimitiveType) Generate(out *Emitter) {
out.Printf(p.Type)
out.Printf("%s", p.Type)
}

type CustomNameType struct {
Expand All @@ -280,7 +280,7 @@ type CustomNameType struct {
func (p CustomNameType) IsNillable() bool { return p.Nillable }

func (p CustomNameType) Generate(out *Emitter) {
out.Printf(p.Type)
out.Printf("%s", p.Type)
}

type MapType struct {
Expand Down
116 changes: 115 additions & 1 deletion pkg/codegen/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import (
"errors"
"fmt"
"math"

"github.com/atombender/go-jsonschema/pkg/mathutils"
"github.com/atombender/go-jsonschema/pkg/schemas"
)

Expand Down Expand Up @@ -33,7 +35,16 @@
}
}

func PrimitiveTypeFromJSONSchemaType(jsType, format string, pointer bool) (Type, error) {
func PrimitiveTypeFromJSONSchemaType(
jsType,
format string,
pointer,
minIntSize bool,
minimum **float64,
maximum **float64,
exclusiveMinimum **any,
exclusiveMaximum **any,
) (Type, error) {
var t Type

switch jsType {
Expand Down Expand Up @@ -119,6 +130,22 @@

case schemas.TypeNameInteger:
t := PrimitiveType{"int"}

if minIntSize {
newType, removeMin, removeMax := getMinIntType(*minimum, *maximum, *exclusiveMinimum, *exclusiveMaximum)
t.Type = newType

if removeMin {
*minimum = nil
*exclusiveMaximum = nil
}

if removeMax {
*maximum = nil
*exclusiveMinimum = nil
}
}

if pointer {
return WrapTypeInPointer(t), nil
}
Expand All @@ -142,3 +169,90 @@

return nil, fmt.Errorf("%w %q", errUnknownJSONSchemaType, jsType)
}

// getMinIntType returns the smallest integer type that can represent the bounds, and if the bounds can be removed.
func getMinIntType(
minimum, maximum *float64, exclusiveMinimum, exclusiveMaximum *any,
) (string, bool, bool) {
nMin, nMax, nExclusiveMin, nExclusiveMax := mathutils.NormalizeBounds(
minimum, maximum, exclusiveMinimum, exclusiveMaximum,
)

if nExclusiveMin && nMin != nil {
*nMin += 1.0
}

if nExclusiveMax && nMax != nil {
*nMax -= 1.0
}

if nMin != nil && *nMin >= 0 {
return adjustForUnsignedBounds(nMin, nMax)
}

return adjustForSignedBounds(nMin, nMax)
}

const i64 = "int64"

func adjustForSignedBounds(nMin, nMax *float64) (string, bool, bool) {
var minRounded, maxRounded float64

if nMin != nil {
minRounded = math.Round(*nMin)
}

if nMax != nil {
maxRounded = math.Round(*nMax)
}

switch {
case nMin == nil && nMax == nil:
return i64, false, false

Check warning on line 211 in pkg/codegen/utils.go

View check run for this annotation

Codecov / codecov/patch

pkg/codegen/utils.go#L210-L211

Added lines #L210 - L211 were not covered by tests

case nMin == nil:
return i64, false, maxRounded == float64(math.MaxInt64)

Check warning on line 214 in pkg/codegen/utils.go

View check run for this annotation

Codecov / codecov/patch

pkg/codegen/utils.go#L213-L214

Added lines #L213 - L214 were not covered by tests

case nMax == nil:
return i64, minRounded == float64(math.MinInt64), false

Check warning on line 217 in pkg/codegen/utils.go

View check run for this annotation

Codecov / codecov/patch

pkg/codegen/utils.go#L216-L217

Added lines #L216 - L217 were not covered by tests

case minRounded < float64(math.MinInt32) || maxRounded > float64(math.MaxInt32):
return i64, minRounded == float64(math.MinInt64), maxRounded == float64(math.MaxInt64)

case minRounded < float64(math.MinInt16) || maxRounded > float64(math.MaxInt16):
return "int32", minRounded == float64(math.MinInt32), maxRounded == float64(math.MaxInt32)

case minRounded < float64(math.MinInt8) || maxRounded > float64(math.MaxInt8):
return "int16", minRounded == float64(math.MinInt16), maxRounded == float64(math.MaxInt16)

default:
return "int8", minRounded == float64(math.MinInt8), maxRounded == float64(math.MaxInt8)
}
}

func adjustForUnsignedBounds(nMin, nMax *float64) (string, bool, bool) {
removeMin := nMin != nil && *nMin == 0.0

var maxRounded float64

if nMax != nil {
maxRounded = math.Round(*nMax)
}

switch {
case nMax == nil:
return "uint64", removeMin, false

Check warning on line 244 in pkg/codegen/utils.go

View check run for this annotation

Codecov / codecov/patch

pkg/codegen/utils.go#L243-L244

Added lines #L243 - L244 were not covered by tests

case maxRounded > float64(math.MaxUint32):
return "uint64", removeMin, maxRounded == float64(math.MaxUint64)

case maxRounded > float64(math.MaxUint16):
return "uint32", removeMin, maxRounded == float64(math.MaxUint32)

case maxRounded > float64(math.MaxUint8):
return "uint16", removeMin, maxRounded == float64(math.MaxUint16)

default:
return "uint8", removeMin, maxRounded == float64(math.MaxUint8)
}
}
4 changes: 4 additions & 0 deletions pkg/generator/config.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package generator

import "github.com/atombender/go-jsonschema/pkg/schemas"

type Config struct {
SchemaMappings []SchemaMapping
ExtraImports bool
Expand All @@ -12,6 +14,8 @@ type Config struct {
Warner func(string)
Tags []string
OnlyModels bool
MinSizedInts bool
Loader schemas.Loader
}

type SchemaMapping struct {
Expand Down
39 changes: 18 additions & 21 deletions pkg/generator/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,19 @@ var (
errEnumNonPrimitiveVal = errors.New("enum has non-primitive value")
errMapURIToPackageName = errors.New("unable to map schema URI to Go package name")
errExpectedNamedType = errors.New("expected named type")
errUnsupportedRefFormat = errors.New("unsupported $ref format")
errConflictSameFile = errors.New("conflict: same file")
errDefinitionDoesNotExistInSchema = errors.New("definition does not exist in schema")
errCannotGenerateReferencedType = errors.New("cannot generate referenced type")
)

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

type qualifiedDefinition struct {
Expand All @@ -56,19 +54,18 @@ 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{},
schemaCacheByFileName: map[string]*schemas.Schema{},
warner: config.Warner,
formatters: formatters,
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,
}

generator.fileLoader = schemas.NewCachedLoader(
schemas.NewFileLoader(config.ResolveExtensions, config.YAMLExtensions),
generator.schemaCacheByFileName,
)
if config.Loader == nil {
generator.loader = schemas.NewDefaultCacheLoader(config.ResolveExtensions, config.YAMLExtensions)
}

return generator, nil
}
Expand Down Expand Up @@ -125,7 +122,7 @@ func (g *Generator) DoFile(fileName string) error {
return fmt.Errorf("error parsing from standard input: %w", err)
}
} else {
schema, err = g.fileLoader.Load(fileName, "")
schema, err = g.loader.Load(fileName, "")
if err != nil {
return fmt.Errorf("error parsing from file %s: %w", fileName, err)
}
Expand Down
Loading