cuelang.org/go@v0.10.1/encoding/jsonschema/ref.go (about)

     1  // Copyright 2020 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package jsonschema
    16  
    17  import (
    18  	"fmt"
    19  	"net/url"
    20  	"path"
    21  	"strconv"
    22  	"strings"
    23  
    24  	"cuelang.org/go/cue"
    25  	"cuelang.org/go/cue/ast"
    26  	"cuelang.org/go/cue/errors"
    27  	"cuelang.org/go/cue/token"
    28  	"cuelang.org/go/internal"
    29  	"cuelang.org/go/mod/module"
    30  )
    31  
    32  func (d *decoder) parseRef(p token.Pos, str string) []string {
    33  	u, err := url.Parse(str)
    34  	if err != nil {
    35  		d.addErr(errors.Newf(p, "invalid JSON reference: %s", err))
    36  		return nil
    37  	}
    38  
    39  	if u.Host != "" || u.Path != "" {
    40  		d.addErr(errors.Newf(p, "external references (%s) not supported in Root", str))
    41  		// TODO: handle
    42  		//    host:
    43  		//      If the host corresponds to a package known to cue,
    44  		//      load it from there. It would prefer schema converted to
    45  		//      CUE, although we could consider loading raw JSON schema
    46  		//      if present.
    47  		//      If not present, advise the user to run cue get.
    48  		//    path:
    49  		//      Look up on file system or relatively to authority location.
    50  		return nil
    51  	}
    52  	fragmentParts, err := splitFragment(u)
    53  	if err != nil {
    54  		d.addErr(errors.Newf(p, "%v", err))
    55  		return nil
    56  	}
    57  	return fragmentParts
    58  }
    59  
    60  // resolveURI parses a URI from n and resolves it in the current context.
    61  // To resolve it in the current context, it looks for the closest URI from
    62  // an $id in the parent scopes and the uses the URI resolution to get the
    63  // new URI.
    64  //
    65  // This method is used to resolve any URI, including those from $id and $ref.
    66  func (s *state) resolveURI(n cue.Value) *url.URL {
    67  	str, ok := s.strValue(n)
    68  	if !ok {
    69  		return nil
    70  	}
    71  
    72  	u, err := url.Parse(str)
    73  	if err != nil {
    74  		s.addErr(errors.Newf(n.Pos(), "invalid JSON reference: %s", err))
    75  		return nil
    76  	}
    77  
    78  	for {
    79  		if s.id != nil {
    80  			u = s.id.ResolveReference(u)
    81  			break
    82  		}
    83  		if s.up == nil {
    84  			break
    85  		}
    86  		s = s.up
    87  	}
    88  
    89  	return u
    90  }
    91  
    92  const topSchema = "_schema"
    93  
    94  // makeCUERef converts a URI into a CUE reference for the current location.
    95  // The returned identifier (or first expression in a selection chain), is
    96  // hardwired to point to the resolved value. This will allow astutil.Sanitize
    97  // to automatically unshadow any shadowed variables.
    98  func (s *state) makeCUERef(n cue.Value, u *url.URL, fragmentParts []string) (_e ast.Expr) {
    99  	switch fn := s.cfg.Map; {
   100  	case fn != nil:
   101  		// TODO: This block is only used in case s.cfg.Map is set, which is
   102  		// currently only used for OpenAPI. Handling should be brought more in
   103  		// line with JSON schema.
   104  		a, err := fn(n.Pos(), fragmentParts)
   105  		if err != nil {
   106  			s.addErr(errors.Newf(n.Pos(), "invalid reference %q: %v", u, err))
   107  			return nil
   108  		}
   109  		if len(a) == 0 {
   110  			// TODO: should we allow inserting at root level?
   111  			s.addErr(errors.Newf(n.Pos(),
   112  				"invalid empty reference returned by map for %q", u))
   113  			return nil
   114  		}
   115  		sel, ok := a[0].(ast.Expr)
   116  		if !ok {
   117  			sel = &ast.BadExpr{}
   118  		}
   119  		for _, l := range a[1:] {
   120  			switch x := l.(type) {
   121  			case *ast.Ident:
   122  				sel = &ast.SelectorExpr{X: sel, Sel: x}
   123  
   124  			case *ast.BasicLit:
   125  				sel = &ast.IndexExpr{X: sel, Index: x}
   126  			}
   127  		}
   128  		return sel
   129  	}
   130  
   131  	var ident *ast.Ident
   132  
   133  	for ; ; s = s.up {
   134  		if s.up == nil {
   135  			switch {
   136  			case u.Host == "" && u.Path == "",
   137  				s.id != nil && s.id.Host == u.Host && s.id.Path == u.Path:
   138  				if len(fragmentParts) == 0 {
   139  					// refers to the top of the file. We will allow this by
   140  					// creating a helper schema as such:
   141  					//   _schema: {...}
   142  					//   _schema
   143  					// This is created at the finalization stage if
   144  					// hasSelfReference is set.
   145  					s.hasSelfReference = true
   146  
   147  					ident = ast.NewIdent(topSchema)
   148  					ident.Node = s.obj
   149  					return ident
   150  				}
   151  
   152  				ident, fragmentParts = s.getNextIdent(n, fragmentParts)
   153  
   154  			case u.Host != "":
   155  				// Reference not found within scope. Create an import reference.
   156  
   157  				// TODO: currently only $ids that are in scope can be
   158  				// referenced. We could consider doing an extra pass to record
   159  				// all '$id's in a file to be able to link to them even if they
   160  				// are not in scope.
   161  				importPath, err := s.cfg.MapURL(u)
   162  				if err != nil {
   163  					ustr := u.String()
   164  					// Avoid producing many errors for the same URL.
   165  					if !s.mapURLErrors[ustr] {
   166  						s.mapURLErrors[ustr] = true
   167  						s.errf(n, "cannot determine import path from URL %q: %v", ustr, err)
   168  					}
   169  					return nil
   170  				}
   171  				ip := module.ParseImportPath(importPath)
   172  				if ip.Qualifier == "" {
   173  					s.errf(n, "cannot determine package name from import path %q", importPath)
   174  					return nil
   175  				}
   176  				ident = ast.NewIdent(ip.Qualifier)
   177  				ident.Node = &ast.ImportSpec{Path: ast.NewString(importPath)}
   178  
   179  			default:
   180  				// Just a path, not sure what that means.
   181  				s.errf(n, "unknown domain for reference %q", u)
   182  				return nil
   183  			}
   184  			break
   185  		}
   186  
   187  		if s.id == nil {
   188  			continue
   189  		}
   190  
   191  		if s.id.Host == u.Host && s.id.Path == u.Path {
   192  			if len(fragmentParts) == 0 {
   193  				if len(s.idRef) == 0 {
   194  					// This is a reference to either root or a schema for which
   195  					// we do not yet support references. See Issue #386.
   196  					if s.up.up != nil {
   197  						s.errf(n, "cannot refer to internal schema %q", u)
   198  						return nil
   199  					}
   200  
   201  					// This is referring to the root scope. There is a dummy
   202  					// state above the root state that we need to update.
   203  					s = s.up
   204  
   205  					// refers to the top of the file. We will allow this by
   206  					// creating a helper schema as such:
   207  					//   _schema: {...}
   208  					//   _schema
   209  					// This is created at the finalization stage if
   210  					// hasSelfReference is set.
   211  					s.hasSelfReference = true
   212  					ident = ast.NewIdent(topSchema)
   213  					ident.Node = s.obj
   214  					return ident
   215  				}
   216  
   217  				x := s.idRef[0]
   218  				if !x.isDef && !ast.IsValidIdent(x.name) {
   219  					s.errf(n, "referring to field %q not supported", x.name)
   220  					return nil
   221  				}
   222  				e := ast.NewIdent(x.name)
   223  				if len(s.idRef) == 1 {
   224  					return e
   225  				}
   226  				return newSel(e, s.idRef[1])
   227  			}
   228  			ident, fragmentParts = s.getNextIdent(n, fragmentParts)
   229  			ident.Node = s.obj
   230  			break
   231  		}
   232  	}
   233  
   234  	return s.newSel(ident, n, fragmentParts)
   235  }
   236  
   237  // getNextSelector translates a JSON Reference path into a CUE path by consuming
   238  // the first path elements and returning the corresponding CUE label.
   239  func (s *state) getNextSelector(v cue.Value, a []string) (l label, tail []string) {
   240  	switch elem := a[0]; elem {
   241  	case "$defs", "definitions":
   242  		if len(a) == 1 {
   243  			s.errf(v, "cannot refer to %s section: must refer to one of its elements", a[0])
   244  			return label{}, nil
   245  		}
   246  
   247  		if name := "#" + a[1]; ast.IsValidIdent(name) {
   248  			return label{name, true}, a[2:]
   249  		}
   250  
   251  		return label{"#", true}, a[1:]
   252  
   253  	case "properties":
   254  		if len(a) == 1 {
   255  			s.errf(v, "cannot refer to %s section: must refer to one of its elements", a[0])
   256  			return label{}, nil
   257  		}
   258  
   259  		return label{a[1], false}, a[2:]
   260  
   261  	case "additionalProperties",
   262  		"patternProperties",
   263  		"items",
   264  		"additionalItems":
   265  		// TODO: as a temporary workaround, include the schema verbatim.
   266  		// TODO: provide definitions for these in CUE.
   267  		s.errf(v, "referring to field %q not yet supported", elem)
   268  
   269  		// Other known fields cannot be supported.
   270  		return label{}, nil
   271  
   272  	default:
   273  		return label{elem, false}, a[1:]
   274  	}
   275  }
   276  
   277  // newSel converts a JSON Reference path and initial CUE identifier to
   278  // a CUE selection path.
   279  func (s *state) newSel(e ast.Expr, v cue.Value, a []string) ast.Expr {
   280  	for len(a) > 0 {
   281  		var label label
   282  		label, a = s.getNextSelector(v, a)
   283  		e = newSel(e, label)
   284  	}
   285  	return e
   286  }
   287  
   288  // newSel converts label to a CUE index and creates an expression to index
   289  // into e.
   290  func newSel(e ast.Expr, label label) ast.Expr {
   291  	if label.isDef {
   292  		return ast.NewSel(e, label.name)
   293  
   294  	}
   295  	if ast.IsValidIdent(label.name) && !internal.IsDefOrHidden(label.name) {
   296  		return ast.NewSel(e, label.name)
   297  	}
   298  	return &ast.IndexExpr{X: e, Index: ast.NewString(label.name)}
   299  }
   300  
   301  func (s *state) setField(lab label, f *ast.Field) {
   302  	x := s.getRef(lab)
   303  	x.field = f
   304  	s.setRef(lab, x)
   305  	x = s.getRef(lab)
   306  }
   307  
   308  func (s *state) getRef(lab label) refs {
   309  	if s.fieldRefs == nil {
   310  		s.fieldRefs = make(map[label]refs)
   311  	}
   312  	x, ok := s.fieldRefs[lab]
   313  	if !ok {
   314  		if lab.isDef ||
   315  			(ast.IsValidIdent(lab.name) && !internal.IsDefOrHidden(lab.name)) {
   316  			x.ident = lab.name
   317  		} else {
   318  			x.ident = "_X" + strconv.Itoa(s.decoder.numID)
   319  			s.decoder.numID++
   320  		}
   321  		s.fieldRefs[lab] = x
   322  	}
   323  	return x
   324  }
   325  
   326  func (s *state) setRef(lab label, r refs) {
   327  	s.fieldRefs[lab] = r
   328  }
   329  
   330  // getNextIdent gets the first CUE reference from a JSON Reference path and
   331  // converts it to a CUE identifier.
   332  func (s *state) getNextIdent(v cue.Value, a []string) (resolved *ast.Ident, tail []string) {
   333  	lab, a := s.getNextSelector(v, a)
   334  
   335  	x := s.getRef(lab)
   336  	ident := ast.NewIdent(x.ident)
   337  	x.refs = append(x.refs, ident)
   338  	s.setRef(lab, x)
   339  
   340  	return ident, a
   341  }
   342  
   343  // linkReferences resolves identifiers to relevant nodes. This allows
   344  // astutil.Sanitize to unshadow nodes if necessary.
   345  func (s *state) linkReferences() {
   346  	for _, r := range s.fieldRefs {
   347  		if r.field == nil {
   348  			// TODO: improve error message.
   349  			s.errf(cue.Value{}, "reference to non-existing value %q", r.ident)
   350  			continue
   351  		}
   352  
   353  		// link resembles the link value. See astutil.Resolve.
   354  		var link ast.Node
   355  
   356  		ident, ok := r.field.Label.(*ast.Ident)
   357  		if ok && ident.Name == r.ident {
   358  			link = r.field.Value
   359  		} else if len(r.refs) > 0 {
   360  			r.field.Label = &ast.Alias{
   361  				Ident: ast.NewIdent(r.ident),
   362  				Expr:  r.field.Label.(ast.Expr),
   363  			}
   364  			link = r.field
   365  		}
   366  
   367  		for _, i := range r.refs {
   368  			i.Node = link
   369  		}
   370  	}
   371  }
   372  
   373  // splitFragment splits the fragment part of a URI into path components
   374  // and removes the fragment part from u.
   375  // The result may be an empty slice.
   376  //
   377  // TODO: use u.RawFragment so that we can accept field names
   378  // that contain `/` characters.
   379  func splitFragment(u *url.URL) ([]string, error) {
   380  	frag := u.EscapedFragment()
   381  	if frag == "" {
   382  		return nil, nil
   383  	}
   384  	if !strings.HasPrefix(frag, "/") {
   385  		return nil, fmt.Errorf("anchors (%s) not supported", frag)
   386  	}
   387  	u.Fragment = ""
   388  	u.RawFragment = ""
   389  
   390  	if s := strings.TrimRight(frag[1:], "/"); s != "" {
   391  		return strings.Split(s, "/"), nil
   392  	}
   393  	return nil, nil
   394  }
   395  
   396  func (d *decoder) mapRef(p token.Pos, str string, ref []string) []ast.Label {
   397  	fn := d.cfg.Map
   398  	if fn == nil {
   399  		fn = jsonSchemaRef
   400  	}
   401  	a, err := fn(p, ref)
   402  	if err != nil {
   403  		if str == "" {
   404  			str = "#/" + strings.Join(ref, "/")
   405  		}
   406  		d.addErr(errors.Newf(p, "invalid reference %q: %v", str, err))
   407  		return nil
   408  	}
   409  	if len(a) == 0 {
   410  		// TODO: should we allow inserting at root level?
   411  		if str == "" {
   412  			str = "#/" + strings.Join(ref, "/")
   413  		}
   414  		d.addErr(errors.Newf(p,
   415  			"invalid empty reference returned by map for %q", str))
   416  		return nil
   417  	}
   418  	return a
   419  }
   420  
   421  func jsonSchemaRef(p token.Pos, a []string) ([]ast.Label, error) {
   422  	// TODO: technically, references could reference a
   423  	// non-definition. We disallow this case for the standard
   424  	// JSON Schema interpretation. We could detect cases that
   425  	// are not definitions and then resolve those as literal
   426  	// values.
   427  	if len(a) != 2 || (a[0] != "definitions" && a[0] != "$defs") {
   428  		return nil, errors.Newf(p,
   429  			// Don't mention the ability to use $defs, as this definition seems
   430  			// to already have been withdrawn from the JSON Schema spec.
   431  			"$ref must be of the form #/definitions/...")
   432  	}
   433  	name := a[1]
   434  	if ast.IsValidIdent(name) &&
   435  		name != rootDefs[1:] &&
   436  		!internal.IsDefOrHidden(name) {
   437  		return []ast.Label{ast.NewIdent("#" + name)}, nil
   438  	}
   439  	return []ast.Label{ast.NewIdent(rootDefs), ast.NewString(name)}, nil
   440  }
   441  
   442  // DefaultMapURL implements the default schema ID to import
   443  // path mapping. It trims off any ".json" suffix and uses the
   444  // package name "schema" if the final component of the path
   445  // isn't a valid CUE identifier.
   446  func DefaultMapURL(u *url.URL) (importPath string, err error) {
   447  	p := u.Path
   448  	base := path.Base(p)
   449  	if !ast.IsValidIdent(base) {
   450  		base = strings.TrimSuffix(base, ".json")
   451  		if !ast.IsValidIdent(base) {
   452  			// Find something more clever to do there. For now just
   453  			// pick "schema" as the package name.
   454  			base = "schema"
   455  		}
   456  		p += ":" + base
   457  	}
   458  	return u.Host + p, nil
   459  }