pkg/list: add list builtins for OpenAPI features

MaxItems, MinItems, UniqueItems, and Contains

Also added conversion to OpenAPI (except for
Contains, which is no supported by the latter).

Change-Id: I2edf4e8572acc08e8ced4a61918dff091325f3a0
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2642
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/builtin.go b/cue/builtin.go
index 08fdcfc..939c3d1 100644
--- a/cue/builtin.go
+++ b/cue/builtin.go
@@ -143,7 +143,7 @@
 	Params: []kind{listKind},
 	Result: intKind,
 	Func: func(c *callCtxt) {
-		iter := c.list(0)
+		iter := c.iter(0)
 		if !iter.Next() {
 			c.ret = &top{baseValue{c.src}}
 			return
@@ -161,7 +161,7 @@
 	Params: []kind{stringKind | bytesKind | listKind | structKind},
 	Result: intKind,
 	Func: func(c *callCtxt) {
-		iter := c.list(0)
+		iter := c.iter(0)
 		d := []dValue{}
 		for iter.Next() {
 			d = append(d, dValue{iter.Value().path.v, false})
@@ -482,7 +482,21 @@
 	return b
 }
 
-func (c *callCtxt) list(i int) (a Iterator) {
+func (c *callCtxt) list(i int) (a []Value) {
+	arg := c.args[i]
+	x := newValueRoot(c.ctx, arg)
+	v, err := x.List()
+	if err != nil {
+		c.invalidArgType(c.args[i], i, "list", err)
+		return a
+	}
+	for v.Next() {
+		a = append(a, v.Value())
+	}
+	return a
+}
+
+func (c *callCtxt) iter(i int) (a Iterator) {
 	arg := c.args[i]
 	x := newValueRoot(c.ctx, arg)
 	v, err := x.List()
diff --git a/cue/builtins.go b/cue/builtins.go
index 1a73490..ca1e556 100644
--- a/cue/builtins.go
+++ b/cue/builtins.go
@@ -19,6 +19,7 @@
 	"math/bits"
 	"path"
 	"regexp"
+	"sort"
 	"strconv"
 	"strings"
 	"text/tabwriter"
@@ -466,6 +467,64 @@
 			},
 		}},
 	},
+	"list": &builtinPkg{
+		native: []*builtin{{
+			Name:   "MinItems",
+			Params: []kind{listKind, intKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				a, n := c.list(0), c.int(1)
+				c.ret = func() interface{} {
+					return len(a) <= n
+				}()
+			},
+		}, {
+			Name:   "MaxItems",
+			Params: []kind{listKind, intKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				a, n := c.list(0), c.int(1)
+				c.ret = func() interface{} {
+					return len(a) <= n
+				}()
+			},
+		}, {
+			Name:   "UniqueItems",
+			Params: []kind{listKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				a := c.list(0)
+				c.ret = func() interface{} {
+					b := []string{}
+					for _, v := range a {
+						b = append(b, fmt.Sprint(v))
+					}
+					sort.Strings(b)
+					for i := 1; i < len(b); i++ {
+						if b[i-1] == b[i] {
+							return false
+						}
+					}
+					return true
+				}()
+			},
+		}, {
+			Name:   "Contains",
+			Params: []kind{listKind, topKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				a, v := c.list(0), c.value(1)
+				c.ret = func() interface{} {
+					for _, w := range a {
+						if v.Equals(w) {
+							return true
+						}
+					}
+					return false
+				}()
+			},
+		}},
+	},
 	"math": &builtinPkg{
 		native: []*builtin{{
 			Name:  "MaxExp",
diff --git a/cue/gen.go b/cue/gen.go
index 3fd9e80..3d5a1c8 100644
--- a/cue/gen.go
+++ b/cue/gen.go
@@ -398,6 +398,8 @@
 		return "strList"
 	case "[]byte":
 		return "bytes"
+	case "[]cue.Value":
+		return "list"
 	case "io.Reader":
 		return "reader"
 	case "time.Time":
diff --git a/doc/tutorial/basics/bottom.md b/doc/tutorial/basics/bottom.md
index 2a36afa..4cdf814 100644
--- a/doc/tutorial/basics/bottom.md
+++ b/doc/tutorial/basics/bottom.md
@@ -31,8 +31,8 @@
 <!-- result -->
 `$ cue eval -i bottom.cue`
 ```
+list: [0, 1, 2]
 a: _|_ /* conflicting values 4 and 5 */
 l: [1, _|_ /* conflicting values 2 and 3 */]
-list: [0, 1, 2]
 val: _|_ /* index 3 out of bounds */
 ```
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index 568d643..bf50916 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -499,6 +499,44 @@
 //   schema: an array instance is valid if at least one element matches
 //   this schema.
 func (b *builder) array(v cue.Value) {
+
+	switch op, a := v.Expr(); op {
+	case cue.CallOp:
+		name := fmt.Sprint(a[0])
+		switch name {
+		case "list.UniqueItems":
+			if len(a) != 1 {
+				b.failf(v, "builtin %v may only be used without arguments", name)
+			}
+			b.setFilter("Schema", "uniqueItems", true)
+			return
+
+		case "list.MinItems":
+			if len(a) != 2 {
+				b.failf(v, "builtin %v must be called with one argument", name)
+			}
+			b.setFilter("Schema", "minItems", b.int(a[1]))
+			return
+
+		case "list.MaxItems":
+			if len(a) != 2 {
+				b.failf(v, "builtin %v must be called with one argument", name)
+			}
+			b.setFilter("Schema", "maxItems", b.int(a[1]))
+			return
+
+		default:
+			b.failf(v, "builtin %v not supported in OpenAPI", name)
+		}
+
+	case cue.NoOp:
+		// TODO: extract format from specific type.
+
+	default:
+		b.failf(v, "unsupported op %v for number type", op)
+		return
+	}
+
 	// Possible conjuncts:
 	//   - one list (CUE guarantees merging all conjuncts)
 	//   - no cap: is unified with list
diff --git a/encoding/openapi/testdata/array.cue b/encoding/openapi/testdata/array.cue
index 3261f81..9f28a52 100644
--- a/encoding/openapi/testdata/array.cue
+++ b/encoding/openapi/testdata/array.cue
@@ -1,6 +1,12 @@
+import "list"
+
 Arrays: {
 	bar?: [...MyEnum]
 	foo?: [...MyStruct]
+
+	baz?: list.UniqueItems()
+
+	qux?: list.MinItems(1) & list.MaxItems(3)
 }
 
 Arrays: {
diff --git a/encoding/openapi/testdata/array.json b/encoding/openapi/testdata/array.json
index 3508df8..976a6be 100644
--- a/encoding/openapi/testdata/array.json
+++ b/encoding/openapi/testdata/array.json
@@ -38,6 +38,15 @@
                         }
                      }
                   }
+               },
+               "baz": {
+                  "type": "array",
+                  "uniqueItems": true
+               },
+               "qux": {
+                  "type": "array",
+                  "minItems": 1,
+                  "maxItems": 3
                }
             }
          },
diff --git a/pkg/list/list.go b/pkg/list/list.go
new file mode 100644
index 0000000..8955103
--- /dev/null
+++ b/pkg/list/list.go
@@ -0,0 +1,59 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package list contains functions for manipulating and examining lists.
+package list
+
+import (
+	"fmt"
+	"sort"
+
+	"cuelang.org/go/cue"
+)
+
+// MinItems reports whether a has at least n items.
+func MinItems(a []cue.Value, n int) bool {
+	return len(a) <= n
+}
+
+// MaxItems reports whether a has at most n items.
+func MaxItems(a []cue.Value, n int) bool {
+	return len(a) <= n
+}
+
+// UniqueItems reports whether all elements in the list are unique.
+func UniqueItems(a []cue.Value) bool {
+	b := []string{}
+	for _, v := range a {
+		b = append(b, fmt.Sprint(v))
+	}
+	sort.Strings(b)
+	for i := 1; i < len(b); i++ {
+		if b[i-1] == b[i] {
+			return false
+		}
+	}
+	return true
+}
+
+// Contains reports whether v is contained in a. The value must be a
+// comparable value.
+func Contains(a []cue.Value, v cue.Value) bool {
+	for _, w := range a {
+		if v.Equals(w) {
+			return true
+		}
+	}
+	return false
+}