From 30bb00e3fb6780d27775b5c219b125cc2abfcb66 Mon Sep 17 00:00:00 2001 From: Andy Lindeman Date: Sun, 26 Feb 2017 17:35:49 -0500 Subject: [PATCH] Allows references that point to parent definitions (#39) * Allows references that point to parent definitions * Introduces a `resolvedSet` structure to mark references that have been (or are currently being) resolved, to avoid infinite loops. * Resolves references eagerly before child elements are resolved. This is more in line with [the spec], which states that elements with `$ref` should be replaced entirely with the representation that they point to. [the spec]: https://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.28 * Bumps Go versions * cover is no longer required to be installed separately * All arguments to exported functions should also be exported --- .travis.yml | 5 ++- gen.go | 64 ++++++++++++++++++++++------------ gen_test.go | 89 ++++++++++++++++++++++++++++++++++++++++++++--- helpers.go | 2 +- reference.go | 11 ++++-- reference_test.go | 6 ++-- schema.go | 2 +- 7 files changed, 143 insertions(+), 36 deletions(-) diff --git a/.travis.yml b/.travis.yml index 354fd17..a98db86 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: go go: - - 1.2 - - 1.3 -install: go get code.google.com/p/go.tools/cmd/cover + - 1.7 + - 1.8 script: go test -race -cover ./... diff --git a/gen.go b/gen.go index 5244235..ee788dd 100644 --- a/gen.go +++ b/gen.go @@ -18,13 +18,23 @@ func init() { templates = template.Must(bundle.Parse(templates)) } +// ResolvedSet stores a set of pointers to objects that have already been +// resolved to prevent infinite loops. +type ResolvedSet map[interface{}]bool + +func (rs ResolvedSet) Insert(o interface{}) { + rs[o] = true +} + +func (rs ResolvedSet) Has(o interface{}) bool { + return rs[o] +} + // Generate generates code according to the schema. func (s *Schema) Generate() ([]byte, error) { var buf bytes.Buffer - for i := 0; i < 2; i++ { - s.Resolve(nil) - } + s = s.Resolve(nil, ResolvedSet{}) name := strings.ToLower(strings.Split(s.Title, " ")[0]) templates.ExecuteTemplate(&buf, "package.tmpl", name) @@ -78,33 +88,43 @@ func (s *Schema) Generate() ([]byte, error) { } // Resolve resolves reference inside the schema. -func (s *Schema) Resolve(r *Schema) *Schema { +func (s *Schema) Resolve(r *Schema, rs ResolvedSet) *Schema { if r == nil { r = s } + + for { + if s.Ref != nil { + s = s.Ref.Resolve(r) + } else if len(s.OneOf) > 0 { + s = s.OneOf[0].Ref.Resolve(r) + } else if len(s.AnyOf) > 0 { + s = s.AnyOf[0].Ref.Resolve(r) + } else { + break + } + } + + if rs.Has(s) { + // Already resolved + return s + } + rs.Insert(s) + for n, d := range s.Definitions { - s.Definitions[n] = d.Resolve(r) + s.Definitions[n] = d.Resolve(r, rs) } for n, p := range s.Properties { - s.Properties[n] = p.Resolve(r) + s.Properties[n] = p.Resolve(r, rs) } for n, p := range s.PatternProperties { - s.PatternProperties[n] = p.Resolve(r) + s.PatternProperties[n] = p.Resolve(r, rs) } if s.Items != nil { - s.Items = s.Items.Resolve(r) - } - if s.Ref != nil { - s = s.Ref.Resolve(r) - } - if len(s.OneOf) > 0 { - s = s.OneOf[0].Ref.Resolve(r) - } - if len(s.AnyOf) > 0 { - s = s.AnyOf[0].Ref.Resolve(r) + s.Items = s.Items.Resolve(r, rs) } for _, l := range s.Links { - l.Resolve(r) + l.Resolve(r, rs) } return s } @@ -303,14 +323,14 @@ func (l *Link) AcceptsCustomType() bool { } // Resolve resolve link schema and href. -func (l *Link) Resolve(r *Schema) { +func (l *Link) Resolve(r *Schema, rs ResolvedSet) { if l.Schema != nil { - l.Schema = l.Schema.Resolve(r) + l.Schema = l.Schema.Resolve(r, rs) } if l.TargetSchema != nil { - l.TargetSchema = l.TargetSchema.Resolve(r) + l.TargetSchema = l.TargetSchema.Resolve(r, rs) } - l.HRef.Resolve(r) + l.HRef.Resolve(r, rs) } // GoType returns Go type for the given schema as string and a bool specifying whether it is required diff --git a/gen_test.go b/gen_test.go index b9efbeb..697f254 100644 --- a/gen_test.go +++ b/gen_test.go @@ -1,11 +1,92 @@ package schematic import ( + "fmt" "reflect" "strings" "testing" ) +var resolveTests = []struct { + Schema *Schema +}{ + { + Schema: &Schema{ + Title: "Selfreferencing", + Type: "object", + Definitions: map[string]*Schema{ + "blog": { + Title: "Blog", + Type: "object", + Links: []*Link{ + { + Title: "Create", + Rel: "create", + HRef: NewHRef("/blogs"), + Method: "POST", + Schema: &Schema{ + Type: "object", + Properties: map[string]*Schema{ + "name": { + Ref: NewReference("#/definitions/blog/definitions/name"), + }, + }, + }, + TargetSchema: &Schema{ + Ref: NewReference("#/definitions/blog"), + }, + }, + { + Title: "Get", + Rel: "self", + HRef: NewHRef("/blogs/{(%23%2Fdefinitions%2Fblog%2Fdefinitions%2Fid)}"), + Method: "GET", + TargetSchema: &Schema{ + Ref: NewReference("#/definitions/blog"), + }, + }, + }, + Definitions: map[string]*Schema{ + "id": { + Type: "string", + }, + "name": { + Type: "string", + }, + }, + Properties: map[string]*Schema{ + "id": { + Ref: NewReference("#/definitions/blog/definitions/id"), + }, + "name": { + Ref: NewReference("#/definitions/blog/definitions/name"), + }, + }, + }, + }, + Properties: map[string]*Schema{ + "blog": { + Ref: NewReference("#/definitions/blog"), + }, + }, + }, + }, +} + +func TestResolve(t *testing.T) { + for i, rt := range resolveTests { + t.Run(fmt.Sprintf("resolveTests[%d]", i), func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatal(r) + } + }() + + rt.Schema.Resolve(nil, ResolvedSet{}) + }) + } +} + var typeTests = []struct { Schema *Schema Type string @@ -226,7 +307,7 @@ var paramsTests = []struct { HRef: NewHRef("/update/"), Schema: &Schema{ Properties: map[string]*Schema{ - "id": &Schema{ + "id": { Type: "string", }, }, @@ -244,7 +325,7 @@ var paramsTests = []struct { HRef: NewHRef("/update/"), Schema: &Schema{ Properties: map[string]*Schema{ - "id": &Schema{ + "id": { Type: "string", }, }, @@ -290,7 +371,7 @@ var paramsTests = []struct { HRef: NewHRef("/results/{(%23%2Fdefinitions%2Fstruct%2Fdefinitions%2Fuuid)}"), Schema: &Schema{ Properties: map[string]*Schema{ - "id": &Schema{ + "id": { Type: "string", }, }, @@ -304,7 +385,7 @@ var paramsTests = []struct { func TestParameters(t *testing.T) { for i, pt := range paramsTests { - pt.Link.Resolve(pt.Schema) + pt.Link.Resolve(pt.Schema, ResolvedSet{}) order, params := pt.Link.Parameters("link") if !reflect.DeepEqual(order, pt.Order) { t.Errorf("%d: wants %v, got %v", i, pt.Order, order) diff --git a/helpers.go b/helpers.go index 41df38b..96ea2e3 100644 --- a/helpers.go +++ b/helpers.go @@ -31,7 +31,7 @@ var helpers = template.FuncMap{ var ( newlines = regexp.MustCompile(`(?m:\s*$)`) acronyms = regexp.MustCompile(`(Url|Http|Id|Io|Uuid|Api|Uri|Ssl|Cname|Oauth|Otp)$`) - camelcase = regexp.MustCompile(`(?m)[-.$/:_{}\s]`) + camelcase = regexp.MustCompile(`(?m)[-.$/:_{}\s]+`) ) func goType(p *Schema) string { diff --git a/reference.go b/reference.go index f5f9eca..ea16075 100644 --- a/reference.go +++ b/reference.go @@ -18,6 +18,13 @@ var href = regexp.MustCompile(`({\([^\)]+)\)}`) // Reference represents a JSON Reference. type Reference string +// NewReference creates a new Reference based on a reference value. +func NewReference(ref string) *Reference { + r := new(Reference) + *r = Reference(ref) + return r +} + // Resolve resolves reference inside a Schema. func (rf Reference) Resolve(r *Schema) *Schema { if !strings.HasPrefix(string(rf), fragment) { @@ -95,7 +102,7 @@ func NewHRef(href string) *HRef { } // Resolve resolves a href inside a Schema. -func (h *HRef) Resolve(r *Schema) { +func (h *HRef) Resolve(r *Schema, rs ResolvedSet) { h.Order = make([]string, 0) h.Schemas = make(map[string]*Schema) for _, v := range href.FindAllString(string(h.href), -1) { @@ -106,7 +113,7 @@ func (h *HRef) Resolve(r *Schema) { parts := strings.Split(u, "/") name := initialLow(fmt.Sprintf("%s-%s", parts[len(parts)-3], parts[len(parts)-1])) h.Order = append(h.Order, name) - h.Schemas[name] = Reference(u).Resolve(r) + h.Schemas[name] = Reference(u).Resolve(r).Resolve(r, rs) } } diff --git a/reference_test.go b/reference_test.go index 86a2b45..604e377 100644 --- a/reference_test.go +++ b/reference_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -var resolveTests = []struct { +var refResolveTests = []struct { Ref string Schema *Schema Resolved *Schema @@ -43,7 +43,7 @@ var resolveTests = []struct { } func TestReferenceResolve(t *testing.T) { - for i, rt := range resolveTests { + for i, rt := range refResolveTests { ref := Reference(rt.Ref) rsl := ref.Resolve(rt.Schema) if !reflect.DeepEqual(rsl, rt.Resolved) { @@ -113,7 +113,7 @@ var hrefTests = []struct { func TestHREfResolve(t *testing.T) { for i, ht := range hrefTests { href := NewHRef(ht.HRef) - href.Resolve(ht.Schema) + href.Resolve(ht.Schema, ResolvedSet{}) if !reflect.DeepEqual(href.Order, ht.Order) { t.Errorf("%d: resolved order don't match, got %v, wants %v", i, href.Order, ht.Order) } diff --git a/schema.go b/schema.go index 4538ab5..f82aca3 100644 --- a/schema.go +++ b/schema.go @@ -57,7 +57,7 @@ type Schema struct { Not *Schema `json:"not,omitempty"` // Links - Links []Link `json:"links,omitempty"` + Links []*Link `json:"links,omitempty"` } // Link represents a Link description.