github.com/solo-io/cue@v0.4.7/encoding/jsonschema/constraints.go (about)

     1  // Copyright 2019 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package jsonschema
    16  
    17  import (
    18  	"fmt"
    19  	"math/big"
    20  	"path"
    21  	"regexp"
    22  
    23  	"github.com/solo-io/cue/cue"
    24  	"github.com/solo-io/cue/cue/ast"
    25  	"github.com/solo-io/cue/cue/errors"
    26  	"github.com/solo-io/cue/cue/token"
    27  	"github.com/solo-io/cue/internal"
    28  )
    29  
    30  // TODO: skip invalid regexps containing ?! and foes.
    31  // alternatively, fall back to  https://github.com/dlclark/regexp2
    32  
    33  type constraint struct {
    34  	key string
    35  
    36  	// phase indicates on which pass c constraint should be added. This ensures
    37  	// that constraints are applied in the correct order. For instance, the
    38  	// "required" constraint validates that a listed field is contained in
    39  	// "properties". For this to work, "properties" must be processed before
    40  	// "required" and thus must have a lower phase number than the latter.
    41  	phase int
    42  
    43  	// Indicates the draft number in which this constraint is defined.
    44  	draft int
    45  	fn    constraintFunc
    46  }
    47  
    48  // A constraintFunc converts a given JSON Schema constraint (specified in n)
    49  // to a CUE constraint recorded in state.
    50  type constraintFunc func(n cue.Value, s *state)
    51  
    52  func p0(name string, f constraintFunc) *constraint {
    53  	return &constraint{key: name, fn: f}
    54  }
    55  
    56  func p1d(name string, draft int, f constraintFunc) *constraint {
    57  	return &constraint{key: name, phase: 1, draft: draft, fn: f}
    58  }
    59  
    60  func p1(name string, f constraintFunc) *constraint {
    61  	return &constraint{key: name, phase: 1, fn: f}
    62  }
    63  
    64  func p2(name string, f constraintFunc) *constraint {
    65  	return &constraint{key: name, phase: 2, fn: f}
    66  }
    67  
    68  func p3(name string, f constraintFunc) *constraint {
    69  	return &constraint{key: name, phase: 3, fn: f}
    70  }
    71  
    72  // TODO:
    73  // writeOnly, readOnly
    74  
    75  var constraintMap = map[string]*constraint{}
    76  
    77  func init() {
    78  	for _, c := range constraints {
    79  		constraintMap[c.key] = c
    80  	}
    81  }
    82  
    83  func addDefinitions(n cue.Value, s *state) {
    84  	if n.Kind() != cue.StructKind {
    85  		s.errf(n, `"definitions" expected an object, found %s`, n.Kind())
    86  	}
    87  
    88  	old := s.isSchema
    89  	s.isSchema = true
    90  	defer func() { s.isSchema = old }()
    91  
    92  	s.processMap(n, func(key string, n cue.Value) {
    93  		name := key
    94  
    95  		var f *ast.Field
    96  
    97  		ident := "#" + name
    98  		if ast.IsValidIdent(ident) {
    99  			f = &ast.Field{Value: s.schema(n, label{ident, true})}
   100  			f.Label = ast.NewIdent(ident)
   101  		} else {
   102  			f = &ast.Field{Value: s.schema(n, label{"#", true}, label{name: name})}
   103  			f.Label = ast.NewString(name)
   104  			ident = "#"
   105  			f = &ast.Field{
   106  				Label: ast.NewIdent("#"),
   107  				Value: ast.NewStruct(f),
   108  			}
   109  		}
   110  
   111  		ast.SetRelPos(f, token.NewSection)
   112  		s.definitions = append(s.definitions, f)
   113  		s.setField(label{name: ident, isDef: true}, f)
   114  	})
   115  }
   116  
   117  var constraints = []*constraint{
   118  	// Meta data.
   119  
   120  	p0("$schema", func(n cue.Value, s *state) {
   121  		// Identifies this as a JSON schema and specifies its version.
   122  		// TODO: extract version.
   123  		s.jsonschema, _ = s.strValue(n)
   124  	}),
   125  
   126  	p0("$id", func(n cue.Value, s *state) {
   127  		// URL: https://domain.com/schemas/foo.json
   128  		// anchors: #identifier
   129  		//
   130  		// TODO: mark identifiers.
   131  
   132  		// Resolution must be relative to parent $id
   133  		// https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.2
   134  		u := s.resolveURI(n)
   135  		if u == nil {
   136  			return
   137  		}
   138  
   139  		if u.Fragment != "" {
   140  			if s.cfg.Strict {
   141  				s.errf(n, "$id URI may not contain a fragment")
   142  			}
   143  			return
   144  		}
   145  		s.id = u
   146  
   147  		obj := s.object(n)
   148  
   149  		// TODO: handle the case where this is always defined and we don't want
   150  		// to include the default value.
   151  		obj.Elts = append(obj.Elts, &ast.Attribute{
   152  			Text: fmt.Sprintf("@jsonschema(id=%q)", u)})
   153  	}),
   154  
   155  	// Generic constraint
   156  
   157  	p1("type", func(n cue.Value, s *state) {
   158  		var types cue.Kind
   159  		set := func(n cue.Value) {
   160  			str, ok := s.strValue(n)
   161  			if !ok {
   162  				s.errf(n, "type value should be a string")
   163  			}
   164  			switch str {
   165  			case "null":
   166  				types |= cue.NullKind
   167  				s.setTypeUsed(n, nullType)
   168  				// TODO: handle OpenAPI restrictions.
   169  			case "boolean":
   170  				types |= cue.BoolKind
   171  				s.setTypeUsed(n, boolType)
   172  			case "string":
   173  				types |= cue.StringKind
   174  				s.setTypeUsed(n, stringType)
   175  			case "number":
   176  				types |= cue.NumberKind
   177  				s.setTypeUsed(n, numType)
   178  			case "integer":
   179  				types |= cue.IntKind
   180  				s.setTypeUsed(n, numType)
   181  				s.add(n, numType, ast.NewIdent("int"))
   182  			case "array":
   183  				types |= cue.ListKind
   184  				s.setTypeUsed(n, arrayType)
   185  			case "object":
   186  				types |= cue.StructKind
   187  				s.setTypeUsed(n, objectType)
   188  
   189  			default:
   190  				s.errf(n, "unknown type %q", n)
   191  			}
   192  		}
   193  
   194  		switch n.Kind() {
   195  		case cue.StringKind:
   196  			set(n)
   197  		case cue.ListKind:
   198  			for i, _ := n.List(); i.Next(); {
   199  				set(i.Value())
   200  			}
   201  		default:
   202  			s.errf(n, `value of "type" must be a string or list of strings`)
   203  		}
   204  
   205  		s.allowedTypes &= types
   206  	}),
   207  
   208  	p1("enum", func(n cue.Value, s *state) {
   209  		var a []ast.Expr
   210  		for _, x := range s.listItems("enum", n, true) {
   211  			a = append(a, s.value(x))
   212  		}
   213  		s.all.add(n, ast.NewBinExpr(token.OR, a...))
   214  	}),
   215  
   216  	// TODO: only allow for OpenAPI.
   217  	p1("nullable", func(n cue.Value, s *state) {
   218  		null := ast.NewNull()
   219  		setPos(null, n)
   220  		s.nullable = null
   221  	}),
   222  
   223  	p1d("const", 6, func(n cue.Value, s *state) {
   224  		s.all.add(n, s.value(n))
   225  	}),
   226  
   227  	p1("default", func(n cue.Value, s *state) {
   228  		sc := *s
   229  		s.default_ = sc.value(n)
   230  		// TODO: must validate that the default is subsumed by the normal value,
   231  		// as CUE will otherwise broaden the accepted values with the default.
   232  		s.examples = append(s.examples, s.default_)
   233  	}),
   234  
   235  	p1("deprecated", func(n cue.Value, s *state) {
   236  		if s.boolValue(n) {
   237  			s.deprecated = true
   238  		}
   239  	}),
   240  
   241  	p1("examples", func(n cue.Value, s *state) {
   242  		if n.Kind() != cue.ListKind {
   243  			s.errf(n, `value of "examples" must be an array, found %v`, n.Kind)
   244  		}
   245  		// TODO: implement examples properly.
   246  		// for _, n := range s.listItems("examples", n, true) {
   247  		// 	if ex := s.value(n); !isAny(ex) {
   248  		// 		s.examples = append(s.examples, ex)
   249  		// 	}
   250  		// }
   251  	}),
   252  
   253  	p1("description", func(n cue.Value, s *state) {
   254  		s.description, _ = s.strValue(n)
   255  	}),
   256  
   257  	p1("title", func(n cue.Value, s *state) {
   258  		s.title, _ = s.strValue(n)
   259  	}),
   260  
   261  	p1d("$comment", 7, func(n cue.Value, s *state) {
   262  	}),
   263  
   264  	p1("$defs", addDefinitions),
   265  	p1("definitions", addDefinitions),
   266  	p1("$ref", func(n cue.Value, s *state) {
   267  		s.usedTypes = allTypes
   268  
   269  		u := s.resolveURI(n)
   270  
   271  		if u.Fragment != "" && !path.IsAbs(u.Fragment) {
   272  			s.addErr(errors.Newf(n.Pos(), "anchors (%s) not supported", u.Fragment))
   273  			// TODO: support anchors
   274  			return
   275  		}
   276  
   277  		expr := s.makeCUERef(n, u)
   278  
   279  		if expr == nil {
   280  			expr = &ast.BadExpr{From: n.Pos()}
   281  		}
   282  
   283  		s.all.add(n, expr)
   284  	}),
   285  
   286  	// Combinators
   287  
   288  	// TODO: work this out in more detail: oneOf and anyOf below have the same
   289  	// implementation in CUE. The distinction is that for anyOf a result is
   290  	// allowed to be ambiguous at the end, whereas for oneOf a disjunction must
   291  	// be fully resolved. There is currently no easy way to set this distinction
   292  	// in CUE.
   293  	//
   294  	// One could correctly write oneOf like this once 'not' is implemented:
   295  	//
   296  	//   oneOf(a, b, c) :-
   297  	//      anyOf(
   298  	//         allOf(a, not(b), not(c)),
   299  	//         allOf(not(a), b, not(c)),
   300  	//         allOf(not(a), not(b), c),
   301  	//   ))
   302  	//
   303  	// This is not necessary if the values are mutually exclusive/ have a
   304  	// discriminator.
   305  
   306  	p2("allOf", func(n cue.Value, s *state) {
   307  		var a []ast.Expr
   308  		for _, v := range s.listItems("allOf", n, false) {
   309  			x, sub := s.schemaState(v, s.allowedTypes, nil, true)
   310  			s.allowedTypes &= sub.allowedTypes
   311  			s.usedTypes |= sub.usedTypes
   312  			if sub.hasConstraints() {
   313  				a = append(a, x)
   314  			}
   315  		}
   316  		if len(a) > 0 {
   317  			s.all.add(n, ast.NewBinExpr(token.AND, a...))
   318  		}
   319  	}),
   320  
   321  	p2("anyOf", func(n cue.Value, s *state) {
   322  		var types cue.Kind
   323  		var a []ast.Expr
   324  		for _, v := range s.listItems("anyOf", n, false) {
   325  			x, sub := s.schemaState(v, s.allowedTypes, nil, true)
   326  			types |= sub.allowedTypes
   327  			a = append(a, x)
   328  		}
   329  		s.allowedTypes &= types
   330  		if len(a) > 0 {
   331  			s.all.add(n, ast.NewBinExpr(token.OR, a...))
   332  		}
   333  	}),
   334  
   335  	p2("oneOf", func(n cue.Value, s *state) {
   336  		var types cue.Kind
   337  		var a []ast.Expr
   338  		hasSome := false
   339  		for _, v := range s.listItems("oneOf", n, false) {
   340  			x, sub := s.schemaState(v, s.allowedTypes, nil, true)
   341  			types |= sub.allowedTypes
   342  
   343  			// TODO: make more finegrained by making it two pass.
   344  			if sub.hasConstraints() {
   345  				hasSome = true
   346  			}
   347  
   348  			if !isAny(x) {
   349  				a = append(a, x)
   350  			}
   351  		}
   352  		s.allowedTypes &= types
   353  		if len(a) > 0 && hasSome {
   354  			s.usedTypes = allTypes
   355  			s.all.add(n, ast.NewBinExpr(token.OR, a...))
   356  		}
   357  
   358  		// TODO: oneOf({a:x}, {b:y}, ..., not(anyOf({a:x}, {b:y}, ...))),
   359  		// can be translated to {} | {a:x}, {b:y}, ...
   360  	}),
   361  
   362  	// String constraints
   363  
   364  	p1("pattern", func(n cue.Value, s *state) {
   365  		str, _ := n.String()
   366  		if _, err := regexp.Compile(str); err != nil {
   367  			if s.cfg.Strict {
   368  				s.errf(n, "unsupported regexp: %v", err)
   369  			}
   370  			return
   371  		}
   372  		s.usedTypes |= cue.StringKind
   373  		s.add(n, stringType, &ast.UnaryExpr{Op: token.MAT, X: s.string(n)})
   374  	}),
   375  
   376  	p1("minLength", func(n cue.Value, s *state) {
   377  		s.usedTypes |= cue.StringKind
   378  		min := s.number(n)
   379  		strings := s.addImport(n, "strings")
   380  		s.add(n, stringType, ast.NewCall(ast.NewSel(strings, "MinRunes"), min))
   381  	}),
   382  
   383  	p1("maxLength", func(n cue.Value, s *state) {
   384  		s.usedTypes |= cue.StringKind
   385  		max := s.number(n)
   386  		strings := s.addImport(n, "strings")
   387  		s.add(n, stringType, ast.NewCall(ast.NewSel(strings, "MaxRunes"), max))
   388  	}),
   389  
   390  	p1d("contentMediaType", 7, func(n cue.Value, s *state) {
   391  		// TODO: only mark as used if it generates something.
   392  		// s.usedTypes |= cue.StringKind
   393  	}),
   394  
   395  	p1d("contentEncoding", 7, func(n cue.Value, s *state) {
   396  		// TODO: only mark as used if it generates something.
   397  		// s.usedTypes |= cue.StringKind
   398  		// 7bit, 8bit, binary, quoted-printable and base64.
   399  		// RFC 2054, part 6.1.
   400  		// https://tools.ietf.org/html/rfc2045
   401  		// TODO: at least handle bytes.
   402  	}),
   403  
   404  	// Number constraints
   405  
   406  	p2("minimum", func(n cue.Value, s *state) {
   407  		s.usedTypes |= cue.NumberKind
   408  		op := token.GEQ
   409  		if s.exclusiveMin {
   410  			op = token.GTR
   411  		}
   412  		s.add(n, numType, &ast.UnaryExpr{Op: op, X: s.number(n)})
   413  	}),
   414  
   415  	p1("exclusiveMinimum", func(n cue.Value, s *state) {
   416  		if n.Kind() == cue.BoolKind {
   417  			s.exclusiveMin = true
   418  			return
   419  		}
   420  		s.usedTypes |= cue.NumberKind
   421  		s.add(n, numType, &ast.UnaryExpr{Op: token.GTR, X: s.number(n)})
   422  	}),
   423  
   424  	p2("maximum", func(n cue.Value, s *state) {
   425  		s.usedTypes |= cue.NumberKind
   426  		op := token.LEQ
   427  		if s.exclusiveMax {
   428  			op = token.LSS
   429  		}
   430  		s.add(n, numType, &ast.UnaryExpr{Op: op, X: s.number(n)})
   431  	}),
   432  
   433  	p1("exclusiveMaximum", func(n cue.Value, s *state) {
   434  		if n.Kind() == cue.BoolKind {
   435  			s.exclusiveMax = true
   436  			return
   437  		}
   438  		s.usedTypes |= cue.NumberKind
   439  		s.add(n, numType, &ast.UnaryExpr{Op: token.LSS, X: s.number(n)})
   440  	}),
   441  
   442  	p1("multipleOf", func(n cue.Value, s *state) {
   443  		s.usedTypes |= cue.NumberKind
   444  		multiple := s.number(n)
   445  		var x big.Int
   446  		_, _ = n.MantExp(&x)
   447  		if x.Cmp(big.NewInt(0)) != 1 {
   448  			s.errf(n, `"multipleOf" value must be < 0; found %s`, n)
   449  		}
   450  		math := s.addImport(n, "math")
   451  		s.add(n, numType, ast.NewCall(ast.NewSel(math, "MultipleOf"), multiple))
   452  	}),
   453  
   454  	// Object constraints
   455  
   456  	p1("properties", func(n cue.Value, s *state) {
   457  		s.usedTypes |= cue.StructKind
   458  		obj := s.object(n)
   459  
   460  		if n.Kind() != cue.StructKind {
   461  			s.errf(n, `"properties" expected an object, found %v`, n.Kind())
   462  		}
   463  
   464  		s.processMap(n, func(key string, n cue.Value) {
   465  			// property?: value
   466  			name := ast.NewString(key)
   467  			expr, state := s.schemaState(n, allTypes, []label{{name: key}}, false)
   468  			f := &ast.Field{Label: name, Value: expr}
   469  			state.doc(f)
   470  			f.Optional = token.Blank.Pos()
   471  			if len(obj.Elts) > 0 && len(f.Comments()) > 0 {
   472  				// TODO: change formatter such that either a a NewSection on the
   473  				// field or doc comment will cause a new section.
   474  				ast.SetRelPos(f.Comments()[0], token.NewSection)
   475  			}
   476  			if state.deprecated {
   477  				switch expr.(type) {
   478  				case *ast.StructLit:
   479  					obj.Elts = append(obj.Elts, addTag(name, "deprecated", ""))
   480  				default:
   481  					f.Attrs = append(f.Attrs, internal.NewAttr("deprecated", ""))
   482  				}
   483  			}
   484  			obj.Elts = append(obj.Elts, f)
   485  			s.setField(label{name: key}, f)
   486  		})
   487  	}),
   488  
   489  	p2("required", func(n cue.Value, s *state) {
   490  		if n.Kind() != cue.ListKind {
   491  			s.errf(n, `value of "required" must be list of strings, found %v`, n.Kind)
   492  			return
   493  		}
   494  
   495  		s.usedTypes |= cue.StructKind
   496  
   497  		// TODO: detect that properties is defined somewhere.
   498  		// s.errf(n, `"required" without a "properties" field`)
   499  		obj := s.object(n)
   500  
   501  		// Create field map
   502  		fields := map[string]*ast.Field{}
   503  		for _, d := range obj.Elts {
   504  			f, ok := d.(*ast.Field)
   505  			if !ok {
   506  				continue // Could be embedding? See cirrus.json
   507  			}
   508  			str, _, err := ast.LabelName(f.Label)
   509  			if err == nil {
   510  				fields[str] = f
   511  			}
   512  		}
   513  
   514  		for _, n := range s.listItems("required", n, true) {
   515  			str, ok := s.strValue(n)
   516  			f := fields[str]
   517  			if f == nil && ok {
   518  				f := &ast.Field{
   519  					Label: ast.NewString(str),
   520  					Value: ast.NewIdent("_"),
   521  				}
   522  				fields[str] = f
   523  				obj.Elts = append(obj.Elts, f)
   524  				continue
   525  			}
   526  			if f.Optional == token.NoPos {
   527  				s.errf(n, "duplicate required field %q", str)
   528  			}
   529  			f.Optional = token.NoPos
   530  		}
   531  	}),
   532  
   533  	p1d("propertyNames", 6, func(n cue.Value, s *state) {
   534  		// [=~pattern]: _
   535  		if names, _ := s.schemaState(n, cue.StringKind, nil, false); !isAny(names) {
   536  			s.usedTypes |= cue.StructKind
   537  			x := ast.NewStruct(ast.NewList(names), ast.NewIdent("_"))
   538  			s.add(n, objectType, x)
   539  		}
   540  	}),
   541  
   542  	// TODO: reenable when we have proper non-monotonic contraint validation.
   543  	// p1("minProperties", func(n cue.Value, s *state) {
   544  	// 	s.usedTypes |= cue.StructKind
   545  
   546  	// 	pkg := s.addImport(n, "struct")
   547  	// 	s.addConjunct(n, ast.NewCall(ast.NewSel(pkg, "MinFields"), s.uint(n)))
   548  	// }),
   549  
   550  	p1("maxProperties", func(n cue.Value, s *state) {
   551  		s.usedTypes |= cue.StructKind
   552  
   553  		pkg := s.addImport(n, "struct")
   554  		x := ast.NewCall(ast.NewSel(pkg, "MaxFields"), s.uint(n))
   555  		s.add(n, objectType, x)
   556  	}),
   557  
   558  	p1("dependencies", func(n cue.Value, s *state) {
   559  		s.usedTypes |= cue.StructKind
   560  
   561  		// Schema and property dependencies.
   562  		// TODO: the easiest implementation is with comprehensions.
   563  		// The nicer implementation is with disjunctions. This has to be done
   564  		// at the very end, replacing properties.
   565  		/*
   566  			*{ property?: _|_ } | {
   567  				property: _
   568  				schema
   569  			}
   570  		*/
   571  	}),
   572  
   573  	p2("patternProperties", func(n cue.Value, s *state) {
   574  		s.usedTypes |= cue.StructKind
   575  		if n.Kind() != cue.StructKind {
   576  			s.errf(n, `value of "patternProperties" must be an an object, found %v`, n.Kind)
   577  		}
   578  		obj := s.object(n)
   579  		existing := excludeFields(s.obj.Elts)
   580  		s.processMap(n, func(key string, n cue.Value) {
   581  			// [!~(properties) & pattern]: schema
   582  			s.patterns = append(s.patterns,
   583  				&ast.UnaryExpr{Op: token.NMAT, X: ast.NewString(key)})
   584  			f := internal.EmbedStruct(ast.NewStruct(&ast.Field{
   585  				Label: ast.NewList(ast.NewBinExpr(token.AND,
   586  					&ast.UnaryExpr{Op: token.MAT, X: ast.NewString(key)},
   587  					existing)),
   588  				Value: s.schema(n),
   589  			}))
   590  			ast.SetRelPos(f, token.NewSection)
   591  			obj.Elts = append(obj.Elts, f)
   592  		})
   593  	}),
   594  
   595  	p3("additionalProperties", func(n cue.Value, s *state) {
   596  		switch n.Kind() {
   597  		case cue.BoolKind:
   598  			s.closeStruct = !s.boolValue(n)
   599  
   600  		case cue.StructKind:
   601  			s.usedTypes |= cue.StructKind
   602  			s.closeStruct = true
   603  			obj := s.object(n)
   604  			if len(obj.Elts) == 0 {
   605  				obj.Elts = append(obj.Elts, &ast.Field{
   606  					Label: ast.NewList(ast.NewIdent("string")),
   607  					Value: s.schema(n),
   608  				})
   609  				return
   610  			}
   611  			// [!~(properties|patternProperties)]: schema
   612  			existing := append(s.patterns, excludeFields(obj.Elts))
   613  			f := internal.EmbedStruct(ast.NewStruct(&ast.Field{
   614  				Label: ast.NewList(ast.NewBinExpr(token.AND, existing...)),
   615  				Value: s.schema(n),
   616  			}))
   617  			obj.Elts = append(obj.Elts, f)
   618  
   619  		default:
   620  			s.errf(n, `value of "additionalProperties" must be an object or boolean`)
   621  		}
   622  	}),
   623  
   624  	// Array constraints.
   625  
   626  	p1("items", func(n cue.Value, s *state) {
   627  		s.usedTypes |= cue.ListKind
   628  		switch n.Kind() {
   629  		case cue.StructKind:
   630  			elem := s.schema(n)
   631  			ast.SetRelPos(elem, token.NoRelPos)
   632  			s.add(n, arrayType, ast.NewList(&ast.Ellipsis{Type: elem}))
   633  
   634  		case cue.ListKind:
   635  			var a []ast.Expr
   636  			for _, n := range s.listItems("items", n, true) {
   637  				v := s.schema(n) // TODO: label with number literal.
   638  				ast.SetRelPos(v, token.NoRelPos)
   639  				a = append(a, v)
   640  			}
   641  			s.list = ast.NewList(a...)
   642  			s.add(n, arrayType, s.list)
   643  
   644  		default:
   645  			s.errf(n, `value of "items" must be an object or array`)
   646  		}
   647  	}),
   648  
   649  	p1("additionalItems", func(n cue.Value, s *state) {
   650  		switch n.Kind() {
   651  		case cue.BoolKind:
   652  			// TODO: support
   653  
   654  		case cue.StructKind:
   655  			if s.list != nil {
   656  				s.usedTypes |= cue.ListKind
   657  				elem := s.schema(n)
   658  				s.list.Elts = append(s.list.Elts, &ast.Ellipsis{Type: elem})
   659  			}
   660  
   661  		default:
   662  			s.errf(n, `value of "additionalItems" must be an object or boolean`)
   663  		}
   664  	}),
   665  
   666  	p1("contains", func(n cue.Value, s *state) {
   667  		s.usedTypes |= cue.ListKind
   668  		list := s.addImport(n, "list")
   669  		// TODO: Passing non-concrete values is not yet supported in CUE.
   670  		if x := s.schema(n); !isAny(x) {
   671  			x := ast.NewCall(ast.NewSel(list, "Contains"), clearPos(x))
   672  			s.add(n, arrayType, x)
   673  		}
   674  	}),
   675  
   676  	// TODO: min/maxContains
   677  
   678  	p1("minItems", func(n cue.Value, s *state) {
   679  		s.usedTypes |= cue.ListKind
   680  		a := []ast.Expr{}
   681  		p, err := n.Uint64()
   682  		if err != nil {
   683  			s.errf(n, "invalid uint")
   684  		}
   685  		for ; p > 0; p-- {
   686  			a = append(a, ast.NewIdent("_"))
   687  		}
   688  		s.add(n, arrayType, ast.NewList(append(a, &ast.Ellipsis{})...))
   689  
   690  		// TODO: use this once constraint resolution is properly implemented.
   691  		// list := s.addImport(n, "list")
   692  		// s.addConjunct(n, ast.NewCall(ast.NewSel(list, "MinItems"), clearPos(s.uint(n))))
   693  	}),
   694  
   695  	p1("maxItems", func(n cue.Value, s *state) {
   696  		s.usedTypes |= cue.ListKind
   697  		list := s.addImport(n, "list")
   698  		x := ast.NewCall(ast.NewSel(list, "MaxItems"), clearPos(s.uint(n)))
   699  		s.add(n, arrayType, x)
   700  
   701  	}),
   702  
   703  	p1("uniqueItems", func(n cue.Value, s *state) {
   704  		s.usedTypes |= cue.ListKind
   705  		if s.boolValue(n) {
   706  			list := s.addImport(n, "list")
   707  			s.add(n, arrayType, ast.NewCall(ast.NewSel(list, "UniqueItems")))
   708  		}
   709  	}),
   710  }
   711  
   712  func clearPos(e ast.Expr) ast.Expr {
   713  	ast.SetRelPos(e, token.NoRelPos)
   714  	return e
   715  }