Skip to content

Commit

Permalink
Detect cyclical dependencies between types that reference each other.
Browse files Browse the repository at this point in the history
Better handling of enum declarations.
  • Loading branch information
atombender committed Oct 5, 2018
1 parent eaa02a8 commit e11bcaa
Show file tree
Hide file tree
Showing 33 changed files with 575 additions and 198 deletions.
18 changes: 18 additions & 0 deletions pkg/codegen/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ import (
"github.com/atombender/go-jsonschema/pkg/schemas"
)

func WrapTypeInPointer(t Type) Type {
if isPointerType(t) {
return t
}
return &PointerType{Type: t}
}

func isPointerType(t Type) bool {
switch x := t.(type) {
case *PointerType:
return true
case *NamedType:
return isPointerType(x.Decl.Type)
default:
return false
}
}

func PrimitiveTypeFromJSONSchemaType(jsType string) (Type, error) {
switch jsType {
case schemas.TypeNameString:
Expand Down
124 changes: 62 additions & 62 deletions pkg/generator/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"unicode"

Expand Down Expand Up @@ -37,6 +36,8 @@ type Generator struct {
emitter *codegen.Emitter
outputs map[string]*output
schemaCacheByFileName map[string]*schemas.Schema
inScope map[qualifiedDefinition]struct{}
warner func(string)
}

func New(config Config) (*Generator, error) {
Expand Down Expand Up @@ -181,7 +182,7 @@ func (g *Generator) beginOutput(
}

output := &output{
warner: g.config.Warner,
warner: g.warner,
file: &codegen.File{
FileName: outputName,
Package: pkg,
Expand Down Expand Up @@ -286,6 +287,11 @@ func (g *schemaGenerator) generateReferencedType(ref string) (codegen.Type, erro
schema = g.schema
}

qual := qualifiedDefinition{
schema: schema,
name: defName,
}

var def *schemas.Type
if defName != "" {
// TODO: Support nested definitions
Expand All @@ -295,14 +301,24 @@ func (g *schemaGenerator) generateReferencedType(ref string) (codegen.Type, erro
return nil, fmt.Errorf("definition %q (from ref %q) does not exist in schema", defName, ref)
}
if def.Type == "" && len(def.Properties) == 0 {
return nil, nil
return &codegen.EmptyInterfaceType{}, nil
}
// Minor hack to make definitions default to being objects
def.Type = schemas.TypeNameObject
defName = g.identifierize(defName)
} else {
def = schema.Type
defName = g.getRootTypeName(schema, fileName)
if def.Type == "" {
// Minor hack to make definitions default to being objects
def.Type = schemas.TypeNameObject
}
}

_, isCycle := g.inScope[qual]
if !isCycle {
g.inScope[qual] = struct{}{}
defer func() {
delete(g.inScope, qual)
}()
}

var sg *schemaGenerator
Expand All @@ -327,6 +343,13 @@ func (g *schemaGenerator) generateReferencedType(ref string) (codegen.Type, erro
return nil, err
}

nt := t.(*codegen.NamedType)

if isCycle {
g.warner(fmt.Sprintf("Cycle detected; must wrap type %s in pointer", nt.Decl.Name))
t = codegen.WrapTypeInPointer(t)
}

if sg.output.file.Package.QualifiedName == g.output.file.Package.QualifiedName {
return t, nil
}
Expand All @@ -344,31 +367,39 @@ func (g *schemaGenerator) generateReferencedType(ref string) (codegen.Type, erro

return &codegen.NamedType{
Package: &sg.output.file.Package,
Decl: t.(*codegen.NamedType).Decl,
Decl: nt.Decl,
}, nil
}

func (g *schemaGenerator) generateDeclaredType(
t *schemas.Type, scope nameScope) (codegen.Type, error) {
if t, ok := g.output.declsBySchema[t]; ok {
return &codegen.NamedType{Decl: t}, nil
if decl, ok := g.output.declsBySchema[t]; ok {
return &codegen.NamedType{Decl: decl}, nil
}

if t.Enum != nil {
return g.generateEnumType(t, scope)
}

decl := codegen.TypeDecl{
Name: g.output.uniqueTypeName(scope.string()),
Comment: t.Description,
}
g.output.declsBySchema[t] = &decl
g.output.declsByName[decl.Name] = &decl

theType, err := g.generateType(t, scope)
if err != nil {
return nil, err
}
if d, ok := theType.(*codegen.NamedType); ok {
return d, nil
if isNamedType(theType) {
// Don't declare named types under a new name
delete(g.output.declsBySchema, t)
delete(g.output.declsByName, decl.Name)
return theType, nil
}
decl.Type = theType

g.output.declsBySchema[t] = &decl
g.output.declsByName[decl.Name] = &decl
g.output.file.Package.AddDecl(&decl)

if structType, ok := theType.(*codegen.StructType); ok {
Expand Down Expand Up @@ -441,28 +472,9 @@ func (g *schemaGenerator) generateType(
if t.Items == nil {
return nil, errors.New("array property must have 'items' set to a type")
}

var elemType codegen.Type
if schemas.IsPrimitiveType(t.Items.Type) {
var err error
elemType, err = codegen.PrimitiveTypeFromJSONSchemaType(t.Items.Type)
if err != nil {
return nil, fmt.Errorf("cannot determine type of field: %s", err)
}
} else if t.Items.Type != "" {
var err error
elemType, err = g.generateDeclaredType(t.Items, scope.add("Elem"))
if err != nil {
return nil, err
}
} else if t.Items.Ref != "" {
var err error
elemType, err = g.generateReferencedType(t.Items.Ref)
if err != nil {
return nil, err
}
} else {
return nil, errors.New("array property must have a type")
elemType, err := g.generateType(t.Items, scope.add("Elem"))
if err != nil {
return nil, err
}
return codegen.ArrayType{elemType}, nil
case schemas.TypeNameObject:
Expand All @@ -474,15 +486,18 @@ func (g *schemaGenerator) generateType(
if t.Ref != "" {
return g.generateReferencedType(t.Ref)
}
return codegen.PrimitiveTypeFromJSONSchemaType(t.Type)
if t.Type != "" {
return codegen.PrimitiveTypeFromJSONSchemaType(t.Type)
}
return codegen.EmptyInterfaceType{}, nil
}

func (g *schemaGenerator) generateStructType(
t *schemas.Type,
scope nameScope) (codegen.Type, error) {
if len(t.Properties) == 0 {
if len(t.Required) > 0 {
g.config.Warner("object type with no properties has required fields; " +
g.warner("Object type with no properties has required fields; " +
"skipping validation code for them since we don't know their types")
}
return &codegen.MapType{
Expand All @@ -507,7 +522,7 @@ func (g *schemaGenerator) generateStructType(
if count, ok := uniqueNames[fieldName]; ok {
uniqueNames[fieldName] = count + 1
fieldName = fmt.Sprintf("%s_%d", fieldName, count+1)
g.config.Warner(fmt.Sprintf("field %q maps to a field by the same name declared "+
g.warner(fmt.Sprintf("field %q maps to a field by the same name declared "+
"in the same struct; it will be declared as %s", name, fieldName))
} else {
uniqueNames[fieldName] = 1
Expand Down Expand Up @@ -542,7 +557,9 @@ func (g *schemaGenerator) generateStructType(
structType.RequiredJSONFields = append(structType.RequiredJSONFields, structField.JSONName)
} else {
// Optional, so must be pointer
structField.Type = codegen.PointerType{Type: structField.Type}
if !structField.Type.IsNillable() {
structField.Type = codegen.WrapTypeInPointer(structField.Type)
}
}

structType.AddField(structField)
Expand Down Expand Up @@ -617,7 +634,7 @@ func (g *schemaGenerator) generateEnumType(
enumType = codegen.PrimitiveType{primitiveType}
}
if wrapInStruct {
g.config.Warner("Enum field wrapped in struct in order to store values of multiple types")
g.warner("Enum field wrapped in struct in order to store values of multiple types")
enumType = &codegen.StructType{
Fields: []codegen.StructField{
{
Expand All @@ -628,12 +645,8 @@ func (g *schemaGenerator) generateEnumType(
}
}

if enumDecl, ok := enumType.(*codegen.NamedType); ok {
return enumDecl, nil
}

enumDecl := codegen.TypeDecl{
Name: g.output.uniqueTypeName(scope.add("Enum").string()),
Name: g.output.uniqueTypeName(scope.string()),
Type: enumType,
}
g.output.file.Package.AddDecl(&enumDecl)
Expand Down Expand Up @@ -749,6 +762,11 @@ type cachedEnum struct {
enum *codegen.TypeDecl
}

type qualifiedDefinition struct {
schema *schemas.Schema
name string
}

type nameScope []string

func newNameScope(s string) nameScope {
Expand All @@ -770,21 +788,3 @@ var (
varNamePlainStruct = "plain"
varNameRawMap = "raw"
)

func sortPropertiesByName(props map[string]*schemas.Type) []string {
names := make([]string, 0, len(props))
for name := range props {
names = append(names, name)
}
sort.Strings(names)
return names
}

func sortDefinitionsByName(defs schemas.Definitions) []string {
names := make([]string, 0, len(defs))
for name := range defs {
names = append(names, name)
}
sort.Strings(names)
return names
}
33 changes: 33 additions & 0 deletions pkg/generator/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"fmt"
"sort"
"unicode"

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

func hashArrayOfValues(values []interface{}) string {
Expand Down Expand Up @@ -67,3 +70,33 @@ func splitIdentifierByCaseAndSeparators(s string) []string {
}
return result
}

func sortPropertiesByName(props map[string]*schemas.Type) []string {
names := make([]string, 0, len(props))
for name := range props {
names = append(names, name)
}
sort.Strings(names)
return names
}

func sortDefinitionsByName(defs schemas.Definitions) []string {
names := make([]string, 0, len(defs))
for name := range defs {
names = append(names, name)
}
sort.Strings(names)
return names
}

func isNamedType(t codegen.Type) bool {
switch x := t.(type) {
case *codegen.NamedType:
return true
case *codegen.PointerType:
if _, ok := x.Type.(*codegen.NamedType); ok {
return true
}
}
return false
}
17 changes: 9 additions & 8 deletions tests/data/core/4.2.1_array.go.output
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@

package test

type 421ArrayMyObjectArrayElem map[string]interface{}
type 421Array struct {
type A421ArrayMyObjectArrayElem map[string]interface{}

type A421Array struct {
// MyBooleanArray corresponds to the JSON schema field "myBooleanArray".
MyBooleanArray *[]bool `json:"myBooleanArray,omitempty"`
MyBooleanArray []bool `json:"myBooleanArray,omitempty"`

// MyNullArray corresponds to the JSON schema field "myNullArray".
MyNullArray *[]interface{} `json:"myNullArray,omitempty"`
MyNullArray []interface{} `json:"myNullArray,omitempty"`

// MyNumberArray corresponds to the JSON schema field "myNumberArray".
MyNumberArray *[]float64 `json:"myNumberArray,omitempty"`
MyNumberArray []float64 `json:"myNumberArray,omitempty"`

// MyObjectArray corresponds to the JSON schema field "myObjectArray".
MyObjectArray *[]421ArrayMyObjectArrayElem `json:"myObjectArray,omitempty"`
MyObjectArray []A421ArrayMyObjectArrayElem `json:"myObjectArray,omitempty"`

// MyStringArray corresponds to the JSON schema field "myStringArray".
MyStringArray *[]string `json:"myStringArray,omitempty"`
}
MyStringArray []string `json:"myStringArray,omitempty"`
}
12 changes: 8 additions & 4 deletions tests/data/core/object.go.output
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT.

package test

import "fmt"
import "encoding/json"

Expand All @@ -12,19 +13,22 @@ type ObjectMyObject struct {
// UnmarshalJSON implements json.Unmarshaler.
func (j *ObjectMyObject) UnmarshalJSON(b []byte) error {
var raw map[string]interface{}
if err := json.Unmarshal(b, &raw); err != nil { return err }
if err := json.Unmarshal(b, &raw); err != nil {
return err
}
if v, ok := raw["myString"]; !ok || v == nil {
return fmt.Errorf("field myString: required")
}
type Plain ObjectMyObject
var plain Plain
if err := json.Unmarshal(b, &plain); err != nil { return err }
if err := json.Unmarshal(b, &plain); err != nil {
return err
}
*j = ObjectMyObject(plain)
return nil
}


type Object struct {
// MyObject corresponds to the JSON schema field "myObject".
MyObject *ObjectMyObject `json:"myObject,omitempty"`
}
}
Loading

0 comments on commit e11bcaa

Please sign in to comment.