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 }