diff --git a/bundle/schema/schema_test.go b/bundle/schema/schema_test.go index bf150020..ad9cb530 100644 --- a/bundle/schema/schema_test.go +++ b/bundle/schema/schema_test.go @@ -404,198 +404,6 @@ func TestErrorWithTrace(t *testing.T) { assert.ErrorContains(t, err, "with depth = 4. traversal trace: root -> resources -> pipelines -> datasets") } -func TestNonAnnotatedFieldsAreSkipped(t *testing.T) { - type MyStruct struct { - Foo string - Bar int `json:"bar"` - } - - elem := MyStruct{} - - schema, err := New(reflect.TypeOf(elem), nil) - require.NoError(t, err) - - jsonSchema, err := json.MarshalIndent(schema, " ", " ") - assert.NoError(t, err) - - expectedSchema := - `{ - "type": "object", - "properties": { - "bar": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}" - } - ] - } - }, - "additionalProperties": false, - "required": [ - "bar" - ] - }` - - t.Log("[DEBUG] actual: ", string(jsonSchema)) - t.Log("[DEBUG] expected: ", expectedSchema) - - assert.Equal(t, expectedSchema, string(jsonSchema)) -} - -func TestDashFieldsAreSkipped(t *testing.T) { - type MyStruct struct { - Foo string `json:"-"` - Bar int `json:"bar"` - } - - elem := MyStruct{} - - schema, err := New(reflect.TypeOf(elem), nil) - require.NoError(t, err) - - jsonSchema, err := json.MarshalIndent(schema, " ", " ") - assert.NoError(t, err) - - expectedSchema := - `{ - "type": "object", - "properties": { - "bar": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}" - } - ] - } - }, - "additionalProperties": false, - "required": [ - "bar" - ] - }` - - t.Log("[DEBUG] actual: ", string(jsonSchema)) - t.Log("[DEBUG] expected: ", expectedSchema) - - assert.Equal(t, expectedSchema, string(jsonSchema)) -} - -func TestPointerInStructSchema(t *testing.T) { - - type Bar struct { - PtrVal2 *int `json:"ptr_val2"` - } - - type Foo struct { - PtrInt *int `json:"ptr_int"` - PtrString *string `json:"ptr_string"` - FloatVal float32 `json:"float_val"` - PtrBar *Bar `json:"ptr_bar"` - Bar *Bar `json:"bar"` - } - - elem := Foo{} - - schema, err := New(reflect.TypeOf(elem), nil) - require.NoError(t, err) - - jsonSchema, err := json.MarshalIndent(schema, " ", " ") - assert.NoError(t, err) - - expectedSchema := - `{ - "type": "object", - "properties": { - "bar": { - "type": "object", - "properties": { - "ptr_val2": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}" - } - ] - } - }, - "additionalProperties": false, - "required": [ - "ptr_val2" - ] - }, - "float_val": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}" - } - ] - }, - "ptr_bar": { - "type": "object", - "properties": { - "ptr_val2": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}" - } - ] - } - }, - "additionalProperties": false, - "required": [ - "ptr_val2" - ] - }, - "ptr_int": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}" - } - ] - }, - "ptr_string": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "ptr_int", - "ptr_string", - "float_val", - "ptr_bar", - "bar" - ] - }` - - t.Log("[DEBUG] actual: ", string(jsonSchema)) - t.Log("[DEBUG] expected: ", expectedSchema) - - assert.Equal(t, expectedSchema, string(jsonSchema)) -} - func TestGenericSchema(t *testing.T) { type Person struct { Name string `json:"name"` @@ -824,93 +632,6 @@ func TestGenericSchema(t *testing.T) { assert.Equal(t, expected, string(jsonSchema)) } -func TestFieldsWithoutOmitEmptyAreRequired(t *testing.T) { - - type Papaya struct { - A int `json:"a,string,omitempty"` - B string `json:"b"` - } - - type MyStruct struct { - Foo string `json:"-,omitempty"` - Bar int `json:"bar"` - Apple int `json:"apple,omitempty"` - Mango int `json:",omitempty"` - Guava int `json:","` - Papaya *Papaya `json:"papaya,"` - } - - elem := MyStruct{} - - schema, err := New(reflect.TypeOf(elem), nil) - require.NoError(t, err) - - jsonSchema, err := json.MarshalIndent(schema, " ", " ") - assert.NoError(t, err) - - expectedSchema := - `{ - "type": "object", - "properties": { - "apple": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}" - } - ] - }, - "bar": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}" - } - ] - }, - "papaya": { - "type": "object", - "properties": { - "a": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}" - } - ] - }, - "b": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "b" - ] - } - }, - "additionalProperties": false, - "required": [ - "bar", - "papaya" - ] - }` - - t.Log("[DEBUG] actual: ", string(jsonSchema)) - t.Log("[DEBUG] expected: ", expectedSchema) - - assert.Equal(t, expectedSchema, string(jsonSchema)) -} - func TestDocIngestionForObject(t *testing.T) { docs := &Docs{ Description: "docs for root", @@ -1272,166 +993,3 @@ func TestErrorIfStructHasLoop(t *testing.T) { _, err := New(reflect.TypeOf(elem), nil) assert.ErrorContains(t, err, "cycle detected. traversal trace: root -> my_mango -> my_guava -> my_papaya -> my_apple") } - -func TestInterfaceGeneratesEmptySchema(t *testing.T) { - type Foo struct { - Apple int `json:"apple"` - Mango interface{} `json:"mango"` - } - - elem := Foo{} - - schema, err := New(reflect.TypeOf(elem), nil) - assert.NoError(t, err) - - jsonSchema, err := json.MarshalIndent(schema, " ", " ") - assert.NoError(t, err) - - expected := - `{ - "type": "object", - "properties": { - "apple": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}" - } - ] - }, - "mango": {} - }, - "additionalProperties": false, - "required": [ - "apple", - "mango" - ] - }` - - t.Log("[DEBUG] actual: ", string(jsonSchema)) - t.Log("[DEBUG] expected: ", expected) - assert.Equal(t, expected, string(jsonSchema)) -} - -func TestBundleReadOnlytag(t *testing.T) { - type Pokemon struct { - Pikachu string `json:"pikachu" bundle:"readonly"` - Raichu string `json:"raichu"` - } - - type Foo struct { - Pokemon *Pokemon `json:"pokemon"` - Apple int `json:"apple"` - Mango string `json:"mango" bundle:"readonly"` - } - - elem := Foo{} - - schema, err := New(reflect.TypeOf(elem), nil) - assert.NoError(t, err) - - jsonSchema, err := json.MarshalIndent(schema, " ", " ") - assert.NoError(t, err) - - expected := - `{ - "type": "object", - "properties": { - "apple": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}" - } - ] - }, - "pokemon": { - "type": "object", - "properties": { - "raichu": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "raichu" - ] - } - }, - "additionalProperties": false, - "required": [ - "pokemon", - "apple" - ] - }` - - t.Log("[DEBUG] actual: ", string(jsonSchema)) - t.Log("[DEBUG] expected: ", expected) - assert.Equal(t, expected, string(jsonSchema)) -} - -func TestBundleInternalTag(t *testing.T) { - type Pokemon struct { - Pikachu string `json:"pikachu" bundle:"internal"` - Raichu string `json:"raichu"` - } - - type Foo struct { - Pokemon *Pokemon `json:"pokemon"` - Apple int `json:"apple"` - Mango string `json:"mango" bundle:"internal"` - } - - elem := Foo{} - - schema, err := New(reflect.TypeOf(elem), nil) - assert.NoError(t, err) - - jsonSchema, err := json.MarshalIndent(schema, " ", " ") - assert.NoError(t, err) - - expected := - `{ - "type": "object", - "properties": { - "apple": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}" - } - ] - }, - "pokemon": { - "type": "object", - "properties": { - "raichu": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "raichu" - ] - } - }, - "additionalProperties": false, - "required": [ - "pokemon", - "apple" - ] - }` - - t.Log("[DEBUG] actual: ", string(jsonSchema)) - t.Log("[DEBUG] expected: ", expected) - assert.Equal(t, expected, string(jsonSchema)) -} diff --git a/libs/jsonschema/from_type.go b/libs/jsonschema/from_type.go index c5f6250a..d0e4e2ac 100644 --- a/libs/jsonschema/from_type.go +++ b/libs/jsonschema/from_type.go @@ -77,7 +77,7 @@ func FromType(typ reflect.Type, fn func(s Schema) Schema) (Schema, error) { fn: fn, } - err := c.walk(typ) + _, err := c.walk(typ) if err != nil { return InvalidSchema, err } @@ -90,6 +90,11 @@ func FromType(typ reflect.Type, fn func(s Schema) Schema) (Schema, error) { } func typePath(typ reflect.Type) string { + // typ.Name() resolves to "" for any type. + if typ.Kind() == reflect.Interface { + return "interface" + } + // For built-in types, return the type name directly. if typ.PkgPath() == "" { return typ.Name() @@ -100,7 +105,7 @@ func typePath(typ reflect.Type) string { // TODO: would a worked based model fit better here? Is this internal API not // the right fit? -func (c *constructor) walk(typ reflect.Type) error { +func (c *constructor) walk(typ reflect.Type) (string, error) { // Dereference pointers if necessary. for typ.Kind() == reflect.Ptr { typ = typ.Elem() @@ -110,7 +115,7 @@ func (c *constructor) walk(typ reflect.Type) error { // Return value directly if it's already been processed. if _, ok := c.definitions[typPath]; ok { - return nil + return "", nil } var s Schema @@ -139,10 +144,10 @@ func (c *constructor) walk(typ reflect.Type) error { // set to null and disallowed in the schema. s = Schema{Type: NullType} default: - return fmt.Errorf("unsupported type: %s", typ.Kind()) + return "", fmt.Errorf("unsupported type: %s", typ.Kind()) } if err != nil { - return err + return "", err } if c.fn != nil { @@ -153,7 +158,7 @@ func (c *constructor) walk(typ reflect.Type) error { // TODO: Apply transformation at the end, to all definitions instead of // during recursive traversal? c.definitions[typPath] = s - return nil + return typPath, nil } // This function returns all member fields of the provided type. @@ -230,12 +235,11 @@ func (c *constructor) fromTypeStruct(typ reflect.Type) (Schema, error) { // Trigger call to fromType, to recursively generate definitions for // the struct field. - err := c.walk(structField.Type) + typPath, err := c.walk(structField.Type) if err != nil { return InvalidSchema, err } - typPath := typePath(structField.Type) refPath := path.Join("#/$defs", typPath) // For non-built-in types, refer to the definition. res.Properties[jsonTags[0]] = &Schema{ @@ -259,12 +263,11 @@ func (c *constructor) fromTypeSlice(typ reflect.Type) (Schema, error) { // Trigger call to fromType, to recursively generate definitions for // the slice element. - err := c.walk(typ.Elem()) + typPath, err := c.walk(typ.Elem()) if err != nil { return InvalidSchema, err } - typPath := typePath(typ.Elem()) refPath := path.Join("#/$defs", typPath) // For non-built-in types, refer to the definition @@ -289,12 +292,11 @@ func (c *constructor) fromTypeMap(typ reflect.Type) (Schema, error) { // Trigger call to fromType, to recursively generate definitions for // the map value. - err := c.walk(typ.Elem()) + typPath, err := c.walk(typ.Elem()) if err != nil { return InvalidSchema, err } - typPath := typePath(typ.Elem()) refPath := path.Join("#/$defs", typPath) // For non-built-in types, refer to the definition diff --git a/libs/jsonschema/from_type_test.go b/libs/jsonschema/from_type_test.go index fd60bf44..9ba06ba9 100644 --- a/libs/jsonschema/from_type_test.go +++ b/libs/jsonschema/from_type_test.go @@ -1,6 +1,7 @@ package jsonschema import ( + "encoding/json" "reflect" "testing" @@ -9,13 +10,23 @@ import ( func TestFromTypeBasic(t *testing.T) { type myStruct struct { - S string `json:"s"` - I int `json:"i"` + S string `json:"s"` + I *int `json:"i,omitempty"` + V interface{} `json:"v,omitempty"` + + // These fields should be ignored in the resulting schema. + NotAnnotated string + DashedTag string `json:"-"` + notExported string `json:"not_exported"` + InternalTagged string `json:"internal_tagged" bundle:"internal"` + DeprecatedTagged string `json:"deprecated_tagged" bundle:"deprecated"` + ReadOnlyTagged string `json:"readonly_tagged" bundle:"readonly"` } strRef := "#/$defs/string" boolRef := "#/$defs/bool" intRef := "#/$defs/int" + interfaceRef := "#/$defs/interface" tcases := []struct { name string @@ -56,6 +67,9 @@ func TestFromTypeBasic(t *testing.T) { expected: Schema{ Type: "object", Definitions: map[string]any{ + "interface": Schema{ + Type: "null", + }, "string": Schema{ Type: "string", }, @@ -70,9 +84,12 @@ func TestFromTypeBasic(t *testing.T) { "i": { Reference: &intRef, }, + "v": { + Reference: &interfaceRef, + }, }, AdditionalProperties: false, - Required: []string{"s", "i"}, + Required: []string{"s"}, }, }, { @@ -113,14 +130,14 @@ func TestFromTypeBasic(t *testing.T) { assert.NoError(t, err) assert.Equal(t, tc.expected, s) - // jsonSchema, err := json.MarshalIndent(s, " ", " ") - // assert.NoError(t, err) + jsonSchema, err := json.MarshalIndent(s, " ", " ") + assert.NoError(t, err) - // expectedJson, err := json.MarshalIndent(tc.expected, " ", " ") - // assert.NoError(t, err) + expectedJson, err := json.MarshalIndent(tc.expected, " ", " ") + assert.NoError(t, err) - // t.Log("[DEBUG] actual: ", string(jsonSchema)) - // t.Log("[DEBUG] expected: ", string(expectedJson)) + t.Log("[DEBUG] actual: ", string(jsonSchema)) + t.Log("[DEBUG] expected: ", string(expectedJson)) }) } }