diff --git a/go/genkit/servers_test.go b/go/genkit/servers_test.go index f590e29861..accbcf6f9e 100644 --- a/go/genkit/servers_test.go +++ b/go/genkit/servers_test.go @@ -96,14 +96,14 @@ func TestDevServer(t *testing.T) { "/custom/devServer/inc": { Key: "/custom/devServer/inc", Name: "devServer/inc", - InputSchema: &jsonschema.Schema{Type: "integer"}, - OutputSchema: &jsonschema.Schema{Type: "integer"}, + InputSchema: &jsonschema.Schema{Type: []string{"integer"}}, + OutputSchema: &jsonschema.Schema{Type: []string{"integer"}}, Metadata: map[string]any{"foo": "bar"}, }, "/custom/devServer/dec": { Key: "/custom/devServer/dec", - InputSchema: &jsonschema.Schema{Type: "integer"}, - OutputSchema: &jsonschema.Schema{Type: "integer"}, + InputSchema: &jsonschema.Schema{Type: []string{"integer"}}, + OutputSchema: &jsonschema.Schema{Type: []string{"integer"}}, Name: "devServer/dec", Metadata: map[string]any{"bar": "baz"}, }, diff --git a/go/go.mod b/go/go.mod index c6133f9d11..f67a6def82 100644 --- a/go/go.mod +++ b/go/go.mod @@ -50,6 +50,7 @@ require ( github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/alonsopf/jsonschema v0.0.0-20241112032241-b0be673d3d4c // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect @@ -95,3 +96,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect google.golang.org/grpc v1.65.0 // indirect ) + +replace github.com/invopop/jsonschema => github.com/alonsopf/jsonschema v0.0.0-20241112034953-8a224be9076f diff --git a/go/go.sum b/go/go.sum index fbd0eb794b..65c49c0e04 100644 --- a/go/go.sum +++ b/go/go.sum @@ -46,6 +46,10 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/alonsopf/jsonschema v0.0.0-20241112032241-b0be673d3d4c h1:eharL8A3PtLM6cFVEJmWUBjpXyAhb/Dfj+5RzrnRcOk= +github.com/alonsopf/jsonschema v0.0.0-20241112032241-b0be673d3d4c/go.mod h1:xAW0fRKWYyjgf/vtEjMlCWc1qprM9DnMtlM8kaZO08E= +github.com/alonsopf/jsonschema v0.0.0-20241112034953-8a224be9076f h1:koSFHSM3XlrN8TqKUxzNz0epkpTaLHEzKWo2QBZyY/Q= +github.com/alonsopf/jsonschema v0.0.0-20241112034953-8a224be9076f/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/ankane/disco-go v0.1.0 h1:nkz+y4O+UFKnEGH8FkJ8wcVwX5boZvaRzJN6EMK7NVw= github.com/ankane/disco-go v0.1.0/go.mod h1:nkR7DLW+KkXeRRAsWk6poMTpTOWp9/4iKYGDwg8dSS0= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= @@ -175,8 +179,6 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksP github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= -github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= diff --git a/go/internal/base/json_test.go b/go/internal/base/json_test.go index 6387acb505..655773bdfe 100644 --- a/go/internal/base/json_test.go +++ b/go/internal/base/json_test.go @@ -72,7 +72,7 @@ func TestSchemaAsMap(t *testing.T) { } type Foo struct { BarField Bar - Str string + Str interface{} `json:"str" jsonschema:"type=number;string"` } want := map[string]any{ @@ -84,12 +84,11 @@ func TestSchemaAsMap(t *testing.T) { "properties": map[string]any{ "Bar": map[string]any{"type": string("string")}, }, - "required": []any{string("Bar")}, - "type": string("object"), + "type": string("object"), }, - "Str": map[string]any{"type": string("string")}, + "str": map[string]any{"type": []any{string("number"), string("string")}}, }, - "required": []any{string("BarField"), string("Str")}, + "required": []any{string("BarField"), string("str")}, "type": string("object"), } diff --git a/go/plugins/dotprompt/picoschema.go b/go/plugins/dotprompt/picoschema.go index ca5b7c00c3..5e56861c41 100644 --- a/go/plugins/dotprompt/picoschema.go +++ b/go/plugins/dotprompt/picoschema.go @@ -46,7 +46,8 @@ func picoschemaToJSONSchema(val any) (*jsonschema.Schema, error) { if err != nil { return nil, err } - s.Type = "object" + + s.Type = []string{"object"} return s, nil } } @@ -64,15 +65,15 @@ func parsePico(val any) (*jsonschema.Schema, error) { case string: typ, desc, found := strings.Cut(val, ",") switch typ { - case "string", "boolean", "null", "number", "integer", "any": + case "string", "boolean", "null", "number", "integer": // Valid types + case "any": + typ = "" default: return nil, fmt.Errorf("picoschema: unsupported scalar type %q", typ) } - if typ == "any" { - typ = "" - } - ret := &jsonschema.Schema{ - Type: typ, + ret := &jsonschema.Schema{} + if typ != "" { + ret.Type = []string{typ} } if found { ret.Description = strings.TrimSpace(desc) @@ -84,7 +85,7 @@ func parsePico(val any) (*jsonschema.Schema, error) { case map[string]any: ret := &jsonschema.Schema{ - Type: "object", + Type: []string{"object"}, Properties: orderedmap.New[string, *jsonschema.Schema](), AdditionalProperties: jsonschema.FalseSchema, } @@ -99,7 +100,15 @@ func parsePico(val any) (*jsonschema.Schema, error) { if err != nil { return nil, err } - + // Only add "null" to the "type" array if Type is already set and not empty + if isOptional { + if len(property.Type) > 0 { + if !contains(property.Type, "null") { + property.Type = append(property.Type, "null") + } + } + // Do not set property.Type to ["null"] if Type is empty or nil + } if !found { ret.Properties.Set(propertyName, property) continue @@ -110,7 +119,7 @@ func parsePico(val any) (*jsonschema.Schema, error) { switch typ { case "array": property = &jsonschema.Schema{ - Type: "array", + Type: []string{"array"}, Items: property, } case "object": @@ -142,6 +151,15 @@ func parsePico(val any) (*jsonschema.Schema, error) { } } +func contains(slice []string, item string) bool { + for _, v := range slice { + if v == item { + return true + } + } + return false +} + // mapToJSONSchema converts a YAML value to a JSONSchema. func mapToJSONSchema(m map[string]any) (*jsonschema.Schema, error) { var ret jsonschema.Schema @@ -166,6 +184,52 @@ func mapToJSONSchema(m map[string]any) (*jsonschema.Schema, error) { } switch rf.Type() { + case reflect.TypeOf([]string(nil)): + switch v := v.(type) { + case string: + if v != "" { + rf.Set(reflect.ValueOf([]string{v})) + } + case []any: + sstrs := make([]string, 0, len(v)) + for i, astr := range v { + s, ok := astr.(string) + if !ok { + return nil, fmt.Errorf("picoschema: found type %T for field element %d of %q, want string", astr, i, k) + } + if s != "" { + sstrs = append(sstrs, s) + } + } + if len(sstrs) > 0 { + rf.Set(reflect.ValueOf(sstrs)) + } + default: + return nil, fmt.Errorf("picoschema: found type %T for field %q, want string or array of strings", v, k) + } + + case reflect.TypeOf(""): + str, ok := v.(string) + if !ok { + return nil, fmt.Errorf("picoschema: found type %T for field %q, want string", v, k) + } + rf.SetString(str) + + case reflect.TypeOf((*uint64)(nil)): + rf.Set(reflect.New(reflect.TypeOf(uint64(0)))) + switch v := v.(type) { + case uint64, uint32, uint16, uint8, int, int8, int16, int32, int64: + rf.Elem().SetUint(reflect.ValueOf(v).Uint()) + default: + return nil, fmt.Errorf("picoschema: found type %T for field %q, want an integer type", v, k) + } + + case reflect.TypeOf(true): + b, ok := v.(bool) + if !ok { + return nil, fmt.Errorf("picoschema: found type %T for field %q, want bool", v, k) + } + rf.SetBool(b) case reflect.TypeFor[any](): rf.Set(reflect.ValueOf(v)) diff --git a/go/plugins/dotprompt/picoschema_test.go b/go/plugins/dotprompt/picoschema_test.go index c4cf669372..693168f67f 100644 --- a/go/plugins/dotprompt/picoschema_test.go +++ b/go/plugins/dotprompt/picoschema_test.go @@ -41,16 +41,8 @@ func TestPicoschema(t *testing.T) { t.Fatal(err) } - skip := map[string]bool{ - "required field": true, - "nested object in array and out": true, - } - for _, test := range tests { t.Run(test.Description, func(t *testing.T) { - if skip[test.Description] { - t.Skip("no support for type as an array") - } var val any if err := yaml.Unmarshal([]byte(test.YAML), &val); err != nil { t.Fatalf("YAML unmarshal failure: %v", err)