Skip to content

Commit

Permalink
Refactor data generator to a separate file
Browse files Browse the repository at this point in the history
  • Loading branch information
brandur committed Jun 23, 2017
1 parent 5e92c3e commit fddb77e
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 195 deletions.
129 changes: 129 additions & 0 deletions generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package main

import (
"fmt"
"strings"
)

var notSupportedErr = fmt.Errorf("Expected response to be a list or include $ref")

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

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

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

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

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

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

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

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

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

innerData, err := g.generateResource(itemsRef)
if err != nil {
return nil, err
}

// This is written to hopefully be a little more forward compatible in that
// it respects the list properties dictated by the included schema rather
// than assuming its own.
listData := make(map[string]interface{})
for key := range properties {
var val interface{}
switch key {
case "data":
val = []interface{}{innerData}
case "has_more":
val = false
case "object":
val = "list"
case "total_count":
val = 1
case "url":
val = requestPath
default:
val = nil
}
listData[key] = val
}
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
// a JSON pointer, so "#/definitions/charge" would become just "charge". This
// is a simplified workaround to avoid bringing in JSON schema infrastructure
// because we can guarantee that the spec we're producing will take a certain
// shape. If this gets too hacky, it will be better to put a more legitimate
// JSON schema parser in place.
func definitionFromJSONPointer(pointer string) (string, error) {
parts := strings.Split(pointer, "/")

if parts[0] != "#" {
return "", fmt.Errorf("Expected '#' in 0th part of pointer %v", pointer)
}

if parts[1] != "definitions" {
return "", fmt.Errorf("Expected 'definitions' in 1st part of pointer %v",
pointer)
}

if len(parts) > 3 {
return "", fmt.Errorf("Pointer too long to be handle %v", pointer)
}

return parts[2], nil
}
85 changes: 85 additions & 0 deletions generator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package main

import (
"fmt"
"testing"

assert "github.com/stretchr/testify/require"
)

func TestGenerateResponseData(t *testing.T) {
var data interface{}
var err error
var generator DataGenerator

// basic reference
generator = DataGenerator{testSpec.Definitions, testFixtures}
data, err = generator.Generate(
JSONSchema(map[string]interface{}{"$ref": "#/definitions/charge"}), "")

assert.Nil(t, err)
assert.Equal(t,
testFixtures.Resources["charge"].(map[string]interface{})["id"],
data.(map[string]interface{})["id"])

// 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,
},
}), "/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"])

// error: unhandled JSON schema type
generator = DataGenerator{testSpec.Definitions, testFixtures}
data, err = generator.Generate(
JSONSchema(map[string]interface{}{}), "")
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"}), "")
assert.Equal(t,
fmt.Errorf("Expected definitions to include doesnt-exist"),
err)

// error: no fixture
generator = DataGenerator{
testSpec.Definitions,
// this is an empty set of fixtures
&Fixtures{
Resources: map[ResourceID]interface{}{},
},
}
data, err = generator.Generate(
JSONSchema(map[string]interface{}{"$ref": "#/definitions/charge"}), "")
assert.Equal(t,
fmt.Errorf("Expected fixtures to include charge"),
err)
}

func TestDefinitionFromJSONPointer(t *testing.T) {
definition, err := definitionFromJSONPointer("#/definitions/charge")
assert.Nil(t, err)
assert.Equal(t, "charge", definition)
}
123 changes: 2 additions & 121 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ func (s *StubServer) handleRequest(w http.ResponseWriter, r *http.Request) {
log.Printf("Response schema: %+v", response.Schema)
}

responseData, err := generateResponseData(response.Schema, r.URL.Path, s.spec.Definitions, s.fixtures)
generator := DataGenerator{s.spec.Definitions, s.fixtures}
responseData, err := generator.Generate(response.Schema, r.URL.Path)
if err != nil {
log.Printf("Couldn't generate response: %v", err)
writeInternalError(w)
Expand Down Expand Up @@ -192,126 +193,6 @@ func countAPIMethods(spec *OpenAPISpec) int {
return count
}

// definitionFromJSONPointer extracts the name of a JSON schema definition from
// a JSON pointer, so "#/definitions/charge" would become just "charge". This
// is a simplified workaround to avoid bringing in JSON schema infrastructure
// because we can guarantee that the spec we're producing will take a certain
// shape. If this gets too hacky, it will be better to put a more legitimate
// JSON schema parser in place.
func definitionFromJSONPointer(pointer string) (string, error) {
parts := strings.Split(pointer, "/")

if parts[0] != "#" {
return "", fmt.Errorf("Expected '#' in 0th part of pointer %v", pointer)
}

if parts[1] != "definitions" {
return "", fmt.Errorf("Expected 'definitions' in 1st part of pointer %v",
pointer)
}

if len(parts) > 3 {
return "", fmt.Errorf("Pointer too long to be handle %v", pointer)
}

return parts[2], nil
}

func generateResponseData(schema JSONSchema, requestPath string,
definitions map[string]OpenAPIDefinition, fixtures *Fixtures) (interface{}, error) {

notSupportedErr := fmt.Errorf("Expected response to be a list or include $ref")

ref, ok := schema["$ref"].(string)
if ok {
return generateResponseResourceData(ref, definitions, fixtures)
}

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

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

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

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

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

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

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

innerData, err := generateResponseResourceData(itemsRef, definitions, fixtures)
if err != nil {
return nil, err
}

// This is written to hopefully be a little more forward compatible in that
// it respects the list properties dictated by the included schema rather
// than assuming its own.
listData := make(map[string]interface{})
for key := range properties {
var val interface{}
switch key {
case "data":
val = []interface{}{innerData}
case "has_more":
val = false
case "object":
val = "list"
case "total_count":
val = 1
case "url":
val = requestPath
default:
val = nil
}
listData[key] = val
}
return listData, nil
}

func generateResponseResourceData(pointer string,
definitions map[string]OpenAPIDefinition, fixtures *Fixtures) (interface{}, error) {

definition, err := definitionFromJSONPointer(pointer)
if err != nil {
return nil, fmt.Errorf("Error extracting definition: %v", err)
}

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

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

return fixture, nil
}

func writeInternalError(w http.ResponseWriter) {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Internal server error")
Expand Down
Loading

0 comments on commit fddb77e

Please sign in to comment.