encoding/jsonschema: fix type exclusion logic

The current implementation misinterpreted the spec
that constraints from a certain type imply a value is of
that type: it does not.

This means that if, for instance, a schema contains only
constraints for a struct, one must still explicitly allow all
other types besides struct in CUE. The new logic does so,
and also addes logic to opimize

Change-Id: I69fecd9b6888b0dad89db194c17acb56841de490
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5648
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/jsonschema/decode.go b/encoding/jsonschema/decode.go
index 09838e6..bf58b4b 100644
--- a/encoding/jsonschema/decode.go
+++ b/encoding/jsonschema/decode.go
@@ -20,6 +20,7 @@
 
 import (
 	"fmt"
+	"math/bits"
 	"sort"
 	"strings"
 
@@ -111,7 +112,7 @@
 	inner := len(ref) - 1
 	name := ref[inner]
 
-	expr, state := root.schemaState(v)
+	expr, state := root.schemaState(v, allTypes, false)
 
 	tags := []string{}
 	if state.jsonschema != "" {
@@ -204,13 +205,15 @@
 type state struct {
 	*decoder
 
+	parent *state
+
 	path []string
 
 	pos cue.Value
 
-	types        []cue.Value
 	typeOptional bool
-	kind         cue.Kind
+	usedTypes    cue.Kind
+	allowedTypes cue.Kind
 
 	default_    ast.Expr
 	examples    []ast.Expr
@@ -229,51 +232,56 @@
 	list *ast.ListLit
 }
 
+func (s *state) hasConstraints() bool {
+	return len(s.conjuncts) > 0 ||
+		len(s.patterns) > 0 ||
+		s.title != "" ||
+		s.description != "" ||
+		s.obj != nil ||
+		s.list != nil
+}
+
+const allTypes = cue.NullKind | cue.BoolKind | cue.NumberKind | cue.IntKind |
+	cue.StringKind | cue.ListKind | cue.StructKind
+
 // finalize constructs a CUE type from the collected constraints.
 func (s *state) finalize() (e ast.Expr) {
-	if s.typeOptional || s.kind != 0 {
-		if len(s.types) > 1 {
-			s.errf(s.pos, "constraints require specific type")
-		}
-		s.types = nil
-	}
-
 	conjuncts := []ast.Expr{}
 	disjuncts := []ast.Expr{}
-	for _, n := range s.types {
-		add := func(e ast.Expr) {
-			disjuncts = append(disjuncts, setPos(e, n))
-		}
-		str, ok := s.strValue(n)
-		if !ok {
-			s.errf(n, "type value should be a string")
-			return
-		}
-		switch str {
-		case "null":
+
+	add := func(e ast.Expr) {
+		disjuncts = append(disjuncts, e) // TODO: use setPos
+	}
+
+	types := s.allowedTypes &^ s.usedTypes
+	if types == allTypes {
+		add(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.NewIdent("null"))
-		case "boolean":
+		case cue.BoolKind:
 			add(ast.NewIdent("bool"))
-		case "string":
+		case cue.StringKind:
 			add(ast.NewIdent("string"))
-		case "number":
-			add(ast.NewIdent("number"))
-		case "integer":
+		case cue.IntKind:
 			add(ast.NewIdent("int"))
-		case "array":
-			if s.kind&cue.ListKind == 0 {
-				add(ast.NewList(&ast.Ellipsis{}))
-			}
-		case "object":
+		case cue.ListKind:
+			add(ast.NewList(&ast.Ellipsis{}))
+		case cue.StructKind:
 			add(ast.NewStruct(&ast.Ellipsis{}))
-		default:
-			s.errf(n, "unknown type %q", n)
 		}
 	}
-	if len(disjuncts) > 0 {
-		conjuncts = append(conjuncts, ast.NewBinExpr(token.OR, disjuncts...))
-	}
 
 	conjuncts = append(conjuncts, s.conjuncts...)
 
@@ -284,17 +292,22 @@
 		conjuncts = append(conjuncts, s.obj)
 	}
 
-	if len(conjuncts) == 0 {
-		return ast.NewString(fmt.Sprint(s.pos))
+	if len(conjuncts) > 0 {
+		disjuncts = append(disjuncts,
+			ast.NewBinExpr(token.AND, conjuncts...))
 	}
 
-	e = ast.NewBinExpr(token.AND, conjuncts...)
+	if len(disjuncts) == 0 {
+		e = &ast.BottomLit{}
+	} else {
+		e = ast.NewBinExpr(token.OR, disjuncts...)
+	}
 
 	if s.default_ != nil {
 		// check conditions where default can be skipped.
 		switch x := s.default_.(type) {
 		case *ast.ListLit:
-			if s.kind == cue.ListKind && len(x.Elts) == 0 {
+			if s.usedTypes == cue.ListKind && len(x.Elts) == 0 {
 				return e
 			}
 		}
@@ -303,6 +316,11 @@
 	return e
 }
 
+func isAny(s ast.Expr) bool {
+	i, ok := s.(*ast.Ident)
+	return ok && i.Name == "_"
+}
+
 func (s *state) comment() *ast.CommentGroup {
 	// Create documentation.
 	doc := strings.TrimSpace(s.title)
@@ -327,18 +345,30 @@
 	}
 }
 
-func (s *state) add(e ast.Expr) {
-	s.conjuncts = append(s.conjuncts, e)
+func (s *state) addConjunct(e ast.Expr) {
+	if !isAny(e) {
+		s.conjuncts = append(s.conjuncts, e)
+	}
 }
 
 func (s *state) schema(n cue.Value) ast.Expr {
-	expr, _ := s.schemaState(n)
+	expr, _ := s.schemaState(n, allTypes, false)
 	// TODO: report unused doc.
 	return expr
 }
 
-func (s *state) schemaState(n cue.Value) (ast.Expr, *state) {
-	state := &state{path: s.path, pos: n, decoder: s.decoder}
+// schemaState is a low-level API for schema. isLogical specifies whether the
+// caller is a logical operator like anyOf, allOf, oneOf, or not.
+func (s *state) schemaState(n cue.Value, types cue.Kind, isLogical bool) (ast.Expr, *state) {
+	state := &state{
+		decoder:      s.decoder,
+		allowedTypes: types,
+		path:         s.path,
+		pos:          n,
+	}
+	if isLogical {
+		state.parent = s
+	}
 
 	if n.Kind() != cue.StructKind {
 		return s.errf(n, "schema expects mapping node, found %s", n.Kind()), state
@@ -365,7 +395,10 @@
 }
 
 func (s *state) value(n cue.Value) ast.Expr {
-	switch n.Kind() {
+	k := n.Kind()
+	s.usedTypes |= k
+	s.allowedTypes &= k
+	switch k {
 	case cue.ListKind:
 		a := []ast.Expr{}
 		for i, _ := n.List(); i.Next(); {
@@ -409,10 +442,16 @@
 	}
 }
 
-func list(n cue.Value) (a []cue.Value) {
+func (s *state) listItems(name string, n cue.Value, allowEmpty bool) (a []cue.Value) {
+	if n.Kind() != cue.ListKind {
+		s.errf(n, `value of %q must be an array, found %v`, name, n.Kind())
+	}
 	for i, _ := n.List(); i.Next(); {
 		a = append(a, i.Value())
 	}
+	if !allowEmpty && len(a) == 0 {
+		s.errf(n, `array for %q must be non-empty`, name)
+	}
 	return a
 }