Skip to content

Commit

Permalink
feat: implement support for sub-schemas
Browse files Browse the repository at this point in the history
- fix: add guard against loading non-json files when reading references in schemas
- feat: introduce basic allOf support
- fix: handle struct slices prop merging for allOf types
- feat: introduce basic anyOf support, fix default value issue.
- fix: make anyOf properties of primitive types dump interface instead of first type
- fix: reduce duplicate types
- fix: furhter reduce duplicate types
- chore: refactor cmp Opts utility
- fix: add graceful handling of some edge cases in code generation
    - empty enum type name
    - missing unmarshal methods for map types
    - "Plain" type naming collisions
    - schema.Type new properties potential (de)serialization
- chore: rename test files after rebase
- chore: cleanup go deps
- chore: fix go linting issues
- chore: integrate new formatter changes after rebase
- fix: set consistent var names in unmarshallers, introduce support for both yaml and json in validators
- chore: remove dead code
- fix: use '>' instead of '>=' to perform max length validation.
- chore: remove 'two' constant
- chore: disable gomnd linter
  • Loading branch information
omissis committed Jan 18, 2024
1 parent 40f48ce commit 7bc36c4
Show file tree
Hide file tree
Showing 49 changed files with 2,469 additions and 377 deletions.
2 changes: 2 additions & 0 deletions .rules/.golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ linters:
# unused
- exhaustruct
- forbidigo
- gomnd

linters-settings:
decorder:
disable-init-func-first-check: false
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ module github.com/atombender/go-jsonschema
go 1.21

require (
dario.cat/mergo v1.0.0
github.com/goccy/go-yaml v1.11.2
github.com/google/go-cmp v0.5.9
github.com/mitchellh/go-wordwrap v1.0.1
github.com/pkg/errors v0.9.1
github.com/sanity-io/litter v1.5.5
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/cpuguy83/go-md2man/v2 v2.0.3/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 Down Expand Up @@ -48,4 +50,5 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
4 changes: 1 addition & 3 deletions go.work.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
github.com/atombender/go-jsonschema/tests/data v0.0.0-20231003003002-2b73c089a581/go.mod h1:kLoRQLRVy+GT9/PG2e3u31DPvDmtFEn7pX6FItvbqlA=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
Expand Down
4 changes: 4 additions & 0 deletions internal/x/text/cases.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ func (c *Caser) Identifierize(s string) string {
}
}

if ident == "" {
return "Undefined"
}

Check warning on line 57 in internal/x/text/cases.go

View check run for this annotation

Codecov / codecov/patch

internal/x/text/cases.go#L56-L57

Added lines #L56 - L57 were not covered by tests

return ident
}

Expand Down
16 changes: 16 additions & 0 deletions pkg/cmputil/opts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package cmputil

import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)

func Opts(t ...any) []cmp.Option {
opts := make([]cmp.Option, 0)

for _, v := range t {
opts = append(opts, cmpopts.IgnoreUnexported(v), cmpopts.IgnoreFields(v, "Ref"), cmpopts.IgnoreFields(v, "AnyOf"))
}

return opts
}
8 changes: 5 additions & 3 deletions pkg/codegen/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,10 @@ func (i *Import) Generate(out *Emitter) {

// TypeDecl is a "type <name> = <definition>".
type TypeDecl struct {
Name string
Type Type
Comment string
Name string
Type Type
Comment string
SchemaType *schemas.Type
}

func (td *TypeDecl) GetName() string {
Expand Down Expand Up @@ -309,6 +310,7 @@ func (NullType) Generate(out *Emitter) {
type StructType struct {
Fields []StructField
RequiredJSONFields []string
DefaultValue interface{}
}

func (StructType) IsNillable() bool { return false }
Expand Down
2 changes: 1 addition & 1 deletion pkg/generator/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
type formatter interface {
addImport(out *codegen.File)

generate(declType codegen.TypeDecl, validators []validator) func(*codegen.Emitter)
generate(output *output, declType codegen.TypeDecl, validators []validator) func(*codegen.Emitter)
enumMarshal(declType codegen.TypeDecl) func(*codegen.Emitter)
enumUnmarshal(
declType codegen.TypeDecl,
Expand Down
17 changes: 11 additions & 6 deletions pkg/generator/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const (
varNamePlainStruct = "plain"
varNameRawMap = "raw"
interfaceTypeName = "interface{}"
typePlain = "Plain"
)

var (
Expand All @@ -26,6 +27,7 @@ var (
errMapURIToPackageName = errors.New("unable to map schema URI to Go package name")
errExpectedNamedType = errors.New("expected named type")
errUnsupportedRefFormat = errors.New("unsupported $ref format")
ErrUnsupportedRefExtension = errors.New("unsupported $ref extension")
errConflictSameFile = errors.New("conflict: same file")
errDefinitionDoesNotExistInSchema = errors.New("definition does not exist in schema")
errCannotGenerateReferencedType = errors.New("cannot generate referenced type")
Expand Down Expand Up @@ -176,10 +178,7 @@ func (g *Generator) findOutputFileForSchemaID(id string) (*output, error) {
return g.beginOutput(id, g.config.DefaultOutputName, g.config.DefaultPackageName)
}

func (g *Generator) beginOutput(
id string,
outputName, packageName string,
) (*output, error) {
func (g *Generator) beginOutput(id, outputName, packageName string) (*output, error) {
if packageName == "" {
return nil, fmt.Errorf("%w: %q", errMapURIToPackageName, id)
}
Expand Down Expand Up @@ -215,9 +214,15 @@ func (g *Generator) beginOutput(
}

func (g *Generator) makeEnumConstantName(typeName, value string) string {
idv := g.caser.Identifierize(value)

if len(typeName) == 0 {
return "Enum" + idv
}

Check warning on line 221 in pkg/generator/generate.go

View check run for this annotation

Codecov / codecov/patch

pkg/generator/generate.go#L220-L221

Added lines #L220 - L221 were not covered by tests

if strings.ContainsAny(typeName[len(typeName)-1:], "0123456789") {
return typeName + "_" + g.caser.Identifierize(value)
return typeName + "_" + idv
}

return typeName + g.caser.Identifierize(value)
return typeName + idv
}
34 changes: 24 additions & 10 deletions pkg/generator/json_formatter.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package generator

import (
"fmt"
"math"
"strings"

"github.com/atombender/go-jsonschema/pkg/codegen"
Expand All @@ -12,29 +14,41 @@ const (

type jsonFormatter struct{}

func (jf *jsonFormatter) generate(declType codegen.TypeDecl, validators []validator) func(*codegen.Emitter) {
func (jf *jsonFormatter) generate(
output *output,
declType codegen.TypeDecl,
validators []validator,
) func(*codegen.Emitter) {
return func(out *codegen.Emitter) {
out.Commentf("Unmarshal%s implements %s.Unmarshaler.", strings.ToUpper(formatJSON), formatJSON)
out.Printlnf("func (j *%s) Unmarshal%s(b []byte) error {", declType.Name, strings.ToUpper(formatJSON))
out.Printlnf("func (j *%s) Unmarshal%s(value []byte) error {", declType.Name, strings.ToUpper(formatJSON))
out.Indent(1)
out.Printlnf("var %s map[string]interface{}", varNameRawMap)
out.Printlnf("if err := %s.Unmarshal(b, &%s); err != nil { return err }",
out.Printlnf("if err := %s.Unmarshal(value, &%s); err != nil { return err }",
formatJSON, varNameRawMap)

for _, v := range validators {
if v.desc().beforeJSONUnmarshal {
v.generate(out)
if v.desc().beforeUnmarshal {
v.generate(out, "json")
}
}

tp := typePlain

if tp == declType.Name {
for i := 0; !output.isUniqueTypeName(tp) && i < math.MaxInt; i++ {
tp = fmt.Sprintf("%s_%d", typePlain, i)

Check warning on line 40 in pkg/generator/json_formatter.go

View check run for this annotation

Codecov / codecov/patch

pkg/generator/json_formatter.go#L39-L40

Added lines #L39 - L40 were not covered by tests
}
}

out.Printlnf("type Plain %s", declType.Name)
out.Printlnf("var %s Plain", varNamePlainStruct)
out.Printlnf("if err := %s.Unmarshal(b, &%s); err != nil { return err }",
out.Printlnf("type %s %s", tp, declType.Name)
out.Printlnf("var %s %s", varNamePlainStruct, tp)
out.Printlnf("if err := %s.Unmarshal(value, &%s); err != nil { return err }",
formatJSON, varNamePlainStruct)

for _, v := range validators {
if !v.desc().beforeJSONUnmarshal {
v.generate(out)
if !v.desc().beforeUnmarshal {
v.generate(out, "json")
}
}

Expand Down
35 changes: 35 additions & 0 deletions pkg/generator/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package generator
import (
"fmt"

"github.com/google/go-cmp/cmp"

"github.com/atombender/go-jsonschema/pkg/cmputil"
"github.com/atombender/go-jsonschema/pkg/codegen"
"github.com/atombender/go-jsonschema/pkg/schemas"
)
Expand All @@ -14,6 +17,38 @@ type output struct {
warner func(string)
}

func (o *output) getDeclByEqualSchema(name string, t *schemas.Type) *codegen.TypeDecl {
v, ok := o.declsByName[name]
if !ok {
o.warner(fmt.Sprintf("Name not found: %s", name))

return nil
}

Check warning on line 26 in pkg/generator/output.go

View check run for this annotation

Codecov / codecov/patch

pkg/generator/output.go#L23-L26

Added lines #L23 - L26 were not covered by tests

if cmp.Equal(v.SchemaType, t, cmputil.Opts(*v.SchemaType, *t)...) {
return v
}

Check warning on line 30 in pkg/generator/output.go

View check run for this annotation

Codecov / codecov/patch

pkg/generator/output.go#L29-L30

Added lines #L29 - L30 were not covered by tests

for count := 1; ; count++ {
suffixed := fmt.Sprintf("%s_%d", name, count)

sv, ok := o.declsByName[suffixed]
if !ok {
return nil
}

if cmp.Equal(sv.SchemaType, t, cmputil.Opts(*sv.SchemaType, *t)...) {
return sv
}

Check warning on line 42 in pkg/generator/output.go

View check run for this annotation

Codecov / codecov/patch

pkg/generator/output.go#L40-L42

Added lines #L40 - L42 were not covered by tests
}
}

func (o *output) isUniqueTypeName(name string) bool {
v, ok := o.declsByName[name]

return !ok || (ok && v.Type == nil)
}

func (o *output) uniqueTypeName(name string) string {
v, ok := o.declsByName[name]

Expand Down
Loading

0 comments on commit 7bc36c4

Please sign in to comment.