From 7c9402cd5f5581b2f04273974ef90bf531ec78e5 Mon Sep 17 00:00:00 2001 From: Torin Sandall Date: Fri, 21 May 2021 07:38:02 -0400 Subject: [PATCH] ast: Treat array.items as dynamic element type (#3477) This commit fixes the conversion to treat array.items schema as the dynamic element type in arrays as opposed to the static element type. If array.items is defined as an object then it applies to ALL elements in the array. On the other hand, if array.items is defined as an array then it applies pairwise to the elements in the array. In order to fix this, we have to expose a new field from the schema library. Signed-off-by: Torin Sandall --- ast/compile.go | 12 +++++++-- ast/schema_test.go | 40 ++++++++++++++++++++++++++--- internal/gojsonschema/schema.go | 4 +-- internal/gojsonschema/subSchema.go | 2 +- internal/gojsonschema/validation.go | 2 +- 5 files changed, 50 insertions(+), 10 deletions(-) diff --git a/ast/compile.go b/ast/compile.go index d7bc434cfa..098f30f674 100644 --- a/ast/compile.go +++ b/ast/compile.go @@ -902,7 +902,7 @@ func parseSchema(schema interface{}) (types.Type, error) { return types.N, nil } else if subSchema.Types.Contains("object") { - if subSchema.PropertiesChildren != nil && len(subSchema.PropertiesChildren) > 0 { + if len(subSchema.PropertiesChildren) > 0 { staticProps := make([]*types.StaticProperty, 0, len(subSchema.PropertiesChildren)) for _, pSchema := range subSchema.PropertiesChildren { newtype, err := parseSchema(pSchema) @@ -916,7 +916,15 @@ func parseSchema(schema interface{}) (types.Type, error) { return types.NewObject(nil, types.NewDynamicProperty(types.A, types.A)), nil } else if subSchema.Types.Contains("array") { - if subSchema.ItemsChildren != nil && len(subSchema.ItemsChildren) > 0 { + if len(subSchema.ItemsChildren) > 0 { + if subSchema.ItemsChildrenIsSingleSchema { + iSchema := subSchema.ItemsChildren[0] + newtype, err := parseSchema(iSchema) + if err != nil { + return nil, fmt.Errorf("unexpected schema type %v", iSchema) + } + return types.NewArray(nil, newtype), nil + } newTypes := make([]types.Type, 0, len(subSchema.ItemsChildren)) for i := 0; i != len(subSchema.ItemsChildren); i++ { iSchema := subSchema.ItemsChildren[i] diff --git a/ast/schema_test.go b/ast/schema_test.go index 53f79ddb0e..ee64347acc 100644 --- a/ast/schema_test.go +++ b/ast/schema_test.go @@ -8,6 +8,8 @@ import ( ) func testParseSchema(t *testing.T, schema string, expectedType types.Type) { + t.Helper() + var sch interface{} err := util.Unmarshal([]byte(schema), &sch) if err != nil { @@ -26,15 +28,14 @@ func testParseSchema(t *testing.T, schema string, expectedType types.Type) { } func TestParseSchemaObject(t *testing.T) { - //Expected type is: object, c: any>>, foo: string> innerObjectStaticProps := []*types.StaticProperty{} innerObjectStaticProps = append(innerObjectStaticProps, &types.StaticProperty{Key: "a", Value: types.N}) - innerObjectStaticProps = append(innerObjectStaticProps, &types.StaticProperty{Key: "b", Value: types.NewArray([]types.Type{types.N}, nil)}) + innerObjectStaticProps = append(innerObjectStaticProps, &types.StaticProperty{Key: "b", Value: types.NewArray(nil, types.N)}) innerObjectStaticProps = append(innerObjectStaticProps, &types.StaticProperty{Key: "c", Value: types.A}) innerObjectType := types.NewObject(innerObjectStaticProps, nil) staticProps := []*types.StaticProperty{} - staticProps = append(staticProps, &types.StaticProperty{Key: "b", Value: types.NewArray([]types.Type{innerObjectType}, nil)}) + staticProps = append(staticProps, &types.StaticProperty{Key: "b", Value: types.NewArray(nil, innerObjectType)}) staticProps = append(staticProps, &types.StaticProperty{Key: "foo", Value: types.S}) expectedType := types.NewObject(staticProps, nil) @@ -54,7 +55,7 @@ func TestSetTypesWithSchemaRef(t *testing.T) { if newtype == nil { t.Fatalf("parseSchema returned nil type") } - if newtype.String() != "object, generateName: string, generation: number, initializers: object>, result: object>, group: string, kind: string, name: string, retryAfterSeconds: number, uid: string>, kind: string, message: string, metadata: object, reason: string, status: string>>, labels: object[any: any], managedFields: array>, name: string, namespace: string, ownerReferences: array>, resourceVersion: string, selfLink: string, uid: string>>" { + if newtype.String() != "object], result: object], group: string, kind: string, name: string, retryAfterSeconds: number, uid: string>, kind: string, message: string, metadata: object, reason: string, status: string>>, labels: object[any: any], managedFields: array[object], name: string, namespace: string, ownerReferences: array[object], resourceVersion: string, selfLink: string, uid: string>>" { t.Fatalf("parseSchema returned an incorrect type: %s", newtype.String()) } } @@ -119,6 +120,37 @@ func TestParseSchemaBasics(t *testing.T) { schema: `{"type": "number"}`, exp: types.N, }, + { + note: "array of objects", + schema: `{ + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "value": {"type": "number"} + } + } + }`, + exp: types.NewArray(nil, types.NewObject([]*types.StaticProperty{ + types.NewStaticProperty("id", types.S), + types.NewStaticProperty("value", types.N), + }, nil)), + }, + { + note: "static array items", + schema: `{ + "type": "array", + "items": [ + {"type": "string"}, + {"type": "number"} + ] + }`, + exp: types.NewArray([]types.Type{ + types.S, + types.N, + }, nil), + }, } for _, tc := range tests { diff --git a/internal/gojsonschema/schema.go b/internal/gojsonschema/schema.go index c12f209fad..f30f36b4b4 100644 --- a/internal/gojsonschema/schema.go +++ b/internal/gojsonschema/schema.go @@ -314,7 +314,7 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *SubSchema) default: return invalidType(StringSchema+"/"+StringArrayOfSchemas, KeyItems) } - currentSchema.itemsChildrenIsSingleSchema = false + currentSchema.ItemsChildrenIsSingleSchema = false } case map[string]interface{}, bool: newSchema := &SubSchema{Parent: currentSchema, Property: KeyItems} @@ -324,7 +324,7 @@ func (d *Schema) parseSchema(documentNode interface{}, currentSchema *SubSchema) if err != nil { return err } - currentSchema.itemsChildrenIsSingleSchema = true + currentSchema.ItemsChildrenIsSingleSchema = true default: return invalidType(StringSchema+"/"+StringArrayOfSchemas, KeyItems) } diff --git a/internal/gojsonschema/subSchema.go b/internal/gojsonschema/subSchema.go index 279606bd5f..4e1886482f 100644 --- a/internal/gojsonschema/subSchema.go +++ b/internal/gojsonschema/subSchema.go @@ -102,7 +102,7 @@ type SubSchema struct { // hierarchy Parent *SubSchema ItemsChildren []*SubSchema - itemsChildrenIsSingleSchema bool + ItemsChildrenIsSingleSchema bool PropertiesChildren []*SubSchema // validation : number / integer diff --git a/internal/gojsonschema/validation.go b/internal/gojsonschema/validation.go index 89bf74103d..f8a11879b7 100644 --- a/internal/gojsonschema/validation.go +++ b/internal/gojsonschema/validation.go @@ -462,7 +462,7 @@ func (v *SubSchema) validateArray(currentSubSchema *SubSchema, value []interface nbValues := len(value) // TODO explain - if currentSubSchema.itemsChildrenIsSingleSchema { + if currentSubSchema.ItemsChildrenIsSingleSchema { for i := range value { subContext := NewJSONContext(strconv.Itoa(i), context) validationResult := currentSubSchema.ItemsChildren[0].subValidateWithContext(value[i], subContext)