Skip to content

Commit 3670e37

Browse files
committed
Add support for refs
1 parent 4473380 commit 3670e37

13 files changed

+517
-84
lines changed

go.mod

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@ module github.com/grafana/json-schema-docs
22

33
go 1.14
44

5-
require github.com/olekukonko/tablewriter v0.0.4
5+
require (
6+
github.com/bitly/go-simplejson v0.5.0
7+
github.com/davecgh/go-spew v1.1.1
8+
github.com/olekukonko/tablewriter v0.0.4
9+
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb
10+
)

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1+
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
2+
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
3+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
15
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
26
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
37
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
48
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
9+
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
10+
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=

main.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package main
22

33
import (
4+
"encoding/json"
45
"flag"
56
"fmt"
7+
"io/ioutil"
68
"log"
79
"os"
810
"text/template"
@@ -27,11 +29,14 @@ func main() {
2729
}
2830
defer f.Close()
2931

30-
schema, err := newSchema(f)
32+
schema, err := newSchema(f, ".")
3133
if err != nil {
3234
log.Fatal(err)
3335
}
3436

37+
b, _ := json.MarshalIndent(schema, "", " ")
38+
ioutil.WriteFile("new.json", b, 0644)
39+
3540
tpl, err := getOrDefaultTemplate(*templatePath)
3641
if err != nil {
3742
log.Fatal(err)

ref.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"log"
8+
"net/http"
9+
"os"
10+
"path/filepath"
11+
"strings"
12+
13+
"github.com/bitly/go-simplejson"
14+
"github.com/xeipuuv/gojsonpointer"
15+
)
16+
17+
func dumpSchema(schem *schema) {
18+
b, _ := json.MarshalIndent(schem, "", " ")
19+
fmt.Println(string(b))
20+
}
21+
22+
// resolveSchema recursively resolves schemas.
23+
func resolveSchema(schem *schema, dir string, root *simplejson.Json) (*schema, error) {
24+
for _, prop := range schem.Properties {
25+
if prop.Ref != "" {
26+
tmp, err := resolveReference(prop.Ref, dir, root)
27+
if err != nil {
28+
return nil, err
29+
}
30+
*prop = *tmp
31+
}
32+
foo, err := resolveSchema(prop, dir, root)
33+
if err != nil {
34+
return nil, err
35+
}
36+
*prop = *foo
37+
}
38+
39+
if schem.Items != nil {
40+
if schem.Items.Ref != "" {
41+
tmp, err := resolveReference(schem.Items.Ref, dir, root)
42+
if err != nil {
43+
return nil, err
44+
}
45+
*schem.Items = *tmp
46+
}
47+
foo, err := resolveSchema(schem.Items, dir, root)
48+
if err != nil {
49+
return nil, err
50+
}
51+
*schem.Items = *foo
52+
}
53+
54+
return schem, nil
55+
}
56+
57+
// resolveReference loads a schema from a $ref.
58+
//
59+
// If ref contains a hashtag (#), the part before represents a cross-schema
60+
// reference, and the part after represents a in-schema reference.
61+
//
62+
// If ref is missing a hashtag, the whole schema is being referenced.
63+
func resolveReference(ref string, dir string, root *simplejson.Json) (*schema, error) {
64+
i := strings.Index(ref, "#")
65+
66+
if i < 0 {
67+
// cross-schema reference to another schema
68+
schema, err := loadSchemaFromPath(filepath.Join(dir, ref))
69+
if err != nil {
70+
return nil, err
71+
}
72+
return schema, nil
73+
} else if i == 0 {
74+
// in-schema reference
75+
return resolveInSchemaReference(ref[i+1:], root)
76+
} else {
77+
78+
schema, err := loadSchemaFromPath(filepath.Join(dir, ref[:i]))
79+
if err != nil {
80+
return nil, err
81+
}
82+
83+
b, _ := json.Marshal(schema)
84+
newRoot, _ := simplejson.NewJson(b)
85+
return resolveInSchemaReference(ref[i+1:], newRoot)
86+
}
87+
88+
}
89+
90+
func resolveInSchemaReference(path string, root *simplejson.Json) (*schema, error) {
91+
// in-schema reference
92+
pointer, err := gojsonpointer.NewJsonPointer(path)
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
v, _, err := pointer.Get(root.MustMap())
98+
if err != nil {
99+
return nil, err
100+
}
101+
102+
var sch schema
103+
b, err := json.Marshal(v)
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
if err := json.Unmarshal(b, &sch); err != nil {
109+
return nil, err
110+
}
111+
112+
return &sch, nil
113+
}
114+
115+
// loadSchemaFromPath returns a schema at a given path.
116+
func loadSchemaFromPath(path string) (*schema, error) {
117+
rc, err := openFileOrURL(path)
118+
if err != nil {
119+
log.Fatal(err)
120+
}
121+
defer rc.Close()
122+
123+
return newSchema(rc, filepath.Dir(path))
124+
}
125+
126+
// openFileOrURL opens a file from a URL or local path.
127+
func openFileOrURL(path string) (io.ReadCloser, error) {
128+
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
129+
resp, err := http.Get(path)
130+
if err != nil {
131+
return nil, err
132+
}
133+
return resp.Body, nil
134+
}
135+
return os.Open(path)
136+
}

schema.go

Lines changed: 68 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,33 @@ package main
33
import (
44
"bytes"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"io"
89
"io/ioutil"
910
"sort"
1011
"strings"
1112

13+
"github.com/bitly/go-simplejson"
1214
"github.com/olekukonko/tablewriter"
1315
)
1416

17+
var errCrossSchemaReference = errors.New("cross-schema reference")
18+
1519
type schema struct {
16-
ID string `json:"$id"`
17-
Schema string `json:"$schema"`
18-
Title string `json:"title"`
19-
Description string `json:"description"`
20-
Required []string `json:"required"`
21-
Type string `json:"type"`
22-
Properties map[string]schema `json:"properties"`
23-
Items *schema `json:"items"`
20+
ID string `json:"$id,omitempty"`
21+
Ref string `json:"$ref,omitempty"`
22+
Schema string `json:"$schema,omitempty"`
23+
Title string `json:"title,omitempty"`
24+
Description string `json:"description,omitempty"`
25+
Required []string `json:"required,omitempty"`
26+
Type string `json:"type,omitempty"`
27+
Properties map[string]*schema `json:"properties,omitempty"`
28+
Items *schema `json:"items,omitempty"`
29+
Definitions map[string]*schema `json:"definitions,omitempty"`
2430
}
2531

26-
func newSchema(r io.Reader) (*schema, error) {
32+
func newSchema(r io.Reader, workingDir string) (*schema, error) {
2733
b, err := ioutil.ReadAll(r)
2834
if err != nil {
2935
return nil, err
@@ -34,7 +40,13 @@ func newSchema(r io.Reader) (*schema, error) {
3440
return nil, err
3541
}
3642

37-
return &data, nil
43+
// Needed for resolving in-schema references.
44+
root, err := simplejson.NewJson(b)
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
return resolveSchema(&data, workingDir, root)
3850
}
3951

4052
// Markdown returns the Markdown representation of the schema.
@@ -63,20 +75,22 @@ func (s schema) Markdown(level int) string {
6375
fmt.Fprintln(&buf)
6476
}
6577

66-
table := tablewriter.NewWriter(&buf)
67-
table.SetHeader([]string{"Property", "Type", "Required", "Description"})
68-
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
69-
table.SetCenterSeparator("|")
70-
table.SetAutoFormatHeaders(false)
71-
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
72-
table.SetAutoWrapText(false)
78+
printProperties(&buf, &s)
79+
80+
// Add padding.
81+
fmt.Fprintln(&buf)
7382

83+
for _, obj := range findDefinitions(&s) {
84+
fmt.Fprintf(&buf, obj.Markdown(level+1))
85+
}
86+
87+
return buf.String()
88+
}
89+
90+
func findDefinitions(s *schema) []*schema {
7491
// Gather all properties of object type so that we can generate the
7592
// properties for them recursively.
76-
var objs []schema
77-
78-
// Buffer all property rows so that we can sort them before printing them.
79-
var rows [][]string
93+
var objs []*schema
8094

8195
for k, p := range s.Properties {
8296
// Use the identifier as the title.
@@ -87,21 +101,49 @@ func (s schema) Markdown(level int) string {
87101

88102
// If the property is an array of objects, use the name of the array
89103
// property as the title.
90-
if p.Type == "array" && p.Items != nil {
91-
if p.Items.Type == "object" {
92-
p.Items.Title = k
93-
objs = append(objs, *p.Items)
104+
if p.Type == "array" {
105+
if p.Items != nil {
106+
if p.Items.Type == "object" {
107+
p.Items.Title = k
108+
objs = append(objs, p.Items)
109+
}
94110
}
95111
}
112+
}
96113

114+
// Sort the object schemas.
115+
sort.Slice(objs, func(i, j int) bool {
116+
return objs[i].Title < objs[j].Title
117+
})
118+
119+
return objs
120+
}
121+
122+
func printProperties(w io.Writer, s *schema) {
123+
table := tablewriter.NewWriter(w)
124+
table.SetHeader([]string{"Property", "Type", "Required", "Description"})
125+
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
126+
table.SetCenterSeparator("|")
127+
table.SetAutoFormatHeaders(false)
128+
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
129+
table.SetAutoWrapText(false)
130+
131+
// Buffer all property rows so that we can sort them before printing them.
132+
var rows [][]string
133+
134+
for k, p := range s.Properties {
97135
// Generate relative links for objects and arrays of objects.
98136
var propType string
99137
switch p.Type {
100138
case "object":
101139
propType = fmt.Sprintf("[%s](#%s)", p.Type, strings.ToLower(k))
102140
case "array":
103141
if p.Items != nil {
104-
propType = fmt.Sprintf("[%s](#%s)", p.Type, strings.ToLower(k))
142+
if p.Items.Type == "object" {
143+
propType = fmt.Sprintf("[%s](#%s)[]", p.Items.Type, strings.ToLower(k))
144+
} else {
145+
propType = fmt.Sprintf("%s[]", p.Items.Type)
146+
}
105147
} else {
106148
propType = p.Type
107149
}
@@ -133,19 +175,6 @@ func (s schema) Markdown(level int) string {
133175

134176
table.AppendBulk(rows)
135177
table.Render()
136-
137-
// Add padding.
138-
fmt.Fprintln(&buf)
139-
140-
// Sort the object schemas before recursing.
141-
sort.Slice(objs, func(i, j int) bool {
142-
return objs[i].Title < objs[j].Title
143-
})
144-
for _, obj := range objs {
145-
fmt.Fprintf(&buf, obj.Markdown(level+1))
146-
}
147-
148-
return buf.String()
149178
}
150179

151180
// in returns true if a string slice contains a specific string.

schema_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func TestSchema(t *testing.T) {
2323
{name: "calendar", schema: "calendar.schema.json"},
2424
{name: "card", schema: "card.schema.json"},
2525
{name: "geographical-location", schema: "geographical-location.schema.json"},
26+
{name: "ref-hell", schema: "ref-hell.schema.json"},
2627
}
2728

2829
for _, tt := range schemaTests {
@@ -33,7 +34,7 @@ func TestSchema(t *testing.T) {
3334
}
3435
defer f.Close()
3536

36-
schema, err := newSchema(f)
37+
schema, err := newSchema(f, "testdata")
3738
if err != nil {
3839
t.Fatal(err)
3940
}
@@ -54,6 +55,7 @@ func TestSchema(t *testing.T) {
5455
}
5556

5657
if !bytes.Equal(buf.Bytes(), g) {
58+
t.Log(buf.String())
5759
t.Errorf("data does not match .golden file")
5860
}
5961
})

testdata/TestSchema_arrays.golden

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,17 @@ A representation of a person, company, organization, or place
22

33
## Properties
44

5-
| Property | Type | Required | Description |
6-
|--------------|----------------------|----------|-------------|
7-
| `fruits` | [array](#fruits) | No | |
8-
| `vegetables` | [array](#vegetables) | No | |
5+
| Property | Type | Required | Description |
6+
|--------------|-------------------------|----------|-------------|
7+
| `fruits` | string[] | No | |
8+
| `vegetables` | [object](#vegetables)[] | No | |
9+
10+
## vegetables
11+
12+
### Properties
13+
14+
| Property | Type | Required | Description |
15+
|--------------|---------|----------|----------------------------|
16+
| `veggieLike` | boolean | **Yes** | Do I like this vegetable? |
17+
| `veggieName` | string | **Yes** | The name of the vegetable. |
918

0 commit comments

Comments
 (0)