Skip to content

Commit

Permalink
Rebuild generator to be more generic + support nested lists
Browse files Browse the repository at this point in the history
  • Loading branch information
brandur committed Jun 23, 2017
1 parent fddb77e commit 75bd26b
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 57 deletions.
124 changes: 90 additions & 34 deletions generator.go
Expand Up @@ -8,51 +8,126 @@ import (
var notSupportedErr = fmt.Errorf("Expected response to be a list or include $ref")

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

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

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

properties, ok := schema["properties"].(map[string]interface{})
func (g *DataGenerator) generateResource(schema JSONSchema) (interface{}, error) {
xResourceID, ok := schema["x-resourceId"].(string)
if !ok {
return nil, notSupportedErr
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 {
if schemaType == "object" {
return map[string]interface{}{}, nil
}
}
return nil, notSupportedErr
}

// Support schemas with no type annotation at all
return map[string]interface{}{}, nil
}

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

func (g *DataGenerator) Generate(schema JSONSchema, requestPath string) (interface{}, error) {
schema, err := g.maybeDereference(schema)
if err != nil {
return nil, err
}

data, err := g.generateResource(schema)
if err != nil {
return nil, err
}

properties, ok := schema["properties"].(map[string]interface{})
if ok {
listData, err := g.maybeGenerateList(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)
if err == notSupportedErr {
continue
}
if err != nil {
return nil, err
}
data.(map[string]interface{})[key] = keyData
}
}

return data, nil
}

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

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

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

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

items, ok := data["items"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("Expected list to include items schema")
return nil, nil
}

itemsRef, ok := items["$ref"].(string)
if !ok {
return nil, fmt.Errorf("Expected items schema to include $ref")
itemsSchema, err := g.maybeDereference(items)
if err != nil {
return nil, err
}

innerData, err := g.generateResource(itemsRef)
itemsData, err := g.Generate(itemsSchema, requestPath)
if err != nil {
return nil, err
}
Expand All @@ -65,7 +140,7 @@ func (g *DataGenerator) Generate(schema JSONSchema, requestPath string) (interfa
var val interface{}
switch key {
case "data":
val = []interface{}{innerData}
val = []interface{}{itemsData}
case "has_more":
val = false
case "object":
Expand All @@ -82,25 +157,6 @@ func (g *DataGenerator) Generate(schema JSONSchema, requestPath string) (interfa
return listData, nil
}

func (g *DataGenerator) generateResource(pointer string) (interface{}, error) {
definition, err := definitionFromJSONPointer(pointer)
if err != nil {
return nil, fmt.Errorf("Error extracting definition: %v", err)
}

resource, ok := g.definitions[definition]
if !ok {
return nil, fmt.Errorf("Expected definitions to include %v", definition)
}

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

return fixture, nil
}

// ---

// definitionFromJSONPointer extracts the name of a JSON schema definition from
Expand Down
55 changes: 39 additions & 16 deletions generator_test.go
Expand Up @@ -7,6 +7,26 @@ import (
assert "github.com/stretchr/testify/require"
)

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",
},
},
"has_more": nil,
"object": map[string]interface{}{
"enum": []interface{}{"list"},
},
"total_count": nil,
"url": nil,
},
})
}

func TestGenerateResponseData(t *testing.T) {
var data interface{}
var err error
Expand All @@ -24,33 +44,34 @@ func TestGenerateResponseData(t *testing.T) {

// list
generator = DataGenerator{testSpec.Definitions, testFixtures}
data, err = generator.Generate(listSchema, "/v1/charges")
assert.Nil(t, err)
assert.Equal(t, "list", data.(map[string]interface{})["object"])
assert.Equal(t, "/v1/charges", data.(map[string]interface{})["url"])
assert.Equal(t,
testFixtures.Resources["charge"].(map[string]interface{})["id"],
data.(map[string]interface{})["data"].([]interface{})[0].(map[string]interface{})["id"])

// nested list
generator = DataGenerator{testSpec.Definitions, testFixtures}
data, err = generator.Generate(
JSONSchema(map[string]interface{}{
"properties": map[string]interface{}{
"data": map[string]interface{}{
"items": map[string]interface{}{
"$ref": "#/definitions/charge",
},
},
"has_more": nil,
"object": map[string]interface{}{
"enum": []interface{}{"list"},
},
"total_count": nil,
"url": nil,
"charges_list": listSchema,
},
}), "/v1/charges")
assert.Nil(t, err)
assert.Equal(t, "list", data.(map[string]interface{})["object"])
assert.Equal(t, "/v1/charges", data.(map[string]interface{})["url"])
chargesList := data.(map[string]interface{})["charges_list"]
assert.Equal(t, "list", chargesList.(map[string]interface{})["object"])
assert.Equal(t, "/v1/charges", chargesList.(map[string]interface{})["url"])
assert.Equal(t,
testFixtures.Resources["charge"].(map[string]interface{})["id"],
data.(map[string]interface{})["data"].([]interface{})[0].(map[string]interface{})["id"])
chargesList.(map[string]interface{})["data"].([]interface{})[0].(map[string]interface{})["id"])

// error: unhandled JSON schema type
generator = DataGenerator{testSpec.Definitions, testFixtures}
data, err = generator.Generate(
JSONSchema(map[string]interface{}{}), "")
JSONSchema(map[string]interface{}{"type": "string"}), "")
assert.Equal(t,
fmt.Errorf("Expected response to be a list or include $ref"),
err)
Expand All @@ -60,7 +81,7 @@ func TestGenerateResponseData(t *testing.T) {
data, err = generator.Generate(
JSONSchema(map[string]interface{}{"$ref": "#/definitions/doesnt-exist"}), "")
assert.Equal(t,
fmt.Errorf("Expected definitions to include doesnt-exist"),
fmt.Errorf("Couldn't dereference: #/definitions/doesnt-exist"),
err)

// error: no fixture
Expand All @@ -78,6 +99,8 @@ func TestGenerateResponseData(t *testing.T) {
err)
}

// ---

func TestDefinitionFromJSONPointer(t *testing.T) {
definition, err := definitionFromJSONPointer("#/definitions/charge")
assert.Nil(t, err)
Expand Down
6 changes: 1 addition & 5 deletions main.go
Expand Up @@ -23,10 +23,6 @@ type HTTPVerb string

type JSONSchema map[string]interface{}

type OpenAPIDefinition struct {
XResourceID ResourceID `json:"x-resourceId"`
}

type OpenAPIParameter struct {
Description string `json:"description"`
In string `json:"in"`
Expand All @@ -50,7 +46,7 @@ type OpenAPIResponse struct {
}

type OpenAPISpec struct {
Definitions map[string]OpenAPIDefinition `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
Expand Up @@ -29,8 +29,8 @@ func init() {
}

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

0 comments on commit 75bd26b

Please sign in to comment.