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 }