encoding/jsonschema: initial JSON Schema to CUE conversion

This does not yet introduce the API, but merely the
conversion of JSON Schema to CUE.
Also, several things, like formats, are not yet supported.

Note that the input to the conversion is CUE itself.
This allows any of the encodings (like YAML or JSON)
to be used as a base. The conversion will preserve line
information from the original file. That is, if a YAML file
was converted to CUE, which is then converted to OpenAPI, the line information in the resulting AST
corresponds to the original YAML.

Change-Id: I512502ce3d98443a530e75f606ca3c533b0c4299
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4604
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/jsonschema/testdata/basic.txtar b/encoding/jsonschema/testdata/basic.txtar
new file mode 100644
index 0000000..0c1b255
--- /dev/null
+++ b/encoding/jsonschema/testdata/basic.txtar
@@ -0,0 +1,53 @@
+-- basic.json --
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+
+  "type": "object",
+  "title": "Main schema",
+  "description": "Specify who you are and all.",
+
+  "properties": {
+    "person": {
+      "description": "A person is a human being.",
+      "type": "object",
+	  "required": [ "name" ],
+      "properties": {
+        "name": { "type": "string" },
+		"address": {
+			"description": "where does this person live?",
+			"type": "string"
+		},
+        "children": {
+          "type": "array",
+          "items": { "type": "string" },
+          "default": []
+        },
+		"home phone": {
+			"type": "string",
+			"deprecated": true
+		}
+      }
+    }
+  }
+}
+
+-- out.cue --
+// Main schema
+// 
+// Specify who you are and all.
+package basic
+
+Schema :: _ @jsonschema(schema="http://json-schema.org/draft-07/schema#")
+Schema :: {
+	// A person is a human being.
+	person?: {
+		name: string
+
+		// where does this person live?
+		address?: string
+		children?: [...string]
+		"home phone"?: string
+		...
+	}
+	...
+}
diff --git a/encoding/jsonschema/testdata/def.txtar b/encoding/jsonschema/testdata/def.txtar
new file mode 100644
index 0000000..d5d92e9
--- /dev/null
+++ b/encoding/jsonschema/testdata/def.txtar
@@ -0,0 +1,63 @@
+// This test tests the conversion and ordering of definitions.
+
+-- definition.json --
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+
+  "$id": "http://cuelang.org/go/encoding/openapi/testdata/order.json",
+
+  "definitions": {
+    "address": {
+      "type": "object",
+      "properties": {
+        "street_address": { "type": "string" },
+        "city":           { "type": "string" },
+        "state":          { "type": "string" }
+      },
+      "required": ["street_address", "city", "state"]
+    },
+    "person": {
+      "type": "object",
+      "properties": {
+        "name": { "type": "string" },
+        "children": {
+          "type": "array",
+          "items": { "$ref": "#/definitions/person" },
+          "default": []
+        }
+      }
+    }
+  },
+
+  "type": "object",
+
+  "properties": {
+    "person": { "$ref": "#/definitions/person" },
+    "billing_address": { "$ref": "#/definitions/address" },
+    "shipping_address": { "$ref": "#/definitions/address" }
+  }
+}
+
+-- out.cue --
+package def
+
+Schema :: _ @jsonschema(schema="http://json-schema.org/draft-07/schema#",id="http://cuelang.org/go/encoding/openapi/testdata/order.json")
+Schema :: {
+	person?:           def.person
+	billing_address?:  def.address
+	shipping_address?: def.address
+	...
+}
+
+def: address :: {
+	street_address: string
+	city:           string
+	state:          string
+	...
+}
+
+def: person :: {
+	name?: string
+	children?: [...def.person]
+	...
+}
diff --git a/encoding/jsonschema/testdata/list.txtar b/encoding/jsonschema/testdata/list.txtar
new file mode 100644
index 0000000..d3046dd
--- /dev/null
+++ b/encoding/jsonschema/testdata/list.txtar
@@ -0,0 +1,36 @@
+-- list.yaml --
+type: object
+
+properties:
+  foo:
+    items:
+      type: string
+
+  tuple:
+    items:
+      - type: string
+      - type: integer
+      - const: 2
+
+  has:
+    contains:
+      const: 3
+
+  size:
+    minItems: 3
+    maxItems: 9
+    uniqueItems: true
+
+additionalProperties: false
+
+-- out.cue --
+package list
+
+import "list"
+
+Schema :: {
+	foo?: [...string]
+	tuple?: [string, int, 2]
+	has?:  list.Contains(3)
+	size?: list.MinItems(3) & list.MaxItems(9) & list.UniqueItems()
+}
diff --git a/encoding/jsonschema/testdata/num.txtar b/encoding/jsonschema/testdata/num.txtar
new file mode 100644
index 0000000..5652628
--- /dev/null
+++ b/encoding/jsonschema/testdata/num.txtar
@@ -0,0 +1,37 @@
+-- type.json --
+{
+  "type": "object",
+
+  "properties": {
+    "constant":  { "const": 2 },
+    "several": {
+      "enum": [ 1, 2, 3, 4 ]
+    },
+    "inclusive": {
+        "type": "number",
+        "minimum": 2,
+        "maximum": 3
+    },
+    "exclusive": {
+        "exclusiveMinimum": 2,
+        "exclusiveMaximum": 3
+    },
+    "cents": {
+      "multipleOf": 0.05
+    }
+  },
+  "additionalProperties": false
+}
+
+-- out.cue --
+package num
+
+import "math"
+
+Schema :: {
+	constant?:  2
+	several?:   1 | 2 | 3 | 4
+	inclusive?: >=2 & <=3
+	exclusive?: >2 & <3
+	cents?:     math.MultipleOf(0.05)
+}
diff --git a/encoding/jsonschema/testdata/object.txtar b/encoding/jsonschema/testdata/object.txtar
new file mode 100644
index 0000000..59bbbe3
--- /dev/null
+++ b/encoding/jsonschema/testdata/object.txtar
@@ -0,0 +1,103 @@
+-- type.json --
+{
+  "type": "object",
+  "title": "Main schema",
+
+  "properties": {
+    "fields" : {
+      "type": "object",
+      "minProperties": 3,
+      "maxProperties": 10,
+      "propertyNames": {
+        "pattern": "^\\P{Lu}"
+      }
+    },
+    "additional": {
+      "type": "object",
+      "properties": {
+        "foo": { "type": "number" },
+        "bar": { "type": "number" }
+      },
+      "additionalProperties": { "type": "string" }
+    },
+    "map": {
+      "type": "object",
+      "additionalProperties": { "type": "string" }
+    },
+    "patterns": {
+      "type": "object",
+      "properties": {
+        "foo": { "type": "number" },
+        "bar": { "type": "number" }
+      },
+      "patternProperties": {
+        "^\\P{Lu}": { "type": "string" },
+        "^\\P{Lo}": { "type": "integer" }
+      }
+    },
+    "patternsNoProps": {
+      "type": "object",
+      "patternProperties": {
+        "^\\P{Lu}": { "type": "string" },
+        "^\\P{Lo}": { "type": "integer" }
+      }
+    },
+    "complex": {
+      "type": "object",
+      "properties": {
+        "foo": { "type": "number" },
+        "bar": { "type": "number" }
+      },
+      "patternProperties": {
+        "^\\P{Lu}": { "type": "string" },
+        "^\\P{Lo}": { "type": "integer" }
+      },
+      "additionalProperties": { "type": "string" }
+    }
+  },
+  "additionalProperties": false
+}
+
+-- out.cue --
+// Main schema
+package object
+
+import "struct"
+
+Schema :: {
+	fields?: struct.MinFields(3) & struct.MaxFields(10) & {
+		[=~"^\\P{Lu}"]: _
+	}
+	additional?: {
+		foo?: number
+		bar?: number
+
+		[!~"^(foo|bar)$"]: string
+	}
+	map?: [string]: string
+	patterns?: {
+		foo?: number
+		bar?: number
+
+		[=~"^\\P{Lu}" & !~"^(foo|bar)$"]: string
+
+		[=~"^\\P{Lo}" & !~"^(foo|bar)$"]: int
+		...
+	}
+	patternsNoProps?: {
+		[=~"^\\P{Lu}" & !~"^()$"]: string
+
+		[=~"^\\P{Lo}" & !~"^()$"]: int
+		...
+	}
+	complex?: {
+		foo?: number
+		bar?: number
+
+		[=~"^\\P{Lu}" & !~"^(foo|bar)$"]: string
+
+		[=~"^\\P{Lo}" & !~"^(foo|bar)$"]: int
+
+		[!~"^\\P{Lu}" & !~"^\\P{Lo}" & !~"^(foo|bar)$"]: string
+	}
+}
diff --git a/encoding/jsonschema/testdata/type.txtar b/encoding/jsonschema/testdata/type.txtar
new file mode 100644
index 0000000..2c13d3f
--- /dev/null
+++ b/encoding/jsonschema/testdata/type.txtar
@@ -0,0 +1,44 @@
+-- type.json --
+{
+  "type": "object",
+  "title": "Main schema",
+
+  "properties": {
+    "intString": {
+      "description": "an integer or string.",
+      "type": [ "string", "integer", "boolean", "array", "null" ]
+    },
+    "object": {
+        "type": "object",
+        "default": {
+            "foo": "bar",
+            "baz": 1.3
+        }
+    },
+    "numOrList": {
+      "oneOf": [
+        { "type": "number" },
+        { "items": { "type": "number" } }
+      ],
+      "default": [ 1, 2, 3 ]
+    }
+  },
+  "additionalProperties": false
+}
+
+-- out.cue --
+// Main schema
+package type
+
+Schema :: {
+	// an integer or string.
+	intString?: string | int | bool | [...] | null
+	object?:    {
+			...
+	} | *{
+		foo: "bar"
+		baz: 1.3
+		...
+	}
+	numOrList?: number | [...number] | *[1, 2, 3]
+}