Skip to content

Commit

Permalink
Allows references that point to parent definitions (#39)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
alindeman authored and cyberdelia committed Feb 26, 2017
1 parent 3f28c7a commit 30bb00e
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 36 deletions.
5 changes: 2 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -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 ./...
64 changes: 42 additions & 22 deletions gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
89 changes: 85 additions & 4 deletions gen_test.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -226,7 +307,7 @@ var paramsTests = []struct {
HRef: NewHRef("/update/"),
Schema: &Schema{
Properties: map[string]*Schema{
"id": &Schema{
"id": {
Type: "string",
},
},
Expand All @@ -244,7 +325,7 @@ var paramsTests = []struct {
HRef: NewHRef("/update/"),
Schema: &Schema{
Properties: map[string]*Schema{
"id": &Schema{
"id": {
Type: "string",
},
},
Expand Down Expand Up @@ -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",
},
},
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
11 changes: 9 additions & 2 deletions reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}
}

Expand Down
6 changes: 3 additions & 3 deletions reference_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"testing"
)

var resolveTests = []struct {
var refResolveTests = []struct {
Ref string
Schema *Schema
Resolved *Schema
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 30bb00e

Please sign in to comment.