Skip to content
Open
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
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
142 changes: 131 additions & 11 deletions pkgs/zss/schema/zss_document_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ var ZSSFieldMetaSchema = z.Struct(z.Shape{

// ZSSTransformerSchema defines the schema for ZSSTransformer
var ZSSTransformerSchema = z.Struct(z.Shape{
"id": z.String().Required(),
"ID": z.String().Required(),
})

// ZSSTestSchema defines the schema for ZSSTest
var ZSSTestSchema = z.Struct(z.Shape{
"id": z.String().Required(),
"ID": z.String().Required(),
"message": z.String().Required(),
"issuePath": z.Slice(z.String()).Required(),
"issuePath": z.Slice(z.String()),
"params": z.EXPERIMENTAL_MAP[string, any](z.String(), z.EXPERIMENTAL_ANY()),
})

Expand All @@ -37,31 +37,151 @@ var ZSSProcessorSchema = z.Struct(z.Shape{
"transformer": z.Ptr(ZSSTransformerSchema),
})

var ZSSCustomTestSchema = z.Struct(z.Shape{
"ID": z.String(),
"message": z.String(),
"issuePath": z.Slice(z.String()),
"params": z.EXPERIMENTAL_MAP[string, any](z.String(), z.EXPERIMENTAL_ANY()),
})

var ZSSCustomProcessorSchema = z.Struct(z.Shape{
"kind": z.StringLike[zconst.ZogProcessor]().OneOf([]zconst.ZogProcessor{zconst.ZogProcessorTest}),
"test": z.Ptr(ZSSCustomTestSchema).NotNil(),
})

// ZSSExtensionSchema defines the schema for ZSSExtension.
var ZSSExtensionSchema = z.Struct(z.Shape{
"URI": URISchema,
"Content": z.EXPERIMENTAL_ANY(),
})

var zssKind = func(k zconst.ZogType) *z.StringSchema[zconst.ZogType] {
return z.StringLike[zconst.ZogType]().OneOf([]zconst.ZogType{k})

}

// ZSSSchemaSchema defines the schema for ZSSSchema.
// Note: defaultValue and catchValue are intentionally loose because ZSS allows arbitrary values.
var ZSSSchemaSchema = z.EXPERIMENTAL_RECURSIVE(func(self z.RecursiveSchema[*z.StructSchema]) *z.StructSchema {
return z.Struct(z.Shape{
"Ref": z.Ptr(z.String()),
"kind": z.StringLike[zconst.ZogType]().OneOf(zconst.ZogTypeValues),
"Extension": z.Ptr(ZSSExtensionSchema),
var ZSSSchemaSchema = z.EXPERIMENTAL_RECURSIVE(func(self z.RecursiveSchema[*z.UnionSchema]) *z.UnionSchema {
ref := z.Struct(z.Shape{
"Ref": z.Ptr(z.String().Required().Min(1)).NotNil(),
})

str := z.Struct(z.Shape{
"Kind": zssKind(zconst.TypeString),
"processors": z.Slice(ZSSProcessorSchema),
"goTypes": z.Slice(ZSSGoTypeSchema),
"required": z.Ptr(ZSSTestSchema),
"defaultValue": z.EXPERIMENTAL_ANY(),
"catchValue": z.EXPERIMENTAL_ANY(),
})

num := z.Struct(z.Shape{
"Kind": zssKind(zconst.TypeNumber),
"processors": z.Slice(ZSSProcessorSchema),
"goTypes": z.Slice(ZSSGoTypeSchema),
"required": z.Ptr(ZSSTestSchema),
"defaultValue": z.EXPERIMENTAL_ANY(),
"catchValue": z.EXPERIMENTAL_ANY(),
})
// bool
bl := z.Struct(z.Shape{
"Kind": zssKind(zconst.TypeBool),
"processors": z.Slice(ZSSProcessorSchema),
"goTypes": z.Slice(ZSSGoTypeSchema),
"required": z.Ptr(ZSSTestSchema),
"defaultValue": z.EXPERIMENTAL_ANY(),
"catchValue": z.EXPERIMENTAL_ANY(),
})

// time
tm := z.Struct(z.Shape{
"Kind": zssKind(zconst.TypeTime),
"format": z.Ptr(z.String()),
"processors": z.Slice(ZSSProcessorSchema),
"goTypes": z.Slice(ZSSGoTypeSchema),
"required": z.Ptr(ZSSTestSchema),
"defaultValue": z.EXPERIMENTAL_ANY(),
"catchValue": z.EXPERIMENTAL_ANY(),
})

list := z.Struct(z.Shape{
"Kind": zssKind(zconst.TypeSlice),
"processors": z.Slice(ZSSProcessorSchema),
"goTypes": z.Slice(ZSSGoTypeSchema),
"element": z.Ptr(self()).NotNil(),
"required": z.Ptr(ZSSTestSchema),
"defaultValue": z.EXPERIMENTAL_ANY(),
"catchValue": z.EXPERIMENTAL_ANY(),
})

mp := z.Struct(z.Shape{
"Kind": zssKind(zconst.TypeMap),
"processors": z.Slice(ZSSProcessorSchema),
"goTypes": z.Slice(ZSSGoTypeSchema),
"key": z.Ptr(self()).NotNil(),
"value": z.Ptr(self()).NotNil(),
"required": z.Ptr(ZSSTestSchema),
"defaultValue": z.EXPERIMENTAL_ANY(),
"catchValue": z.EXPERIMENTAL_ANY(),
})

strct := z.Struct(z.Shape{
"Kind": zssKind(zconst.TypeStruct),
"processors": z.Slice(ZSSProcessorSchema),
"goTypes": z.Slice(ZSSGoTypeSchema),
"required": z.Ptr(ZSSTestSchema),
"defaultValue": z.EXPERIMENTAL_ANY(),
"fields": z.EXPERIMENTAL_MAP[string, *zsscore.ZSSSchema](z.String(), z.Ptr(self())),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject nil schemas in required recursive positions.

fields, union children, and the element fields for ptr/preprocess/boxed accept nil pointers. For example, a union with two null children passes Min(2) despite containing no schemas.

Proposed fix
-		"fields":       z.EXPERIMENTAL_MAP[string, *zsscore.ZSSSchema](z.String(), z.Ptr(self())),
+		"fields":       z.EXPERIMENTAL_MAP[string, *zsscore.ZSSSchema](z.String(), z.Ptr(self()).NotNil()),

-		"element":  z.Ptr(self()),
+		"element":  z.Ptr(self()).NotNil(),

-		"element": z.Ptr(self()),
+		"element": z.Ptr(self()).NotNil(),

-		"element": z.Ptr(self()),
+		"element": z.Ptr(self()).NotNil(),

-		"children": z.Slice(z.Ptr(self())).Required().Min(2),
+		"children": z.Slice(z.Ptr(self()).NotNil()).Required().Min(2),

Also applies to: 139-160, 172-175

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkgs/zss/schema/zss_document_schema.go` at line 135, Update the recursive
schema definitions for fields, union children, and the element positions of
ptr/preprocess/boxed to reject nil schema pointers during validation. Ensure
required-position checks count only valid non-nil schemas, so unions cannot
satisfy their minimum child count with nil entries and recursive fields cannot
contain nil values.

"fieldMeta": z.EXPERIMENTAL_MAP[string, zsscore.ZSSFieldMeta](z.String(), ZSSFieldMetaSchema),
"element": z.Ptr(self()),
"key": z.Ptr(self()),
"value": z.Ptr(self()),
})

ptr := z.Struct(z.Shape{
"Kind": zssKind(zconst.TypePtr),
"element": z.Ptr(self()),
"goTypes": z.Slice(ZSSGoTypeSchema),
"required": z.Ptr(ZSSTestSchema),
})

custom := z.Struct(z.Shape{
"Kind": zssKind(zconst.TypeCustom),
"goTypes": z.Slice(ZSSGoTypeSchema),
"processors": z.Slice(ZSSCustomProcessorSchema),
})

preprocess := z.Struct(z.Shape{
"Kind": zssKind(zconst.TypePreprocess),
"element": z.Ptr(self()),
"goTypes": z.Slice(ZSSGoTypeSchema),
})

boxed := z.Struct(z.Shape{
"Kind": zssKind(zconst.TypeBoxed),
"element": z.Ptr(self()),
"goTypes": z.Slice(ZSSGoTypeSchema),
})

anySchema := z.Struct(z.Shape{
"Kind": zssKind(zconst.TypeAny),
"processors": z.Slice(ZSSProcessorSchema),
"required": z.Ptr(ZSSTestSchema),
"defaultValue": z.EXPERIMENTAL_ANY(),
"catchValue": z.EXPERIMENTAL_ANY(),
})

union := z.Struct(z.Shape{
"Kind": zssKind(zconst.TypeUnion),
"children": z.Slice(z.Ptr(self())).Required().Min(2),
})

extended := z.Struct(z.Shape{
"Kind": zssKind("extension"),
"Extension": z.Ptr(ZSSExtensionSchema).NotNil(),
})

return z.EXPERIMENTAL_UNION([]z.ZogSchema{
union, ref, str, num, bl, tm, list, mp, strct, ptr, custom, preprocess, boxed, anySchema, extended,
})
})

var URISchema = z.String().Match(zsscore.ZSS_URI_REGEX).Required()
Expand Down
47 changes: 47 additions & 0 deletions pkgs/zss/toZSS_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,20 @@ func stripGoTypes(schema *zss.ZSSSchema) {
stripGoTypes(schema.Element)
stripGoTypes(schema.Key)
stripGoTypes(schema.Value)
for _, child := range schema.Children {
stripGoTypes(child)
}
}

func assertValidZSSDocument(t *testing.T, doc zss.ZSSDocument) {
t.Helper()
assert.Nil(t, zssschema.ZSSDocumentSchema.Validate(&doc))
}

func TestToJsonString(t *testing.T) {
s := zog.String().Required().Default("Testing!").Catch("Testing2!").Min(1)
d := zog.EXPERIMENTAL_TO_ZSS[string](s)
assertValidZSSDocument(t, d)
serialized, err := json.Marshal(withoutGoTypes(d))
assert.Nil(t, err)
assert.NotNil(t, serialized)
Expand Down Expand Up @@ -86,6 +95,7 @@ func TestToJsonString(t *testing.T) {
func TestToJsonPtr(t *testing.T) {
s := zog.Ptr(zog.String().Required().Default("Testing!").Catch("Testing2!").Min(1))
d := zog.EXPERIMENTAL_TO_ZSS[*string](s)
assertValidZSSDocument(t, d)
serialized, err := json.Marshal(withoutGoTypes(d))
assert.Nil(t, err)
assert.NotNil(t, serialized)
Expand Down Expand Up @@ -133,6 +143,7 @@ func TestToJsonStructShape(t *testing.T) {
"age": zog.Int().Optional(),
})
d := zog.EXPERIMENTAL_TO_ZSS[User](s)
assertValidZSSDocument(t, d)
serialized, err := json.Marshal(d)
assert.Nil(t, err)
assert.NotNil(t, serialized)
Expand All @@ -159,6 +170,7 @@ func TestToJsonStructShape(t *testing.T) {
func TestToJsonNumber(t *testing.T) {
s := zog.Int().Required().Default(42).GT(0)
d := zog.EXPERIMENTAL_TO_ZSS[int](s)
assertValidZSSDocument(t, d)
serialized, err := json.Marshal(withoutGoTypes(d))
assert.Nil(t, err)
assert.NotNil(t, serialized)
Expand Down Expand Up @@ -194,6 +206,7 @@ func TestToJsonNumber(t *testing.T) {
func TestToJsonBool(t *testing.T) {
s := zog.Bool().Required().Default(true)
d := zog.EXPERIMENTAL_TO_ZSS[bool](s)
assertValidZSSDocument(t, d)
serialized, err := json.Marshal(withoutGoTypes(d))
assert.Nil(t, err)
assert.NotNil(t, serialized)
Expand All @@ -212,9 +225,36 @@ func TestToJsonBool(t *testing.T) {
assert.Equal(t, normalize(expected), normalize(string(serialized)))
}

func TestToJsonUnion(t *testing.T) {
s := zog.EXPERIMENTAL_UNION([]zog.ZogSchema{
zog.String(),
zog.Int(),
})
d := zog.EXPERIMENTAL_TO_ZSS[any](s)
assertValidZSSDocument(t, d)
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)))
}

func TestToJsonTime(t *testing.T) {
s := zog.Time().Required()
d := zog.EXPERIMENTAL_TO_ZSS[time.Time](s)
assertValidZSSDocument(t, d)
serialized, err := json.Marshal(withoutGoTypes(d))
assert.Nil(t, err)
assert.NotNil(t, serialized)
Expand All @@ -235,6 +275,7 @@ func TestToJsonTime(t *testing.T) {
func TestToJsonSlice(t *testing.T) {
s := zog.Slice(zog.String().Min(1)).Required().Min(1)
d := zog.EXPERIMENTAL_TO_ZSS[[]string](s)
assertValidZSSDocument(t, d)
serialized, err := json.Marshal(withoutGoTypes(d))
assert.Nil(t, err)
assert.NotNil(t, serialized)
Expand Down Expand Up @@ -293,6 +334,7 @@ func TestToJsonStruct(t *testing.T) {
"age": zog.Int().Optional(),
})
d := zog.EXPERIMENTAL_TO_ZSS[User](s)
assertValidZSSDocument(t, d)
serialized, err := json.Marshal(withoutGoTypes(d))
assert.Nil(t, err)
assert.NotNil(t, serialized)
Expand Down Expand Up @@ -326,6 +368,7 @@ func TestToJsonPreprocess(t *testing.T) {
zog.String().Min(1),
)
d := zog.EXPERIMENTAL_TO_ZSS[string](s)
assertValidZSSDocument(t, d)
serialized, err := json.Marshal(withoutGoTypes(d))
assert.Nil(t, err)
assert.NotNil(t, serialized)
Expand Down Expand Up @@ -364,6 +407,7 @@ func TestToJsonBoxed(t *testing.T) {
func(s string, ctx zog.Ctx) (StringBox, error) { return StringBox{V: s}, nil },
)
d := zog.EXPERIMENTAL_TO_ZSS[StringBox](s)
assertValidZSSDocument(t, d)
serialized, err := json.Marshal(withoutGoTypes(d))
assert.Nil(t, err)
assert.NotNil(t, serialized)
Expand Down Expand Up @@ -395,6 +439,7 @@ func TestToJsonBoxed(t *testing.T) {
func TestToJsonMap(t *testing.T) {
s := zog.EXPERIMENTAL_MAP[string, int](zog.String().Min(1), zog.Int().GT(0)).Required().Min(2)
d := zog.EXPERIMENTAL_TO_ZSS[map[string]int](s)
assertValidZSSDocument(t, d)
serialized, err := json.Marshal(withoutGoTypes(d))
assert.Nil(t, err)
assert.NotNil(t, serialized)
Expand Down Expand Up @@ -465,6 +510,7 @@ func TestToJsonCustom(t *testing.T) {
return *valPtr == "valid"
})
d := zog.EXPERIMENTAL_TO_ZSS[string](s)
assertValidZSSDocument(t, d)
serialized, err := json.Marshal(withoutGoTypes(d))
assert.Nil(t, err)
assert.NotNil(t, serialized)
Expand Down Expand Up @@ -501,6 +547,7 @@ func TestToJsonRecursiveUsesRefs(t *testing.T) {
})

d := zog.EXPERIMENTAL_TO_ZSS[*Node](s)
assertValidZSSDocument(t, d)
serialized, err := json.Marshal(withoutGoTypes(d))
assert.Nil(t, err)
assert.NotNil(t, serialized)
Expand Down
Loading
Loading