encoding/jsonschema: improve handling of $id and $ref
Fixes most issues in Issue #378. Remaining problems are mostly
related to invalid JSON schema, if one counts the lack of an $id
field as an invalid schema.
Used Draft 8 (2019-09) as reference.
- Keep stack of $ids
- resolve nested $ids and $refs relative to top stack
- Fixes several bugs
- Allow nested definitions
- Allow references to non-definitions
Note:
- OpenAPI still has its own mechanism. This should be improved at
some point to be more unified with JSON schema.
Also:
- copy metadata in Sanitize
Fixes #378. File separate issues for remaining issues.
Change-Id: I310d13fe378ff4837c13336d01e0102cb6d49382
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/6142
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
Reviewed-by: Paul Jolly <paul@myitcv.org.uk>
diff --git a/cmd/cue/cmd/testdata/script/def_jsonschema.txt b/cmd/cue/cmd/testdata/script/def_jsonschema.txt
index 551374f..5e0fce7 100644
--- a/cmd/cue/cmd/testdata/script/def_jsonschema.txt
+++ b/cmd/cue/cmd/testdata/script/def_jsonschema.txt
@@ -69,10 +69,10 @@
-- expect-stderr2 --
age: conflicting values "twenty" and >=0 (mismatched types string and number):
- 13:7
+ 14:7
./data.yaml:1:7
-- expect-stderr3 --
age: conflicting values "twenty" and >=0 (mismatched types string and number):
- 13:7
+ 14:7
./data.yaml:1:7
-- cue.mod --
diff --git a/cue/ast/astutil/sanitize.go b/cue/ast/astutil/sanitize.go
index c5b3092..2980be7 100644
--- a/cue/ast/astutil/sanitize.go
+++ b/cue/ast/astutil/sanitize.go
@@ -291,7 +291,13 @@
var isNew bool
name, isNew = z.addRename(y.Name, x)
if isNew {
- x.Label = &ast.Alias{Ident: ast.NewIdent(name), Expr: y}
+ ident := ast.NewIdent(name)
+ // Move formatting and comments from original label to alias
+ // identifier.
+ CopyMeta(ident, y)
+ ast.SetRelPos(y, token.NoRelPos)
+ ast.SetComments(y, nil)
+ x.Label = &ast.Alias{Ident: ident, Expr: y}
}
default:
diff --git a/encoding/jsonschema/constraints.go b/encoding/jsonschema/constraints.go
index ffac830..a9260c6 100644
--- a/encoding/jsonschema/constraints.go
+++ b/encoding/jsonschema/constraints.go
@@ -15,10 +15,14 @@
package jsonschema
import (
+ "fmt"
"math/big"
+ "path"
+ "regexp"
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/errors"
"cuelang.org/go/cue/token"
"cuelang.org/go/internal"
)
@@ -81,25 +85,32 @@
s.errf(n, `"definitions" expected an object, found %s`, n.Kind())
}
- if len(s.path) != 1 {
- s.errf(n, `"definitions" only allowed at root`)
- }
-
old := s.isSchema
s.isSchema = true
defer func() { s.isSchema = old }()
s.processMap(n, func(key string, n cue.Value) {
- name := s.path[len(s.path)-1]
- a, _ := jsonSchemaRef(n.Pos(), []string{"definitions", name})
+ name := key
- f := &ast.Field{Label: a[len(a)-1], Value: s.schema(n)}
- for i := len(a) - 2; i >= 0; i-- {
- f = &ast.Field{Label: a[i], Value: ast.NewStruct(f)}
+ var f *ast.Field
+
+ ident := "#" + name
+ if ast.IsValidIdent(ident) {
+ f = &ast.Field{Value: s.schema(n, label{ident, true})}
+ f.Label = ast.NewIdent(ident)
+ } else {
+ f = &ast.Field{Value: s.schema(n, label{"#", true}, label{name: name})}
+ f.Label = ast.NewString(name)
+ ident = "#"
+ f = &ast.Field{
+ Label: ast.NewIdent("#"),
+ Value: ast.NewStruct(f),
+ }
}
ast.SetRelPos(f, token.NewSection)
s.definitions = append(s.definitions, f)
+ s.setField(label{name: ident, isDef: true}, f)
})
}
@@ -114,11 +125,32 @@
p0("$id", func(n cue.Value, s *state) {
// URL: https://domain.com/schemas/foo.json
- // Use Title(foo) as CUE identifier.
// anchors: #identifier
//
// TODO: mark identifiers.
- s.id, _ = s.strValue(n)
+
+ // Resolution must be relative to parent $id
+ // https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.2
+ u := s.resolveURI(n)
+ if u == nil {
+ return
+ }
+
+ if u.Fragment != "" {
+ if s.cfg.Strict {
+ s.errf(n, "$id URI may not contain a fragment")
+ }
+ return
+ }
+ s.id = u
+
+ if s.obj == nil {
+ s.obj = &ast.StructLit{}
+ }
+ // TODO: handle the case where this is always defined and we don't want
+ // to include the default value.
+ s.obj.Elts = append(s.obj.Elts, &ast.Attribute{
+ Text: fmt.Sprintf("@jsonschema(id=%q)", u)})
}),
// Generic constraint
@@ -221,30 +253,22 @@
p1("definitions", addDefinitions),
p1("$ref", func(n cue.Value, s *state) {
s.usedTypes = allTypes
- str, _ := s.strValue(n)
- refs := s.parseRef(n.Pos(), str)
- var a []ast.Label
- if refs != nil {
- a = s.mapRef(n.Pos(), str, refs)
- }
- if a == nil {
- s.addConjunct(&ast.BadExpr{From: n.Pos()})
+
+ u := s.resolveURI(n)
+
+ if u.Fragment != "" && !path.IsAbs(u.Fragment) {
+ s.addErr(errors.Newf(n.Pos(), "anchors (%s) not supported", u.Fragment))
+ // TODO: support anchors
return
}
- sel, ok := a[0].(ast.Expr)
- if !ok {
- sel = &ast.BadExpr{}
- }
- for _, l := range a[1:] {
- switch x := l.(type) {
- case *ast.Ident:
- sel = &ast.SelectorExpr{X: sel, Sel: x}
- case *ast.BasicLit:
- sel = &ast.IndexExpr{X: sel, Index: x}
- }
+ expr := s.makeCUERef(n, u)
+
+ if expr == nil {
+ expr = &ast.BadExpr{From: n.Pos()}
}
- s.addConjunct(sel)
+
+ s.addConjunct(expr)
}),
// Combinators
@@ -270,7 +294,7 @@
p2("allOf", func(n cue.Value, s *state) {
var a []ast.Expr
for _, v := range s.listItems("allOf", n, false) {
- x, sub := s.schemaState(v, s.allowedTypes, true)
+ x, sub := s.schemaState(v, s.allowedTypes, nil, true)
s.allowedTypes &= sub.allowedTypes
s.usedTypes |= sub.usedTypes
if sub.hasConstraints() {
@@ -286,7 +310,7 @@
var types cue.Kind
var a []ast.Expr
for _, v := range s.listItems("anyOf", n, false) {
- x, sub := s.schemaState(v, s.allowedTypes, true)
+ x, sub := s.schemaState(v, s.allowedTypes, nil, true)
types |= sub.allowedTypes
if sub.hasConstraints() {
a = append(a, x)
@@ -303,7 +327,7 @@
var a []ast.Expr
hasSome := false
for _, v := range s.listItems("oneOf", n, false) {
- x, sub := s.schemaState(v, s.allowedTypes, true)
+ x, sub := s.schemaState(v, s.allowedTypes, nil, true)
types |= sub.allowedTypes
// TODO: make more finegrained by making it two pass.
@@ -328,6 +352,13 @@
// String constraints
p1("pattern", func(n cue.Value, s *state) {
+ str, _ := n.String()
+ if _, err := regexp.Compile(str); err != nil {
+ if s.cfg.Strict {
+ s.errf(n, "unsupported regexp: %v", err)
+ }
+ return
+ }
s.usedTypes |= cue.StringKind
s.addConjunct(&ast.UnaryExpr{Op: token.MAT, X: s.string(n)})
}),
@@ -411,9 +442,9 @@
s.processMap(n, func(key string, n cue.Value) {
// property?: value
- label := ast.NewString(key)
- expr, state := s.schemaState(n, allTypes, false)
- f := &ast.Field{Label: label, Value: expr}
+ name := ast.NewString(key)
+ expr, state := s.schemaState(n, allTypes, []label{{name: key}}, false)
+ f := &ast.Field{Label: name, Value: expr}
state.doc(f)
f.Optional = token.Blank.Pos()
if len(s.obj.Elts) > 0 && len(f.Comments()) > 0 {
@@ -424,12 +455,13 @@
if state.deprecated {
switch expr.(type) {
case *ast.StructLit:
- s.obj.Elts = append(s.obj.Elts, addTag(label, "deprecated", ""))
+ s.obj.Elts = append(s.obj.Elts, addTag(name, "deprecated", ""))
default:
f.Attrs = append(f.Attrs, internal.NewAttr("deprecated", ""))
}
}
s.obj.Elts = append(s.obj.Elts, f)
+ s.setField(label{name: key}, f)
})
}),
@@ -481,14 +513,14 @@
p1d("propertyNames", 6, func(n cue.Value, s *state) {
// [=~pattern]: _
- if names, _ := s.schemaState(n, cue.StringKind, false); !isAny(names) {
+ if names, _ := s.schemaState(n, cue.StringKind, nil, false); !isAny(names) {
s.usedTypes |= cue.StructKind
s.addConjunct(ast.NewStruct(ast.NewList((names)), ast.NewIdent("_")))
}
}),
// TODO: reenable when we have proper non-monotonic contraint validation.
- // p0("minProperties", func(n cue.Value, s *state) {
+ // p1("minProperties", func(n cue.Value, s *state) {
// s.usedTypes |= cue.StructKind
// pkg := s.addImport("struct")
@@ -589,7 +621,7 @@
case cue.ListKind:
var a []ast.Expr
for _, n := range s.listItems("items", n, true) {
- v := s.schema(n)
+ v := s.schema(n) // TODO: label with number literal.
ast.SetRelPos(v, token.NoRelPos)
a = append(a, v)
}
diff --git a/encoding/jsonschema/decode.go b/encoding/jsonschema/decode.go
index a4cd51a..cbc7bb1 100644
--- a/encoding/jsonschema/decode.go
+++ b/encoding/jsonschema/decode.go
@@ -21,6 +21,7 @@
import (
"fmt"
"math/bits"
+ "net/url"
"strings"
"cuelang.org/go/cue"
@@ -39,11 +40,9 @@
// A decoder converts JSON schema to CUE.
type decoder struct {
- cfg *Config
-
- errs errors.Error
-
- definitions []ast.Decl
+ cfg *Config
+ errs errors.Error
+ numID int // for creating unique numbers: increment on each use
}
// addImport registers
@@ -93,7 +92,6 @@
}
f.Decls = append(f.Decls, a...)
- f.Decls = append(f.Decls, d.definitions...)
_ = astutil.Sanitize(f)
@@ -111,15 +109,12 @@
root.isSchema = true
}
- expr, state := root.schemaState(v, allTypes, false)
+ expr, state := root.schemaState(v, allTypes, nil, false)
tags := []string{}
if state.jsonschema != "" {
tags = append(tags, fmt.Sprintf("schema=%q", state.jsonschema))
}
- if state.id != "" {
- tags = append(tags, fmt.Sprintf("id=%q", state.id))
- }
if name == nil {
if len(tags) > 0 {
@@ -164,6 +159,16 @@
expr = ast.NewStruct(ref[i], expr)
}
+ if root.hasSelfReference {
+ return []ast.Decl{
+ &ast.EmbedDecl{Expr: ast.NewIdent(topSchema)},
+ &ast.Field{
+ Label: ast.NewIdent(topSchema),
+ Value: &ast.StructLit{Elts: a},
+ },
+ }
+ }
+
return a
}
@@ -224,10 +229,14 @@
isSchema bool // for omitting ellipsis in an ast.File
+ up *state
parent *state
path []string
+ // idRef is used to refer to this schema in case it defines an $id.
+ idRef []label
+
pos cue.Value
typeOptional bool
@@ -240,17 +249,35 @@
description string
deprecated bool
jsonschema string
- id string
+ id *url.URL // base URI for $ref
+
+ definitions []ast.Decl
conjuncts []ast.Expr
- obj *ast.StructLit
+ // Used for inserting definitions, properties, etc.
+ hasSelfReference bool
+ obj *ast.StructLit
+ // Complete at finalize.
+ fieldRefs map[label]refs
+
closeStruct bool
patterns []ast.Expr
list *ast.ListLit
}
+type label struct {
+ name string
+ isDef bool
+}
+
+type refs struct {
+ field *ast.Field
+ ident string
+ refs []*ast.Ident
+}
+
func (s *state) hasConstraints() bool {
return len(s.conjuncts) > 0 ||
len(s.patterns) > 0 ||
@@ -332,6 +359,20 @@
}
e = ast.NewBinExpr(token.OR, e, &ast.UnaryExpr{Op: token.MUL, X: s.default_})
}
+
+ if len(s.definitions) > 0 {
+ if st, ok := e.(*ast.StructLit); ok {
+ st.Elts = append(st.Elts, s.definitions...)
+ } else {
+ st = ast.NewStruct()
+ st.Elts = append(st.Elts, &ast.EmbedDecl{Expr: e})
+ st.Elts = append(st.Elts, s.definitions...)
+ e = st
+ }
+ }
+
+ s.linkReferences()
+
return e
}
@@ -370,20 +411,22 @@
}
}
-func (s *state) schema(n cue.Value) ast.Expr {
- expr, _ := s.schemaState(n, allTypes, false)
+func (s *state) schema(n cue.Value, idRef ...label) ast.Expr {
+ expr, _ := s.schemaState(n, allTypes, idRef, false)
// TODO: report unused doc.
return expr
}
// schemaState is a low-level API for schema. isLogical specifies whether the
// caller is a logical operator like anyOf, allOf, oneOf, or not.
-func (s *state) schemaState(n cue.Value, types cue.Kind, isLogical bool) (ast.Expr, *state) {
+func (s *state) schemaState(n cue.Value, types cue.Kind, idRef []label, isLogical bool) (ast.Expr, *state) {
state := &state{
+ up: s,
isSchema: s.isSchema,
decoder: s.decoder,
allowedTypes: types,
path: s.path,
+ idRef: idRef,
pos: n,
}
if isLogical {
diff --git a/encoding/jsonschema/decode_test.go b/encoding/jsonschema/decode_test.go
index a1c960e..39c09f4 100644
--- a/encoding/jsonschema/decode_test.go
+++ b/encoding/jsonschema/decode_test.go
@@ -102,12 +102,14 @@
if expr != nil {
b, err := format.Node(expr, format.Simplify())
if err != nil {
- t.Fatal(err)
+ t.Fatal(errors.Details(err, nil))
}
// verify the generated CUE.
- if _, err = r.Compile(fullpath, b); err != nil {
- t.Fatal(errors.Details(err, nil))
+ if !bytes.Contains(a.Comment, []byte("#noverify")) {
+ if _, err = r.Compile(fullpath, b); err != nil {
+ t.Fatal(errors.Details(err, nil))
+ }
}
b = bytes.TrimSpace(b)
diff --git a/encoding/jsonschema/ref.go b/encoding/jsonschema/ref.go
index b0fa788..26443fe 100644
--- a/encoding/jsonschema/ref.go
+++ b/encoding/jsonschema/ref.go
@@ -17,8 +17,10 @@
import (
"net/url"
"path"
+ "strconv"
"strings"
+ "cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/token"
@@ -56,7 +58,340 @@
// means that %2F gets translated to `/` before it can be split. This, in
// turn, means that field names cannot have a `/` as name.
+ return splitFragment(u)
+}
+
+// resolveURI parses a URI from n and resolves it in the current context.
+// To resolve it in the current context, it looks for the closest URI from
+// an $id in the parent scopes and the uses the URI resolution to get the
+// new URI.
+//
+// This method is used to resolve any URI, including those from $id and $ref.
+func (s *state) resolveURI(n cue.Value) *url.URL {
+ str, ok := s.strValue(n)
+ if !ok {
+ return nil
+ }
+
+ u, err := url.Parse(str)
+ if err != nil {
+ s.addErr(errors.Newf(n.Pos(), "invalid JSON reference: %s", err))
+ return nil
+ }
+
+ for {
+ if s.id != nil {
+ u = s.id.ResolveReference(u)
+ break
+ }
+ if s.up == nil {
+ break
+ }
+ s = s.up
+ }
+
+ return u
+}
+
+const topSchema = "_schema"
+
+// makeCUERef converts a URI into a CUE reference for the current location.
+// The returned identifier (or first expression in a selection chain), is
+// hardwired to point to the resolved value. This will allow astutil.Sanitize
+// to automatically unshadow any shadowed variables.
+func (s *state) makeCUERef(n cue.Value, u *url.URL) ast.Expr {
+ a := splitFragment(u)
+
+ switch fn := s.cfg.Map; {
+ case fn != nil:
+ // TODO: This block is only used in case s.cfg.Map is set, which is
+ // currently only used for OpenAPI. Handling should be brought more in
+ // line with JSON schema.
+ a, err := fn(n.Pos(), a)
+ if err != nil {
+ s.addErr(errors.Newf(n.Pos(), "invalid reference %q: %v", u, err))
+ return nil
+ }
+ if len(a) == 0 {
+ // TODO: should we allow inserting at root level?
+ s.addErr(errors.Newf(n.Pos(),
+ "invalid empty reference returned by map for %q", u))
+ return nil
+ }
+ sel, ok := a[0].(ast.Expr)
+ if !ok {
+ sel = &ast.BadExpr{}
+ }
+ for _, l := range a[1:] {
+ switch x := l.(type) {
+ case *ast.Ident:
+ sel = &ast.SelectorExpr{X: sel, Sel: x}
+
+ case *ast.BasicLit:
+ sel = &ast.IndexExpr{X: sel, Index: x}
+ }
+ }
+ return sel
+ }
+
+ var ident *ast.Ident
+
+ for ; ; s = s.up {
+ if s.up == nil {
+ switch {
+ case u.Host == "" && u.Path == "",
+ s.id != nil && s.id.Host == u.Host && s.id.Path == u.Path:
+ if len(a) == 0 {
+ // refers to the top of the file. We will allow this by
+ // creating a helper schema as such:
+ // _schema: {...}
+ // _schema
+ // This is created at the finalization stage if
+ // hasSelfReference is set.
+ s.hasSelfReference = true
+
+ ident = ast.NewIdent(topSchema)
+ ident.Node = s.obj
+ return ident
+ }
+
+ ident, a = s.getNextIdent(n, a)
+
+ case u.Host != "":
+ // Reference not found within scope. Create an import reference.
+
+ // TODO: allow the configuration to specify a map from
+ // URI domain+paths to CUE packages.
+
+ // TODO: currently only $ids that are in scope can be
+ // referenced. We could consider doing an extra pass to record
+ // all '$id's in a file to be able to link to them even if they
+ // are not in scope.
+ p := u.Path
+
+ base := path.Base(p)
+ if !ast.IsValidIdent(base) {
+ if strings.HasSuffix(base, ".json") {
+ base = base[:len(base)-len(".json")]
+ }
+ if !ast.IsValidIdent(base) {
+ // Find something more clever to do there. For now just
+ // pick "schema" as the package name.
+ base = "schema"
+ }
+ p += ":" + base
+ }
+
+ ident = ast.NewIdent(base)
+ ident.Node = &ast.ImportSpec{Path: ast.NewString(u.Host + p)}
+
+ default:
+ // Just a path, not sure what that means.
+ s.errf(n, "unknown domain for reference %q", u)
+ return nil
+ }
+ break
+ }
+
+ if s.id == nil {
+ continue
+ }
+
+ if s.id.Host == u.Host && s.id.Path == u.Path {
+ if len(a) == 0 {
+ if len(s.idRef) == 0 {
+ // This is a reference to either root or a schema for which
+ // we do not yet support references. See Issue #386.
+ if s.up.up != nil {
+ s.errf(n, "cannot refer to internal schema %q", u)
+ return nil
+ }
+
+ // This is referring to the root scope. There is a dummy
+ // state above the root state that we need to update.
+ s = s.up
+
+ // refers to the top of the file. We will allow this by
+ // creating a helper schema as such:
+ // _schema: {...}
+ // _schema
+ // This is created at the finalization stage if
+ // hasSelfReference is set.
+ s.hasSelfReference = true
+ ident = ast.NewIdent(topSchema)
+ ident.Node = s.obj
+ return ident
+ }
+
+ x := s.idRef[0]
+ if !x.isDef && !ast.IsValidIdent(x.name) {
+ s.errf(n, "referring to field %q not supported", x.name)
+ return nil
+ }
+ e := ast.NewIdent(x.name)
+ if len(s.idRef) == 1 {
+ return e
+ }
+ return newSel(e, s.idRef[1])
+ }
+ ident, a = s.getNextIdent(n, a)
+ ident.Node = s.obj
+ break
+ }
+ }
+
+ return s.newSel(ident, n, a)
+}
+
+// getNextSelector translates a JSON Reference path into a CUE path by consuming
+// the first path elements and returning the corresponding CUE label.
+func (s *state) getNextSelector(v cue.Value, a []string) (l label, tail []string) {
+ switch elem := a[0]; elem {
+ case "$defs", "definitions":
+ if len(a) == 1 {
+ s.errf(v, "cannot refer to %s section: must refer to one of its elements", a[0])
+ return label{}, nil
+ }
+
+ if name := "#" + a[1]; ast.IsValidIdent(name) {
+ return label{name, true}, a[2:]
+ }
+
+ return label{"#", true}, a[1:]
+
+ case "properties":
+ if len(a) == 1 {
+ s.errf(v, "cannot refer to %s section: must refer to one of its elements", a[0])
+ return label{}, nil
+ }
+
+ return label{a[1], false}, a[2:]
+
+ default:
+ return label{elem, false}, a[1:]
+
+ case "additionalProperties",
+ "patternProperties",
+ "items",
+ "additionalItems":
+ // TODO: as a temporary workaround, include the schema verbatim.
+ // TODO: provide definitions for these in CUE.
+ s.errf(v, "referring to field %q not yet supported", elem)
+
+ // Other known fields cannot be supported.
+ return label{}, nil
+ }
+}
+
+// newSel converts a JSON Reference path and initial CUE identifier to
+// a CUE selection path.
+func (s *state) newSel(e ast.Expr, v cue.Value, a []string) ast.Expr {
+ for len(a) > 0 {
+ var label label
+ label, a = s.getNextSelector(v, a)
+ e = newSel(e, label)
+ }
+ return e
+}
+
+// newSel converts label to a CUE index and creates an expression to index
+// into e.
+func newSel(e ast.Expr, label label) ast.Expr {
+ if label.isDef {
+ return ast.NewSel(e, label.name)
+
+ }
+ if ast.IsValidIdent(label.name) && !internal.IsDefOrHidden(label.name) {
+ return ast.NewSel(e, label.name)
+ }
+ return &ast.IndexExpr{X: e, Index: ast.NewString(label.name)}
+}
+
+func (s *state) setField(lab label, f *ast.Field) {
+ x := s.getRef(lab)
+ x.field = f
+ s.setRef(lab, x)
+ x = s.getRef(lab)
+}
+
+func (s *state) getRef(lab label) refs {
+ if s.fieldRefs == nil {
+ s.fieldRefs = make(map[label]refs)
+ }
+ x, ok := s.fieldRefs[lab]
+ if !ok {
+ if lab.isDef ||
+ (ast.IsValidIdent(lab.name) && !internal.IsDefOrHidden(lab.name)) {
+ x.ident = lab.name
+ } else {
+ x.ident = "_X" + strconv.Itoa(s.decoder.numID)
+ s.decoder.numID++
+ }
+ s.fieldRefs[lab] = x
+ }
+ return x
+}
+
+func (s *state) setRef(lab label, r refs) {
+ s.fieldRefs[lab] = r
+}
+
+// getNextIdent gets the first CUE reference from a JSON Reference path and
+// converts it to a CUE identifier.
+func (s *state) getNextIdent(v cue.Value, a []string) (resolved *ast.Ident, tail []string) {
+ lab, a := s.getNextSelector(v, a)
+
+ x := s.getRef(lab)
+ ident := ast.NewIdent(x.ident)
+ x.refs = append(x.refs, ident)
+ s.setRef(lab, x)
+
+ return ident, a
+}
+
+// linkReferences resolves identifiers to relevant nodes. This allows
+// astutil.Sanitize to unshadow nodes if necessary.
+func (s *state) linkReferences() {
+ for _, r := range s.fieldRefs {
+ if r.field == nil {
+ // TODO: improve error message.
+ s.errf(cue.Value{}, "reference to non-existing value %q", r.ident)
+ continue
+ }
+
+ // link resembles the link value. See astutil.Resolve.
+ var link ast.Node
+
+ ident, ok := r.field.Label.(*ast.Ident)
+ if ok && ident.Name == r.ident {
+ link = r.field.Value
+ } else if len(r.refs) > 0 {
+ r.field.Label = &ast.Alias{
+ Ident: ast.NewIdent(r.ident),
+ Expr: r.field.Label.(ast.Expr),
+ }
+ link = r.field
+ }
+
+ for _, i := range r.refs {
+ i.Node = link
+ }
+ }
+}
+
+// splitFragment splits the fragment part of a URI into path components. The
+// result may be an empty slice.
+//
+// TODO: this requires RawFragment introduced in go1.15 to function properly.
+// As for now, CUE still uses go1.12.
+func splitFragment(u *url.URL) []string {
+ if u.Fragment == "" {
+ return nil
+ }
s := strings.TrimRight(u.Fragment[1:], "/")
+ if s == "" {
+ return nil
+ }
return strings.Split(s, "/")
}
diff --git a/encoding/jsonschema/testdata/def.txtar b/encoding/jsonschema/testdata/def.txtar
index 9b47661..35ea0a3 100644
--- a/encoding/jsonschema/testdata/def.txtar
+++ b/encoding/jsonschema/testdata/def.txtar
@@ -39,7 +39,8 @@
}
-- out.cue --
-@jsonschema(schema="http://json-schema.org/draft-07/schema#",id="http://cuelang.org/go/encoding/openapi/testdata/order.json")
+@jsonschema(schema="http://json-schema.org/draft-07/schema#")
+@jsonschema(id="http://cuelang.org/go/encoding/openapi/testdata/order.json")
person?: #["per-son"]
billing_address?: #address
shipping_address?: #address
diff --git a/encoding/jsonschema/testdata/ref.txtar b/encoding/jsonschema/testdata/ref.txtar
new file mode 100644
index 0000000..50d56ab
--- /dev/null
+++ b/encoding/jsonschema/testdata/ref.txtar
@@ -0,0 +1,111 @@
+// This test tests the conversion and ordering of $defs.
+
+#noverify
+
+-- definition.json --
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+
+ "$id": "http://cuelang.org/go/encoding/openapi/testdata/order.json",
+
+ "$defs": {
+ "address": {
+ "type": "object",
+ "properties": {
+ "city": { "type": "string" }
+ }
+ },
+ "int": {
+ "type": "integer"
+ },
+ "string-int": {
+ "type": [ "integer", "string" ]
+ },
+ "person": {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "children": {
+ "type": "object",
+ "properties": {
+ "x": { "$ref": "http://acme.com/external.json#/properties/foo" },
+
+ "a": { "$ref": "#/$defs/int" },
+ "b": { "$ref": "http://cuelang.org/person.json#/$defs/int" },
+ "c": { "$ref": "http://cuelang.org/go/encoding/openapi/testdata/order.json#/$defs/int" },
+ "d": { "$ref": "http://cuelang.org/go/encoding/openapi/testdata/order.json#/$defs/address" },
+ "e": { "$ref": "http://cuelang.org/go/encoding/openapi/testdata/order.json#/$defs/string-int" },
+ "f": { "$ref": "http://cuelang.org/person.json" },
+ "g": { "$ref": "http://acme.com/external.json#/definitions/foo" },
+ "h": { "$ref": "http://acme.com/external.json#/properties/foo" },
+ "i": { "$ref": "http://acme.com/external.json" },
+ "j": { "$ref": "http://acme.com/external-foo.json" },
+ "k": { "$ref": "http://acme.com/external-bar.json" },
+ "z": {}
+ }
+ }
+ },
+ "$id": "http://cuelang.org/person.json",
+ "$defs": {
+ "int": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+
+ "type": "object",
+
+ "properties": {
+ "person": { "$ref": "#/$defs/person" },
+ "billing_address": { "$ref": "#/$defs/address" },
+ "shipping_address": { "$ref": "#/$defs/address" }
+ }
+}
+
+-- out.cue --
+import (
+ "acme.com/external.json:external"
+ "acme.com/external-foo.json:schema"
+ schema_5 "acme.com/external-bar.json:schema"
+)
+
+@jsonschema(schema="http://json-schema.org/draft-07/schema#")
+@jsonschema(id="http://cuelang.org/go/encoding/openapi/testdata/order.json")
+person?: #person
+billing_address?: #address
+shipping_address?: #address
+
+#int_1=#int: int
+
+#address: {
+ city?: string
+ ...
+}
+
+#: "string-int": int | string
+
+#person: {
+ @jsonschema(id="http://cuelang.org/person.json")
+ name?: string
+ children?: {
+ x?: external.foo
+ a?: #int
+ b?: #int
+ c?: #int_1
+ d?: #address
+ e?: #["string-int"]
+ f?: #person
+ g?: external.#foo
+ h?: external.foo
+ i?: external
+ j?: schema
+ k?: schema_5
+ z?: _
+ ...
+ }
+
+ #int: int
+ ...
+}
+...
diff --git a/encoding/jsonschema/testdata/refroot.txtar b/encoding/jsonschema/testdata/refroot.txtar
new file mode 100644
index 0000000..c090506
--- /dev/null
+++ b/encoding/jsonschema/testdata/refroot.txtar
@@ -0,0 +1,27 @@
+// This test tests the conversion and ordering of $defs.
+
+-- definition.json --
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+
+ "$id": "http://cuelang.org/go/encoding/openapi/testdata/order.json",
+
+ "properties": {
+ "value": {},
+ "next": { "$ref": "#" }
+ }
+}
+
+-- out.cue --
+_schema
+_schema: {
+ @jsonschema(schema="http://json-schema.org/draft-07/schema#")
+ number | null | bool | string | [...] | {
+ @jsonschema(id="http://cuelang.org/go/encoding/openapi/testdata/order.json")
+ value?: _
+ next?: _schema_1
+ ...
+ }
+}
+
+let _schema_1 = _schema
diff --git a/encoding/jsonschema/testdata/refroot2.txtar b/encoding/jsonschema/testdata/refroot2.txtar
new file mode 100644
index 0000000..20e6361
--- /dev/null
+++ b/encoding/jsonschema/testdata/refroot2.txtar
@@ -0,0 +1,24 @@
+// This test tests the conversion and ordering of $defs.
+
+-- definition.json --
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+
+ "properties": {
+ "value": {},
+ "next": { "$ref": "#" }
+ }
+}
+
+-- out.cue --
+_schema
+_schema: {
+ @jsonschema(schema="http://json-schema.org/draft-07/schema#")
+ number | null | bool | string | [...] | {
+ value?: _
+ next?: _schema_1
+ ...
+ }
+}
+
+let _schema_1 = _schema