internal/filetypes: add auto interpretation
- automatically detect OpenAPI and JSONSchema
based on strict criteria.
- json+schema no longer magically maps to jsonschema
For inputs, auto mode is now automatically enabled for .json
and .yaml/yml files. Any explicit tag, like json: or data: disables
auto mode.
Change-Id: I391179c4542b823c428e4989e31381e00caa4a45
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5410
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/internal/encoding/detect.go b/internal/encoding/detect.go
new file mode 100644
index 0000000..b125a3f
--- /dev/null
+++ b/internal/encoding/detect.go
@@ -0,0 +1,67 @@
+// Copyright 2020 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 encoding
+
+import (
+ "net/url"
+ "path"
+ "strings"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/build"
+)
+
+// Detect detects the interpretation.
+func Detect(v cue.Value) (i build.Interpretation) {
+ switch {
+ case isOpenAPI(v):
+ return build.OpenAPI
+ case isJSONSchema(v):
+ return build.JSONSchema
+ }
+ return i
+}
+
+func isOpenAPI(v cue.Value) bool {
+ s, _ := v.Lookup("openapi").String()
+ if !strings.HasPrefix(s, "3.") {
+ return false
+ }
+ if _, err := v.Lookup("info", "title").String(); err != nil {
+ return false
+ }
+ if _, err := v.Lookup("info", "version").String(); err != nil {
+ return false
+ }
+ return true
+}
+
+func isJSONSchema(v cue.Value) bool {
+ s, err := v.Lookup("$schema").String()
+ if err != nil {
+ return false
+ }
+ u, err := url.Parse(s)
+ if err != nil {
+ return false
+ }
+ if u.Hostname() != "json-schema.org" {
+ return false
+ }
+ if _, base := path.Split(u.EscapedPath()); base != "schema" {
+ return false
+ }
+ return true
+}
diff --git a/internal/encoding/detect_test.go b/internal/encoding/detect_test.go
new file mode 100644
index 0000000..bdb1b9b
--- /dev/null
+++ b/internal/encoding/detect_test.go
@@ -0,0 +1,101 @@
+// Copyright 2020 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 encoding
+
+import (
+ "testing"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/build"
+)
+
+func TestDetect(t *testing.T) {
+ testCases := []struct {
+ name string
+ in string
+ out build.Interpretation
+ }{{
+ name: "validOpenAPI",
+ in: `
+ openapi: "3.0.0"
+ info: title: "Foo"
+ info: version: "v1alpha1"
+ `,
+ out: build.OpenAPI,
+ }, {
+ name: "noOpenAPI",
+ in: `
+ info: title: "Foo"
+ info: version: "v1alpha1"
+ `,
+ }, {
+ name: "noTitle",
+ in: `
+ openapi: "3.0.0"
+ info: version: "v1alpha1"
+ `,
+ }, {
+ name: "noVersion",
+ in: `
+ openapi: "3.0.0"
+ info: title: "Foo"
+ `,
+ }, {
+ name: "validJSONSchema",
+ in: `
+ $schema: "https://json-schema.org/schema#"
+ `,
+ out: build.JSONSchema,
+ }, {
+ name: "validJSONSchema",
+ in: `
+ $schema: "https://json-schema.org/draft-07/schema#"
+ `,
+ out: build.JSONSchema,
+ }, {
+ name: "noSchema",
+ in: `
+ $id: "https://acme.com/schema#"
+ `,
+ }, {
+ name: "wrongHost",
+ in: `
+ $schema: "https://acme.com/schema#"
+ `,
+ }, {
+ name: "invalidURL",
+ in: `
+ $schema: "://json-schema.org/draft-07"
+ `,
+ }, {
+ name: "invalidPath",
+ in: `
+ $schema: "https://json-schema.org/draft-07"
+ `,
+ }}
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ var r cue.Runtime
+ inst, err := r.Compile(tc.name, tc.in)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := Detect(inst.Value())
+ if got != tc.out {
+ t.Errorf("got %v; want %v", got, tc.out)
+ }
+ })
+ }
+}
diff --git a/internal/encoding/encoding.go b/internal/encoding/encoding.go
index 83de8a3..14d579b 100644
--- a/internal/encoding/encoding.go
+++ b/internal/encoding/encoding.go
@@ -45,7 +45,7 @@
cfg *Config
closer io.Closer
next func() (ast.Expr, error)
- interpret func(*cue.Instance) (file *ast.File, id string, err error)
+ interpret interpretFunc
expr ast.Expr
file *ast.File
filename string // may change on iteration for some formats
@@ -54,6 +54,8 @@
err error
}
+type interpretFunc func(*cue.Instance) (file *ast.File, id string, err error)
+
// ID returns a canonical identifier for the decoded object or "" if no such
// identifier could be found.
func (i *Decoder) ID() string {
@@ -69,6 +71,7 @@
return
}
// Decoder level
+ i.file = nil
i.expr, i.err = i.next()
i.index++
if i.err != nil {
@@ -77,7 +80,8 @@
// Interpretations
if i.interpret != nil {
var r cue.Runtime
- inst, err := r.CompileFile(i.File())
+ i.file = i.File()
+ inst, err := r.CompileFile(i.file)
if err != nil {
i.err = err
return
@@ -176,33 +180,22 @@
switch f.Interpretation {
case "":
+ case build.Auto:
+ openAPI := openAPIFunc(cfg, f)
+ jsonSchema := jsonSchemaFunc(cfg, f)
+ i.interpret = func(inst *cue.Instance) (file *ast.File, id string, err error) {
+ switch Detect(inst.Value()) {
+ case build.JSONSchema:
+ return jsonSchema(inst)
+ case build.OpenAPI:
+ return openAPI(inst)
+ }
+ return i.file, "", i.err
+ }
case build.OpenAPI:
- i.interpret = func(i *cue.Instance) (file *ast.File, id string, err error) {
- cfg := &openapi.Config{PkgName: cfg.PkgName}
- file, err = simplify(openapi.Extract(i, cfg))
- return file, "", err
- }
+ i.interpret = openAPIFunc(cfg, f)
case build.JSONSchema:
- i.interpret = func(i *cue.Instance) (file *ast.File, id string, err error) {
- id = f.Tags["id"]
- if id == "" {
- id, _ = i.Lookup("$id").String()
- }
- if id != "" {
- u, err := url.Parse(id)
- if err != nil {
- return nil, "", errors.Wrapf(err, token.NoPos, "invalid id")
- }
- u.Scheme = ""
- id = strings.TrimPrefix(u.String(), "//")
- }
- cfg := &jsonschema.Config{
- ID: id,
- PkgName: cfg.PkgName,
- }
- file, err = simplify(jsonschema.Extract(i, cfg))
- return file, id, err
- }
+ i.interpret = jsonSchemaFunc(cfg, f)
default:
i.err = fmt.Errorf("unsupported interpretation %q", f.Interpretation)
}
@@ -237,6 +230,37 @@
return i
}
+func jsonSchemaFunc(cfg *Config, f *build.File) interpretFunc {
+ return func(i *cue.Instance) (file *ast.File, id string, err error) {
+ id = f.Tags["id"]
+ if id == "" {
+ id, _ = i.Lookup("$id").String()
+ }
+ if id != "" {
+ u, err := url.Parse(id)
+ if err != nil {
+ return nil, "", errors.Wrapf(err, token.NoPos, "invalid id")
+ }
+ u.Scheme = ""
+ id = strings.TrimPrefix(u.String(), "//")
+ }
+ cfg := &jsonschema.Config{
+ ID: id,
+ PkgName: cfg.PkgName,
+ }
+ file, err = simplify(jsonschema.Extract(i, cfg))
+ return file, id, err
+ }
+}
+
+func openAPIFunc(c *Config, f *build.File) interpretFunc {
+ cfg := &openapi.Config{PkgName: c.PkgName}
+ return func(i *cue.Instance) (file *ast.File, id string, err error) {
+ file, err = simplify(openapi.Extract(i, cfg))
+ return file, "", err
+ }
+}
+
func reader(f *build.File, stdin io.Reader) (io.ReadCloser, error) {
switch s := f.Source.(type) {
case nil: