encoding/jsonschema: constraints don't imply types
This was a major bug based on the wrong assumption that
a JSON Schema constraint implies the type for that constraint.
Instead, the contraints only apply if a value is of a certain
type.
To keep diffs to a minimum, and to improve the overal output,
it now explicitly sorts literal
composit types (structs and lists) at the end of a series
of conjunctions.
Change-Id: Ice98a2bc00ae4a68170cd6cd726565a453b1187f
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/6302
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/jsonschema/constraints.go b/encoding/jsonschema/constraints.go
index d838d9c..89333f6 100644
--- a/encoding/jsonschema/constraints.go
+++ b/encoding/jsonschema/constraints.go
@@ -144,12 +144,11 @@
}
s.id = u
- if s.obj == nil {
- s.obj = &ast.StructLit{}
- }
+ obj := s.object(n)
+
// TODO: handle the case where this is always defined and we don't want
// to include the default value.
- s.obj.Elts = append(s.obj.Elts, &ast.Attribute{
+ obj.Elts = append(obj.Elts, &ast.Attribute{
Text: fmt.Sprintf("@jsonschema(id=%q)", u)})
}),
@@ -165,19 +164,27 @@
switch str {
case "null":
types |= cue.NullKind
+ s.setTypeUsed(n, nullType)
// TODO: handle OpenAPI restrictions.
case "boolean":
types |= cue.BoolKind
+ s.setTypeUsed(n, boolType)
case "string":
types |= cue.StringKind
+ s.setTypeUsed(n, stringType)
case "number":
types |= cue.NumberKind
+ s.setTypeUsed(n, numType)
case "integer":
types |= cue.IntKind
+ s.setTypeUsed(n, numType)
+ s.add(n, numType, ast.NewIdent("int"))
case "array":
types |= cue.ListKind
+ s.setTypeUsed(n, arrayType)
case "object":
types |= cue.StructKind
+ s.setTypeUsed(n, objectType)
default:
s.errf(n, "unknown type %q", n)
@@ -203,19 +210,17 @@
for _, x := range s.listItems("enum", n, true) {
a = append(a, s.value(x))
}
- s.addConjunct(n, ast.NewBinExpr(token.OR, a...))
- s.typeOptional = true
+ s.all.add(n, ast.NewBinExpr(token.OR, a...))
}),
p1d("const", 6, func(n cue.Value, s *state) {
- s.addConjunct(n, s.value(n))
+ s.all.add(n, s.value(n))
}),
p1("default", func(n cue.Value, s *state) {
- allowed, used := s.allowedTypes, s.usedTypes
- s.default_ = s.value(n)
- s.allowedTypes, s.usedTypes = allowed, used
- // must validate that the default is subsumed by the normal value,
+ sc := *s
+ s.default_ = sc.value(n)
+ // TODO: must validate that the default is subsumed by the normal value,
// as CUE will otherwise broaden the accepted values with the default.
s.examples = append(s.examples, s.default_)
}),
@@ -268,7 +273,7 @@
expr = &ast.BadExpr{From: n.Pos()}
}
- s.addConjunct(n, expr)
+ s.all.add(n, expr)
}),
// Combinators
@@ -302,7 +307,7 @@
}
}
if len(a) > 0 {
- s.conjuncts = append(s.conjuncts, ast.NewBinExpr(token.AND, a...))
+ s.all.add(n, ast.NewBinExpr(token.AND, a...))
}
}),
@@ -312,13 +317,11 @@
for _, v := range s.listItems("anyOf", n, false) {
x, sub := s.schemaState(v, s.allowedTypes, nil, true)
types |= sub.allowedTypes
- if sub.hasConstraints() {
- a = append(a, x)
- }
+ a = append(a, x)
}
s.allowedTypes &= types
if len(a) > 0 {
- s.conjuncts = append(s.conjuncts, ast.NewBinExpr(token.OR, a...))
+ s.all.add(n, ast.NewBinExpr(token.OR, a...))
}
}),
@@ -342,7 +345,7 @@
s.allowedTypes &= types
if len(a) > 0 && hasSome {
s.usedTypes = allTypes
- s.conjuncts = append(s.conjuncts, ast.NewBinExpr(token.OR, a...))
+ s.all.add(n, ast.NewBinExpr(token.OR, a...))
}
// TODO: oneOf({a:x}, {b:y}, ..., not(anyOf({a:x}, {b:y}, ...))),
@@ -360,22 +363,21 @@
return
}
s.usedTypes |= cue.StringKind
- s.addConjunct(n, &ast.UnaryExpr{Op: token.MAT, X: s.string(n)})
+ s.add(n, stringType, &ast.UnaryExpr{Op: token.MAT, X: s.string(n)})
}),
p1("minLength", func(n cue.Value, s *state) {
s.usedTypes |= cue.StringKind
min := s.number(n)
strings := s.addImport(n, "strings")
- s.addConjunct(n, ast.NewCall(ast.NewSel(strings, "MinRunes"), min))
-
+ s.add(n, stringType, ast.NewCall(ast.NewSel(strings, "MinRunes"), min))
}),
p1("maxLength", func(n cue.Value, s *state) {
s.usedTypes |= cue.StringKind
max := s.number(n)
strings := s.addImport(n, "strings")
- s.addConjunct(n, ast.NewCall(ast.NewSel(strings, "MaxRunes"), max))
+ s.add(n, stringType, ast.NewCall(ast.NewSel(strings, "MaxRunes"), max))
}),
p1d("contentMediaType", 7, func(n cue.Value, s *state) {
@@ -396,24 +398,24 @@
p1("minimum", func(n cue.Value, s *state) {
s.usedTypes |= cue.NumberKind
- s.addConjunct(n, &ast.UnaryExpr{Op: token.GEQ, X: s.number(n)})
+ s.add(n, numType, &ast.UnaryExpr{Op: token.GEQ, X: s.number(n)})
}),
p1("exclusiveMinimum", func(n cue.Value, s *state) {
// TODO: should we support Draft 4 booleans?
s.usedTypes |= cue.NumberKind
- s.addConjunct(n, &ast.UnaryExpr{Op: token.GTR, X: s.number(n)})
+ s.add(n, numType, &ast.UnaryExpr{Op: token.GTR, X: s.number(n)})
}),
p1("maximum", func(n cue.Value, s *state) {
s.usedTypes |= cue.NumberKind
- s.addConjunct(n, &ast.UnaryExpr{Op: token.LEQ, X: s.number(n)})
+ s.add(n, numType, &ast.UnaryExpr{Op: token.LEQ, X: s.number(n)})
}),
p1("exclusiveMaximum", func(n cue.Value, s *state) {
// TODO: should we support Draft 4 booleans?
s.usedTypes |= cue.NumberKind
- s.addConjunct(n, &ast.UnaryExpr{Op: token.LSS, X: s.number(n)})
+ s.add(n, numType, &ast.UnaryExpr{Op: token.LSS, X: s.number(n)})
}),
p1("multipleOf", func(n cue.Value, s *state) {
@@ -425,17 +427,15 @@
s.errf(n, `"multipleOf" value must be < 0; found %s`, n)
}
math := s.addImport(n, "math")
- s.addConjunct(n, ast.NewCall(ast.NewSel(math, "MultipleOf"), multiple))
+ s.add(n, numType, ast.NewCall(ast.NewSel(math, "MultipleOf"), multiple))
}),
// Object constraints
p1("properties", func(n cue.Value, s *state) {
s.usedTypes |= cue.StructKind
+ obj := s.object(n)
- if s.obj == nil {
- s.obj = &ast.StructLit{}
- }
if n.Kind() != cue.StructKind {
s.errf(n, `"properties" expected an object, found %v`, n.Kind)
}
@@ -447,7 +447,7 @@
f := &ast.Field{Label: name, Value: expr}
state.doc(f)
f.Optional = token.Blank.Pos()
- if len(s.obj.Elts) > 0 && len(f.Comments()) > 0 {
+ if len(obj.Elts) > 0 && len(f.Comments()) > 0 {
// TODO: change formatter such that either a a NewSection on the
// field or doc comment will cause a new section.
ast.SetRelPos(f.Comments()[0], token.NewSection)
@@ -455,12 +455,12 @@
if state.deprecated {
switch expr.(type) {
case *ast.StructLit:
- s.obj.Elts = append(s.obj.Elts, addTag(name, "deprecated", ""))
+ obj.Elts = append(obj.Elts, addTag(name, "deprecated", ""))
default:
f.Attrs = append(f.Attrs, internal.NewAttr("deprecated", ""))
}
}
- s.obj.Elts = append(s.obj.Elts, f)
+ obj.Elts = append(obj.Elts, f)
s.setField(label{name: key}, f)
})
}),
@@ -473,15 +473,13 @@
s.usedTypes |= cue.StructKind
- if s.obj == nil {
- s.obj = &ast.StructLit{}
- // TODO: detect that properties is defined somewhere.
- // s.errf(n, `"required" without a "properties" field`)
- }
+ // TODO: detect that properties is defined somewhere.
+ // s.errf(n, `"required" without a "properties" field`)
+ obj := s.object(n)
// Create field map
fields := map[string]*ast.Field{}
- for _, d := range s.obj.Elts {
+ for _, d := range obj.Elts {
f, ok := d.(*ast.Field)
if !ok {
continue // Could be embedding? See cirrus.json
@@ -501,7 +499,7 @@
Value: ast.NewIdent("_"),
}
fields[str] = f
- s.obj.Elts = append(s.obj.Elts, f)
+ obj.Elts = append(obj.Elts, f)
continue
}
if f.Optional == token.NoPos {
@@ -515,7 +513,8 @@
// [=~pattern]: _
if names, _ := s.schemaState(n, cue.StringKind, nil, false); !isAny(names) {
s.usedTypes |= cue.StructKind
- s.addConjunct(n, ast.NewStruct(ast.NewList((names)), ast.NewIdent("_")))
+ x := ast.NewStruct(ast.NewList(names), ast.NewIdent("_"))
+ s.add(n, objectType, x)
}
}),
@@ -531,7 +530,8 @@
s.usedTypes |= cue.StructKind
pkg := s.addImport(n, "struct")
- s.addConjunct(n, ast.NewCall(ast.NewSel(pkg, "MaxFields"), s.uint(n)))
+ x := ast.NewCall(ast.NewSel(pkg, "MaxFields"), s.uint(n))
+ s.add(n, objectType, x)
}),
p1("dependencies", func(n cue.Value, s *state) {
@@ -554,9 +554,7 @@
if n.Kind() != cue.StructKind {
s.errf(n, `value of "patternProperties" must be an an object, found %v`, n.Kind)
}
- if s.obj == nil {
- s.obj = &ast.StructLit{}
- }
+ obj := s.object(n)
existing := excludeFields(s.obj.Elts)
s.processMap(n, func(key string, n cue.Value) {
// [!~(properties) & pattern]: schema
@@ -569,7 +567,7 @@
Value: s.schema(n),
}))
ast.SetRelPos(f, token.NewSection)
- s.obj.Elts = append(s.obj.Elts, f)
+ obj.Elts = append(obj.Elts, f)
})
}),
@@ -584,24 +582,21 @@
case cue.StructKind:
s.usedTypes |= cue.StructKind
s.closeStruct = true
- if s.obj == nil {
- s.obj = &ast.StructLit{}
- }
- if len(s.obj.Elts) == 0 {
- s.obj.Elts = append(s.obj.Elts, &ast.Field{
+ obj := s.object(n)
+ if len(obj.Elts) == 0 {
+ obj.Elts = append(obj.Elts, &ast.Field{
Label: ast.NewList(ast.NewIdent("string")),
Value: s.schema(n),
})
return
}
// [!~(properties|patternProperties)]: schema
- existing := append(s.patterns, excludeFields(s.obj.Elts))
+ existing := append(s.patterns, excludeFields(obj.Elts))
f := internal.EmbedStruct(ast.NewStruct(&ast.Field{
Label: ast.NewList(ast.NewBinExpr(token.AND, existing...)),
Value: s.schema(n),
}))
- ast.SetRelPos(f, token.NewSection)
- s.obj.Elts = append(s.obj.Elts, f)
+ obj.Elts = append(obj.Elts, f)
default:
s.errf(n, `value of "additionalProperties" must be an object or boolean`)
@@ -616,7 +611,7 @@
case cue.StructKind:
elem := s.schema(n)
ast.SetRelPos(elem, token.NoRelPos)
- s.addConjunct(n, ast.NewList(&ast.Ellipsis{Type: elem}))
+ s.add(n, arrayType, ast.NewList(&ast.Ellipsis{Type: elem}))
case cue.ListKind:
var a []ast.Expr
@@ -626,7 +621,7 @@
a = append(a, v)
}
s.list = ast.NewList(a...)
- s.addConjunct(n, s.list)
+ s.add(n, arrayType, s.list)
default:
s.errf(n, `value of "items" must be an object or array`)
@@ -655,7 +650,8 @@
list := s.addImport(n, "list")
// TODO: Passing non-concrete values is not yet supported in CUE.
if x := s.schema(n); !isAny(x) {
- s.addConjunct(n, ast.NewCall(ast.NewSel(list, "Contains"), clearPos(x)))
+ x := ast.NewCall(ast.NewSel(list, "Contains"), clearPos(x))
+ s.add(n, arrayType, x)
}
}),
@@ -671,7 +667,7 @@
for ; p > 0; p-- {
a = append(a, ast.NewIdent("_"))
}
- s.addConjunct(n, ast.NewList(append(a, &ast.Ellipsis{})...))
+ s.add(n, arrayType, ast.NewList(append(a, &ast.Ellipsis{})...))
// TODO: use this once constraint resolution is properly implemented.
// list := s.addImport(n, "list")
@@ -681,14 +677,16 @@
p1("maxItems", func(n cue.Value, s *state) {
s.usedTypes |= cue.ListKind
list := s.addImport(n, "list")
- s.addConjunct(n, ast.NewCall(ast.NewSel(list, "MaxItems"), clearPos(s.uint(n))))
+ x := ast.NewCall(ast.NewSel(list, "MaxItems"), clearPos(s.uint(n)))
+ s.add(n, arrayType, x)
+
}),
p1("uniqueItems", func(n cue.Value, s *state) {
s.usedTypes |= cue.ListKind
if s.boolValue(n) {
list := s.addImport(n, "list")
- s.addConjunct(n, ast.NewCall(ast.NewSel(list, "UniqueItems")))
+ s.add(n, arrayType, ast.NewCall(ast.NewSel(list, "UniqueItems")))
}
}),
}
diff --git a/encoding/jsonschema/decode.go b/encoding/jsonschema/decode.go
index 20ffc73..4be4718 100644
--- a/encoding/jsonschema/decode.go
+++ b/encoding/jsonschema/decode.go
@@ -20,8 +20,8 @@
import (
"fmt"
- "math/bits"
"net/url"
+ "sort"
"strings"
"cuelang.org/go/cue"
@@ -225,6 +225,85 @@
// const draftCutoff = 5
+type coreType int
+
+const (
+ nullType coreType = iota
+ boolType
+ numType
+ stringType
+ arrayType
+ objectType
+
+ numCoreTypes
+)
+
+var coreToCUE = []cue.Kind{
+ nullType: cue.NullKind,
+ boolType: cue.BoolKind,
+ numType: cue.FloatKind,
+ stringType: cue.StringKind,
+ arrayType: cue.ListKind,
+ objectType: cue.StructKind,
+}
+
+func kindToAST(k cue.Kind) ast.Expr {
+ switch k {
+ case cue.NullKind:
+ // TODO: handle OpenAPI restrictions.
+ return ast.NewNull()
+ case cue.BoolKind:
+ return ast.NewIdent("bool")
+ case cue.FloatKind:
+ return ast.NewIdent("number")
+ case cue.StringKind:
+ return ast.NewIdent("string")
+ case cue.ListKind:
+ return ast.NewList(&ast.Ellipsis{})
+ case cue.StructKind:
+ return ast.NewStruct(&ast.Ellipsis{})
+ }
+ return nil
+}
+
+var coreTypeName = []string{
+ nullType: "null",
+ boolType: "bool",
+ numType: "number",
+ stringType: "string",
+ arrayType: "array",
+ objectType: "object",
+}
+
+type constraintInfo struct {
+ // typ is an identifier for the root type, if present.
+ // This can be omitted if there are constraints.
+ typ ast.Expr
+ constraints []ast.Expr
+}
+
+func (c *constraintInfo) setTypeUsed(n cue.Value, t coreType) {
+ c.typ = kindToAST(coreToCUE[t])
+ setPos(c.typ, n)
+ ast.SetRelPos(c.typ, token.NoRelPos)
+}
+
+func (c *constraintInfo) add(n cue.Value, x ast.Expr) {
+ if !isAny(x) {
+ setPos(x, n)
+ ast.SetRelPos(x, token.NoRelPos)
+ c.constraints = append(c.constraints, x)
+ }
+}
+
+func (s *state) add(n cue.Value, t coreType, x ast.Expr) {
+ s.types[t].add(n, x)
+}
+
+func (s *state) setTypeUsed(n cue.Value, t coreType) {
+ s.types[t].setTypeUsed(n, t)
+}
+
type state struct {
*decoder
@@ -240,7 +319,10 @@
pos cue.Value
- typeOptional bool
+ // The constraints in types represent disjunctions per type.
+ types [numCoreTypes]constraintInfo
+ all constraintInfo // values and oneOf etc.
+
usedTypes cue.Kind
allowedTypes cue.Kind
@@ -254,8 +336,6 @@
definitions []ast.Decl
- conjuncts []ast.Expr
-
// Used for inserting definitions, properties, etc.
hasSelfReference bool
obj *ast.StructLit
@@ -279,9 +359,24 @@
refs []*ast.Ident
}
+func (s *state) object(n cue.Value) *ast.StructLit {
+ if s.obj == nil {
+ s.obj = &ast.StructLit{}
+ s.add(n, objectType, s.obj)
+ }
+ return s.obj
+}
+
func (s *state) hasConstraints() bool {
- return len(s.conjuncts) > 0 ||
- len(s.patterns) > 0 ||
+ if len(s.all.constraints) > 0 {
+ return true
+ }
+ for _, t := range s.types {
+ if len(t.constraints) > 0 {
+ return true
+ }
+ }
+ return len(s.patterns) > 0 ||
s.title != "" ||
s.description != "" ||
s.obj != nil
@@ -295,59 +390,77 @@
conjuncts := []ast.Expr{}
disjuncts := []ast.Expr{}
- add := func(e ast.Expr) {
- disjuncts = append(disjuncts, e) // TODO: use setPos
- }
-
types := s.allowedTypes &^ s.usedTypes
if types == allTypes {
- add(ast.NewIdent("_"))
+ disjuncts = append(disjuncts, ast.NewIdent("_"))
types = 0
}
- if types&cue.FloatKind != 0 {
- add(ast.NewIdent("number"))
- types &^= cue.IntKind
- }
- for types != 0 {
- k := cue.Kind(1 << uint(bits.TrailingZeros(uint(types))))
- types &^= k
- switch k {
- case cue.NullKind:
- // TODO: handle OpenAPI restrictions.
- add(ast.NewNull())
- case cue.BoolKind:
- add(ast.NewIdent("bool"))
- case cue.StringKind:
- add(ast.NewIdent("string"))
- case cue.IntKind:
- add(ast.NewIdent("int"))
- case cue.ListKind:
- add(ast.NewList(&ast.Ellipsis{}))
- case cue.StructKind:
- add(ast.NewStruct(&ast.Ellipsis{}))
+ // Sort literal structs and list last for nicer formatting.
+ sort.SliceStable(s.types[arrayType].constraints, func(i, j int) bool {
+ _, ok := s.types[arrayType].constraints[i].(*ast.ListLit)
+ return !ok
+ })
+ sort.SliceStable(s.types[objectType].constraints, func(i, j int) bool {
+ _, ok := s.types[objectType].constraints[i].(*ast.StructLit)
+ return !ok
+ })
+
+ for i, t := range s.types {
+ k := coreToCUE[i]
+ isAllowed := s.allowedTypes&k != 0
+ if len(t.constraints) > 0 {
+ if t.typ == nil && !isAllowed {
+ for _, c := range t.constraints {
+ s.addErr(errors.Newf(c.Pos(),
+ "constraint not allowed because type %s is excluded",
+ coreTypeName[i],
+ ))
+ }
+ continue
+ }
+ x := ast.NewBinExpr(token.AND, t.constraints...)
+ disjuncts = append(disjuncts, x)
+ } else if s.usedTypes&k != 0 {
+ continue
+ } else if t.typ != nil {
+ if !isAllowed {
+ s.addErr(errors.Newf(t.typ.Pos(),
+ "constraint not allowed because type %s is excluded",
+ coreTypeName[i],
+ ))
+ continue
+ }
+ disjuncts = append(disjuncts, t.typ)
+ } else if types&k != 0 {
+ x := kindToAST(k)
+ if x != nil {
+ disjuncts = append(disjuncts, x)
+ }
}
}
- conjuncts = append(conjuncts, s.conjuncts...)
+ conjuncts = append(conjuncts, s.all.constraints...)
- if s.obj != nil {
+ obj := s.obj
+ if obj == nil {
+ obj, _ = s.types[objectType].typ.(*ast.StructLit)
+ }
+ if obj != nil {
// TODO: may need to explicitly close.
if !s.closeStruct {
- s.obj.Elts = append(s.obj.Elts, &ast.Ellipsis{})
+ obj.Elts = append(obj.Elts, &ast.Ellipsis{})
}
- conjuncts = append(conjuncts, s.obj)
}
- if len(conjuncts) > 0 {
- disjuncts = append(disjuncts,
- ast.NewBinExpr(token.AND, conjuncts...))
+ if len(disjuncts) > 0 {
+ conjuncts = append(conjuncts, ast.NewBinExpr(token.OR, disjuncts...))
}
- if len(disjuncts) == 0 {
+ if len(conjuncts) == 0 {
e = &ast.BottomLit{}
} else {
- e = ast.NewBinExpr(token.OR, disjuncts...)
+ e = ast.NewBinExpr(token.AND, conjuncts...)
}
if s.default_ != nil {
@@ -406,14 +519,6 @@
}
}
-func (s *state) addConjunct(n cue.Value, e ast.Expr) {
- if !isAny(e) {
- ast.SetPos(e, n.Pos())
- ast.SetRelPos(e, token.NoRelPos)
- s.conjuncts = append(s.conjuncts, e)
- }
-}
-
func (s *state) schema(n cue.Value, idRef ...label) ast.Expr {
expr, _ := s.schemaState(n, allTypes, idRef, false)
// TODO: report unused doc.
diff --git a/encoding/jsonschema/decode_test.go b/encoding/jsonschema/decode_test.go
index 39c09f4..adc5bd6 100644
--- a/encoding/jsonschema/decode_test.go
+++ b/encoding/jsonschema/decode_test.go
@@ -69,6 +69,7 @@
var in *cue.Instance
var out, errout []byte
outIndex := -1
+ errIndex := -1
for i, f := range a.Files {
switch path.Ext(f.Name) {
@@ -81,22 +82,31 @@
outIndex = i
case ".err":
errout = f.Data
+ errIndex = i
}
}
if err != nil {
t.Fatal(err)
}
+ updated := false
+
expr, err := Extract(in, cfg)
- if err != nil && errout == nil {
- t.Fatal(errors.Details(err, nil))
- }
- got := []byte(nil)
if err != nil {
- got = []byte(err.Error())
- }
- if !cmp.Equal(errout, got) {
- t.Error(cmp.Diff(string(got), string(errout)))
+ got := []byte(errors.Details(err, nil))
+
+ got = bytes.TrimSpace(got)
+ errout = bytes.TrimSpace(errout)
+
+ switch {
+ case !cmp.Equal(errout, got):
+ if *update {
+ a.Files[errIndex].Data = got
+ updated = true
+ break
+ }
+ t.Error(cmp.Diff(string(got), string(errout)))
+ }
}
if expr != nil {
@@ -115,19 +125,24 @@
b = bytes.TrimSpace(b)
out = bytes.TrimSpace(out)
- if !cmp.Equal(b, out) {
+ switch {
+ case !cmp.Equal(b, out):
if *update {
+ updated = true
a.Files[outIndex].Data = b
- b = txtar.Format(a)
- err = ioutil.WriteFile(fullpath, b, 0644)
- if err != nil {
- t.Fatal(err)
- }
- return
+ break
}
t.Error(cmp.Diff(string(out), string(b)))
}
}
+
+ if updated {
+ b := txtar.Format(a)
+ err = ioutil.WriteFile(fullpath, b, 0644)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
})
return nil
})
diff --git a/encoding/jsonschema/testdata/emptyanyof.txtar b/encoding/jsonschema/testdata/emptyanyof.txtar
new file mode 100644
index 0000000..0313e6d
--- /dev/null
+++ b/encoding/jsonschema/testdata/emptyanyof.txtar
@@ -0,0 +1,26 @@
+-- emptyanyof.json --
+{
+ "$defs": {
+ "shell": {
+ "description": "Specify a shell.",
+ "type": "string",
+ "anyOf": [
+ {
+ },
+ {
+ "enum": [
+ "bash",
+ "sh",
+ "cmd",
+ "powershell"
+ ]
+ }
+ ]
+ }
+ }
+}
+
+-- out.cue --
+_
+
+#shell: (string | ("bash" | "sh" | "cmd" | "powershell")) & string
diff --git a/encoding/jsonschema/testdata/err.txtar b/encoding/jsonschema/testdata/err.txtar
new file mode 100644
index 0000000..2175de8
--- /dev/null
+++ b/encoding/jsonschema/testdata/err.txtar
@@ -0,0 +1,20 @@
+-- type.json --
+{
+ "type": "object",
+
+ "properties": {
+ "multi": {
+ "type": [ "integer" ],
+ "minimum": 2,
+ "maximum": 3,
+ "maxLength": 5
+ }
+ },
+ "additionalProperties": false
+}
+
+-- out.err --
+constraint not allowed because type string is excluded:
+ type.json:9:22
+-- out.cue --
+multi?: int & >=2 & <=3
diff --git a/encoding/jsonschema/testdata/list.txtar b/encoding/jsonschema/testdata/list.txtar
index 3f7bd0a..c48c862 100644
--- a/encoding/jsonschema/testdata/list.txtar
+++ b/encoding/jsonschema/testdata/list.txtar
@@ -41,5 +41,5 @@
foo?: [...string]
tuple?: [string, int, 2]
has?: list.Contains(3)
-size?: [_, _, _, ...] & list.MaxItems(9) & list.UniqueItems()
+size?: list.UniqueItems() & list.MaxItems(9) & [_, _, _, ...]
additional?: [int, int, ...string]
diff --git a/encoding/jsonschema/testdata/num.txtar b/encoding/jsonschema/testdata/num.txtar
index 1968c3a..cbbd984 100644
--- a/encoding/jsonschema/testdata/num.txtar
+++ b/encoding/jsonschema/testdata/num.txtar
@@ -13,10 +13,16 @@
"maximum": 3
},
"exclusive": {
- "type": "number",
+ "type": "integer",
"exclusiveMinimum": 2,
"exclusiveMaximum": 3
},
+ "multi": {
+ "type": [ "integer", "string" ],
+ "minimum": 2,
+ "maximum": 3,
+ "maxLength": 5
+ },
"cents": {
"type": "number",
"multipleOf": 0.05
@@ -26,10 +32,14 @@
}
-- out.cue --
-import "math"
+import (
+ "strings"
+ "math"
+)
constant?: 2
several?: 1 | 2 | 3 | 4
inclusive?: >=2 & <=3
-exclusive?: >2 & <3
+exclusive?: int & >2 & <3
+multi?: int & >=2 & <=3 | strings.MaxRunes(5)
cents?: math.MultipleOf(0.05)
diff --git a/encoding/jsonschema/testdata/object.txtar b/encoding/jsonschema/testdata/object.txtar
index 9f773e1..1e6644e 100644
--- a/encoding/jsonschema/testdata/object.txtar
+++ b/encoding/jsonschema/testdata/object.txtar
@@ -53,6 +53,15 @@
"^\\P{Lo}": { "type": "integer" }
},
"additionalProperties": { "type": "string" }
+ },
+ "multi": {
+ "type": [ "object", "number" ],
+ "properties": {
+ "foo": { "type": "number" },
+ "bar": { "type": "number" }
+ },
+ "maxProperties": 5,
+ "minimum": 7
}
},
"additionalProperties": false
@@ -68,7 +77,6 @@
additional?: {
foo?: number
bar?: number
-
{[!~"^(foo|bar)$"]: string}
}
map?: [string]: string
@@ -94,6 +102,10 @@
{[=~"^\\P{Lu}" & !~"^(foo|bar)$"]: string}
{[=~"^\\P{Lo}" & !~"^(foo|bar)$"]: int}
-
{[!~"^\\P{Lu}" & !~"^\\P{Lo}" & !~"^(foo|bar)$"]: string}
}
+multi?: >=7 | struct.MaxFields(5) & {
+ foo?: number
+ bar?: number
+ ...
+}
diff --git a/encoding/jsonschema/testdata/refroot.txtar b/encoding/jsonschema/testdata/refroot.txtar
index c090506..6f33439 100644
--- a/encoding/jsonschema/testdata/refroot.txtar
+++ b/encoding/jsonschema/testdata/refroot.txtar
@@ -16,7 +16,7 @@
_schema
_schema: {
@jsonschema(schema="http://json-schema.org/draft-07/schema#")
- number | null | bool | string | [...] | {
+ null | bool | number | string | [...] | {
@jsonschema(id="http://cuelang.org/go/encoding/openapi/testdata/order.json")
value?: _
next?: _schema_1
diff --git a/encoding/jsonschema/testdata/refroot2.txtar b/encoding/jsonschema/testdata/refroot2.txtar
index 20e6361..e4a162c 100644
--- a/encoding/jsonschema/testdata/refroot2.txtar
+++ b/encoding/jsonschema/testdata/refroot2.txtar
@@ -14,7 +14,7 @@
_schema
_schema: {
@jsonschema(schema="http://json-schema.org/draft-07/schema#")
- number | null | bool | string | [...] | {
+ null | bool | number | string | [...] | {
value?: _
next?: _schema_1
...
diff --git a/encoding/jsonschema/testdata/typedis.txtar b/encoding/jsonschema/testdata/typedis.txtar
index 6bf10ac..d2a3aeb 100644
--- a/encoding/jsonschema/testdata/typedis.txtar
+++ b/encoding/jsonschema/testdata/typedis.txtar
@@ -34,8 +34,6 @@
}
]
},
-
-
"empty": {
"allOf": [
{ "type": "object" },
@@ -44,11 +42,14 @@
}
}
}
+-- out.err --
+constraint not allowed because type string is excluded:
+ type.json:39:23
-- out.cue --
// Main schema
intOrString1?: int | string
intOrString2?: int | string
-intOrString3?: int | string
-disjunction?: int | string | >=3
+intOrString3?: string | int
+disjunction?: string | int | int & >=3
empty?: _|_
...
diff --git a/encoding/jsonschema/testdata/used.txtar b/encoding/jsonschema/testdata/used.txtar
new file mode 100644
index 0000000..b22c92f
--- /dev/null
+++ b/encoding/jsonschema/testdata/used.txtar
@@ -0,0 +1,48 @@
+-- used.json --
+{
+ "$defs": {
+ "enum": {
+ "type": "string",
+ "enum": [ "a", "b", "c" ]
+ },
+ "lists": {
+ "description": "Single item or lists of various lengths.",
+ "oneOf": [
+ {
+ "type": "string",
+ "enum": [ "a", "b", "c" ]
+ },
+ {
+ "type": "array",
+ "oneOf": [
+ {
+ "items": [ { "const": "X" } ]
+ },
+ {
+ "items": [
+ { "const": "X" },
+ {
+ "type": "string",
+ "enum": [ "a", "b", "c" ]
+ }
+ ]
+ },
+ {
+ "items": [
+ { "const": "X" },
+ { "enum": [ "d", "e", "f" ] }
+ ]
+ }
+ ],
+ "additionalItems": false
+ }
+ ]
+ }
+ }
+}
+-- out.cue --
+_
+
+#enum: "a" | "b" | "c"
+
+#lists: "a" | "b" | "c" | (["X"] | ["X", "a" | "b" | "c"] | ["X", "d" | "e" | "f"])