1package jsonschema
2
3import (
4	"encoding/json"
5	"io/ioutil"
6	"net"
7	"net/url"
8	"path/filepath"
9	"reflect"
10	"strings"
11	"testing"
12	"time"
13
14	"github.com/stretchr/testify/require"
15)
16
17type GrandfatherType struct {
18	FamilyName string `json:"family_name" jsonschema:"required"`
19}
20
21type SomeBaseType struct {
22	SomeBaseProperty     int `json:"some_base_property"`
23	SomeBasePropertyYaml int `yaml:"some_base_property_yaml"`
24	// The jsonschema required tag is nonsensical for private and ignored properties.
25	// Their presence here tests that the fields *will not* be required in the output
26	// schema, even if they are tagged required.
27	somePrivateBaseProperty   string          `json:"i_am_private" jsonschema:"required"`
28	SomeIgnoredBaseProperty   string          `json:"-" jsonschema:"required"`
29	SomeSchemaIgnoredProperty string          `jsonschema:"-,required"`
30	Grandfather               GrandfatherType `json:"grand"`
31
32	SomeUntaggedBaseProperty           bool `jsonschema:"required"`
33	someUnexportedUntaggedBaseProperty bool
34}
35
36type MapType map[string]interface{}
37
38type nonExported struct {
39	PublicNonExported  int
40	privateNonExported int
41}
42
43type ProtoEnum int32
44
45func (ProtoEnum) EnumDescriptor() ([]byte, []int) { return []byte(nil), []int{0} }
46
47const (
48	Unset ProtoEnum = iota
49	Great
50)
51
52type TestUser struct {
53	SomeBaseType
54	nonExported
55	MapType
56
57	ID      int                    `json:"id" jsonschema:"required"`
58	Name    string                 `json:"name" jsonschema:"required,minLength=1,maxLength=20,pattern=.*,description=this is a property,title=the name,example=joe,example=lucy,default=alex"`
59	Friends []int                  `json:"friends,omitempty" jsonschema_description:"list of IDs, omitted when empty"`
60	Tags    map[string]interface{} `json:"tags,omitempty"`
61
62	TestFlag       bool
63	IgnoredCounter int `json:"-"`
64
65	// Tests for RFC draft-wright-json-schema-validation-00, section 7.3
66	BirthDate time.Time `json:"birth_date,omitempty"`
67	Website   url.URL   `json:"website,omitempty"`
68	IPAddress net.IP    `json:"network_address,omitempty"`
69
70	// Tests for RFC draft-wright-json-schema-hyperschema-00, section 4
71	Photo []byte `json:"photo,omitempty" jsonschema:"required"`
72
73	// Tests for jsonpb enum support
74	Feeling ProtoEnum `json:"feeling,omitempty"`
75	Age     int       `json:"age" jsonschema:"minimum=18,maximum=120,exclusiveMaximum=true,exclusiveMinimum=true"`
76	Email   string    `json:"email" jsonschema:"format=email"`
77
78	// Test for "extras" support
79	Baz string `jsonschema_extras:"foo=bar,hello=world,foo=bar1"`
80
81	// Tests for simple enum tags
82	Color      string  `json:"color" jsonschema:"enum=red,enum=green,enum=blue"`
83	Rank       int     `json:"rank,omitempty" jsonschema:"enum=1,enum=2,enum=3"`
84	Multiplier float64 `json:"mult,omitempty" jsonschema:"enum=1.0,enum=1.5,enum=2.0"`
85}
86
87type CustomTime time.Time
88
89type CustomTypeField struct {
90	CreatedAt CustomTime
91}
92
93type RootOneOf struct {
94	Field1 string      `json:"field1" jsonschema:"oneof_required=group1"`
95	Field2 string      `json:"field2" jsonschema:"oneof_required=group2"`
96	Field3 interface{} `json:"field3" jsonschema:"oneof_type=string;array"`
97	Field4 string      `json:"field4" jsonschema:"oneof_required=group1"`
98	Field5 ChildOneOf  `json:"child"`
99}
100
101type ChildOneOf struct {
102	Child1 string      `json:"child1" jsonschema:"oneof_required=group1"`
103	Child2 string      `json:"child2" jsonschema:"oneof_required=group2"`
104	Child3 interface{} `json:"child3" jsonschema:"oneof_required=group2,oneof_type=string;array"`
105	Child4 string      `json:"child4" jsonschema:"oneof_required=group1"`
106}
107
108func TestSchemaGeneration(t *testing.T) {
109	tests := []struct {
110		typ       interface{}
111		reflector *Reflector
112		fixture   string
113	}{
114		{&RootOneOf{}, &Reflector{RequiredFromJSONSchemaTags: true}, "fixtures/oneof.json"},
115		{&TestUser{}, &Reflector{}, "fixtures/defaults.json"},
116		{&TestUser{}, &Reflector{AllowAdditionalProperties: true}, "fixtures/allow_additional_props.json"},
117		{&TestUser{}, &Reflector{RequiredFromJSONSchemaTags: true}, "fixtures/required_from_jsontags.json"},
118		{&TestUser{}, &Reflector{ExpandedStruct: true}, "fixtures/defaults_expanded_toplevel.json"},
119		{&TestUser{}, &Reflector{IgnoredTypes: []interface{}{GrandfatherType{}}}, "fixtures/ignore_type.json"},
120		{&TestUser{}, &Reflector{DoNotReference: true}, "fixtures/no_reference.json"},
121		{&TestUser{}, &Reflector{FullyQualifyTypeNames: true}, "fixtures/fully_qualified.json"},
122		{&TestUser{}, &Reflector{DoNotReference: true, FullyQualifyTypeNames: true}, "fixtures/no_ref_qual_types.json"},
123		{&CustomTypeField{}, &Reflector{
124			TypeMapper: func(i reflect.Type) *Type {
125				if i == reflect.TypeOf(CustomTime{}) {
126					return &Type{
127						Type:   "string",
128						Format: "date-time",
129					}
130				}
131				return nil
132			},
133		}, "fixtures/custom_type.json"},
134	}
135
136	for _, tt := range tests {
137		name := strings.TrimSuffix(filepath.Base(tt.fixture), ".json")
138		t.Run(name, func(t *testing.T) {
139			f, err := ioutil.ReadFile(tt.fixture)
140			require.NoError(t, err)
141
142			actualSchema := tt.reflector.Reflect(tt.typ)
143			expectedSchema := &Schema{}
144
145			err = json.Unmarshal(f, expectedSchema)
146			require.NoError(t, err)
147
148			expectedJSON, _ := json.MarshalIndent(expectedSchema, "", "  ")
149			actualJSON, _ := json.MarshalIndent(actualSchema, "", "  ")
150			require.Equal(t, string(expectedJSON), string(actualJSON))
151		})
152	}
153}
154
155func TestBaselineUnmarshal(t *testing.T) {
156	expectedJSON, err := ioutil.ReadFile("fixtures/defaults.json")
157	require.NoError(t, err)
158
159	reflector := &Reflector{}
160	actualSchema := reflector.Reflect(&TestUser{})
161
162	actualJSON, _ := json.MarshalIndent(actualSchema, "", "  ")
163
164	require.Equal(t, strings.Replace(string(expectedJSON), `\/`, "/", -1), string(actualJSON))
165}
166