Skip to content

Commit

Permalink
Merge pull request #66 from miracl/schema-refs
Browse files Browse the repository at this point in the history
fixes #53 support $ref in applyDefaults
  • Loading branch information
andy-miracl authored Apr 17, 2018
2 parents c85a0e9 + ed74d5d commit c54d702
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 53 deletions.
135 changes: 82 additions & 53 deletions schema.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package conflate

import (
"github.com/xeipuuv/gojsonreference"
"github.com/xeipuuv/gojsonschema"
"reflect"
"strings"
Expand Down Expand Up @@ -53,10 +54,10 @@ func convertJSONContext(jsonCtx string) context {
}

func applyDefaults(pData interface{}, schema interface{}) error {
return applyDefaultsRecursive(rootContext(), pData, schema)
return applyDefaultsRecursive(rootContext(), schema, pData, schema)
}

func applyDefaultsRecursive(ctx context, pData interface{}, schema interface{}) error {
func applyDefaultsRecursive(ctx context, rootSchema interface{}, pData interface{}, schema interface{}) error {
if pData == nil {
return makeContextError(ctx, "Destination value must not be nil")
}
Expand All @@ -72,6 +73,23 @@ func applyDefaultsRecursive(ctx context, pData interface{}, schema interface{})
return makeContextError(ctx, "Schema section is not a map")
}

val, ok := schemaNode["$ref"]
if ok {
ref, ok := val.(string)
if !ok {
return makeContextError(ctx, makeError("Reference is not a string '%v'", ref).Error())
}
jref, err := gojsonreference.NewJsonReference(ref)
if err != nil {
return makeContextError(ctx, wrapError(err, "Invalid reference '%v'", ref).Error())
}
subSchema, _, err := jref.GetPointer().Get(rootSchema)
if subSchema == nil || err != nil {
return makeContextError(ctx, wrapError(err, "Cannot find reference '%v'", ref).Error())
}
return applyDefaultsRecursive(ctx.add(ref), rootSchema, pData, subSchema)
}

for k := range schemaNode {
switch k {
case "anyOf":
Expand All @@ -97,65 +115,76 @@ func applyDefaultsRecursive(ctx context, pData interface{}, schema interface{})
data = dataVal.Interface()
}

var err error
switch schemaType {
case "object":
if data == nil {
break
}
dataProps, ok := data.(map[string]interface{})
if !ok {
return makeContextError(ctx, "Node should be an 'object'")
}
if dataProps == nil {
return nil
}
var schemaProps map[string]interface{}
if props, ok := schemaNode["properties"]; ok {
schemaProps = props.(map[string]interface{})
for name, schemaProp := range schemaProps {
dataProp := dataProps[name]
err := applyDefaultsRecursive(ctx.add(name), &dataProp, schemaProp)
if err != nil {
return wrapError(err, "Failed to apply defaults to object property")
}
if dataProp != nil {
dataProps[name] = dataProp
}
err = applyObjectDefaults(ctx, rootSchema, data, schemaNode)
case "array":
err = applyArrayDefaults(ctx, rootSchema, data, schemaNode)
}
return err
}

func applyObjectDefaults(ctx context, rootSchema interface{}, data interface{}, schemaNode map[string]interface{}) error {
if data == nil {
return nil
}
dataProps, ok := data.(map[string]interface{})
if !ok {
return makeContextError(ctx, "Node should be an 'object'")
}
if dataProps == nil {
return nil
}
var schemaProps map[string]interface{}
if props, ok := schemaNode["properties"]; ok {
schemaProps = props.(map[string]interface{})
for name, schemaProp := range schemaProps {
dataProp := dataProps[name]
err := applyDefaultsRecursive(ctx.add(name), rootSchema, &dataProp, schemaProp)
if err != nil {
return wrapError(err, "Failed to apply defaults to object property")
}
if dataProp != nil {
dataProps[name] = dataProp
}
}
if addProps, ok := schemaNode["additionalProperties"]; ok {
if addProps, ok = addProps.(map[string]interface{}); ok {
for name, dataProp := range dataProps {
if schemaProps == nil || schemaProps[name] == nil {
err := applyDefaultsRecursive(ctx.add(name), &dataProp, addProps)
if err != nil {
return wrapError(err, "Failed to apply defaults to additional object property")
}
if dataProp != nil {
dataProps[name] = dataProp
}
}
if addProps, ok := schemaNode["additionalProperties"]; ok {
if addProps, ok = addProps.(map[string]interface{}); ok {
for name, dataProp := range dataProps {
if schemaProps == nil || schemaProps[name] == nil {
err := applyDefaultsRecursive(ctx.add(name), rootSchema, &dataProp, addProps)
if err != nil {
return wrapError(err, "Failed to apply defaults to additional object property")
}
if dataProp != nil {
dataProps[name] = dataProp
}
}
}
}
case "array":
if data == nil {
break
}
dataItems, ok := data.([]interface{})
if !ok {
return makeContextError(ctx, "Node should be an 'array'")
}
if items, ok := schemaNode["items"]; ok {
schemaItem := items.(map[string]interface{})
for i, dataItem := range dataItems {
err := applyDefaultsRecursive(ctx.addInt(i), &dataItem, schemaItem)
if err != nil {
return wrapError(err, "Failed to apply defaults to array item")
}
if dataItem != nil {
dataItems[i] = dataItem
}
}
return nil
}

func applyArrayDefaults(ctx context, rootSchema interface{}, data interface{}, schemaNode map[string]interface{}) error {
if data == nil {
return nil
}
dataItems, ok := data.([]interface{})
if !ok {
return makeContextError(ctx, "Node should be an 'array'")
}
if items, ok := schemaNode["items"]; ok {
schemaItem := items.(map[string]interface{})
for i, dataItem := range dataItems {
err := applyDefaultsRecursive(ctx.addInt(i), rootSchema, &dataItem, schemaItem)
if err != nil {
return wrapError(err, "Failed to apply defaults to array item")
}
if dataItem != nil {
dataItems[i] = dataItem
}
}
}
Expand Down
88 changes: 88 additions & 0 deletions schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,94 @@ func TestApplyDefaults_MissingIntFields(t *testing.T) {
assert.Equal(t, 1.0, arrObj["int"])
}

func TestApplyDefaults_Ref(t *testing.T) {
var schemaData = []byte(`
{
"type": "object",
"definitions": {
"int": { "type": "integer", "default": 1 }
},
"properties": {
"int": { "$ref": "#/definitions/int" },
"obj": { "$ref": "#" }
}
}`)
var rawData = []byte(` { "int": null, "obj": { "int": null} }`)

var data map[string]interface{}
err := JSONUnmarshal(rawData, &data)
assert.Nil(t, err)
var schema interface{}
err = JSONUnmarshal(schemaData, &schema)
assert.Nil(t, err)
err = applyDefaults(&data, schema)
assert.Nil(t, err)
assert.Equal(t, map[string]interface{}{"int": 1.0, "obj": map[string]interface{}{"int": 1.0}}, data)
}

func TestApplyDefaults_RefNotStringError(t *testing.T) {
var schemaData = []byte(`
{
"type": "object",
"properties": {
"int": { "$ref": {} }
}
}`)
var rawData = []byte(` { "int": 123 }`)

var data map[string]interface{}
err := JSONUnmarshal(rawData, &data)
assert.Nil(t, err)
var schema interface{}
err = JSONUnmarshal(schemaData, &schema)
assert.Nil(t, err)
err = applyDefaults(&data, schema)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "Reference is not a string")
}

func TestApplyDefaults_RefInvalidError(t *testing.T) {
var schemaData = []byte(`
{
"type": "object",
"properties": {
"int": { "$ref": "://x/y" }
}
}`)
var rawData = []byte(` { "int": 123 }`)

var data map[string]interface{}
err := JSONUnmarshal(rawData, &data)
assert.Nil(t, err)
var schema interface{}
err = JSONUnmarshal(schemaData, &schema)
assert.Nil(t, err)
err = applyDefaults(&data, schema)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "Invalid reference")
}

func TestApplyDefaults_RefPointerError(t *testing.T) {
var schemaData = []byte(`
{
"type": "object",
"properties": {
"int": { "$ref": "#/missing" }
}
}`)
var rawData = []byte(` { "int": 123 }`)

var data map[string]interface{}
err := JSONUnmarshal(rawData, &data)
assert.Nil(t, err)
var schema interface{}
err = JSONUnmarshal(schemaData, &schema)
assert.Nil(t, err)
err = applyDefaults(&data, schema)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "Cannot find reference")
}

// -----------

var testSchemaData = []byte(`
Expand Down

0 comments on commit c54d702

Please sign in to comment.