diff --git a/cmd/cue/cmd/testdata/script/def_jsonschema_from_cue.txtar b/cmd/cue/cmd/testdata/script/def_jsonschema_from_cue.txtar index 2d1bc274c..7723854f3 100644 --- a/cmd/cue/cmd/testdata/script/def_jsonschema_from_cue.txtar +++ b/cmd/cue/cmd/testdata/script/def_jsonschema_from_cue.txtar @@ -12,15 +12,15 @@ cmp stdout golden_cue.output t1?: int -- golden_cue.output -- $schema: "https://json-schema.org/draft/2020-12/schema" +type: "object" properties: t1: type: "integer" -type: "object" -- golden_json.output -- { "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", "properties": { "t1": { "type": "integer" } - }, - "type": "object" + } } diff --git a/encoding/jsonschema/generate.go b/encoding/jsonschema/generate.go index 70d8eecdc..fe32e4c9f 100644 --- a/encoding/jsonschema/generate.go +++ b/encoding/jsonschema/generate.go @@ -15,8 +15,9 @@ package jsonschema import ( - "encoding/json" + "cmp" "fmt" + "maps" "reflect" "slices" "strings" @@ -25,6 +26,7 @@ import ( "cuelang.org/go/cue" "cuelang.org/go/cue/ast" "cuelang.org/go/cue/errors" + "cuelang.org/go/cue/token" ) // GenerateConfig configures JSON Schema generation from CUE values. @@ -60,6 +62,9 @@ func Generate(v cue.Value, cfg *GenerateConfig) (ast.Expr, error) { if cfg.Version == VersionUnknown { cfg.Version = VersionDraft2020_12 } + if cfg.Version != VersionDraft2020_12 { + return nil, fmt.Errorf("only version %v is supported for generating JSON Schema for now", VersionDraft2020_12) + } g := &generator{ cfg: cfg, @@ -68,29 +73,42 @@ func Generate(v cue.Value, cfg *GenerateConfig) (ast.Expr, error) { item := g.makeItem(v) item = mergeAllOf(item) item = enumFromConst(item) - m := item.generate(g) + expr := item.generate(g) + + // Check if the result is a boolean literal + if lit, ok := expr.(*ast.BasicLit); ok && (lit.Kind == token.TRUE || lit.Kind == token.FALSE) { + if lit.Kind == token.FALSE { + // There should already be an error; if not, create one + if g.err == nil { + g.addError(v, fmt.Errorf("schema cannot be satisfied")) + } + return nil, g.err + } + // true means empty struct + expr = &ast.StructLit{} + } + + // The result should be a struct literal + st, ok := expr.(*ast.StructLit) + if !ok { + return nil, fmt.Errorf("expected struct literal from generate, got %T", expr) + } // Add schema version metadata and definitions. - m["$schema"] = VersionDraft2020_12.String() + fields := []ast.Decl{makeField("$schema", ast.NewString(cfg.Version.String()))} if len(g.defs) != 0 { - defs := make(map[string]any) - for name, def := range g.defs { - defs[name] = def.generate(g) + defFields := make([]ast.Decl, 0, len(g.defs)) + for _, name := range slices.Sorted(maps.Keys(g.defs)) { + defFields = append(defFields, makeField(name, g.defs[name].generate(g))) } - m["$defs"] = defs + fields = append(fields, makeField("$defs", &ast.StructLit{Elts: defFields})) } + fields = append(fields, st.Elts...) + if g.err != nil { return nil, g.err } - // TODO it would be nice if we could encode the resulting map - // elements in some kind of logical order rather than purely - // lexical, such as $schema at the start, "type" before "properties" - // etc. - finalv := v.Context().Encode(m) - if err := finalv.Err(); err != nil { - return nil, err - } - return finalv.Syntax().(ast.Expr), nil + return makeSchemaStructLit(fields...), nil } // mergeAllOf returns the item with adjacent itemAllOf nodes @@ -142,7 +160,7 @@ func enumFromConst(it item) item { // TODO this doesn't cover cases where there are some // const values and some noncrete values. it1 := &itemEnum{ - values: make([]json.RawMessage, 0, len(it.elems)), + values: make([]ast.Expr, 0, len(it.elems)), } for _, e := range it.elems { it1.values = append(it1.values, e.(*itemConst).value) @@ -230,14 +248,19 @@ func (g *generator) makeItem(v cue.Value) item { // Binary operations can't be expressed in JSON Schema. break } - data, err := args[0].MarshalJSON() - if err != nil { + if !args[0].IsConcrete() { // If it's not concrete, we can't represent it in JSON Schema // so accept anything. return &itemTrue{} } + syntax := args[0].Syntax() + expr, ok := syntax.(ast.Expr) + if !ok { + g.addError(args[0], fmt.Errorf("expected expression from Syntax, got %T", syntax)) + return &itemFalse{} + } it := &itemConst{ - value: data, + value: expr, } if op == cue.EqualOp { return it @@ -284,14 +307,14 @@ func (g *generator) makeItem(v cue.Value) item { return g.makeCallItem(v, args) } if isConcreteScalar(v) { - data, err := v.MarshalJSON() - if err != nil { - // Shouldn't happen. - g.addError(v, err) + syntax := v.Syntax() + expr, ok := syntax.(ast.Expr) + if !ok { + g.addError(v, fmt.Errorf("expected expression from Syntax, got %T", syntax)) return &itemFalse{} } return &itemConst{ - value: data, + value: expr, } } kind := v.IncompleteKind() @@ -457,6 +480,9 @@ func (g *generator) makeStructItem(v cue.Value) item { item: g.makeItem(iter.Value()), }) } + slices.SortFunc(props.elems, func(e1, e2 property) int { + return cmp.Compare(e1.name, e2.name) + }) if len(props.elems) == 0 && len(props.required) == 0 { return &itemTrue{} } diff --git a/encoding/jsonschema/generate_items.go b/encoding/jsonschema/generate_items.go index fae147b00..5d863191b 100644 --- a/encoding/jsonschema/generate_items.go +++ b/encoding/jsonschema/generate_items.go @@ -15,22 +15,23 @@ package jsonschema import ( - "encoding/json" + "cmp" "fmt" - "maps" "slices" "cuelang.org/go/cue" + "cuelang.org/go/cue/ast" + "cuelang.org/go/cue/token" ) // TODO use a defined order when keywords are marshaled // so that we always put $schema at the start, for example. // item represents a JSON Schema constraint or structure that can be -// converted to a map representation for serialization. +// converted to an AST representation for serialization. type item interface { - // generate returns the JSON object representation of this item. - generate(g *generator) map[string]any + // generate returns the AST representation of this item. + generate(g *generator) ast.Expr // apply invokes f on each sub-item, replacing each with the // item returned, and returns the new item (or the same if nothing has changed). @@ -41,8 +42,8 @@ type item interface { // itemTrue represents a schema that accepts any value (true schema) type itemTrue struct{} -func (i *itemTrue) generate(g *generator) map[string]any { - return map[string]any{} +func (i *itemTrue) generate(g *generator) ast.Expr { + return ast.NewBool(true) } func (i *itemTrue) apply(f func(item) item) item { @@ -52,8 +53,8 @@ func (i *itemTrue) apply(f func(item) item) item { // itemFalse represents a schema that accepts no values (false schema) type itemFalse struct{} -func (i *itemFalse) generate(g *generator) map[string]any { - return singleKeyword("not", map[string]any{}) +func (i *itemFalse) generate(g *generator) ast.Expr { + return ast.NewBool(false) } func (i *itemFalse) apply(f func(item) item) item { @@ -69,41 +70,79 @@ func (i *itemAllOf) add(it item) { i.elems = append(i.elems, it) } -func (i *itemAllOf) generate(g *generator) map[string]any { +func (i *itemAllOf) generate(g *generator) ast.Expr { // Because a single json schema object is essentially an allOf itself, // we can merge objects that don't share keywords // but we also have to be careful not to merge keywords // that interact with one another (for example `properties` and `patternProperties`). - var unmerged []map[string]any - final := make(map[string]any) + var unmerged []ast.Expr + var finalFields []ast.Decl + finalFieldNames := make(map[string]bool) + for _, e := range i.elems { - m := e.generate(g) + expr := e.generate(g) + + if lit, ok := expr.(*ast.BasicLit); ok { + switch lit.Kind { + case token.TRUE: + // true does nothing, so can be ignored. + continue + case token.FALSE: + // false means everything is false. + return expr + } + } + + // Try to extract struct literal fields for merging + st, ok := expr.(*ast.StructLit) + if !ok { + // A schema should only ever encode to a bool or a struct. + panic(fmt.Errorf("unexpected expression in itemAllOf: %T", expr)) + } + + // Check if we can merge these fields with existing ones avoidMerging := false - for k := range m { - // If the keyword interacts with any member already in final, - // avoid merging, or the keyword is already present in final. - if _, ok := final[k]; ok { + loop: + for _, decl := range st.Elts { + name := fieldLabel(decl) + if name == "" { + panic(fmt.Errorf("unexpected element in struct %#v", decl)) + } + if finalFieldNames[name] { + // Field already exists in merge target. avoidMerging = true break } - for _, ik := range keywordInteractions[k] { - if _, ok := final[ik]; ok { + for _, ik := range keywordInteractions[name] { + if finalFieldNames[ik] { + // Field interacts with one of the other fields in merge target. avoidMerging = true - break + break loop } } } + if avoidMerging { - unmerged = append(unmerged, m) - } else { - maps.Copy(final, m) + unmerged = append(unmerged, expr) + continue + } + // Merge the fields + for _, decl := range st.Elts { + finalFieldNames[fieldLabel(decl)] = true + finalFields = append(finalFields, decl) } } + if len(unmerged) == 0 { - return final + return makeSchemaStructLit(finalFields...) + } + + // Add the merged fields as one element if non-empty + if len(finalFields) > 0 { + unmerged = append(unmerged, makeSchemaStructLit(finalFields...)) } - unmerged = append(unmerged, final) - return singleKeyword("allOf", unmerged) + + return singleKeyword("allOf", ast.NewList(unmerged...)) } func (i *itemAllOf) apply(f func(item) item) item { @@ -119,8 +158,8 @@ type itemOneOf struct { elems []item } -func (i *itemOneOf) generate(g *generator) map[string]any { - return singleKeyword("oneOf", generateSlice(g, i.elems)) +func (i *itemOneOf) generate(g *generator) ast.Expr { + return singleKeyword("oneOf", generateList(g, i.elems)) } func (i *itemOneOf) apply(f func(item) item) item { @@ -136,8 +175,8 @@ type itemAnyOf struct { elems []item } -func (i *itemAnyOf) generate(g *generator) map[string]any { - return singleKeyword("anyOf", generateSlice(g, i.elems)) +func (i *itemAnyOf) generate(g *generator) ast.Expr { + return singleKeyword("anyOf", generateList(g, i.elems)) } func (i *itemAnyOf) apply(f func(item) item) item { @@ -153,7 +192,7 @@ type itemNot struct { elem item } -func (i *itemNot) generate(g *generator) map[string]any { +func (i *itemNot) generate(g *generator) ast.Expr { return singleKeyword("not", i.elem.generate(g)) } @@ -166,14 +205,12 @@ func (i *itemNot) apply(f func(item) item) item { } // itemConst represents a constant value constraint. -// The value represents the actual constant in question, -// as a raw message so that we avoid unnecessary -// round-tripping from CUE->value->JSON. +// The value represents the actual constant in question as an AST expression. type itemConst struct { - value json.RawMessage + value ast.Expr } -func (i *itemConst) generate(g *generator) map[string]any { +func (i *itemConst) generate(g *generator) ast.Expr { return singleKeyword("const", i.value) } @@ -183,13 +220,12 @@ func (i *itemConst) apply(f func(item) item) item { // itemEnum represents an "enum" constraint. // Each value represents one possible value of the enum. -// We use raw JSON for the same reason that [itemConst] uses it. type itemEnum struct { - values []json.RawMessage + values []ast.Expr } -func (i *itemEnum) generate(g *generator) map[string]any { - return singleKeyword("enum", i.values) +func (i *itemEnum) generate(g *generator) ast.Expr { + return singleKeyword("enum", ast.NewList(i.values...)) } func (i *itemEnum) apply(f func(item) item) item { @@ -200,8 +236,8 @@ type itemRef struct { defName string } -func (i *itemRef) generate(g *generator) map[string]any { - return singleKeyword("$ref", "#/$defs/"+i.defName) +func (i *itemRef) generate(g *generator) ast.Expr { + return singleKeyword("$ref", ast.NewString("#/$defs/"+i.defName)) } func (i *itemRef) apply(f func(item) item) item { @@ -213,11 +249,15 @@ type itemType struct { kinds []string } -func (i *itemType) generate(g *generator) map[string]any { +func (i *itemType) generate(g *generator) ast.Expr { if len(i.kinds) == 1 { - return singleKeyword("type", i.kinds[0]) + return singleKeyword("type", ast.NewString(i.kinds[0])) + } + exprs := make([]ast.Expr, len(i.kinds)) + for i, k := range i.kinds { + exprs[i] = ast.NewString(k) } - return singleKeyword("type", i.kinds) + return singleKeyword("type", ast.NewList(exprs...)) } func (i *itemType) apply(f func(item) item) item { @@ -229,8 +269,8 @@ type itemFormat struct { format string } -func (i *itemFormat) generate(g *generator) map[string]any { - return singleKeyword("format", i.format) +func (i *itemFormat) generate(g *generator) ast.Expr { + return singleKeyword("format", ast.NewString(i.format)) } func (i *itemFormat) apply(f func(item) item) item { @@ -242,8 +282,8 @@ type itemPattern struct { regexp string } -func (i *itemPattern) generate(g *generator) map[string]any { - return singleKeyword("pattern", i.regexp) +func (i *itemPattern) generate(g *generator) ast.Expr { + return singleKeyword("pattern", ast.NewString(i.regexp)) } func (i *itemPattern) apply(f func(item) item) item { @@ -258,7 +298,7 @@ type itemBounds struct { n float64 } -func (i *itemBounds) generate(g *generator) map[string]any { +func (i *itemBounds) generate(g *generator) ast.Expr { var keyword string switch i.constraint { case cue.LessThanOp: @@ -272,7 +312,7 @@ func (i *itemBounds) generate(g *generator) map[string]any { default: panic(fmt.Errorf("unexpected bound operand %v", i.constraint)) } - return singleKeyword(keyword, i.n) + return singleKeyword(keyword, ast.NewLit(token.FLOAT, fmt.Sprint(i.n))) } func (i *itemBounds) apply(f func(item) item) item { @@ -284,8 +324,8 @@ type itemMultipleOf struct { n float64 } -func (i *itemMultipleOf) generate(g *generator) map[string]any { - return singleKeyword("multipleOf", i.n) +func (i *itemMultipleOf) generate(g *generator) ast.Expr { + return singleKeyword("multipleOf", ast.NewLit(token.FLOAT, fmt.Sprint(i.n))) } func (i *itemMultipleOf) apply(f func(item) item) item { @@ -298,7 +338,7 @@ type itemLengthBounds struct { n int } -func (i *itemLengthBounds) generate(g *generator) map[string]any { +func (i *itemLengthBounds) generate(g *generator) ast.Expr { var keyword string switch i.constraint { case cue.LessThanEqualOp: @@ -309,7 +349,7 @@ func (i *itemLengthBounds) generate(g *generator) map[string]any { panic("unexpected constraint in length bounds") } - return singleKeyword(keyword, i.n) + return singleKeyword(keyword, ast.NewLit(token.INT, fmt.Sprint(i.n))) } func (i *itemLengthBounds) apply(f func(item) item) item { @@ -322,7 +362,7 @@ type itemItemsBounds struct { n int } -func (i *itemItemsBounds) generate(g *generator) map[string]any { +func (i *itemItemsBounds) generate(g *generator) ast.Expr { var keyword string switch i.constraint { case cue.LessThanEqualOp: @@ -332,7 +372,7 @@ func (i *itemItemsBounds) generate(g *generator) map[string]any { default: panic("unexpected constraint in items bounds") } - return singleKeyword(keyword, i.n) + return singleKeyword(keyword, ast.NewLit(token.INT, fmt.Sprint(i.n))) } func (i *itemItemsBounds) apply(f func(item) item) item { @@ -345,7 +385,7 @@ type itemPropertyBounds struct { n int } -func (i *itemPropertyBounds) generate(g *generator) map[string]any { +func (i *itemPropertyBounds) generate(g *generator) ast.Expr { var keyword string switch i.constraint { case cue.LessThanEqualOp: @@ -355,7 +395,7 @@ func (i *itemPropertyBounds) generate(g *generator) map[string]any { default: panic("unexpected constraint in items bounds") } - return singleKeyword(keyword, i.n) + return singleKeyword(keyword, ast.NewLit(token.INT, fmt.Sprint(i.n))) } func (i *itemPropertyBounds) apply(f func(item) item) item { @@ -367,7 +407,7 @@ type itemItems struct { elem item } -func (i *itemItems) generate(g *generator) map[string]any { +func (i *itemItems) generate(g *generator) ast.Expr { return singleKeyword("items", i.elem.generate(g)) } @@ -384,8 +424,8 @@ type itemPrefixItems struct { elems []item } -func (i *itemPrefixItems) generate(g *generator) map[string]any { - return singleKeyword("items", generateSlice(g, i.elems)) +func (i *itemPrefixItems) generate(g *generator) ast.Expr { + return singleKeyword("items", generateList(g, i.elems)) } func (i *itemPrefixItems) apply(f func(item) item) item { @@ -403,15 +443,15 @@ type itemContains struct { max *int } -func (i *itemContains) generate(g *generator) map[string]any { - m := singleKeyword("contains", i.elem.generate(g)) +func (i *itemContains) generate(g *generator) ast.Expr { + fields := []ast.Decl{makeField("contains", i.elem.generate(g))} if i.min != nil { - m["minContains"] = *i.min + fields = append(fields, makeField("minContains", ast.NewLit(token.INT, fmt.Sprint(*i.min)))) } if i.max != nil { - m["maxContains"] = *i.max + fields = append(fields, makeField("maxContains", ast.NewLit(token.INT, fmt.Sprint(*i.max)))) } - return m + return makeSchemaStructLit(fields...) } func (i *itemContains) apply(f func(item) item) item { @@ -435,16 +475,20 @@ type itemProperties struct { // TODO patternProperties, additionalProperties } -func (i *itemProperties) generate(g *generator) map[string]any { - properties := make(map[string]any) - for _, prop := range i.elems { - properties[prop.name] = prop.item.generate(g) +func (i *itemProperties) generate(g *generator) ast.Expr { + propFields := make([]ast.Decl, len(i.elems)) + for j, prop := range i.elems { + propFields[j] = makeField(prop.name, prop.item.generate(g)) } - m := singleKeyword("properties", properties) + fields := []ast.Decl{makeField("properties", &ast.StructLit{Elts: propFields})} if len(i.required) > 0 { - m["required"] = i.required + reqExprs := make([]ast.Expr, len(i.required)) + for j, r := range i.required { + reqExprs[j] = ast.NewString(r) + } + fields = append(fields, makeField("required", ast.NewList(reqExprs...))) } - return m + return makeSchemaStructLit(fields...) } func (i *itemProperties) apply(f func(item) item) item { @@ -494,17 +538,15 @@ type itemIfThenElse struct { elseElem item } -func (i *itemIfThenElse) generate(g *generator) map[string]any { - m := map[string]any{ - "if": i.ifElem.generate(g), - } +func (i *itemIfThenElse) generate(g *generator) ast.Expr { + fields := []ast.Decl{makeField("if", i.ifElem.generate(g))} if i.thenElem != nil { - m["then"] = i.thenElem.generate(g) + fields = append(fields, makeField("then", i.thenElem.generate(g))) } if i.elseElem != nil { - m["else"] = i.elseElem.generate(g) + fields = append(fields, makeField("else", i.elseElem.generate(g))) } - return m + return makeSchemaStructLit(fields...) } func (i *itemIfThenElse) apply(f func(item) item) item { @@ -523,24 +565,24 @@ func (i *itemIfThenElse) apply(f func(item) item) item { return &itemIfThenElse{ifElem: ifElem, thenElem: thenElem, elseElem: elseElem} } -func generateSlice(g *generator, items []item) []any { - return mapSlice(items, func(it item) any { - return it.generate(g) - }) +func generateList(g *generator, items []item) ast.Expr { + exprs := make([]ast.Expr, len(items)) + for i, it := range items { + exprs[i] = it.generate(g) + } + return ast.NewList(exprs...) } -func singleKeyword(name string, val any) map[string]any { - return map[string]any{ - name: val, - } +func singleKeyword(name string, val ast.Expr) ast.Expr { + return makeSchemaStructLit(makeField(name, val)) } // keywordGroups holds sets of JSON Schema keywords that // interact directly with one another and therefore should not // be merged with other keywords in the same group. var keywordGroups = [][]string{ - {"contains", "maxContains", "minContains"}, {"properties", "patternProperties", "additionalProperties"}, + {"contains", "maxContains", "minContains"}, {"items", "additionalItems", "prefixItems"}, {"if", "then", "else"}, } @@ -556,3 +598,62 @@ var keywordInteractions = func() map[string][]string { } return m }() + +// fieldLabel extracts the field label name from a declaration. +func fieldLabel(d ast.Decl) string { + if f, ok := d.(*ast.Field); ok { + if name, _, _ := ast.LabelName(f.Label); name != "" { + return name + } + } + return "" +} + +// makeField creates a field with a string label and given value. +func makeField(name string, value ast.Expr) *ast.Field { + return &ast.Field{ + Label: ast.NewStringLabel(name), + Value: value, + } +} + +// makeSchemaStructLit creates a struct literal representing a JSON Schema +// schema, with fields in schema-centric order. +func makeSchemaStructLit(fields ...ast.Decl) *ast.StructLit { + slices.SortFunc(fields, func(a, b ast.Decl) int { + return cmpSchemaLabels(fieldLabel(a), fieldLabel(b)) + }) + return &ast.StructLit{ + Elts: fields, + } +} + +func cmpSchemaLabels(l1, l2 string) int { + return cmp.Or(cmp.Compare(labelPriority(l1), labelPriority(l2)), cmp.Compare(l1, l2)) +} + +// labelPriorityValues holds priority groups for sorting label names. +var labelPriorityValues = func() map[string]int { + // Always put these keywords at the start. + m := map[string]int{ + "$schema": 0, + "$defs": 1, + "type": 2, + } + // It's nice to group related keywords together. + n := len(m) + for i, g := range keywordGroups { + for _, name := range g { + m[name] = n + i + 1 + } + } + // Anything else gets put at the end in lexical order. + return m +}() + +func labelPriority(s string) int { + if pri, ok := labelPriorityValues[s]; ok { + return pri + } + return 1000 +} diff --git a/encoding/jsonschema/testdata/generate/basic.txtar b/encoding/jsonschema/testdata/generate/basic.txtar index 540b12f56..bf5b9d27b 100644 --- a/encoding/jsonschema/testdata/generate/basic.txtar +++ b/encoding/jsonschema/testdata/generate/basic.txtar @@ -22,6 +22,7 @@ ok: data: { -- out/generate-v3/schema -- { $schema: "https://json-schema.org/draft/2020-12/schema" + type: "object" properties: { t1: { type: "integer" @@ -45,8 +46,7 @@ ok: data: { type: "integer" }] } - t7: {} + t7: true } - type: "object" } -- out/generate-v3/ok -- diff --git a/encoding/jsonschema/testdata/generate/callop.txtar b/encoding/jsonschema/testdata/generate/callop.txtar index d737e22a9..f6e3e400c 100644 --- a/encoding/jsonschema/testdata/generate/callop.txtar +++ b/encoding/jsonschema/testdata/generate/callop.txtar @@ -70,92 +70,92 @@ badScore2: { -- out/generate-v3/schema -- { $schema: "https://json-schema.org/draft/2020-12/schema" + type: "object" properties: { date: { - format: "date" type: "string" + format: "date" } dateTime: { - format: "date-time" type: "string" + format: "date-time" } longString: { - minLength: 5 type: "string" + minLength: 5 } multiple: { - multipleOf: 5.0 type: "number" + multipleOf: 5 } score: { allOf: [{ - multipleOf: 1e+1 type: "number" + multipleOf: 10 }, { type: "integer" }] } shortString: { - maxLength: 10 type: "string" + maxLength: 10 } timeOnly: { - format: "time" type: "string" + format: "time" } userName: { allOf: [{ - maxLength: 20 type: "string" + maxLength: 20 }, { - minLength: 3 type: "string" + minLength: 3 }] } } - type: "object" } -- out/generate-v3/badDate -- badDate.data.date: invalid value "2025-10-40" (does not satisfy time.Format("2006-01-02")): error in call to time.Format: invalid time "2025-10-40": - 1:81 + 1:113 ./datatest/tests.cue:30:14 -- out/generate-v3/badDateTime -- badDateTime.data.dateTime: invalid value "2025-10-02T13" (does not satisfy time.Time): error in call to time.Time: invalid time "2025-10-02T13": - 1:126 + 1:158 ./datatest/tests.cue:26:18 -- out/generate-v3/badScore2 -- badScore2.data.score: invalid value 5 (does not satisfy matchN): 1 matched, expected 2: - 1:262 + 1:278 ./datatest/tests.cue:42:15 -badScore2.data.score: invalid value 5 (does not satisfy math.MultipleOf(1E+1)): - 1:272 - 1:262 +badScore2.data.score: invalid value 5 (does not satisfy math.MultipleOf(10)): + 1:304 + 1:278 ./datatest/tests.cue:42:15 -- out/generate-v3/badTime -- badTime.data.date: invalid value "25:00:10" (does not satisfy time.Format("2006-01-02")): error in call to time.Format: invalid time "25:00:10": - 1:81 + 1:113 ./datatest/tests.cue:34:14 -- out/generate-v3/badUserName -- badUserName.data.userName: invalid value "x" (does not satisfy matchN): 1 matched, expected 2: - 1:432 + 1:446 ./datatest/tests.cue:38:18 badUserName.data.userName: invalid value "x" (does not satisfy strings.MinRunes(3)): - 1:475 - 1:432 - 1:475 + 1:505 + 1:446 + 1:505 ./datatest/tests.cue:38:18 -- out/generate-v3/notMultiple -- notMultiple.data.multiple: invalid value 1 (does not satisfy math.MultipleOf(5)): - 1:221 + 1:253 ./datatest/tests.cue:22:18 -- out/generate-v3/ok -- -- out/generate-v3/stringTooLong -- stringTooLong.data.shortString: invalid value "01234567890" (does not satisfy strings.MaxRunes(10)): - 1:343 - 1:343 + 1:373 + 1:373 ./datatest/tests.cue:14:21 -- out/generate-v3/stringTooShort -- stringTooShort.data.longString: invalid value "x" (does not satisfy strings.MinRunes(5)): - 1:178 - 1:178 + 1:210 + 1:210 ./datatest/tests.cue:18:20 diff --git a/encoding/jsonschema/testdata/generate/const.txtar b/encoding/jsonschema/testdata/generate/const.txtar index cd6f9e1e6..21f9f2b23 100644 --- a/encoding/jsonschema/testdata/generate/const.txtar +++ b/encoding/jsonschema/testdata/generate/const.txtar @@ -48,6 +48,7 @@ errorNonString: { -- out/generate-v3/schema -- { $schema: "https://json-schema.org/draft/2020-12/schema" + type: "object" properties: { bool: { const: true @@ -65,30 +66,29 @@ errorNonString: { const: "something" } } - type: "object" } -- out/generate-v3/errorFloatForInt -- errorFloatForInt.data.int: conflicting values 2.0 and 2 (mismatched types float and int): - 1:124 + 1:140 ./datatest/tests.cue:12:8 -- out/generate-v3/errorNonBool -- errorNonBool.data.bool: conflicting values "foo" and true (mismatched types string and bool): - 1:81 + 1:97 ./datatest/tests.cue:20:14 -- out/generate-v3/errorNonEqualFloat -- errorNonEqualFloat.data.float: conflicting values 1.5 and 1.6: - 1:104 + 1:120 ./datatest/tests.cue:24:15 -- out/generate-v3/errorNonEqualInt -- errorNonEqualInt.data.int: conflicting values 2 and 9: - 1:124 + 1:140 ./datatest/tests.cue:28:13 -- out/generate-v3/errorNonNull -- errorNonNull.data.null: conflicting values 1 and null (mismatched types int and null): - 1:143 + 1:159 ./datatest/tests.cue:32:14 -- out/generate-v3/errorNonString -- errorNonString.data.string: conflicting values "something" and "other": - 1:167 + 1:183 ./datatest/tests.cue:36:16 -- out/generate-v3/ok1 -- diff --git a/encoding/jsonschema/testdata/generate/definitions.txtar b/encoding/jsonschema/testdata/generate/definitions.txtar index f7275f320..fa7cf7a37 100644 --- a/encoding/jsonschema/testdata/generate/definitions.txtar +++ b/encoding/jsonschema/testdata/generate/definitions.txtar @@ -18,8 +18,10 @@ missingX: { } -- out/generate-v3/schema -- { + $schema: "https://json-schema.org/draft/2020-12/schema" $defs: { "#D1": { + type: "object" properties: { x: { type: "integer" @@ -29,22 +31,20 @@ missingX: { } } required: ["x"] - type: "object" } "#D2": { type: "string" } } - $schema: "https://json-schema.org/draft/2020-12/schema" + type: "object" properties: { foo: { $ref: "#/$defs/#D1" } } required: ["foo"] - type: "object" } -- out/generate-v3/simple -- -- out/generate-v3/missingX -- missingX.data.foo.x: field is required but not present: - 1:18 + 1:91 diff --git a/encoding/jsonschema/testdata/generate/fields.txtar b/encoding/jsonschema/testdata/generate/fields.txtar index cee48feb8..b4fd08f92 100644 --- a/encoding/jsonschema/testdata/generate/fields.txtar +++ b/encoding/jsonschema/testdata/generate/fields.txtar @@ -9,6 +9,7 @@ t6: {x?: 4, y: 1} // fully concrete struct -> optional -- out/generate-v3/schema -- { $schema: "https://json-schema.org/draft/2020-12/schema" + type: "object" properties: { t1: { type: "integer" @@ -23,15 +24,16 @@ t6: {x?: 4, y: 1} // fully concrete struct -> optional const: true } t5: { + type: "object" properties: { x: { type: "integer" } } required: ["x"] - type: "object" } t6: { + type: "object" properties: { x: { const: 4 @@ -40,9 +42,7 @@ t6: {x?: 4, y: 1} // fully concrete struct -> optional const: 1 } } - type: "object" } } required: ["t2", "t3", "t5"] - type: "object" } diff --git a/encoding/jsonschema/testdata/generate/unary.txtar b/encoding/jsonschema/testdata/generate/unary.txtar index 2fe0b14ae..cbb54245a 100644 --- a/encoding/jsonschema/testdata/generate/unary.txtar +++ b/encoding/jsonschema/testdata/generate/unary.txtar @@ -73,16 +73,17 @@ errorNotLessEqual: { -- out/generate-v3/schema -- { $schema: "https://json-schema.org/draft/2020-12/schema" + type: "object" properties: { t01: { - pattern: "^foo" type: "string" + pattern: "^foo" } t02: { + type: "string" not: { pattern: "bar" } - type: "string" } t03: { const: 1 @@ -93,20 +94,20 @@ errorNotLessEqual: { } } t05: { - exclusiveMinimum: 5.0 type: "number" + exclusiveMinimum: 5 } t06: { - minimum: 5.0 type: "number" + minimum: 5 } t07: { - exclusiveMaximum: 1e+1 type: "number" + exclusiveMaximum: 10 } t08: { - maximum: 1e+1 type: "number" + maximum: 10 } t09: { type: "string" @@ -115,41 +116,40 @@ errorNotLessEqual: { type: "string" } } - type: "object" } -- out/generate-v3/ok -- -- out/generate-v3/errorEqual1 -- -- out/generate-v3/errorEqual2 -- errorEqual2.data.t04: invalid value 1 (does not satisfy matchN): 1 matched, expected 0: - 1:187 + 1:203 ./datatest/tests.cue:39:13 -- out/generate-v3/errorMatchingRegexp -- errorMatchingRegexp.data.t02: invalid value "xbary" (does not satisfy matchN): 1 matched, expected 0: - 1:121 + 1:153 ./datatest/tests.cue:23:13 -- out/generate-v3/errorNonMatchingRegexp -- errorNonMatchingRegexp.data.t01: invalid value "nfoo" (out of bound =~"^foo"): - 1:80 + 1:112 ./datatest/tests.cue:19:13 -- out/generate-v3/errorNotGreater -- errorNotGreater.data.t05: invalid value 5 (out of bound >5): - 1:213 + 1:245 ./datatest/tests.cue:43:13 -- out/generate-v3/errorNotGreaterEqual -- errorNotGreaterEqual.data.t06: invalid value 4.99 (out of bound >=5): - 1:258 + 1:290 ./datatest/tests.cue:47:13 -- out/generate-v3/errorNotLess -- -errorNotLess.data.t07: invalid value 10 (out of bound <1E+1): - 1:294 +errorNotLess.data.t07: invalid value 10 (out of bound <10): + 1:326 ./datatest/tests.cue:51:13 -- out/generate-v3/errorNotLessEqual -- -errorNotLessEqual.data.t08: invalid value 10.001 (out of bound <=1E+1): - 1:342 +errorNotLessEqual.data.t08: invalid value 10.001 (out of bound <=10): + 1:372 ./datatest/tests.cue:55:13 -- out/generate-v3/errorUnequal -- errorUnequal.data.t03: conflicting values 1 and 2: - 1:169 + 1:185 ./datatest/tests.cue:27:13 -- out/generate-v3/ok1 -- -- out/generate-v3/ok2 --