Skip to content

Commit

Permalink
Move to a fully typed JSON schema system
Browse files Browse the repository at this point in the history
  • Loading branch information
brandur committed Jun 23, 2017
1 parent 75bd26b commit 3a0fc9f
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 66 deletions.
66 changes: 27 additions & 39 deletions generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,32 @@ import (
var notSupportedErr = fmt.Errorf("Expected response to be a list or include $ref")

type DataGenerator struct {
definitions map[string]JSONSchema
definitions map[string]*JSONSchema
fixtures *Fixtures
}

func (g *DataGenerator) maybeDereference(schema JSONSchema) (JSONSchema, error) {
ref, ok := schema["$ref"].(string)
if ok {
definition, err := definitionFromJSONPointer(ref)
func (g *DataGenerator) maybeDereference(schema *JSONSchema) (*JSONSchema, error) {
if schema.Ref != "" {
definition, err := definitionFromJSONPointer(schema.Ref)
if err != nil {
return nil, err
}

schema, ok = g.definitions[definition]
newSchema, ok := g.definitions[definition]
if !ok {
return nil, fmt.Errorf("Couldn't dereference: %v", ref)
return nil, fmt.Errorf("Couldn't dereference: %v", schema.Ref)
}
schema = newSchema
}
return schema, nil
}

func (g *DataGenerator) generateResource(schema JSONSchema) (interface{}, error) {
xResourceID, ok := schema["x-resourceId"].(string)
if !ok {
schemaType, ok := schema["type"].(string)
if ok {
if schemaType == "object" {
return map[string]interface{}{}, nil
}
return nil, notSupportedErr
}

// Types are also allowed to be an array of types
schemaTypes, ok := schema["type"].([]string)
if ok {
for _, schemaType := range schemaTypes {
func (g *DataGenerator) generateResource(schema *JSONSchema) (interface{}, error) {
if schema.XResourceID == "" {
// Technically type can also be just a string, but we're not going to
// support this for now.
if schema.Type != nil {
for _, schemaType := range schema.Type {
if schemaType == "object" {
return map[string]interface{}{}, nil
}
Expand All @@ -54,14 +45,14 @@ func (g *DataGenerator) generateResource(schema JSONSchema) (interface{}, error)
return map[string]interface{}{}, nil
}

fixture, ok := g.fixtures.Resources[ResourceID(xResourceID)]
fixture, ok := g.fixtures.Resources[ResourceID(schema.XResourceID)]
if !ok {
return nil, fmt.Errorf("Expected fixtures to include %v", xResourceID)
return nil, fmt.Errorf("Expected fixtures to include %v", schema.XResourceID)
}
return fixture, nil
}

func (g *DataGenerator) Generate(schema JSONSchema, requestPath string) (interface{}, error) {
func (g *DataGenerator) Generate(schema *JSONSchema, requestPath string) (interface{}, error) {
schema, err := g.maybeDereference(schema)
if err != nil {
return nil, err
Expand All @@ -72,18 +63,17 @@ func (g *DataGenerator) Generate(schema JSONSchema, requestPath string) (interfa
return nil, err
}

properties, ok := schema["properties"].(map[string]interface{})
if ok {
listData, err := g.maybeGenerateList(properties, requestPath)
if schema.Properties != nil {
listData, err := g.maybeGenerateList(schema.Properties, requestPath)
if err != nil {
return nil, err
}
if listData != nil {
return listData, nil
}

for key, property := range properties {
keyData, err := g.Generate(property.(JSONSchema), requestPath)
for key, property := range schema.Properties {
keyData, err := g.Generate(property, requestPath)
if err == notSupportedErr {
continue
}
Expand All @@ -97,32 +87,30 @@ func (g *DataGenerator) Generate(schema JSONSchema, requestPath string) (interfa
return data, nil
}

func (g *DataGenerator) maybeGenerateList(properties map[string]interface{}, requestPath string) (interface{}, error) {
object, ok := properties["object"].(map[string]interface{})
func (g *DataGenerator) maybeGenerateList(properties map[string]*JSONSchema, requestPath string) (interface{}, error) {
object, ok := properties["object"]
if !ok {
return nil, nil
}

objectEnum, ok := object["enum"].([]interface{})
if !ok {
if object.Enum == nil {
return nil, nil
}

if objectEnum[0] != interface{}("list") {
if object.Enum[0] != "list" {
return nil, nil
}

data, ok := properties["data"].(map[string]interface{})
data, ok := properties["data"]
if !ok {
return nil, nil
}

items, ok := data["items"].(map[string]interface{})
if !ok {
if data.Items == nil {
return nil, nil
}

itemsSchema, err := g.maybeDereference(items)
itemsSchema, err := g.maybeDereference(data.Items)
if err != nil {
return nil, err
}
Expand Down
32 changes: 16 additions & 16 deletions generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,24 @@ import (
assert "github.com/stretchr/testify/require"
)

var listSchema JSONSchema
var listSchema *JSONSchema

func init() {
listSchema = JSONSchema(map[string]interface{}{
"properties": map[string]interface{}{
"data": map[string]interface{}{
"items": map[string]interface{}{
"$ref": "#/definitions/charge",
listSchema = &JSONSchema{
Properties: map[string]*JSONSchema{
"data": &JSONSchema{
Items: &JSONSchema{
Ref: "#/definitions/charge",
},
},
"has_more": nil,
"object": map[string]interface{}{
"enum": []interface{}{"list"},
"object": &JSONSchema{
Enum: []string{"list"},
},
"total_count": nil,
"url": nil,
},
})
}
}

func TestGenerateResponseData(t *testing.T) {
Expand All @@ -35,7 +35,7 @@ func TestGenerateResponseData(t *testing.T) {
// basic reference
generator = DataGenerator{testSpec.Definitions, testFixtures}
data, err = generator.Generate(
JSONSchema(map[string]interface{}{"$ref": "#/definitions/charge"}), "")
&JSONSchema{Ref: "#/definitions/charge"}, "")

assert.Nil(t, err)
assert.Equal(t,
Expand All @@ -55,11 +55,11 @@ func TestGenerateResponseData(t *testing.T) {
// nested list
generator = DataGenerator{testSpec.Definitions, testFixtures}
data, err = generator.Generate(
JSONSchema(map[string]interface{}{
"properties": map[string]interface{}{
&JSONSchema{
Properties: map[string]*JSONSchema{
"charges_list": listSchema,
},
}), "/v1/charges")
}, "/v1/charges")
assert.Nil(t, err)
chargesList := data.(map[string]interface{})["charges_list"]
assert.Equal(t, "list", chargesList.(map[string]interface{})["object"])
Expand All @@ -71,15 +71,15 @@ func TestGenerateResponseData(t *testing.T) {
// error: unhandled JSON schema type
generator = DataGenerator{testSpec.Definitions, testFixtures}
data, err = generator.Generate(
JSONSchema(map[string]interface{}{"type": "string"}), "")
&JSONSchema{Type: []string{"string"}}, "")
assert.Equal(t,
fmt.Errorf("Expected response to be a list or include $ref"),
err)

// error: no definition in OpenAPI
generator = DataGenerator{testSpec.Definitions, testFixtures}
data, err = generator.Generate(
JSONSchema(map[string]interface{}{"$ref": "#/definitions/doesnt-exist"}), "")
&JSONSchema{Ref: "#/definitions/doesnt-exist"}, "")
assert.Equal(t,
fmt.Errorf("Couldn't dereference: #/definitions/doesnt-exist"),
err)
Expand All @@ -93,7 +93,7 @@ func TestGenerateResponseData(t *testing.T) {
},
}
data, err = generator.Generate(
JSONSchema(map[string]interface{}{"$ref": "#/definitions/charge"}), "")
&JSONSchema{Ref: "#/definitions/charge"}, "")
assert.Equal(t,
fmt.Errorf("Expected fixtures to include charge"),
err)
Expand Down
29 changes: 20 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,25 @@ type Fixtures struct {

type HTTPVerb string

type JSONSchema map[string]interface{}
type JSONSchema struct {
Enum []string `json:"enum"`
Items *JSONSchema `json:"items"`
Properties map[string]*JSONSchema `json:"properties"`
Type []string `json:"type"`

// Ref is populated if this JSON Schema is actually a JSON reference, and
// it defines the location of the actual schema definition.
Ref string `json:"$ref"`

XResourceID string `json:"x-resourceId"`
}

type OpenAPIParameter struct {
Description string `json:"description"`
In string `json:"in"`
Name string `json:"name"`
Required bool `json:"required"`
Schema JSONSchema `json:"schema"`
Description string `json:"description"`
In string `json:"in"`
Name string `json:"name"`
Required bool `json:"required"`
Schema *JSONSchema `json:"schema"`
}

type OpenAPIMethod struct {
Expand All @@ -41,12 +52,12 @@ type OpenAPIMethod struct {
type OpenAPIPath string

type OpenAPIResponse struct {
Description string `json:"description"`
Schema JSONSchema `json:"schema"`
Description string `json:"description"`
Schema *JSONSchema `json:"schema"`
}

type OpenAPISpec struct {
Definitions map[string]JSONSchema `json:"definitions"`
Definitions map[string]*JSONSchema `json:"definitions"`
Paths map[OpenAPIPath]map[HTTPVerb]*OpenAPIMethod `json:"paths"`
}

Expand Down
4 changes: 2 additions & 2 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ func init() {
}

testSpec = &OpenAPISpec{
Definitions: map[string]JSONSchema{
"charge": {"x-resourceId": "charge"},
Definitions: map[string]*JSONSchema{
"charge": {XResourceID: "charge"},
},
Paths: map[OpenAPIPath]map[HTTPVerb]*OpenAPIMethod{
OpenAPIPath("/v1/charges"): {
Expand Down

0 comments on commit 3a0fc9f

Please sign in to comment.