Skip to content
1 change: 1 addition & 0 deletions pkgs/zss/core/zss_structures.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type ZSSSchema struct {
Element *ZSSSchema `json:"element,omitempty"` // ptr, slice, preprocess, boxed
Key *ZSSSchema `json:"key,omitempty"` // map only
Value *ZSSSchema `json:"value,omitempty"` // map only
Children []*ZSSSchema `json:"children,omitempty"` // union only
Required *ZSSTest `json:"required,omitempty"`
DefaultValue any `json:"defaultValue,omitempty"`
CatchValue any `json:"catchValue,omitempty"`
Expand Down
14 changes: 14 additions & 0 deletions pkgs/zss/jsonschema/draft2020_12/jsonschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ func (c converter) convertSchema(schema *zsscore.ZSSSchema) (Schema, error) {
out, err = c.convertStruct(schema)
case zconst.TypePtr:
out, err = c.convertPtr(schema)
case zconst.TypeUnion:
out, err = c.convertUnion(schema)
case zconst.TypePreprocess, zconst.TypeBoxed:
out, err = c.convertSchema(schema.Element)
case zconst.TypeAny, zconst.TypeCustom:
Expand Down Expand Up @@ -187,6 +189,18 @@ func (c converter) convertPtr(schema *zsscore.ZSSSchema) (Schema, error) {
return nullable(inner), nil
}

func (c converter) convertUnion(schema *zsscore.ZSSSchema) (Schema, error) {
children := make([]any, 0, len(schema.Children))
for i, child := range schema.Children {
converted, err := c.convertSchema(child)
if err != nil {
return nil, fmt.Errorf("convert union child %d: %w", i, err)
}
children = append(children, converted)
}
return Schema{"anyOf": children}, nil
}

func nullable(schema Schema) Schema {
typeValue, ok := schema["type"]
if !ok {
Expand Down
19 changes: 19 additions & 0 deletions pkgs/zss/jsonschema/jsonschema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,25 @@ func TestFromZSSConvertsContainers(t *testing.T) {
}, schema)
}

func TestFromZSSConvertsUnion(t *testing.T) {
doc := zsscore.ZSSDocument{Root: &zsscore.ZSSSchema{Kind: zconst.TypeUnion, Children: []*zsscore.ZSSSchema{
{Kind: zconst.TypeString},
{Kind: zconst.TypeNumber},
}}}

schema, err := zjsonschema.FromZSS(doc, zjsonschema.Options{})
require.NoError(t, err)
requireValidJSONSchema(t, schema)

assert.Equal(t, zjsonschema.Schema{
"$schema": string(zjsonschema.Draft2020_12),
"anyOf": []any{
zjsonschema.Schema{"type": "string"},
zjsonschema.Schema{"type": "number"},
},
}, schema)
}

func TestFromZSSConvertsPointerNullability(t *testing.T) {
optional, err := zjsonschema.FromZSS(zsscore.ZSSDocument{Root: &zsscore.ZSSSchema{Kind: zconst.TypePtr, Element: &zsscore.ZSSSchema{Kind: zconst.TypeString}}}, zjsonschema.Options{})
require.NoError(t, err)
Expand Down
1 change: 1 addition & 0 deletions pkgs/zss/schema/zss_document_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ var ZSSSchemaSchema = z.EXPERIMENTAL_RECURSIVE(func(self z.RecursiveSchema[*z.St
"element": z.Ptr(self()),
"key": z.Ptr(self()),
"value": z.Ptr(self()),
"children": z.Slice(z.Ptr(self())),
"required": z.Ptr(ZSSTestSchema),
"defaultValue": z.EXPERIMENTAL_ANY(),
"catchValue": z.EXPERIMENTAL_ANY(),
Expand Down
29 changes: 29 additions & 0 deletions pkgs/zss/toZSS_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ func stripGoTypes(schema *zss.ZSSSchema) {
stripGoTypes(schema.Element)
stripGoTypes(schema.Key)
stripGoTypes(schema.Value)
for _, child := range schema.Children {
stripGoTypes(child)
}
}

func TestToJsonString(t *testing.T) {
Expand Down Expand Up @@ -212,6 +215,32 @@ func TestToJsonBool(t *testing.T) {
assert.Equal(t, normalize(expected), normalize(string(serialized)))
}

func TestToJsonUnion(t *testing.T) {
s := zog.Union([]zog.ZogSchema{
zog.String(),
zog.Int(),
})
d := zog.EXPERIMENTAL_TO_ZSS[any](s)
serialized, err := json.Marshal(withoutGoTypes(d))
assert.Nil(t, err)
assert.NotNil(t, serialized)

expected := baseZSSJson(`{
"kind": "union",
"children": [
{
"kind": "string"
},
{
"kind": "number"
}
]
}`)

assert.Equal(t, normalize(expected), normalize(string(serialized)))
assert.Nil(t, zssschema.ZSSDocumentSchema.Validate(&d))
}

func TestToJsonTime(t *testing.T) {
s := zog.Time().Required()
d := zog.EXPERIMENTAL_TO_ZSS[time.Time](s)
Expand Down
99 changes: 99 additions & 0 deletions union.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package zog

import (
"github.com/Oudwins/zog/conf"
p "github.com/Oudwins/zog/pkgs/internals"
zss "github.com/Oudwins/zog/pkgs/zss/core"
"github.com/Oudwins/zog/zconst"
)

var _ ZogSchema = &UnionSchema{}

type UnionSchema struct {
schemas []ZogSchema
}

func Union(schemas []ZogSchema, options ...SchemaOption) *UnionSchema {
s := &UnionSchema{
schemas: schemas,
Comment thread
Oudwins marked this conversation as resolved.
}
for _, opt := range options {
opt(s)
}
return s
}

func (u *UnionSchema) Parse(data any, dest any, options ...ExecOption) p.ZogIssueList {
errs := p.NewErrsList()
defer errs.Free()
ctx := p.NewExecCtx(errs, conf.IssueFormatter)
defer ctx.Free()
for _, opt := range options {
opt(ctx)
}
path := p.NewPathBuilder()
defer path.Free()
sctx := ctx.NewSchemaCtx(data, dest, path, u.getType())
defer sctx.Free()
u.process(sctx)
return errs.List
}

func (u *UnionSchema) Validate(dest any, options ...ExecOption) p.ZogIssueList {
errs := p.NewErrsList()
defer errs.Free()
ctx := p.NewExecCtx(errs, conf.IssueFormatter)
defer ctx.Free()
for _, opt := range options {
opt(ctx)
}
path := p.NewPathBuilder()
defer path.Free()
sctx := ctx.NewSchemaCtx(dest, dest, path, u.getType())
defer sctx.Free()
u.validate(sctx)
return errs.List
}

func (u *UnionSchema) process(ctx *p.SchemaCtx) {
// Wrap the context and only go to the next one on fail. Keeping all the errors and appending at the end
listStart := len(ctx.Errors.List)
for _, s := range u.schemas {
numIssues := len(ctx.Errors.List)
s.process(ctx)
if len(ctx.Errors.List) == numIssues {
ctx.Errors.List = ctx.Errors.List[:listStart]
return // success
}
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}

func (u *UnionSchema) validate(ctx *p.SchemaCtx) {
// Wrap the context and only go to the next one on fail. Keeping all the errors and appending at the end
listStart := len(ctx.Errors.List)
for _, s := range u.schemas {
numIssues := len(ctx.Errors.List)
s.validate(ctx)
if len(ctx.Errors.List) == numIssues {
ctx.Errors.List = ctx.Errors.List[:listStart]
return // success
}
}

Comment thread
Oudwins marked this conversation as resolved.
}
Comment thread
Oudwins marked this conversation as resolved.
func (u *UnionSchema) getType() zconst.ZogType {
return zconst.TypeUnion
}
func (u *UnionSchema) setCoercer(c CoercerFunc) {}

func (u *UnionSchema) toZSS(ctx *ZSSSerializeCtx) *zss.ZSSSchema {
children := make([]*zss.ZSSSchema, 0, len(u.schemas))
for _, schema := range u.schemas {
children = append(children, schema.toZSS(ctx))
}

return &zss.ZSSSchema{
Kind: zconst.TypeUnion,
Children: children,
}
}
Loading
Loading