Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 23 additions & 11 deletions go/plugins/dotprompt/dotprompt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ func TestPrompts(t *testing.T) {
"type": "object",
"required": [
"food"
]
],
"additionalProperties": false
}`,
output: `{
"properties": {
Expand Down Expand Up @@ -72,11 +73,13 @@ func TestPrompts(t *testing.T) {
"required": [
"name",
"quantity"
]
],
"additionalProperties": false
},
"type": "array"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"ingredients",
Expand Down Expand Up @@ -150,20 +153,29 @@ func cmpSchema(t *testing.T, got *jsonschema.Schema, want string) string {
return ""
}

// JSON sorts maps but not slices.
// jsonschema slices are not sorted consistently.
sortSchemaSlices(got)

data, err := json.Marshal(got)
jsonGot, err := convertSchema(got)
if err != nil {
t.Fatal(err)
}
var jsonGot, jsonWant any
if err := json.Unmarshal(data, &jsonGot); err != nil {
t.Fatal(err)
}
var jsonWant any
if err := json.Unmarshal([]byte(want), &jsonWant); err != nil {
t.Fatalf("unmarshaling %q failed: %v", want, err)
}
return cmp.Diff(jsonWant, jsonGot)
}

// convertSchema marshals s to JSON, then unmarshals the result.
func convertSchema(s *jsonschema.Schema) (any, error) {
// JSON sorts maps but not slices.
// jsonschema slices are not sorted consistently.
sortSchemaSlices(s)
data, err := json.Marshal(s)
if err != nil {
return nil, err
}
var a any
if err := json.Unmarshal(data, &a); err != nil {
return nil, err
}
return a, nil
}
106 changes: 62 additions & 44 deletions go/plugins/dotprompt/picoschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"strings"

"github.com/invopop/jsonschema"
"github.com/wk8/go-ordered-map/v2"
orderedmap "github.com/wk8/go-ordered-map/v2"
)

// picoschemaToJSONSchema turns picoschema input into a JSONSchema.
Expand Down Expand Up @@ -57,71 +57,89 @@ func picoschemaToJSONSchema(val any) (*jsonschema.Schema, error) {

// parsePico parses picoschema from the result of the YAML parser.
func parsePico(val any) (*jsonschema.Schema, error) {
if str, ok := val.(string); ok {
typ, desc, found := strings.Cut(str, ",")
switch val := val.(type) {
default:
return nil, fmt.Errorf("picoschema: value %v of type %[1]T is not an object, slice or string", val)

case string:
typ, desc, found := strings.Cut(val, ",")
switch typ {
case "string", "boolean", "null", "number", "integer":
case "string", "boolean", "null", "number", "integer", "any":
default:
return nil, fmt.Errorf("picoschema: unsupported scalar type %q", typ)
}
if typ == "any" {
typ = ""
}
ret := &jsonschema.Schema{
Type: typ,
}
if found {
ret.Description = strings.TrimSpace(desc)
}
return ret, nil
}

m, ok := val.(map[string]any)
if !ok {
return nil, fmt.Errorf("picoschema: value %v of type %T is not an object or a string", val, val)
}
case []any: // assume enum
return &jsonschema.Schema{Enum: val}, nil

ret := &jsonschema.Schema{
Type: "object",
Properties: orderedmap.New[string, *jsonschema.Schema](),
}
for k, v := range m {
name, typ, found := strings.Cut(k, "(")
propertyName, isOptional := strings.CutSuffix(name, "?")
if !isOptional {
ret.Required = append(ret.Required, propertyName)
case map[string]any:
ret := &jsonschema.Schema{
Type: "object",
Properties: orderedmap.New[string, *jsonschema.Schema](),
AdditionalProperties: jsonschema.FalseSchema,
}
for k, v := range val {
name, typ, found := strings.Cut(k, "(")
propertyName, isOptional := strings.CutSuffix(name, "?")
if name != "" && !isOptional {
ret.Required = append(ret.Required, propertyName)
}

property, err := parsePico(v)
if err != nil {
return nil, err
}
property, err := parsePico(v)
if err != nil {
return nil, err
}

if !found {
ret.Properties.Set(propertyName, property)
continue
}
if !found {
ret.Properties.Set(propertyName, property)
continue
}

typ = strings.TrimSuffix(typ, ")")
typ, desc, found := strings.Cut(strings.TrimSuffix(typ, ")"), ",")
switch typ {
case "array":
property = &jsonschema.Schema{
Type: "array",
Items: property,
}
case "object":
// Use property unchanged.
case "enum":
if property.Enum == nil {
return nil, fmt.Errorf("picoschema: enum value %v is not an array", property)
}
if isOptional {
property.Enum = append(property.Enum, nil)
}

case "*":
ret.AdditionalProperties = property
continue
default:
return nil, fmt.Errorf("picoschema: parenthetical type %q is none of %q", typ,
[]string{"object", "array", "enum", "*"})

typ = strings.TrimSuffix(typ, ")")
typ, desc, found := strings.Cut(strings.TrimSuffix(typ, ")"), ",")
switch typ {
case "array":
property = &jsonschema.Schema{
Type: "array",
Items: property,
}
case "object":
// Use property unchanged.
default:
return nil, fmt.Errorf("picoschema: parenthetical type %q is neither %q nor %q", typ, "object", "array")

}
if found {
property.Description = strings.TrimSpace(desc)
}

if found {
property.Description = strings.TrimSpace(desc)
ret.Properties.Set(propertyName, property)
}

ret.Properties.Set(propertyName, property)
return ret, nil
}

return ret, nil
}

// mapToJSONSchema converts a YAML value to a JSONSchema.
Expand Down
Loading