blob: c57253c1f5480885743ab5947e3163774a66b618 [file] [log] [blame]
// 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 jsonschema
import (
"net/url"
"path"
"strconv"
"strings"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/token"
"cuelang.org/go/internal"
"cuelang.org/go/internal/legacy/cue"
)
func (d *decoder) parseRef(p token.Pos, str string) []string {
u, err := url.Parse(str)
if err != nil {
d.addErr(errors.Newf(p, "invalid JSON reference: %s", err))
return nil
}
if u.Host != "" || u.Path != "" {
d.addErr(errors.Newf(p, "external references (%s) not supported", str))
// TODO: handle
// host:
// If the host corresponds to a package known to cue,
// load it from there. It would prefer schema converted to
// CUE, although we could consider loading raw JSON schema
// if present.
// If not present, advise the user to run cue get.
// path:
// Look up on file system or relatively to authority location.
return nil
}
if !path.IsAbs(u.Fragment) {
d.addErr(errors.Newf(p, "anchors (%s) not supported", u.Fragment))
// TODO: support anchors
return nil
}
// NOTE: Go bug?: url.URL has no raw representation of the fragment. This
// 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, "/")
}
func (d *decoder) mapRef(p token.Pos, str string, ref []string) []ast.Label {
fn := d.cfg.Map
if fn == nil {
fn = jsonSchemaRef
}
a, err := fn(p, ref)
if err != nil {
if str == "" {
str = "#/" + strings.Join(ref, "/")
}
d.addErr(errors.Newf(p, "invalid reference %q: %v", str, err))
return nil
}
if len(a) == 0 {
// TODO: should we allow inserting at root level?
if str == "" {
str = "#/" + strings.Join(ref, "/")
}
d.addErr(errors.Newf(p,
"invalid empty reference returned by map for %q", str))
return nil
}
return a
}
func jsonSchemaRef(p token.Pos, a []string) ([]ast.Label, error) {
// TODO: technically, references could reference a
// non-definition. We disallow this case for the standard
// JSON Schema interpretation. We could detect cases that
// are not definitions and then resolve those as literal
// values.
if len(a) != 2 || (a[0] != "definitions" && a[0] != "$defs") {
return nil, errors.Newf(p,
// Don't mention the ability to use $defs, as this definition seems
// to already have been withdrawn from the JSON Schema spec.
"$ref must be of the form #/definitions/...")
}
name := a[1]
if ast.IsValidIdent(name) &&
name != rootDefs[1:] &&
!internal.IsDefOrHidden(name) {
return []ast.Label{ast.NewIdent("#" + name)}, nil
}
return []ast.Label{ast.NewIdent(rootDefs), ast.NewString(name)}, nil
}