diff --git a/client/client_test.go b/client/client_test.go index a1d4717..06d93df 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -132,7 +132,7 @@ func TestClientCall(t *testing.T) { Type: protocol.Object, Properties: map[string]*protocol.Property{ "timezone": { - Type: "string", + Type: protocol.PropertyType{protocol.String}, Description: "current time timezone", }, }, diff --git a/examples/middleware_example/main.go b/examples/middleware_example/main.go index fcd9362..106c0c4 100644 --- a/examples/middleware_example/main.go +++ b/examples/middleware_example/main.go @@ -145,14 +145,14 @@ func main() { Name: "hello", Description: "Say hello to someone", InputSchema: protocol.InputSchema{ - Type: "object", + Type: protocol.Object, Properties: map[string]*protocol.Property{ "name": { - Type: protocol.String, + Type: protocol.PropertyType{protocol.String}, Description: "Name to greet", }, "auth_token": { - Type: protocol.String, + Type: protocol.PropertyType{protocol.String}, Description: "Authentication token (required)", }, }, @@ -165,14 +165,14 @@ func main() { Name: "counter", Description: "Count something", InputSchema: protocol.InputSchema{ - Type: "object", + Type: protocol.Object, Properties: map[string]*protocol.Property{ "count": { - Type: protocol.Number, + Type: protocol.PropertyType{protocol.Number}, Description: "Number to count", }, "auth_token": { - Type: protocol.String, + Type: protocol.PropertyType{protocol.String}, Description: "Authentication token (required)", }, }, diff --git a/protocol/schema_generate.go b/protocol/schema_generate.go index 9f2262f..5e16eeb 100644 --- a/protocol/schema_generate.go +++ b/protocol/schema_generate.go @@ -1,6 +1,7 @@ package protocol import ( + "encoding/json" "fmt" "reflect" "strconv" @@ -21,8 +22,54 @@ const ( Boolean DataType = "boolean" ) +// PropertyType is a JSON Schema "type" keyword: either a single type string or a union (array of strings). +type PropertyType []DataType + +func (pt PropertyType) MarshalJSON() ([]byte, error) { + if len(pt) == 0 { + return json.Marshal("") + } + if len(pt) == 1 { + return json.Marshal(string(pt[0])) + } + ss := make([]string, len(pt)) + for i, t := range pt { + ss[i] = string(t) + } + return json.Marshal(ss) +} + +func (pt *PropertyType) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + *pt = nil + return nil + } + if string(data) == "null" { + *pt = nil + return nil + } + var s string + if err := json.Unmarshal(data, &s); err == nil { + if s == "" { + *pt = nil + return nil + } + *pt = PropertyType{DataType(s)} + return nil + } + var arr []string + if err := json.Unmarshal(data, &arr); err != nil { + return err + } + *pt = make(PropertyType, len(arr)) + for i, x := range arr { + (*pt)[i] = DataType(x) + } + return nil +} + type Property struct { - Type DataType `json:"type"` + Type PropertyType `json:"type"` // Description is the description of the schema. Description string `json:"description,omitempty"` // Items specifies which data type an array contains, if the schema type is Array. @@ -220,7 +267,7 @@ func reflectSchemaByObject(t reflect.Type) (*Property, error) { } property := &Property{ - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: properties, Required: requiredFields, } @@ -232,16 +279,16 @@ func reflectSchemaByType(t reflect.Type) (*Property, error) { switch t.Kind() { case reflect.String: - s.Type = String + s.Type = PropertyType{String} case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - s.Type = Integer + s.Type = PropertyType{Integer} case reflect.Float32, reflect.Float64: - s.Type = Number + s.Type = PropertyType{Number} case reflect.Bool: - s.Type = Boolean + s.Type = PropertyType{Boolean} case reflect.Slice, reflect.Array: - s.Type = Array + s.Type = PropertyType{Array} items, err := reflectSchemaByType(t.Elem()) if err != nil { return nil, err @@ -252,14 +299,14 @@ func reflectSchemaByType(t reflect.Type) (*Property, error) { if err != nil { return nil, err } - object.Type = ObjectT + object.Type = PropertyType{ObjectT} s = object case reflect.Map: if t.Key().Kind() != reflect.String { return nil, fmt.Errorf("map key type %s is not supported", t.Key().Kind()) } object := &Property{ - Type: ObjectT, + Type: PropertyType{ObjectT}, } s = object case reflect.Ptr: diff --git a/protocol/schema_generate_test.go b/protocol/schema_generate_test.go index b830994..b2a8fc9 100644 --- a/protocol/schema_generate_test.go +++ b/protocol/schema_generate_test.go @@ -65,26 +65,26 @@ func TestGenerateSchemaFromReqStruct(t *testing.T) { Type: Object, Properties: map[string]*Property{ "string": { - Type: String, + Type: PropertyType{String}, Description: "string", }, "number": { - Type: Number, + Type: PropertyType{Number}, }, "string4enum": { - Type: String, + Type: PropertyType{String}, Enum: []any{"a", "b", "c"}, }, "integer4enum": { - Type: Integer, + Type: PropertyType{Integer}, Enum: []any{1, 2, 3}, }, "number4enum": { - Type: Number, + Type: PropertyType{Number}, Enum: []any{1.1, 2.2, 3.3}, }, "number4enum2": { - Type: Integer, + Type: PropertyType{Integer}, Enum: []any{1, 2, 3}, }, }, @@ -100,26 +100,26 @@ func TestGenerateSchemaFromReqStruct(t *testing.T) { Type: Object, Properties: map[string]*Property{ "string": { - Type: String, + Type: PropertyType{String}, Description: "string", }, "number": { - Type: Number, + Type: PropertyType{Number}, }, "string4enum": { - Type: String, + Type: PropertyType{String}, Enum: []any{"a", "b", "c"}, }, "integer4enum": { - Type: Integer, + Type: PropertyType{Integer}, Enum: []any{1, 2, 3}, }, "number4enum": { - Type: Number, + Type: PropertyType{Number}, Enum: []any{1.1, 2.2, 3.3}, }, "number4enum2": { - Type: Integer, + Type: PropertyType{Integer}, Enum: []any{1, 2, 3}, }, }, @@ -135,31 +135,31 @@ func TestGenerateSchemaFromReqStruct(t *testing.T) { Type: Object, Properties: map[string]*Property{ "string": { - Type: String, + Type: PropertyType{String}, Description: "string", }, "extraField": { - Type: String, + Type: PropertyType{String}, Description: "extra string enum", Enum: []any{"a", "b", "c"}, }, "number": { - Type: Number, + Type: PropertyType{Number}, }, "string4enum": { - Type: String, + Type: PropertyType{String}, Enum: []any{"a", "b", "c"}, }, "integer4enum": { - Type: Integer, + Type: PropertyType{Integer}, Enum: []any{1, 2, 3}, }, "number4enum": { - Type: Number, + Type: PropertyType{Number}, Enum: []any{1.1, 2.2, 3.3}, }, "number4enum2": { - Type: Integer, + Type: PropertyType{Integer}, Enum: []any{1, 2, 3}, }, }, @@ -225,20 +225,20 @@ func TestGenerateSchemaFromReqStruct(t *testing.T) { Type: Object, Properties: map[string]*Property{ "name": { - Type: String, + Type: PropertyType{String}, Description: "user name", }, "age": { - Type: Integer, + Type: PropertyType{Integer}, }, "address": { - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ "city": { - Type: String, + Type: PropertyType{String}, }, "street": { - Type: String, + Type: PropertyType{String}, }, }, Required: []string{"city"}, @@ -260,13 +260,13 @@ func TestGenerateSchemaFromReqStruct(t *testing.T) { Type: Object, Properties: map[string]*Property{ "id": { - Type: Integer, + Type: PropertyType{Integer}, }, "email": { - Type: String, + Type: PropertyType{String}, }, "active": { - Type: Boolean, + Type: PropertyType{Boolean}, }, }, Required: []string{"id", "active"}, @@ -289,19 +289,19 @@ func TestGenerateSchemaFromReqStruct(t *testing.T) { Type: Object, Properties: map[string]*Property{ "user": { - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ "name": { - Type: String, + Type: PropertyType{String}, }, "info": { - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ "age": { - Type: Integer, + Type: PropertyType{Integer}, }, "active": { - Type: Boolean, + Type: PropertyType{Boolean}, }, }, Required: []string{"active"}, @@ -327,13 +327,13 @@ func TestGenerateSchemaFromReqStruct(t *testing.T) { Type: Object, Properties: map[string]*Property{ "user": { - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ "name": { - Type: String, + Type: PropertyType{String}, }, "info": { - Type: ObjectT, + Type: PropertyType{ObjectT}, }, }, Required: []string{"name", "info"}, @@ -402,6 +402,18 @@ func compareInputSchema(a, b *InputSchema) bool { return true } +func equalPropertyType(a, b PropertyType) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + // compareProperty Compare the contents of two Property structures func compareProperty(a, b *Property) bool { if a == nil && b == nil { @@ -410,7 +422,7 @@ func compareProperty(a, b *Property) bool { if a == nil || b == nil { return false } - if a.Type != b.Type { + if !equalPropertyType(a.Type, b.Type) { return false } if a.Description != b.Description { @@ -528,29 +540,29 @@ func TestGenerateSchemaWithDefaultValues(t *testing.T) { Type: Object, Properties: map[string]*Property{ "string_with_default": { - Type: String, + Type: PropertyType{String}, Default: "hello", }, "int_with_default": { - Type: Integer, + Type: PropertyType{Integer}, Default: 42, }, "float_with_default": { - Type: Number, + Type: PropertyType{Number}, Default: 3.14, }, "bool_with_default": { - Type: Boolean, + Type: PropertyType{Boolean}, Default: true, }, "required_string": { - Type: String, + Type: PropertyType{String}, Description: "required field", }, "array_with_default": { - Type: Array, + Type: PropertyType{Array}, Items: &Property{ - Type: String, + Type: PropertyType{String}, }, Default: "[\"item1\",\"item2\"]", }, diff --git a/protocol/schema_validate.go b/protocol/schema_validate.go index 0c5c42e..e4e2985 100644 --- a/protocol/schema_validate.go +++ b/protocol/schema_validate.go @@ -29,7 +29,7 @@ func VerifyAndUnmarshal(content json.RawMessage, v any) error { } return verifySchemaAndUnmarshal(Property{ - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: schema.Properties, Required: schema.Required, }, content, v) @@ -48,7 +48,23 @@ func verifySchemaAndUnmarshal(schema Property, content []byte, v any) error { } func validate(schema Property, data any) bool { - switch schema.Type { + types := schema.Type + if len(types) == 0 { + return false + } + if len(types) == 1 { + return validateWithType(types[0], schema, data) + } + for _, typ := range types { + if validateWithType(typ, schema, data) { + return true + } + } + return false +} + +func validateWithType(typ DataType, schema Property, data any) bool { + switch typ { case ObjectT: return validateObject(schema, data) case Array: diff --git a/protocol/schema_validate_test.go b/protocol/schema_validate_test.go index b219a2a..74dc224 100644 --- a/protocol/schema_validate_test.go +++ b/protocol/schema_validate_test.go @@ -17,83 +17,83 @@ func Test_Validate(t *testing.T) { want bool }{ // string integer number boolean - {"", args{data: "ABC", schema: Property{Type: String}}, true}, - {"", args{data: 123, schema: Property{Type: String}}, false}, - {"", args{data: "a", schema: Property{Type: String, Enum: []any{"a", "b", "c"}}}, true}, - {"", args{data: "d", schema: Property{Type: String, Enum: []any{"a", "b", "c"}}}, false}, - {"", args{data: 123, schema: Property{Type: Integer}}, true}, - {"", args{data: 123.4, schema: Property{Type: Integer}}, false}, - {"", args{data: 1, schema: Property{Type: Integer, Enum: []any{1, 2, 3}}}, true}, - {"", args{data: 4, schema: Property{Type: Integer, Enum: []any{1, 2, 3}}}, false}, - {"", args{data: "ABC", schema: Property{Type: Number}}, false}, - {"", args{data: 123, schema: Property{Type: Number}}, true}, - {"", args{data: 1.1, schema: Property{Type: Number, Enum: []any{1.1, 2.2, 3.3}}}, true}, - {"", args{data: 4.4, schema: Property{Type: Number, Enum: []any{1.1, 2.2, 3.3}}}, false}, - {"", args{data: 1, schema: Property{Type: Number, Enum: []any{1, 2, 3}}}, true}, - {"", args{data: 4, schema: Property{Type: Number, Enum: []any{1, 2, 3}}}, false}, - {"", args{data: false, schema: Property{Type: Boolean}}, true}, - {"", args{data: 123, schema: Property{Type: Boolean}}, false}, - {"", args{data: nil, schema: Property{Type: Null}}, true}, - {"", args{data: 0, schema: Property{Type: Null}}, false}, + {"", args{data: "ABC", schema: Property{Type: PropertyType{String}}}, true}, + {"", args{data: 123, schema: Property{Type: PropertyType{String}}}, false}, + {"", args{data: "a", schema: Property{Type: PropertyType{String}, Enum: []any{"a", "b", "c"}}}, true}, + {"", args{data: "d", schema: Property{Type: PropertyType{String}, Enum: []any{"a", "b", "c"}}}, false}, + {"", args{data: 123, schema: Property{Type: PropertyType{Integer}}}, true}, + {"", args{data: 123.4, schema: Property{Type: PropertyType{Integer}}}, false}, + {"", args{data: 1, schema: Property{Type: PropertyType{Integer}, Enum: []any{1, 2, 3}}}, true}, + {"", args{data: 4, schema: Property{Type: PropertyType{Integer}, Enum: []any{1, 2, 3}}}, false}, + {"", args{data: "ABC", schema: Property{Type: PropertyType{Number}}}, false}, + {"", args{data: 123, schema: Property{Type: PropertyType{Number}}}, true}, + {"", args{data: 1.1, schema: Property{Type: PropertyType{Number}, Enum: []any{1.1, 2.2, 3.3}}}, true}, + {"", args{data: 4.4, schema: Property{Type: PropertyType{Number}, Enum: []any{1.1, 2.2, 3.3}}}, false}, + {"", args{data: 1, schema: Property{Type: PropertyType{Number}, Enum: []any{1, 2, 3}}}, true}, + {"", args{data: 4, schema: Property{Type: PropertyType{Number}, Enum: []any{1, 2, 3}}}, false}, + {"", args{data: false, schema: Property{Type: PropertyType{Boolean}}}, true}, + {"", args{data: 123, schema: Property{Type: PropertyType{Boolean}}}, false}, + {"", args{data: nil, schema: Property{Type: PropertyType{Null}}}, true}, + {"", args{data: 0, schema: Property{Type: PropertyType{Null}}}, false}, // array {"", args{ data: []any{"a", "b", "c"}, schema: Property{ - Type: Array, Items: &Property{Type: String}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{String}}, }, }, true}, {"", args{ data: []any{1, 2, 3}, schema: Property{ - Type: Array, Items: &Property{Type: String}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{String}}, }, }, false}, {"", args{ data: []any{"a"}, schema: Property{ - Type: Array, Items: &Property{Type: String, Enum: []any{"a", "b", "c"}}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{String}, Enum: []any{"a", "b", "c"}}, }, }, true}, {"", args{ data: []any{"a", "b", "c"}, schema: Property{ - Type: Array, Items: &Property{Type: String, Enum: []any{"a", "b", "c"}}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{String}, Enum: []any{"a", "b", "c"}}, }, }, true}, {"", args{ data: []any{"d"}, schema: Property{ - Type: Array, Items: &Property{Type: String, Enum: []any{"a", "b", "c"}}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{String}, Enum: []any{"a", "b", "c"}}, }, }, false}, {"", args{ data: []any{"a", "b", "c", "d"}, schema: Property{ - Type: Array, Items: &Property{Type: String, Enum: []any{"a", "b", "c"}}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{String}, Enum: []any{"a", "b", "c"}}, }, }, false}, {"", args{ data: []any{1, 2, 3}, schema: Property{ - Type: Array, Items: &Property{Type: Integer}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Integer}}, }, }, true}, {"", args{ data: []any{1, 2, 3.4}, schema: Property{ - Type: Array, Items: &Property{Type: Integer}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Integer}}, }, }, false}, {"", args{ data: []any{1}, schema: Property{ - Type: Array, Items: &Property{Type: Integer, Enum: []any{1, 2, 3}}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Integer}, Enum: []any{1, 2, 3}}, }, }, true}, {"", args{ data: []any{1, 2, 3}, schema: Property{ - Type: Array, Items: &Property{Type: Integer, Enum: []any{1, 2, 3}}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Integer}, Enum: []any{1, 2, 3}}, }, }, true}, {"", args{ data: []any{1, 2, 3, 4}, schema: Property{ - Type: Array, Items: &Property{Type: Integer, Enum: []any{1, 2, 3}}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Integer}, Enum: []any{1, 2, 3}}, }, }, false}, {"", args{ data: []any{4}, schema: Property{ - Type: Array, Items: &Property{Type: Integer, Enum: []any{1, 2, 3}}, + Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Integer}, Enum: []any{1, 2, 3}}, }, }, false}, // object @@ -104,12 +104,12 @@ func Test_Validate(t *testing.T) { "boolean": false, "array": []any{1, 2, 3}, }, schema: Property{ - Type: ObjectT, Properties: map[string]*Property{ - "string": {Type: String}, - "integer": {Type: Integer}, - "number": {Type: Number}, - "boolean": {Type: Boolean}, - "array": {Type: Array, Items: &Property{Type: Number}}, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ + "string": {Type: PropertyType{String}}, + "integer": {Type: PropertyType{Integer}}, + "number": {Type: PropertyType{Number}}, + "boolean": {Type: PropertyType{Boolean}}, + "array": {Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Number}}}, }, Required: []string{"string"}, }}, true}, @@ -119,12 +119,12 @@ func Test_Validate(t *testing.T) { "boolean": false, "array": []any{1, 2, 3}, }, schema: Property{ - Type: ObjectT, Properties: map[string]*Property{ - "string": {Type: String}, - "integer": {Type: Integer}, - "number": {Type: Number}, - "boolean": {Type: Boolean}, - "array": {Type: Array, Items: &Property{Type: Number}}, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ + "string": {Type: PropertyType{String}}, + "integer": {Type: PropertyType{Integer}}, + "number": {Type: PropertyType{Number}}, + "boolean": {Type: PropertyType{Boolean}}, + "array": {Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Number}}}, }, Required: []string{"string"}, }}, false}, @@ -135,12 +135,12 @@ func Test_Validate(t *testing.T) { "number4Int": 1, "array": []any{1, 2, 3}, }, schema: Property{ - Type: ObjectT, Properties: map[string]*Property{ - "string": {Type: String, Enum: []any{"a", "b", "c"}}, - "integer": {Type: Integer, Enum: []any{1, 2, 3}}, - "number": {Type: Number, Enum: []any{1.1, 2.2, 3.3}}, - "number4Int": {Type: Number, Enum: []any{1, 2, 3}}, - "array": {Type: Array, Items: &Property{Type: Number}, Enum: []any{1, 2, 3}}, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ + "string": {Type: PropertyType{String}, Enum: []any{"a", "b", "c"}}, + "integer": {Type: PropertyType{Integer}, Enum: []any{1, 2, 3}}, + "number": {Type: PropertyType{Number}, Enum: []any{1.1, 2.2, 3.3}}, + "number4Int": {Type: PropertyType{Number}, Enum: []any{1, 2, 3}}, + "array": {Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Number}}, Enum: []any{1, 2, 3}}, }, Required: []string{"string"}, }}, true}, @@ -151,12 +151,12 @@ func Test_Validate(t *testing.T) { "number4Int": 4, "array": []any{4}, }, schema: Property{ - Type: ObjectT, Properties: map[string]*Property{ - "string": {Type: String, Enum: []any{"a", "b", "c"}}, - "integer": {Type: Integer, Enum: []any{1, 2, 3}}, - "number": {Type: Number, Enum: []any{1.1, 2.2, 3.3}}, - "number4Int": {Type: Number, Enum: []any{1, 2, 3}}, - "array": {Type: Array, Items: &Property{Type: Number}, Enum: []any{1, 2, 3}}, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ + "string": {Type: PropertyType{String}, Enum: []any{"a", "b", "c"}}, + "integer": {Type: PropertyType{Integer}, Enum: []any{1, 2, 3}}, + "number": {Type: PropertyType{Number}, Enum: []any{1.1, 2.2, 3.3}}, + "number4Int": {Type: PropertyType{Number}, Enum: []any{1, 2, 3}}, + "array": {Type: PropertyType{Array}, Items: &Property{Type: PropertyType{Number}}, Enum: []any{1, 2, 3}}, }, Required: []string{"string"}, }}, false}, @@ -169,16 +169,16 @@ func Test_Validate(t *testing.T) { }, }, }, schema: Property{ - Type: ObjectT, Properties: map[string]*Property{ + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ "user": { - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ - "name": {Type: String}, + "name": {Type: PropertyType{String}}, "info": { - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ - "age": {Type: Integer}, - "active": {Type: Boolean}, + "age": {Type: PropertyType{Integer}}, + "active": {Type: PropertyType{Boolean}}, }, Required: []string{"active"}, }, @@ -197,16 +197,16 @@ func Test_Validate(t *testing.T) { }, }, }, schema: Property{ - Type: ObjectT, Properties: map[string]*Property{ + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ "user": { - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ - "name": {Type: String}, + "name": {Type: PropertyType{String}}, "info": { - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ - "age": {Type: Integer}, - "active": {Type: Boolean}, + "age": {Type: PropertyType{Integer}}, + "active": {Type: PropertyType{Boolean}}, }, Required: []string{"active"}, }, @@ -239,10 +239,10 @@ func TestUnmarshal(t *testing.T) { }{ {"", args{ schema: Property{ - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ - "string": {Type: String}, - "number": {Type: Number}, + "string": {Type: PropertyType{String}}, + "number": {Type: PropertyType{Number}}, }, }, content: []byte(`{"string":"abc","number":123.4}`), @@ -253,10 +253,10 @@ func TestUnmarshal(t *testing.T) { }, false}, {"", args{ schema: Property{ - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ - "string": {Type: String}, - "number": {Type: Number}, + "string": {Type: PropertyType{String}}, + "number": {Type: PropertyType{Number}}, }, Required: []string{"string", "number"}, }, @@ -268,10 +268,10 @@ func TestUnmarshal(t *testing.T) { }, true}, {"validate integer", args{ schema: Property{ - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ - "string": {Type: String}, - "integer": {Type: Integer}, + "string": {Type: PropertyType{String}}, + "integer": {Type: PropertyType{Integer}}, }, Required: []string{"string", "integer"}, }, @@ -283,10 +283,10 @@ func TestUnmarshal(t *testing.T) { }, false}, {"validate integer failed", args{ schema: Property{ - Type: ObjectT, + Type: PropertyType{ObjectT}, Properties: map[string]*Property{ - "string": {Type: String}, - "integer": {Type: Integer}, + "string": {Type: PropertyType{String}}, + "integer": {Type: PropertyType{Integer}}, }, Required: []string{"string", "integer"}, }, @@ -462,3 +462,65 @@ func TestVerifyAndUnmarshal(t *testing.T) { }) } } + +func TestInputSchema_JSON_unionType(t *testing.T) { + const payload = `{"type":"object","properties":{"x":{"type":["string","number"]}}}` + var schema InputSchema + if err := json.Unmarshal([]byte(payload), &schema); err != nil { + t.Fatal(err) + } + x, ok := schema.Properties["x"] + if !ok || x == nil { + t.Fatal("missing x") + } + if len(x.Type) != 2 || x.Type[0] != String || x.Type[1] != Number { + t.Fatalf("Type = %#v", x.Type) + } +} + +func Test_validate_unionType(t *testing.T) { + schema := Property{Type: PropertyType{String, Number}} + if !validate(schema, "Mon") { + t.Fatal("string value should match union string|number") + } + if !validate(schema, float64(0)) { + t.Fatal("number value should match union string|number") + } + if validate(schema, false) { + t.Fatal("bool should not match union string|number") + } +} + +// TestIssue198_ListTools_jsonUnmarshal reproduces GitHub issue #198: remote schemas used +// JSON Schema type as an array (union) and sometimes $ref under items; decoding must not error. +func TestIssue198_ListTools_jsonUnmarshal(t *testing.T) { + payload := `{"tools":[` + + `{"name":"generate_heatmap_chart","inputSchema":{` + + `"type":"object","properties":{` + + `"data":{"type":"array","items":{` + + `"type":"object","properties":{` + + `"x":{"type":["string","number"]},` + + `"y":{"type":["string","number"]},` + + `"value":{"type":"number"}},"required":["x","y","value"],"additionalProperties":false` + + `}}},"required":["data"],"additionalProperties":false}},` + + `{"name":"generate_treemap_chart","inputSchema":{` + + `"type":"object","properties":{` + + `"data":{"type":"array","items":{` + + `"type":"object","properties":{` + + `"name":{"type":"string"},"value":{"type":"number"},` + + `"children":{"type":"array","items":{"$ref":"#/properties/data/items"}}},` + + `"required":["name","value"],"additionalProperties":false` + + `}}},"required":["data"],"additionalProperties":false}}` + + `]}` + var got ListToolsResult + if err := json.Unmarshal([]byte(payload), &got); err != nil { + t.Fatalf("unmarshal ListToolsResult: %v", err) + } + if len(got.Tools) != 2 { + t.Fatalf("tools len = %d", len(got.Tools)) + } + heat := got.Tools[0].InputSchema.Properties["data"].Items.Properties["x"] + if len(heat.Type) != 2 || heat.Type[0] != String || heat.Type[1] != Number { + t.Fatalf("heatmap x.Type = %#v", heat.Type) + } +}