Skip to content

Commit

Permalink
[Go] test picoschema with yaml test cases (#528)
Browse files Browse the repository at this point in the history
Use the yaml file of test cases to test the Go picoschema implementation.

This uncovered a few problems. Some new features were implemented in
JS but not in Go; they are implemented here.

More seriously, it was discovered that the JSON schema package we
are using, github.com/invopop/jsonschema, does not support
arrays for the "type" field, and probably never will (see
invopop/jsonschema#134). Tests that
require that are skipped.
  • Loading branch information
jba authored Jul 4, 2024
1 parent b1be343 commit 6276db4
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 195 deletions.
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

0 comments on commit 6276db4

Please sign in to comment.