pkg/strings: add MinRunes and MaxRunes

Also adds respective conversions for the OpenAPI
encoding.

cue/builtins.go is generated using go generate

Change-Id: I3b709cb56bd59fbe08a0376d4b4760b9b21a2e3a
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2625
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/builtin_test.go b/cue/builtin_test.go
index 3ebd34e..d93bd05 100644
--- a/cue/builtin_test.go
+++ b/cue/builtin_test.go
@@ -165,6 +165,18 @@
 		test("strings", `strings.ToTitle("alpha")`),
 		`"Alpha"`,
 	}, {
+		test("strings", `strings.MaxRunes(3) & "foo"`),
+		`"foo"`,
+	}, {
+		test("strings", `strings.MaxRunes(3) & "quux"`),
+		`_|_(invalid value "quux" (does not satisfy strings.MaxRunes(3)))`,
+	}, {
+		test("strings", `strings.MinRunes(1) & "e"`),
+		`"e"`,
+	}, {
+		test("strings", `strings.MinRunes(0) & "e"`),
+		`_|_(invalid value "e" (does not satisfy strings.MinRunes(0)))`,
+	}, {
 		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 862042d..1fd88a3 100644
--- a/cue/builtins.go
+++ b/cue/builtins.go
@@ -1489,6 +1489,27 @@
 	},
 	"strings": &builtinPkg{
 		native: []*builtin{{
+			Name:   "MinRunes",
+			Params: []kind{stringKind, intKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				s, max := c.string(0), c.int(1)
+				c.ret = func() interface{} {
+
+					return len([]rune(s)) <= max
+				}()
+			},
+		}, {
+			Name:   "MaxRunes",
+			Params: []kind{stringKind, intKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				s, max := c.string(0), c.int(1)
+				c.ret = func() interface{} {
+					return len([]rune(s)) <= max
+				}()
+			},
+		}, {
 			Name:   "ToTitle",
 			Params: []kind{stringKind},
 			Result: stringKind,
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index eaa0df4..8387c72 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -15,6 +15,7 @@
 package openapi
 
 import (
+	"fmt"
 	"math"
 	"math/big"
 	"path"
@@ -612,8 +613,8 @@
 
 // string supports the following options:
 //
-// - maxLenght (Unicode codepoints)
-// - minLenght (Unicode codepoints)
+// - maxLength (Unicode codepoints)
+// - minLength (Unicode codepoints)
 // - pattern (a regexp)
 //
 // The regexp pattern is as follows, and is limited to be a  strict subset of RE2:
@@ -658,7 +659,7 @@
 			return
 		}
 		if op == cue.RegexMatchOp {
-			b.setFilter("schema", "pattern", s)
+			b.setFilter("Schema", "pattern", s)
 		} else {
 			b.setNot("pattern", s)
 		}
@@ -670,6 +671,22 @@
 	case cue.NoOp, cue.SelectorOp:
 		// TODO: determine formats from specific types.
 
+	case cue.CallOp:
+		name := fmt.Sprint(a[0])
+		field := ""
+		switch name {
+		case "strings.MinRunes":
+			field = "minLength"
+		case "strings.MaxRunes":
+			field = "maxLength"
+		default:
+			b.failf(v, "builtin %v not supported in OpenAPI", name)
+		}
+		if len(a) != 2 {
+			b.failf(v, "builtin %v may only be used with single argument", name)
+		}
+		b.setFilter("Schema", field, b.int(a[1]))
+
 	default:
 		b.failf(v, "unsupported op %v for string type", op)
 	}
diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go
index d5eb721..b085f75 100644
--- a/encoding/openapi/openapi_test.go
+++ b/encoding/openapi/openapi_test.go
@@ -51,6 +51,10 @@
 		"array.json",
 		defaultConfig,
 	}, {
+		"strings.cue",
+		"strings.json",
+		defaultConfig,
+	}, {
 		"oneof.cue",
 		"oneof.json",
 		defaultConfig,
@@ -82,6 +86,9 @@
 			inst := cue.Build(load.Instances([]string{filename}, nil))[0]
 
 			b, err := Gen(inst, tc.config)
+			if err != nil {
+				t.Fatal(err)
+			}
 			var out = &bytes.Buffer{}
 			_ = json.Indent(out, b, "", "   ")
 
diff --git a/encoding/openapi/testdata/strings.cue b/encoding/openapi/testdata/strings.cue
new file mode 100644
index 0000000..f0d61fe
--- /dev/null
+++ b/encoding/openapi/testdata/strings.cue
@@ -0,0 +1,9 @@
+import "strings"
+
+MyType: {
+	myString: strings.MinRunes(1) & strings.MaxRunes(5)
+
+	myPattern: =~"foo.*bar"
+
+	myAntiPattern: !~"foo.*bar"
+}
diff --git a/encoding/openapi/testdata/strings.json b/encoding/openapi/testdata/strings.json
new file mode 100644
index 0000000..cff15f4
--- /dev/null
+++ b/encoding/openapi/testdata/strings.json
@@ -0,0 +1,34 @@
+{
+   "openapi": "3.0.0",
+   "info": {},
+   "components": {
+      "schemas": {
+         "MyType": {
+            "type": "object",
+            "required": [
+               "myString",
+               "myPattern",
+               "myAntiPattern"
+            ],
+            "properties": {
+               "myString": {
+                  "type": "string",
+                  "minLength": 1,
+                  "maxLength": 5
+               },
+               "myPattern": {
+                  "type": "string",
+                  "pattern": "foo.*bar"
+               },
+               "myAntiPattern": {
+                  "not": {
+                     "type": "string",
+                     "pattern": "foo.*bar"
+                  },
+                  "type": "string"
+               }
+            }
+         }
+      }
+   }
+}
\ No newline at end of file
diff --git a/pkg/strings/manual.go b/pkg/strings/manual.go
index 7018115..849a0e3 100644
--- a/pkg/strings/manual.go
+++ b/pkg/strings/manual.go
@@ -12,6 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// Package strings implements simple functions to manipulate UTF-8 encoded
+// strings.package strings.
+//
+// Some of the functions in this package are specifically intended as field
+// constraints. For instance, MaxRunes as used in this CUE program
+//
+//    import "strings"
+//
+//    myString: strings.MaxRunes(5)
+//
+// specifies that the myString should be at most 5 code points.
 package strings
 
 import (
@@ -19,6 +30,24 @@
 	"unicode"
 )
 
+// MinRunes reports whether the number of runes (Unicode codepoints) in a string
+// is at least a certain minimum. MinRunes can be used a a field constraint to
+// except all strings for which this property holds.
+func MinRunes(s string, max int) bool {
+	// TODO: CUE strings cannot be invalid UTF-8. In case this changes, we need
+	// to use the following conversion to count properly:
+	// s, _ = unicodeenc.UTF8.NewDecoder().String(s)
+	return len([]rune(s)) <= max
+}
+
+// MaxRunes reports whether the number of runes (Unicode codepoints) in a string
+// exceeds a certain maximum. MaxRunes can be used a a field constraint to
+// except all strings for which this property holds
+func MaxRunes(s string, max int) bool {
+	// See comment in MinRunes implementation.
+	return len([]rune(s)) <= max
+}
+
 // ToTitle returns a copy of the string s with all Unicode letters that begin
 // words mapped to their title case.
 func ToTitle(s string) string {