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