pkg/struct: add Min/MaxFields builtins

- also define their mappings to OpenAPI

Issue #56

Change-Id: I3db075ea25d5e5df8bf3a003fd881862e73be683
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2685
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/builtin_test.go b/cue/builtin_test.go
index b2a487f..3248f5d 100644
--- a/cue/builtin_test.go
+++ b/cue/builtin_test.go
@@ -255,6 +255,21 @@
 		test("strings", `strings.MinRunes(0) & "e"`),
 		`_|_(invalid value "e" (does not satisfy strings.MinRunes(0)))`,
 	}, {
+		test("struct", `struct.MinFields(0) & ""`),
+		`_|_(conflicting values MinFields (0) and "" (mismatched types struct and string))`,
+	}, {
+		test("struct", `struct.MinFields(0) & {a: 1}`),
+		`{a: 1}`,
+	}, {
+		test("struct", `struct.MinFields(2) & {a: 1}`),
+		`_|_(invalid value {a: 1} (does not satisfy struct.MinFields(2)))`,
+	}, {
+		test("struct", `struct.MaxFields(0) & {a: 1}`),
+		`_|_(invalid value {a: 1} (does not satisfy struct.MaxFields(0)))`,
+	}, {
+		test("struct", `struct.MaxFields(2) & {a: 1}`),
+		`{a: 1}`,
+	}, {
 		test("math/bits", `bits.And(0x10000000000000F0E, 0xF0F7)`), `6`,
 	}, {
 		test("math/bits", `bits.Or(0x100000000000000F0, 0x0F)`),
diff --git a/cue/builtins.go b/cue/builtins.go
index b75ddc0..db27401 100644
--- a/cue/builtins.go
+++ b/cue/builtins.go
@@ -2238,6 +2238,39 @@
 			},
 		}},
 	},
+	"struct": &builtinPkg{
+		native: []*builtin{{
+			Name:   "MinFields",
+			Params: []kind{structKind, intKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				object, n := c.structVal(0), c.int(1)
+				c.ret, c.err = func() (interface{}, error) {
+					iter := object.Fields(Hidden(false), Optional(false))
+					count := 0
+					for iter.Next() {
+						count++
+					}
+					return count >= n, nil
+				}()
+			},
+		}, {
+			Name:   "MaxFields",
+			Params: []kind{structKind, intKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				object, n := c.structVal(0), c.int(1)
+				c.ret, c.err = func() (interface{}, error) {
+					iter := object.Fields(Hidden(false), Optional(false))
+					count := 0
+					for iter.Next() {
+						count++
+					}
+					return count <= n, nil
+				}()
+			},
+		}},
+	},
 	"text/tabwriter": &builtinPkg{
 		native: []*builtin{{
 			Name:   "Write",
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index 70ab78c..f6ddbbc 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -466,6 +466,36 @@
 	// object composed of the same type, if a property is required and set to a
 	// constant value for each type, it is a discriminator.
 
+	switch op, a := v.Expr(); op {
+	case cue.CallOp:
+		name := fmt.Sprint(a[0])
+		switch name {
+		case "struct.MinFields":
+			if len(a) != 2 {
+				b.failf(v, "builtin %v must be called with one argument", name)
+			}
+			b.setFilter("Schema", "minProperties", b.int(a[1]))
+			return
+
+		case "struct.MaxFields":
+			if len(a) != 2 {
+				b.failf(v, "builtin %v must be called with one argument", name)
+			}
+			b.setFilter("Schema", "maxProperties", 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
+	}
+
 	required := []string{}
 	for i, _ := v.Fields(cue.Optional(false), cue.Hidden(false)); i.Next(); {
 		required = append(required, i.Label())
diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go
index 75e9fcc..9fbe314 100644
--- a/encoding/openapi/openapi_test.go
+++ b/encoding/openapi/openapi_test.go
@@ -51,6 +51,10 @@
 		"array.json",
 		defaultConfig,
 	}, {
+		"struct.cue",
+		"struct.json",
+		defaultConfig,
+	}, {
 		"strings.cue",
 		"strings.json",
 		defaultConfig,
diff --git a/encoding/openapi/testdata/struct.cue b/encoding/openapi/testdata/struct.cue
new file mode 100644
index 0000000..875c714
--- /dev/null
+++ b/encoding/openapi/testdata/struct.cue
@@ -0,0 +1,8 @@
+import "struct"
+
+MyMap: struct.MinFields(4)
+MyMap: struct.MaxFields(9)
+
+MyType: {
+	map: MyMap
+}
diff --git a/encoding/openapi/testdata/struct.json b/encoding/openapi/testdata/struct.json
new file mode 100644
index 0000000..a4c2b13
--- /dev/null
+++ b/encoding/openapi/testdata/struct.json
@@ -0,0 +1,24 @@
+{
+   "openapi": "3.0.0",
+   "info": {},
+   "components": {
+      "schemas": {
+         "MyMap": {
+            "type": "object",
+            "minProperties": 4,
+            "maxProperties": 9
+         },
+         "MyType": {
+            "type": "object",
+            "required": [
+               "map"
+            ],
+            "properties": {
+               "map": {
+                  "$ref": "#/components/schemas/MyMap"
+               }
+            }
+         }
+      }
+   }
+}
\ No newline at end of file
diff --git a/pkg/struct/struct.go b/pkg/struct/struct.go
new file mode 100644
index 0000000..00094e1
--- /dev/null
+++ b/pkg/struct/struct.go
@@ -0,0 +1,46 @@
+// 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 struct defines utilities for struct types.
+package structs
+
+import (
+	"cuelang.org/go/cue"
+)
+
+// MinFields validates the minimum number of fields that are part of a struct.
+//
+// Only fields that are part of the data model count. This excludes hidden
+// fields, optional fields, and definitions.
+func MinFields(object *cue.Struct, n int) (bool, error) {
+	iter := object.Fields(cue.Hidden(false), cue.Optional(false))
+	count := 0
+	for iter.Next() {
+		count++
+	}
+	return count >= n, nil
+}
+
+// MaxFields validates the maximum number of fields that are part of a struct.
+//
+// Only fields that are part of the data model count. This excludes hidden
+// fields, optional fields, and definitions.
+func MaxFields(object *cue.Struct, n int) (bool, error) {
+	iter := object.Fields(cue.Hidden(false), cue.Optional(false))
+	count := 0
+	for iter.Next() {
+		count++
+	}
+	return count <= n, nil
+}