added support for additionalProperties = false for validation and a test for a map of struct

This commit is contained in:
Shreyas Goenka 2023-01-17 17:53:58 +01:00
parent 91617e50c1
commit 753c54e899
No known key found for this signature in database
GPG Key ID: 92A07DF49CCB0622
2 changed files with 67 additions and 26 deletions

View File

@ -19,34 +19,26 @@ type Schema struct {
// Type of the object // Type of the object
Type JavascriptType `json:"type"` Type JavascriptType `json:"type"`
// TODO: See what happens if these REQUIRED constraint is not satisfied
// Type == object if this is non empty
// keys are named properties of the object // keys are named properties of the object
// values are json schema for the values of the named properties // values are json schema for the values of the named properties
Properties map[string]*Schema `json:"properties,omitempty"` Properties map[string]*Schema `json:"properties,omitempty"`
// REQUIRED: Type == array if this is non empty
// the schema for all values of the array // the schema for all values of the array
Items *Schema `json:"items,omitempty"` Items *Schema `json:"items,omitempty"`
// REQUIRED: Type == object if this is non empty
// the schema for any properties not mentioned in the Schema.Properties field. // the schema for any properties not mentioned in the Schema.Properties field.
// we leverage this to validate Maps in bundle configuration // we leverage this to validate Maps in bundle configuration
AdditionalProperties *Schema `json:"additionalProperties,omitempty"` // OR
// a boolean type with value false
//
// Its type during runtime will either be *Schema or bool
AdditionalProperties interface{} `json:"additionalProperties,omitempty"`
// REQUIRED: Type == object if this is non empty
// required properties for the object. Any propertites listed here should // required properties for the object. Any propertites listed here should
// also be listed in Schema.Properties // also be listed in Schema.Properties
Required []string `json:"required,omitempty"` Required []string `json:"required,omitempty"`
} }
// NOTE about loops in golangType: Right now we error out if there is a loop
// in the types traversed when generating the json schema. This can be solved
// using $refs but there is complexity around making sure we do not create json
// schemas where properties indirectly refer to each other, which would be an
// invalid json schema. See https://json-schema.org/understanding-json-schema/structuring.html#recursion
// for more details
/* /*
This function translates golang types into json schema. Here is the mapping This function translates golang types into json schema. Here is the mapping
between json schema types and golang types between json schema types and golang types
@ -77,6 +69,7 @@ type Schema struct {
- []MyStruct -> { - []MyStruct -> {
type: object type: object
properties: {} properties: {}
additionalProperties: false
} }
for details visit: https://json-schema.org/understanding-json-schema/reference/object.html#properties for details visit: https://json-schema.org/understanding-json-schema/reference/object.html#properties
*/ */
@ -142,10 +135,15 @@ func errWithTrace(prefix string, trace *list.List) error {
return fmt.Errorf("[ERROR] " + prefix + ". traversal trace: " + traceString) return fmt.Errorf("[ERROR] " + prefix + ". traversal trace: " + traceString)
} }
// A wrapper over toProperty function with checks for an cycles to avoid being // A wrapper over toSchema function to detect cycles in the bundle config struct
// stuck in an loop when traversing the config struct
func safeToSchema(golangType reflect.Type, seenTypes map[reflect.Type]struct{}, debugTrace *list.List) (*Schema, error) { func safeToSchema(golangType reflect.Type, seenTypes map[reflect.Type]struct{}, debugTrace *list.List) (*Schema, error) {
// detect cycles. Fail if a cycle is detected // WE ERROR OUT IF THERE ARE CYCLES IN THE JSON SCHEMA
// There are mechanisms to deal with cycles though recursive identifiers in json
// schema. However if we use them, we would need to make sure we are able to detect
// cycles two properties (directly or indirectly) pointing to each other
//
// see: https://json-schema.org/understanding-json-schema/structuring.html#recursion
// for details
_, ok := seenTypes[golangType] _, ok := seenTypes[golangType]
if ok { if ok {
fmt.Println("[DEBUG] traceSet: ", seenTypes) fmt.Println("[DEBUG] traceSet: ", seenTypes)
@ -244,7 +242,7 @@ func toSchema(golangType reflect.Type, seenTypes map[reflect.Type]struct{}, debu
} }
// case map // case map
var additionalProperties *Schema var additionalProperties interface{}
if golangType.Kind() == reflect.Map { if golangType.Kind() == reflect.Map {
if golangType.Key().Kind() != reflect.String { if golangType.Key().Kind() != reflect.String {
return nil, fmt.Errorf("only string keyed maps allowed") return nil, fmt.Errorf("only string keyed maps allowed")
@ -285,6 +283,9 @@ func toSchema(golangType reflect.Type, seenTypes map[reflect.Type]struct{}, debu
// remove current field from debug trace // remove current field from debug trace
back := debugTrace.Back() back := debugTrace.Back()
debugTrace.Remove(back) debugTrace.Remove(back)
// set Schema.AdditionalProperties to false
additionalProperties = false
} }
} }

View File

@ -21,6 +21,7 @@ import (
// TODO: Have a test that combines multiple different cases // TODO: Have a test that combines multiple different cases
// TODO: have a test for what happens when omitempty in different cases: primitives, object, map, array // TODO: have a test for what happens when omitempty in different cases: primitives, object, map, array
func TestIntSchema(t *testing.T) { func TestIntSchema(t *testing.T) {
var elemInt int var elemInt int
@ -109,7 +110,7 @@ func TestStructOfPrimitivesSchema(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
expected := expected :=
`{ ` {
"type": "object", "type": "object",
"properties": { "properties": {
"bool_val": { "bool_val": {
@ -154,7 +155,8 @@ func TestStructOfPrimitivesSchema(t *testing.T) {
"uint_val": { "uint_val": {
"type": "number" "type": "number"
} }
} },
"additionalProperties": false
}` }`
t.Log("[DEBUG] actual: ", string(jsonSchema)) t.Log("[DEBUG] actual: ", string(jsonSchema))
@ -200,11 +202,14 @@ func TestStructOfStructsSchema(t *testing.T) {
"b": { "b": {
"type": "string" "type": "string"
} }
},
"additionalProperties": false
} }
},
"additionalProperties": false
} }
} },
} "additionalProperties": false
}
}` }`
t.Log("[DEBUG] actual: ", string(jsonSchema)) t.Log("[DEBUG] actual: ", string(jsonSchema))
@ -242,9 +247,11 @@ func TestStructOfMapsSchema(t *testing.T) {
"type": "number" "type": "number"
} }
} }
},
"additionalProperties": false
} }
} },
} "additionalProperties": false
}` }`
t.Log("[DEBUG] actual: ", string(jsonSchema)) t.Log("[DEBUG] actual: ", string(jsonSchema))
@ -282,9 +289,11 @@ func TestStructOfSliceSchema(t *testing.T) {
"type": "string" "type": "string"
} }
} }
},
"additionalProperties": false
} }
} },
} "additionalProperties": false
}` }`
t.Log("[DEBUG] actual: ", string(jsonSchema)) t.Log("[DEBUG] actual: ", string(jsonSchema))
@ -314,6 +323,37 @@ func TestMapOfPrimitivesSchema(t *testing.T) {
assert.Equal(t, expected, string(jsonSchema)) assert.Equal(t, expected, string(jsonSchema))
} }
func TestMapOfStructSchema(t *testing.T) {
type Foo struct {
MyInt int `json:"my_int"`
}
var elem map[string]Foo
schema, err := NewSchema(reflect.TypeOf(elem))
assert.NoError(t, err)
jsonSchema, err := json.MarshalIndent(schema, " ", " ")
assert.NoError(t, err)
expected :=
`{
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"my_int": {
"type": "number"
}
}
}
}`
t.Log("[DEBUG] actual: ", string(jsonSchema))
t.Log("[DEBUG] expected: ", expected)
assert.Equal(t, expected, string(jsonSchema))
}
func TestObjectSchema(t *testing.T) { func TestObjectSchema(t *testing.T) {
type Person struct { type Person struct {
Name string `json:"name"` Name string `json:"name"`