mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
schemaOf fell through to {"type":"object"} for an interface, and the
projector did the same for the `any` prim, so a field holding a token
string or an array was described as an object. The published examples that
carried one failed against their own schema — Hanzo's auto.runIn.auth and
tool.toolCall.arguments — and every generated client typed the field wrong.
An interface is now the unconstrained schema a silent marshaler already gets.
Co-authored-by: Hanzo Dev <[email protected]>
111 lines
3.9 KiB
Go
111 lines
3.9 KiB
Go
package zip
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// quoted is the shape that made this necessary: a number carried as a quoted
|
|
// decimal, because JSON numbers are float64 and a uint64 above 2^53 does not
|
|
// survive one. It writes its own JSON, so its fields are not the wire form.
|
|
type quoted uint64
|
|
|
|
func (q quoted) MarshalJSON() ([]byte, error) { return []byte(`"0"`), nil }
|
|
|
|
func (quoted) JSONSchema() map[string]any {
|
|
return map[string]any{"type": "string", "pattern": "^[0-9]+$"}
|
|
}
|
|
|
|
// silent marshals itself and says nothing about its shape — the case that must
|
|
// still fall back rather than guess.
|
|
type silent uint64
|
|
|
|
func (silent) MarshalJSON() ([]byte, error) { return []byte(`"0"`), nil }
|
|
|
|
// onPointer declares the shape on the pointer, which is where MarshalJSON is
|
|
// usually written.
|
|
type onPointer struct{ n uint64 }
|
|
|
|
func (o *onPointer) MarshalJSON() ([]byte, error) { return []byte(`"0"`), nil }
|
|
func (o *onPointer) JSONSchema() map[string]any { return map[string]any{"type": "string"} }
|
|
|
|
type nilSchema uint64
|
|
|
|
func (nilSchema) MarshalJSON() ([]byte, error) { return []byte(`0`), nil }
|
|
func (nilSchema) JSONSchema() map[string]any { return nil }
|
|
|
|
func TestDeclaredSchemaBeatsTheEmptyObject(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
in reflect.Type
|
|
want map[string]any
|
|
}{
|
|
{"a marshaler that states its shape", reflect.TypeOf(quoted(0)),
|
|
map[string]any{"type": "string", "pattern": "^[0-9]+$"}},
|
|
{"declared on the pointer", reflect.TypeOf(onPointer{}),
|
|
map[string]any{"type": "string"}},
|
|
{"a marshaler that states nothing stays unconstrained", reflect.TypeOf(silent(0)),
|
|
map[string]any{}},
|
|
{"a nil answer is not a shape", reflect.TypeOf(nilSchema(0)),
|
|
map[string]any{}},
|
|
{"time.Time keeps the shape it already had", reflect.TypeOf(time.Time{}),
|
|
map[string]any{"type": "string", "format": "date-time"}},
|
|
} {
|
|
got := schemaOf(tc.in, nil, nil)
|
|
if !reflect.DeepEqual(got, tc.want) {
|
|
t.Errorf("%s: got %v want %v", tc.name, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The document is assembled by mutation, so a type's answer must not be the
|
|
// map the document then edits — one field's constraints would leak into every
|
|
// other field of the same type.
|
|
func TestTheTypesAnswerIsNotShared(t *testing.T) {
|
|
a := schemaOf(reflect.TypeOf(quoted(0)), nil, nil)
|
|
a["description"] = "the first field"
|
|
b := schemaOf(reflect.TypeOf(quoted(0)), nil, nil)
|
|
if _, leaked := b["description"]; leaked {
|
|
t.Fatal("editing one field's schema changed the next one")
|
|
}
|
|
}
|
|
|
|
// The point of the whole exercise: a reply carrying such a field describes it,
|
|
// instead of publishing an untyped value a generated client cannot use.
|
|
func TestAReplyFieldIsDescribed(t *testing.T) {
|
|
type reply struct {
|
|
Height quoted `json:"height"`
|
|
}
|
|
got := rootSchemaOf(reflect.TypeOf(reply{}), nil)
|
|
props, _ := got["properties"].(map[string]any)
|
|
h, _ := props["height"].(map[string]any)
|
|
if h["type"] != "string" {
|
|
b, _ := json.Marshal(got)
|
|
t.Fatalf("height was not described: %s", b)
|
|
}
|
|
}
|
|
|
|
// An interface holds any JSON value, so its schema constrains nothing — the
|
|
// same answer a silent marshaler gets. It was "object", and a string in one
|
|
// failed validation against the document that described it.
|
|
func TestAnInterfaceIsAnyJSON(t *testing.T) {
|
|
type in struct {
|
|
Auth any `json:"auth"`
|
|
Args map[string]any `json:"args"`
|
|
}
|
|
f, _ := reflect.TypeOf(in{}).FieldByName("Auth")
|
|
if got := schemaOf(f.Type, nil, nil); !reflect.DeepEqual(got, map[string]any{}) {
|
|
t.Errorf("any: got %v want {}", got)
|
|
}
|
|
f, _ = reflect.TypeOf(in{}).FieldByName("Args")
|
|
want := map[string]any{"type": "object", "additionalProperties": map[string]any{}}
|
|
if got := schemaOf(f.Type, nil, nil); !reflect.DeepEqual(got, want) {
|
|
t.Errorf("map[string]any: got %v want %v", got, want)
|
|
}
|
|
if got := primSchema("any"); !reflect.DeepEqual(got, map[string]any{}) {
|
|
t.Errorf(`primSchema("any"): got %v want {}`, got)
|
|
}
|
|
}
|