cuelang.org/go@v0.13.0/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  	"encoding/base64"
    19  	"fmt"
    20  	"net/url"
    21  	"path"
    22  	"slices"
    23  	"strconv"
    24  	"strings"
    25  
    26  	"cuelang.org/go/cue"
    27  	"cuelang.org/go/cue/ast"
    28  	"cuelang.org/go/cue/errors"
    29  	"cuelang.org/go/cue/token"
    30  	"cuelang.org/go/internal"
    31  )
    32  
    33  func parseRootRef(str string) (cue.Path, error) {
    34  	u, err := url.Parse(str)
    35  	if err != nil {
    36  		return cue.Path{}, fmt.Errorf("invalid JSON reference: %s", err)
    37  	}
    38  	if u.Host != "" || u.Path != "" || u.Opaque != "" {
    39  		return cue.Path{}, fmt.Errorf("external references (%s) not supported in Root", str)
    40  	}
    41  	// As a special case for backward compatibility, treat
    42  	// trim a final slash because the docs specifically
    43  	// mention that #/ refers to the root document
    44  	// and the openapi code uses #/components/schemas/.
    45  	// (technically a trailing slash `/` means there's an empty
    46  	// final element).
    47  	u.Fragment = strings.TrimSuffix(u.Fragment, "/")
    48  	fragmentParts := slices.Collect(jsonPointerTokens(u.Fragment))
    49  	var selectors []cue.Selector
    50  	for _, r := range fragmentParts {
    51  		if i, err := strconv.ParseUint(r, 10, 64); err == nil && strconv.FormatUint(i, 10) == r {
    52  			// Technically this is incorrect because a numeric element
    53  			// could also be a string selector and the resulting path
    54  			// will not allow that.
    55  			selectors = append(selectors, cue.Index(int64(i)))
    56  		} else {
    57  			selectors = append(selectors, cue.Str(r))
    58  		}
    59  	}
    60  	return cue.MakePath(selectors...), nil
    61  }
    62  
    63  var errRefNotFound = errors.New("JSON Pointer reference not found")
    64  
    65  func lookupJSONPointer(v cue.Value, p string) (_ cue.Value, _err error) {
    66  	// TODO(go1.23) for part := range jsonPointerTokens(p)
    67  	jsonPointerTokens(p)(func(part string) bool {
    68  		// Note: a JSON Pointer doesn't distinguish between indexing
    69  		// and struct lookup. We have to use the value itself to decide
    70  		// which operation is appropriate.
    71  		v, _ = v.Default()
    72  		switch v.Kind() {
    73  		case cue.StructKind:
    74  			v = v.LookupPath(cue.MakePath(cue.Str(part)))
    75  		case cue.ListKind:
    76  			idx := int64(0)
    77  			if len(part) > 1 && part[0] == '0' {
    78  				// Leading zeros are not allowed
    79  				_err = errRefNotFound
    80  				return false
    81  			}
    82  			idx, err := strconv.ParseInt(part, 10, 64)
    83  			if err != nil {
    84  				_err = errRefNotFound
    85  				return false
    86  			}
    87  			v = v.LookupPath(cue.MakePath(cue.Index(idx)))
    88  		}
    89  		if !v.Exists() {
    90  			_err = errRefNotFound
    91  			return false
    92  		}
    93  		return true
    94  	})
    95  	return v, _err
    96  }
    97  
    98  func sameSchemaRoot(u1, u2 *url.URL) bool {
    99  	return u1.Host == u2.Host && u1.Path == u2.Path && u1.Opaque == u2.Opaque
   100  }
   101  
   102  // resolveURI parses a URI from s and resolves it in the current context.
   103  // To resolve it in the current context, it looks for the closest URI from
   104  // an $id in the parent scopes and the uses the URI resolution to get the
   105  // new URI.
   106  //
   107  // This method is used to resolve any URI, including those from $id and $ref.
   108  func (s *state) resolveURI(n cue.Value) *url.URL {
   109  	str, ok := s.strValue(n)
   110  	if !ok {
   111  		return nil
   112  	}
   113  
   114  	u, err := url.Parse(str)
   115  	if err != nil {
   116  		s.addErr(errors.Newf(n.Pos(), "invalid JSON reference: %v", err))
   117  		return nil
   118  	}
   119  
   120  	if u.IsAbs() {
   121  		// Absolute URI: no need to walk up the tree.
   122  		if u.Host == DefaultRootIDHost {
   123  			// No-one should be using the default root ID explicitly.
   124  			s.errf(n, "invalid use of default root ID host (%v) in URI", DefaultRootIDHost)
   125  			return nil
   126  		}
   127  		return u
   128  	}
   129  
   130  	return s.schemaRoot().id.ResolveReference(u)
   131  }
   132  
   133  // schemaRoot returns the state for the nearest enclosing
   134  // schema that has its own schema ID.
   135  func (s *state) schemaRoot() *state {
   136  	for ; s != nil; s = s.up {
   137  		if s.id != nil {
   138  			return s
   139  		}
   140  	}
   141  	// Should never happen, as we ensure there's always an absolute
   142  	// URI at the root.
   143  	panic("unreachable")
   144  }
   145  
   146  // DefaultMapRef implements the default logic for mapping a schema location
   147  // to CUE.
   148  // It uses a heuristic to map the URL host and path to an import path,
   149  // and maps the fragment part according to the following:
   150  //
   151  //	#                    <empty path>
   152  //	#/definitions/foo   #foo or #."foo"
   153  //	#/$defs/foo   #foo or #."foo"
   154  func DefaultMapRef(loc SchemaLoc) (importPath string, path cue.Path, err error) {
   155  	return defaultMapRef(loc, defaultMap, DefaultMapURL)
   156  }
   157  
   158  // defaultMapRef implements the default MapRef semantics
   159  // in terms of the default Map and MapURL functions provided
   160  // in the configuration.
   161  func defaultMapRef(
   162  	loc SchemaLoc,
   163  	mapFn func(pos token.Pos, path []string) ([]ast.Label, error),
   164  	mapURLFn func(u *url.URL) (importPath string, path cue.Path, err error),
   165  ) (importPath string, path cue.Path, err error) {
   166  	var fragment string
   167  	if loc.IsLocal {
   168  		fragment = cuePathToJSONPointer(loc.Path)
   169  	} else {
   170  		// It's external: use mapURLFn.
   171  		u := ref(*loc.ID)
   172  		fragment = loc.ID.Fragment
   173  		u.Fragment = ""
   174  		var err error
   175  		importPath, path, err = mapURLFn(u)
   176  		if err != nil {
   177  			return "", cue.Path{}, err
   178  		}
   179  	}
   180  	if len(fragment) > 0 && fragment[0] != '/' {
   181  		return "", cue.Path{}, fmt.Errorf("anchors (%s) not supported", fragment)
   182  	}
   183  	parts := slices.Collect(jsonPointerTokens(fragment))
   184  	labels, err := mapFn(token.Pos{}, parts)
   185  	if err != nil {
   186  		return "", cue.Path{}, err
   187  	}
   188  	relPath, err := labelsToCUEPath(labels)
   189  	if err != nil {
   190  		return "", cue.Path{}, err
   191  	}
   192  	return importPath, pathConcat(path, relPath), nil
   193  }
   194  
   195  func defaultMap(p token.Pos, a []string) ([]ast.Label, error) {
   196  	if len(a) == 0 {
   197  		return nil, nil
   198  	}
   199  	// TODO: technically, references could reference a
   200  	// non-definition. We disallow this case for the standard
   201  	// JSON Schema interpretation. We could detect cases that
   202  	// are not definitions and then resolve those as literal
   203  	// values.
   204  	if len(a) != 2 || (a[0] != "definitions" && a[0] != "$defs") {
   205  		// It's an internal reference (or a nested definition reference).
   206  		// Fall back to defining it in the internal namespace.
   207  		// TODO this is needlessly inefficient, as we're putting something
   208  		// back together that was already joined before defaultMap was
   209  		// invoked. This does avoid dual implementations though.
   210  		p := jsonPointerFromTokens(slices.Values(a))
   211  		return []ast.Label{ast.NewIdent("_#defs"), ast.NewString(p)}, nil
   212  	}
   213  	name := a[1]
   214  	if ast.IsValidIdent(name) &&
   215  		name != rootDefs[1:] &&
   216  		!internal.IsDefOrHidden(name) {
   217  		return []ast.Label{ast.NewIdent("#" + name)}, nil
   218  	}
   219  	return []ast.Label{ast.NewIdent(rootDefs), ast.NewString(name)}, nil
   220  }
   221  
   222  // DefaultMapURL implements the default schema ID to import
   223  // path mapping. It trims off any ".json" suffix and uses the
   224  // package name "schema" if the final component of the path
   225  // isn't a valid CUE identifier.
   226  //
   227  // Deprecated: The [Config.MapURL] API is superceded in
   228  // factor of [Config.MapRef].
   229  func DefaultMapURL(u *url.URL) (string, cue.Path, error) {
   230  	p := u.Path
   231  	base := path.Base(p)
   232  	if !ast.IsValidIdent(base) {
   233  		base = strings.TrimSuffix(base, ".json")
   234  		if !ast.IsValidIdent(base) {
   235  			// Find something more clever to do there. For now just
   236  			// pick "schema" as the package name.
   237  			base = "schema"
   238  		}
   239  		p += ":" + base
   240  	}
   241  	if u.Opaque != "" {
   242  		// TODO don't use base64 unless we really have to.
   243  		return base64.RawURLEncoding.EncodeToString([]byte(u.Opaque)), cue.Path{}, nil
   244  	}
   245  	return u.Host + p, cue.Path{}, nil
   246  }