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