Serge Bazanski | cc25bdf | 2018-10-25 14:02:58 +0200 | [diff] [blame] | 1 | // Copyright 2015 go-swagger maintainers |
| 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 spec |
| 16 | |
| 17 | import ( |
| 18 | "encoding/json" |
| 19 | "fmt" |
| 20 | "log" |
| 21 | "net/url" |
| 22 | "os" |
| 23 | "path" |
| 24 | "path/filepath" |
| 25 | "reflect" |
| 26 | "strings" |
| 27 | "sync" |
| 28 | |
| 29 | "github.com/go-openapi/jsonpointer" |
| 30 | "github.com/go-openapi/swag" |
| 31 | ) |
| 32 | |
| 33 | // ExpandOptions provides options for expand. |
| 34 | type ExpandOptions struct { |
| 35 | RelativeBase string |
| 36 | SkipSchemas bool |
| 37 | ContinueOnError bool |
| 38 | AbsoluteCircularRef bool |
| 39 | } |
| 40 | |
| 41 | // ResolutionCache a cache for resolving urls |
| 42 | type ResolutionCache interface { |
| 43 | Get(string) (interface{}, bool) |
| 44 | Set(string, interface{}) |
| 45 | } |
| 46 | |
| 47 | type simpleCache struct { |
| 48 | lock sync.RWMutex |
| 49 | store map[string]interface{} |
| 50 | } |
| 51 | |
| 52 | var resCache ResolutionCache |
| 53 | |
| 54 | func init() { |
| 55 | resCache = initResolutionCache() |
| 56 | } |
| 57 | |
| 58 | // initResolutionCache initializes the URI resolution cache |
| 59 | func initResolutionCache() ResolutionCache { |
| 60 | return &simpleCache{store: map[string]interface{}{ |
| 61 | "http://swagger.io/v2/schema.json": MustLoadSwagger20Schema(), |
| 62 | "http://json-schema.org/draft-04/schema": MustLoadJSONSchemaDraft04(), |
| 63 | }} |
| 64 | } |
| 65 | |
| 66 | // resolverContext allows to share a context during spec processing. |
| 67 | // At the moment, it just holds the index of circular references found. |
| 68 | type resolverContext struct { |
| 69 | // circulars holds all visited circular references, which allows shortcuts. |
| 70 | // NOTE: this is not just a performance improvement: it is required to figure out |
| 71 | // circular references which participate several cycles. |
| 72 | // This structure is privately instantiated and needs not be locked against |
| 73 | // concurrent access, unless we chose to implement a parallel spec walking. |
| 74 | circulars map[string]bool |
| 75 | basePath string |
| 76 | } |
| 77 | |
| 78 | func newResolverContext(originalBasePath string) *resolverContext { |
| 79 | return &resolverContext{ |
| 80 | circulars: make(map[string]bool), |
| 81 | basePath: originalBasePath, // keep the root base path in context |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | // Get retrieves a cached URI |
| 86 | func (s *simpleCache) Get(uri string) (interface{}, bool) { |
| 87 | debugLog("getting %q from resolution cache", uri) |
| 88 | s.lock.RLock() |
| 89 | v, ok := s.store[uri] |
| 90 | debugLog("got %q from resolution cache: %t", uri, ok) |
| 91 | |
| 92 | s.lock.RUnlock() |
| 93 | return v, ok |
| 94 | } |
| 95 | |
| 96 | // Set caches a URI |
| 97 | func (s *simpleCache) Set(uri string, data interface{}) { |
| 98 | s.lock.Lock() |
| 99 | s.store[uri] = data |
| 100 | s.lock.Unlock() |
| 101 | } |
| 102 | |
| 103 | // ResolveRefWithBase resolves a reference against a context root with preservation of base path |
| 104 | func ResolveRefWithBase(root interface{}, ref *Ref, opts *ExpandOptions) (*Schema, error) { |
| 105 | resolver, err := defaultSchemaLoader(root, opts, nil, nil) |
| 106 | if err != nil { |
| 107 | return nil, err |
| 108 | } |
| 109 | specBasePath := "" |
| 110 | if opts != nil && opts.RelativeBase != "" { |
| 111 | specBasePath, _ = absPath(opts.RelativeBase) |
| 112 | } |
| 113 | |
| 114 | result := new(Schema) |
| 115 | if err := resolver.Resolve(ref, result, specBasePath); err != nil { |
| 116 | return nil, err |
| 117 | } |
| 118 | return result, nil |
| 119 | } |
| 120 | |
| 121 | // ResolveRef resolves a reference against a context root |
| 122 | // ref is guaranteed to be in root (no need to go to external files) |
| 123 | // ResolveRef is ONLY called from the code generation module |
| 124 | func ResolveRef(root interface{}, ref *Ref) (*Schema, error) { |
| 125 | res, _, err := ref.GetPointer().Get(root) |
| 126 | if err != nil { |
| 127 | panic(err) |
| 128 | } |
| 129 | switch sch := res.(type) { |
| 130 | case Schema: |
| 131 | return &sch, nil |
| 132 | case *Schema: |
| 133 | return sch, nil |
| 134 | case map[string]interface{}: |
| 135 | b, _ := json.Marshal(sch) |
| 136 | newSch := new(Schema) |
| 137 | _ = json.Unmarshal(b, newSch) |
| 138 | return newSch, nil |
| 139 | default: |
| 140 | return nil, fmt.Errorf("unknown type for the resolved reference") |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | // ResolveParameter resolves a parameter reference against a context root |
| 145 | func ResolveParameter(root interface{}, ref Ref) (*Parameter, error) { |
| 146 | return ResolveParameterWithBase(root, ref, nil) |
| 147 | } |
| 148 | |
| 149 | // ResolveParameterWithBase resolves a parameter reference against a context root and base path |
| 150 | func ResolveParameterWithBase(root interface{}, ref Ref, opts *ExpandOptions) (*Parameter, error) { |
| 151 | resolver, err := defaultSchemaLoader(root, opts, nil, nil) |
| 152 | if err != nil { |
| 153 | return nil, err |
| 154 | } |
| 155 | |
| 156 | result := new(Parameter) |
| 157 | if err := resolver.Resolve(&ref, result, ""); err != nil { |
| 158 | return nil, err |
| 159 | } |
| 160 | return result, nil |
| 161 | } |
| 162 | |
| 163 | // ResolveResponse resolves response a reference against a context root |
| 164 | func ResolveResponse(root interface{}, ref Ref) (*Response, error) { |
| 165 | return ResolveResponseWithBase(root, ref, nil) |
| 166 | } |
| 167 | |
| 168 | // ResolveResponseWithBase resolves response a reference against a context root and base path |
| 169 | func ResolveResponseWithBase(root interface{}, ref Ref, opts *ExpandOptions) (*Response, error) { |
| 170 | resolver, err := defaultSchemaLoader(root, opts, nil, nil) |
| 171 | if err != nil { |
| 172 | return nil, err |
| 173 | } |
| 174 | |
| 175 | result := new(Response) |
| 176 | if err := resolver.Resolve(&ref, result, ""); err != nil { |
| 177 | return nil, err |
| 178 | } |
| 179 | return result, nil |
| 180 | } |
| 181 | |
| 182 | // ResolveItems resolves header and parameter items reference against a context root and base path |
| 183 | func ResolveItems(root interface{}, ref Ref, opts *ExpandOptions) (*Items, error) { |
| 184 | resolver, err := defaultSchemaLoader(root, opts, nil, nil) |
| 185 | if err != nil { |
| 186 | return nil, err |
| 187 | } |
| 188 | basePath := "" |
| 189 | if opts.RelativeBase != "" { |
| 190 | basePath = opts.RelativeBase |
| 191 | } |
| 192 | result := new(Items) |
| 193 | if err := resolver.Resolve(&ref, result, basePath); err != nil { |
| 194 | return nil, err |
| 195 | } |
| 196 | return result, nil |
| 197 | } |
| 198 | |
| 199 | // ResolvePathItem resolves response a path item against a context root and base path |
| 200 | func ResolvePathItem(root interface{}, ref Ref, opts *ExpandOptions) (*PathItem, error) { |
| 201 | resolver, err := defaultSchemaLoader(root, opts, nil, nil) |
| 202 | if err != nil { |
| 203 | return nil, err |
| 204 | } |
| 205 | basePath := "" |
| 206 | if opts.RelativeBase != "" { |
| 207 | basePath = opts.RelativeBase |
| 208 | } |
| 209 | result := new(PathItem) |
| 210 | if err := resolver.Resolve(&ref, result, basePath); err != nil { |
| 211 | return nil, err |
| 212 | } |
| 213 | return result, nil |
| 214 | } |
| 215 | |
| 216 | type schemaLoader struct { |
| 217 | root interface{} |
| 218 | options *ExpandOptions |
| 219 | cache ResolutionCache |
| 220 | context *resolverContext |
| 221 | loadDoc func(string) (json.RawMessage, error) |
| 222 | } |
| 223 | |
| 224 | var idPtr, _ = jsonpointer.New("/id") |
| 225 | var refPtr, _ = jsonpointer.New("/$ref") |
| 226 | |
| 227 | // PathLoader function to use when loading remote refs |
| 228 | var PathLoader func(string) (json.RawMessage, error) |
| 229 | |
| 230 | func init() { |
| 231 | PathLoader = func(path string) (json.RawMessage, error) { |
| 232 | data, err := swag.LoadFromFileOrHTTP(path) |
| 233 | if err != nil { |
| 234 | return nil, err |
| 235 | } |
| 236 | return json.RawMessage(data), nil |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | func defaultSchemaLoader( |
| 241 | root interface{}, |
| 242 | expandOptions *ExpandOptions, |
| 243 | cache ResolutionCache, |
| 244 | context *resolverContext) (*schemaLoader, error) { |
| 245 | |
| 246 | if cache == nil { |
| 247 | cache = resCache |
| 248 | } |
| 249 | if expandOptions == nil { |
| 250 | expandOptions = &ExpandOptions{} |
| 251 | } |
| 252 | absBase, _ := absPath(expandOptions.RelativeBase) |
| 253 | if context == nil { |
| 254 | context = newResolverContext(absBase) |
| 255 | } |
| 256 | return &schemaLoader{ |
| 257 | root: root, |
| 258 | options: expandOptions, |
| 259 | cache: cache, |
| 260 | context: context, |
| 261 | loadDoc: func(path string) (json.RawMessage, error) { |
| 262 | debugLog("fetching document at %q", path) |
| 263 | return PathLoader(path) |
| 264 | }, |
| 265 | }, nil |
| 266 | } |
| 267 | |
| 268 | func idFromNode(node interface{}) (*Ref, error) { |
| 269 | if idValue, _, err := idPtr.Get(node); err == nil { |
| 270 | if refStr, ok := idValue.(string); ok && refStr != "" { |
| 271 | idRef, err := NewRef(refStr) |
| 272 | if err != nil { |
| 273 | return nil, err |
| 274 | } |
| 275 | return &idRef, nil |
| 276 | } |
| 277 | } |
| 278 | return nil, nil |
| 279 | } |
| 280 | |
| 281 | func nextRef(startingNode interface{}, startingRef *Ref, ptr *jsonpointer.Pointer) *Ref { |
| 282 | if startingRef == nil { |
| 283 | return nil |
| 284 | } |
| 285 | |
| 286 | if ptr == nil { |
| 287 | return startingRef |
| 288 | } |
| 289 | |
| 290 | ret := startingRef |
| 291 | var idRef *Ref |
| 292 | node := startingNode |
| 293 | |
| 294 | for _, tok := range ptr.DecodedTokens() { |
| 295 | node, _, _ = jsonpointer.GetForToken(node, tok) |
| 296 | if node == nil { |
| 297 | break |
| 298 | } |
| 299 | |
| 300 | idRef, _ = idFromNode(node) |
| 301 | if idRef != nil { |
| 302 | nw, err := ret.Inherits(*idRef) |
| 303 | if err != nil { |
| 304 | break |
| 305 | } |
| 306 | ret = nw |
| 307 | } |
| 308 | |
| 309 | refRef, _, _ := refPtr.Get(node) |
| 310 | if refRef != nil { |
| 311 | var rf Ref |
| 312 | switch value := refRef.(type) { |
| 313 | case string: |
| 314 | rf, _ = NewRef(value) |
| 315 | } |
| 316 | nw, err := ret.Inherits(rf) |
| 317 | if err != nil { |
| 318 | break |
| 319 | } |
| 320 | nwURL := nw.GetURL() |
| 321 | if nwURL.Scheme == "file" || (nwURL.Scheme == "" && nwURL.Host == "") { |
| 322 | nwpt := filepath.ToSlash(nwURL.Path) |
| 323 | if filepath.IsAbs(nwpt) { |
| 324 | _, err := os.Stat(nwpt) |
| 325 | if err != nil { |
| 326 | nwURL.Path = filepath.Join(".", nwpt) |
| 327 | } |
| 328 | } |
| 329 | } |
| 330 | |
| 331 | ret = nw |
| 332 | } |
| 333 | |
| 334 | } |
| 335 | |
| 336 | return ret |
| 337 | } |
| 338 | |
| 339 | // normalize absolute path for cache. |
| 340 | // on Windows, drive letters should be converted to lower as scheme in net/url.URL |
| 341 | func normalizeAbsPath(path string) string { |
| 342 | u, err := url.Parse(path) |
| 343 | if err != nil { |
| 344 | debugLog("normalize absolute path failed: %s", err) |
| 345 | return path |
| 346 | } |
| 347 | return u.String() |
| 348 | } |
| 349 | |
| 350 | // base or refPath could be a file path or a URL |
| 351 | // given a base absolute path and a ref path, return the absolute path of refPath |
| 352 | // 1) if refPath is absolute, return it |
| 353 | // 2) if refPath is relative, join it with basePath keeping the scheme, hosts, and ports if exists |
| 354 | // base could be a directory or a full file path |
| 355 | func normalizePaths(refPath, base string) string { |
| 356 | refURL, _ := url.Parse(refPath) |
| 357 | if path.IsAbs(refURL.Path) || filepath.IsAbs(refPath) { |
| 358 | // refPath is actually absolute |
| 359 | if refURL.Host != "" { |
| 360 | return refPath |
| 361 | } |
| 362 | parts := strings.Split(refPath, "#") |
| 363 | result := filepath.FromSlash(parts[0]) |
| 364 | if len(parts) == 2 { |
| 365 | result += "#" + parts[1] |
| 366 | } |
| 367 | return result |
| 368 | } |
| 369 | |
| 370 | // relative refPath |
| 371 | baseURL, _ := url.Parse(base) |
| 372 | if !strings.HasPrefix(refPath, "#") { |
| 373 | // combining paths |
| 374 | if baseURL.Host != "" { |
| 375 | baseURL.Path = path.Join(path.Dir(baseURL.Path), refURL.Path) |
| 376 | } else { // base is a file |
| 377 | newBase := fmt.Sprintf("%s#%s", filepath.Join(filepath.Dir(base), filepath.FromSlash(refURL.Path)), refURL.Fragment) |
| 378 | return newBase |
| 379 | } |
| 380 | |
| 381 | } |
| 382 | // copying fragment from ref to base |
| 383 | baseURL.Fragment = refURL.Fragment |
| 384 | return baseURL.String() |
| 385 | } |
| 386 | |
| 387 | // denormalizePaths returns to simplest notation on file $ref, |
| 388 | // i.e. strips the absolute path and sets a path relative to the base path. |
| 389 | // |
| 390 | // This is currently used when we rewrite ref after a circular ref has been detected |
| 391 | func denormalizeFileRef(ref *Ref, relativeBase, originalRelativeBase string) *Ref { |
| 392 | debugLog("denormalizeFileRef for: %s", ref.String()) |
| 393 | |
| 394 | if ref.String() == "" || ref.IsRoot() || ref.HasFragmentOnly { |
| 395 | return ref |
| 396 | } |
| 397 | // strip relativeBase from URI |
| 398 | relativeBaseURL, _ := url.Parse(relativeBase) |
| 399 | relativeBaseURL.Fragment = "" |
| 400 | |
| 401 | if relativeBaseURL.IsAbs() && strings.HasPrefix(ref.String(), relativeBase) { |
| 402 | // this should work for absolute URI (e.g. http://...): we have an exact match, just trim prefix |
| 403 | r, _ := NewRef(strings.TrimPrefix(ref.String(), relativeBase)) |
| 404 | return &r |
| 405 | } |
| 406 | |
| 407 | if relativeBaseURL.IsAbs() { |
| 408 | // other absolute URL get unchanged (i.e. with a non-empty scheme) |
| 409 | return ref |
| 410 | } |
| 411 | |
| 412 | // for relative file URIs: |
| 413 | originalRelativeBaseURL, _ := url.Parse(originalRelativeBase) |
| 414 | originalRelativeBaseURL.Fragment = "" |
| 415 | if strings.HasPrefix(ref.String(), originalRelativeBaseURL.String()) { |
| 416 | // the resulting ref is in the expanded spec: return a local ref |
| 417 | r, _ := NewRef(strings.TrimPrefix(ref.String(), originalRelativeBaseURL.String())) |
| 418 | return &r |
| 419 | } |
| 420 | |
| 421 | // check if we may set a relative path, considering the original base path for this spec. |
| 422 | // Example: |
| 423 | // spec is located at /mypath/spec.json |
| 424 | // my normalized ref points to: /mypath/item.json#/target |
| 425 | // expected result: item.json#/target |
| 426 | parts := strings.Split(ref.String(), "#") |
| 427 | relativePath, err := filepath.Rel(path.Dir(originalRelativeBaseURL.String()), parts[0]) |
| 428 | if err != nil { |
| 429 | // there is no common ancestor (e.g. different drives on windows) |
| 430 | // leaves the ref unchanged |
| 431 | return ref |
| 432 | } |
| 433 | if len(parts) == 2 { |
| 434 | relativePath += "#" + parts[1] |
| 435 | } |
| 436 | r, _ := NewRef(relativePath) |
| 437 | return &r |
| 438 | } |
| 439 | |
| 440 | // relativeBase could be an ABSOLUTE file path or an ABSOLUTE URL |
| 441 | func normalizeFileRef(ref *Ref, relativeBase string) *Ref { |
| 442 | // This is important for when the reference is pointing to the root schema |
| 443 | if ref.String() == "" { |
| 444 | r, _ := NewRef(relativeBase) |
| 445 | return &r |
| 446 | } |
| 447 | |
| 448 | debugLog("normalizing %s against %s", ref.String(), relativeBase) |
| 449 | |
| 450 | s := normalizePaths(ref.String(), relativeBase) |
| 451 | r, _ := NewRef(s) |
| 452 | return &r |
| 453 | } |
| 454 | |
| 455 | func (r *schemaLoader) resolveRef(ref *Ref, target interface{}, basePath string) error { |
| 456 | tgt := reflect.ValueOf(target) |
| 457 | if tgt.Kind() != reflect.Ptr { |
| 458 | return fmt.Errorf("resolve ref: target needs to be a pointer") |
| 459 | } |
| 460 | |
| 461 | refURL := ref.GetURL() |
| 462 | if refURL == nil { |
| 463 | return nil |
| 464 | } |
| 465 | |
| 466 | var res interface{} |
| 467 | var data interface{} |
| 468 | var err error |
| 469 | // Resolve against the root if it isn't nil, and if ref is pointing at the root, or has a fragment only which means |
| 470 | // it is pointing somewhere in the root. |
| 471 | root := r.root |
| 472 | if (ref.IsRoot() || ref.HasFragmentOnly) && root == nil && basePath != "" { |
| 473 | if baseRef, erb := NewRef(basePath); erb == nil { |
| 474 | root, _, _, _ = r.load(baseRef.GetURL()) |
| 475 | } |
| 476 | } |
| 477 | if (ref.IsRoot() || ref.HasFragmentOnly) && root != nil { |
| 478 | data = root |
| 479 | } else { |
| 480 | baseRef := normalizeFileRef(ref, basePath) |
| 481 | debugLog("current ref is: %s", ref.String()) |
| 482 | debugLog("current ref normalized file: %s", baseRef.String()) |
| 483 | data, _, _, err = r.load(baseRef.GetURL()) |
| 484 | if err != nil { |
| 485 | return err |
| 486 | } |
| 487 | } |
| 488 | |
| 489 | res = data |
| 490 | if ref.String() != "" { |
| 491 | res, _, err = ref.GetPointer().Get(data) |
| 492 | if err != nil { |
| 493 | return err |
| 494 | } |
| 495 | } |
| 496 | if err := swag.DynamicJSONToStruct(res, target); err != nil { |
| 497 | return err |
| 498 | } |
| 499 | |
| 500 | return nil |
| 501 | } |
| 502 | |
| 503 | func (r *schemaLoader) load(refURL *url.URL) (interface{}, url.URL, bool, error) { |
| 504 | debugLog("loading schema from url: %s", refURL) |
| 505 | toFetch := *refURL |
| 506 | toFetch.Fragment = "" |
| 507 | |
| 508 | normalized := normalizeAbsPath(toFetch.String()) |
| 509 | |
| 510 | data, fromCache := r.cache.Get(normalized) |
| 511 | if !fromCache { |
| 512 | b, err := r.loadDoc(normalized) |
| 513 | if err != nil { |
| 514 | return nil, url.URL{}, false, err |
| 515 | } |
| 516 | |
| 517 | if err := json.Unmarshal(b, &data); err != nil { |
| 518 | return nil, url.URL{}, false, err |
| 519 | } |
| 520 | r.cache.Set(normalized, data) |
| 521 | } |
| 522 | |
| 523 | return data, toFetch, fromCache, nil |
| 524 | } |
| 525 | |
| 526 | // Resolve resolves a reference against basePath and stores the result in target |
| 527 | // Resolve is not in charge of following references, it only resolves ref by following its URL |
| 528 | // if the schema that ref is referring to has more refs in it. Resolve doesn't resolve them |
| 529 | // if basePath is an empty string, ref is resolved against the root schema stored in the schemaLoader struct |
| 530 | func (r *schemaLoader) Resolve(ref *Ref, target interface{}, basePath string) error { |
| 531 | return r.resolveRef(ref, target, basePath) |
| 532 | } |
| 533 | |
| 534 | // absPath returns the absolute path of a file |
| 535 | func absPath(fname string) (string, error) { |
| 536 | if strings.HasPrefix(fname, "http") { |
| 537 | return fname, nil |
| 538 | } |
| 539 | if filepath.IsAbs(fname) { |
| 540 | return fname, nil |
| 541 | } |
| 542 | wd, err := os.Getwd() |
| 543 | return filepath.Join(wd, fname), err |
| 544 | } |
| 545 | |
| 546 | // ExpandSpec expands the references in a swagger spec |
| 547 | func ExpandSpec(spec *Swagger, options *ExpandOptions) error { |
| 548 | resolver, err := defaultSchemaLoader(spec, options, nil, nil) |
| 549 | // Just in case this ever returns an error. |
| 550 | if shouldStopOnError(err, resolver.options) { |
| 551 | return err |
| 552 | } |
| 553 | |
| 554 | // getting the base path of the spec to adjust all subsequent reference resolutions |
| 555 | specBasePath := "" |
| 556 | if options != nil && options.RelativeBase != "" { |
| 557 | specBasePath, _ = absPath(options.RelativeBase) |
| 558 | } |
| 559 | |
| 560 | if options == nil || !options.SkipSchemas { |
| 561 | for key, definition := range spec.Definitions { |
| 562 | var def *Schema |
| 563 | var err error |
| 564 | if def, err = expandSchema(definition, []string{fmt.Sprintf("#/definitions/%s", key)}, resolver, specBasePath); shouldStopOnError(err, resolver.options) { |
| 565 | return err |
| 566 | } |
| 567 | if def != nil { |
| 568 | spec.Definitions[key] = *def |
| 569 | } |
| 570 | } |
| 571 | } |
| 572 | |
| 573 | for key, parameter := range spec.Parameters { |
| 574 | if err := expandParameter(¶meter, resolver, specBasePath); shouldStopOnError(err, resolver.options) { |
| 575 | return err |
| 576 | } |
| 577 | spec.Parameters[key] = parameter |
| 578 | } |
| 579 | |
| 580 | for key, response := range spec.Responses { |
| 581 | if err := expandResponse(&response, resolver, specBasePath); shouldStopOnError(err, resolver.options) { |
| 582 | return err |
| 583 | } |
| 584 | spec.Responses[key] = response |
| 585 | } |
| 586 | |
| 587 | if spec.Paths != nil { |
| 588 | for key, path := range spec.Paths.Paths { |
| 589 | if err := expandPathItem(&path, resolver, specBasePath); shouldStopOnError(err, resolver.options) { |
| 590 | return err |
| 591 | } |
| 592 | spec.Paths.Paths[key] = path |
| 593 | } |
| 594 | } |
| 595 | |
| 596 | return nil |
| 597 | } |
| 598 | |
| 599 | func shouldStopOnError(err error, opts *ExpandOptions) bool { |
| 600 | if err != nil && !opts.ContinueOnError { |
| 601 | return true |
| 602 | } |
| 603 | |
| 604 | if err != nil { |
| 605 | log.Println(err) |
| 606 | } |
| 607 | |
| 608 | return false |
| 609 | } |
| 610 | |
| 611 | // baseForRoot loads in the cache the root document and produces a fake "root" base path entry |
| 612 | // for further $ref resolution |
| 613 | func baseForRoot(root interface{}, cache ResolutionCache) string { |
| 614 | // cache the root document to resolve $ref's |
| 615 | const rootBase = "root" |
| 616 | if root != nil { |
| 617 | base, _ := absPath(rootBase) |
| 618 | normalizedBase := normalizeAbsPath(base) |
| 619 | debugLog("setting root doc in cache at: %s", normalizedBase) |
| 620 | if cache == nil { |
| 621 | cache = resCache |
| 622 | } |
| 623 | cache.Set(normalizedBase, root) |
| 624 | return rootBase |
| 625 | } |
| 626 | return "" |
| 627 | } |
| 628 | |
| 629 | // ExpandSchema expands the refs in the schema object with reference to the root object |
| 630 | // go-openapi/validate uses this function |
| 631 | // notice that it is impossible to reference a json schema in a different file other than root |
| 632 | func ExpandSchema(schema *Schema, root interface{}, cache ResolutionCache) error { |
| 633 | opts := &ExpandOptions{ |
| 634 | // when a root is specified, cache the root as an in-memory document for $ref retrieval |
| 635 | RelativeBase: baseForRoot(root, cache), |
| 636 | SkipSchemas: false, |
| 637 | ContinueOnError: false, |
| 638 | // when no base path is specified, remaining $ref (circular) are rendered with an absolute path |
| 639 | AbsoluteCircularRef: true, |
| 640 | } |
| 641 | return ExpandSchemaWithBasePath(schema, cache, opts) |
| 642 | } |
| 643 | |
| 644 | // ExpandSchemaWithBasePath expands the refs in the schema object, base path configured through expand options |
| 645 | func ExpandSchemaWithBasePath(schema *Schema, cache ResolutionCache, opts *ExpandOptions) error { |
| 646 | if schema == nil { |
| 647 | return nil |
| 648 | } |
| 649 | |
| 650 | var basePath string |
| 651 | if opts.RelativeBase != "" { |
| 652 | basePath, _ = absPath(opts.RelativeBase) |
| 653 | } |
| 654 | |
| 655 | resolver, err := defaultSchemaLoader(nil, opts, cache, nil) |
| 656 | if err != nil { |
| 657 | return err |
| 658 | } |
| 659 | |
| 660 | refs := []string{""} |
| 661 | var s *Schema |
| 662 | if s, err = expandSchema(*schema, refs, resolver, basePath); err != nil { |
| 663 | return err |
| 664 | } |
| 665 | *schema = *s |
| 666 | return nil |
| 667 | } |
| 668 | |
| 669 | func expandItems(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) { |
| 670 | if target.Items != nil { |
| 671 | if target.Items.Schema != nil { |
| 672 | t, err := expandSchema(*target.Items.Schema, parentRefs, resolver, basePath) |
| 673 | if err != nil { |
| 674 | return nil, err |
| 675 | } |
| 676 | *target.Items.Schema = *t |
| 677 | } |
| 678 | for i := range target.Items.Schemas { |
| 679 | t, err := expandSchema(target.Items.Schemas[i], parentRefs, resolver, basePath) |
| 680 | if err != nil { |
| 681 | return nil, err |
| 682 | } |
| 683 | target.Items.Schemas[i] = *t |
| 684 | } |
| 685 | } |
| 686 | return &target, nil |
| 687 | } |
| 688 | |
| 689 | // basePathFromSchemaID returns a new basePath based on an existing basePath and a schema ID |
| 690 | func basePathFromSchemaID(oldBasePath, id string) string { |
| 691 | u, err := url.Parse(oldBasePath) |
| 692 | if err != nil { |
| 693 | panic(err) |
| 694 | } |
| 695 | uid, err := url.Parse(id) |
| 696 | if err != nil { |
| 697 | panic(err) |
| 698 | } |
| 699 | |
| 700 | if path.IsAbs(uid.Path) { |
| 701 | return id |
| 702 | } |
| 703 | u.Path = path.Join(path.Dir(u.Path), uid.Path) |
| 704 | return u.String() |
| 705 | } |
| 706 | |
| 707 | // isCircular detects cycles in sequences of $ref. |
| 708 | // It relies on a private context (which needs not be locked). |
| 709 | func (r *schemaLoader) isCircular(ref *Ref, basePath string, parentRefs ...string) (foundCycle bool) { |
| 710 | normalizedRef := normalizePaths(ref.String(), basePath) |
| 711 | if _, ok := r.context.circulars[normalizedRef]; ok { |
| 712 | // circular $ref has been already detected in another explored cycle |
| 713 | foundCycle = true |
| 714 | return |
| 715 | } |
| 716 | foundCycle = swag.ContainsStringsCI(parentRefs, normalizedRef) |
| 717 | if foundCycle { |
| 718 | r.context.circulars[normalizedRef] = true |
| 719 | } |
| 720 | return |
| 721 | } |
| 722 | |
| 723 | func updateBasePath(transitive *schemaLoader, resolver *schemaLoader, basePath string) string { |
| 724 | if transitive != resolver { |
| 725 | debugLog("got a new resolver") |
| 726 | if transitive.options != nil && transitive.options.RelativeBase != "" { |
| 727 | basePath, _ = absPath(transitive.options.RelativeBase) |
| 728 | debugLog("new basePath = %s", basePath) |
| 729 | } |
| 730 | } |
| 731 | |
| 732 | return basePath |
| 733 | } |
| 734 | |
| 735 | func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) { |
| 736 | if target.Ref.String() == "" && target.Ref.IsRoot() { |
| 737 | // normalizing is important |
| 738 | newRef := normalizeFileRef(&target.Ref, basePath) |
| 739 | target.Ref = *newRef |
| 740 | return &target, nil |
| 741 | |
| 742 | } |
| 743 | |
| 744 | /* change the base path of resolution when an ID is encountered |
| 745 | otherwise the basePath should inherit the parent's */ |
| 746 | // important: ID can be relative path |
| 747 | if target.ID != "" { |
| 748 | debugLog("schema has ID: %s", target.ID) |
| 749 | // handling the case when id is a folder |
| 750 | // remember that basePath has to be a file |
| 751 | refPath := target.ID |
| 752 | if strings.HasSuffix(target.ID, "/") { |
| 753 | // path.Clean here would not work correctly if basepath is http |
| 754 | refPath = fmt.Sprintf("%s%s", refPath, "placeholder.json") |
| 755 | } |
| 756 | basePath = normalizePaths(refPath, basePath) |
| 757 | } |
| 758 | |
| 759 | /* Explain here what this function does */ |
| 760 | var t *Schema |
| 761 | /* if Ref is found, everything else doesn't matter */ |
| 762 | /* Ref also changes the resolution scope of children expandSchema */ |
| 763 | if target.Ref.String() != "" { |
| 764 | /* Here the resolution scope is changed because a $ref was encountered */ |
| 765 | normalizedRef := normalizeFileRef(&target.Ref, basePath) |
| 766 | normalizedBasePath := normalizedRef.RemoteURI() |
| 767 | |
| 768 | if resolver.isCircular(normalizedRef, basePath, parentRefs...) { |
| 769 | // this means there is a cycle in the recursion tree: return the Ref |
| 770 | // - circular refs cannot be expanded. We leave them as ref. |
| 771 | // - denormalization means that a new local file ref is set relative to the original basePath |
| 772 | debugLog("shortcut circular ref: basePath: %s, normalizedPath: %s, normalized ref: %s", |
| 773 | basePath, normalizedBasePath, normalizedRef.String()) |
| 774 | if !resolver.options.AbsoluteCircularRef { |
| 775 | target.Ref = *denormalizeFileRef(normalizedRef, normalizedBasePath, resolver.context.basePath) |
| 776 | } else { |
| 777 | target.Ref = *normalizedRef |
| 778 | } |
| 779 | return &target, nil |
| 780 | } |
| 781 | |
| 782 | debugLog("basePath: %s", basePath) |
| 783 | if Debug { |
| 784 | b, _ := json.Marshal(target) |
| 785 | debugLog("calling Resolve with target: %s", string(b)) |
| 786 | } |
| 787 | if err := resolver.Resolve(&target.Ref, &t, basePath); shouldStopOnError(err, resolver.options) { |
| 788 | return nil, err |
| 789 | } |
| 790 | |
| 791 | if t != nil { |
| 792 | parentRefs = append(parentRefs, normalizedRef.String()) |
| 793 | var err error |
| 794 | transitiveResolver, err := transitiveResolver(basePath, target.Ref, resolver) |
| 795 | if shouldStopOnError(err, resolver.options) { |
| 796 | return nil, err |
| 797 | } |
| 798 | |
| 799 | basePath = updateBasePath(transitiveResolver, resolver, normalizedBasePath) |
| 800 | |
| 801 | return expandSchema(*t, parentRefs, transitiveResolver, basePath) |
| 802 | } |
| 803 | } |
| 804 | |
| 805 | t, err := expandItems(target, parentRefs, resolver, basePath) |
| 806 | if shouldStopOnError(err, resolver.options) { |
| 807 | return &target, err |
| 808 | } |
| 809 | if t != nil { |
| 810 | target = *t |
| 811 | } |
| 812 | |
| 813 | for i := range target.AllOf { |
| 814 | t, err := expandSchema(target.AllOf[i], parentRefs, resolver, basePath) |
| 815 | if shouldStopOnError(err, resolver.options) { |
| 816 | return &target, err |
| 817 | } |
| 818 | target.AllOf[i] = *t |
| 819 | } |
| 820 | for i := range target.AnyOf { |
| 821 | t, err := expandSchema(target.AnyOf[i], parentRefs, resolver, basePath) |
| 822 | if shouldStopOnError(err, resolver.options) { |
| 823 | return &target, err |
| 824 | } |
| 825 | target.AnyOf[i] = *t |
| 826 | } |
| 827 | for i := range target.OneOf { |
| 828 | t, err := expandSchema(target.OneOf[i], parentRefs, resolver, basePath) |
| 829 | if shouldStopOnError(err, resolver.options) { |
| 830 | return &target, err |
| 831 | } |
| 832 | if t != nil { |
| 833 | target.OneOf[i] = *t |
| 834 | } |
| 835 | } |
| 836 | if target.Not != nil { |
| 837 | t, err := expandSchema(*target.Not, parentRefs, resolver, basePath) |
| 838 | if shouldStopOnError(err, resolver.options) { |
| 839 | return &target, err |
| 840 | } |
| 841 | if t != nil { |
| 842 | *target.Not = *t |
| 843 | } |
| 844 | } |
| 845 | for k := range target.Properties { |
| 846 | t, err := expandSchema(target.Properties[k], parentRefs, resolver, basePath) |
| 847 | if shouldStopOnError(err, resolver.options) { |
| 848 | return &target, err |
| 849 | } |
| 850 | if t != nil { |
| 851 | target.Properties[k] = *t |
| 852 | } |
| 853 | } |
| 854 | if target.AdditionalProperties != nil && target.AdditionalProperties.Schema != nil { |
| 855 | t, err := expandSchema(*target.AdditionalProperties.Schema, parentRefs, resolver, basePath) |
| 856 | if shouldStopOnError(err, resolver.options) { |
| 857 | return &target, err |
| 858 | } |
| 859 | if t != nil { |
| 860 | *target.AdditionalProperties.Schema = *t |
| 861 | } |
| 862 | } |
| 863 | for k := range target.PatternProperties { |
| 864 | t, err := expandSchema(target.PatternProperties[k], parentRefs, resolver, basePath) |
| 865 | if shouldStopOnError(err, resolver.options) { |
| 866 | return &target, err |
| 867 | } |
| 868 | if t != nil { |
| 869 | target.PatternProperties[k] = *t |
| 870 | } |
| 871 | } |
| 872 | for k := range target.Dependencies { |
| 873 | if target.Dependencies[k].Schema != nil { |
| 874 | t, err := expandSchema(*target.Dependencies[k].Schema, parentRefs, resolver, basePath) |
| 875 | if shouldStopOnError(err, resolver.options) { |
| 876 | return &target, err |
| 877 | } |
| 878 | if t != nil { |
| 879 | *target.Dependencies[k].Schema = *t |
| 880 | } |
| 881 | } |
| 882 | } |
| 883 | if target.AdditionalItems != nil && target.AdditionalItems.Schema != nil { |
| 884 | t, err := expandSchema(*target.AdditionalItems.Schema, parentRefs, resolver, basePath) |
| 885 | if shouldStopOnError(err, resolver.options) { |
| 886 | return &target, err |
| 887 | } |
| 888 | if t != nil { |
| 889 | *target.AdditionalItems.Schema = *t |
| 890 | } |
| 891 | } |
| 892 | for k := range target.Definitions { |
| 893 | t, err := expandSchema(target.Definitions[k], parentRefs, resolver, basePath) |
| 894 | if shouldStopOnError(err, resolver.options) { |
| 895 | return &target, err |
| 896 | } |
| 897 | if t != nil { |
| 898 | target.Definitions[k] = *t |
| 899 | } |
| 900 | } |
| 901 | return &target, nil |
| 902 | } |
| 903 | |
| 904 | func derefPathItem(pathItem *PathItem, parentRefs []string, resolver *schemaLoader, basePath string) error { |
| 905 | curRef := pathItem.Ref.String() |
| 906 | if curRef != "" { |
| 907 | normalizedRef := normalizeFileRef(&pathItem.Ref, basePath) |
| 908 | normalizedBasePath := normalizedRef.RemoteURI() |
| 909 | |
| 910 | if resolver.isCircular(normalizedRef, basePath, parentRefs...) { |
| 911 | return nil |
| 912 | } |
| 913 | |
| 914 | if err := resolver.Resolve(&pathItem.Ref, pathItem, basePath); shouldStopOnError(err, resolver.options) { |
| 915 | return err |
| 916 | } |
| 917 | |
| 918 | if pathItem.Ref.String() != "" && pathItem.Ref.String() != curRef && basePath != normalizedBasePath { |
| 919 | parentRefs = append(parentRefs, normalizedRef.String()) |
| 920 | return derefPathItem(pathItem, parentRefs, resolver, normalizedBasePath) |
| 921 | } |
| 922 | } |
| 923 | |
| 924 | return nil |
| 925 | } |
| 926 | |
| 927 | func expandPathItem(pathItem *PathItem, resolver *schemaLoader, basePath string) error { |
| 928 | if pathItem == nil { |
| 929 | return nil |
| 930 | } |
| 931 | |
| 932 | parentRefs := []string{} |
| 933 | if err := derefPathItem(pathItem, parentRefs, resolver, basePath); shouldStopOnError(err, resolver.options) { |
| 934 | return err |
| 935 | } |
| 936 | if pathItem.Ref.String() != "" { |
| 937 | var err error |
| 938 | resolver, err = transitiveResolver(basePath, pathItem.Ref, resolver) |
| 939 | if shouldStopOnError(err, resolver.options) { |
| 940 | return err |
| 941 | } |
| 942 | } |
| 943 | pathItem.Ref = Ref{} |
| 944 | |
| 945 | // Currently unused: |
| 946 | //parentRefs = parentRefs[0:] |
| 947 | |
| 948 | for idx := range pathItem.Parameters { |
| 949 | if err := expandParameter(&(pathItem.Parameters[idx]), resolver, basePath); shouldStopOnError(err, resolver.options) { |
| 950 | return err |
| 951 | } |
| 952 | } |
| 953 | if err := expandOperation(pathItem.Get, resolver, basePath); shouldStopOnError(err, resolver.options) { |
| 954 | return err |
| 955 | } |
| 956 | if err := expandOperation(pathItem.Head, resolver, basePath); shouldStopOnError(err, resolver.options) { |
| 957 | return err |
| 958 | } |
| 959 | if err := expandOperation(pathItem.Options, resolver, basePath); shouldStopOnError(err, resolver.options) { |
| 960 | return err |
| 961 | } |
| 962 | if err := expandOperation(pathItem.Put, resolver, basePath); shouldStopOnError(err, resolver.options) { |
| 963 | return err |
| 964 | } |
| 965 | if err := expandOperation(pathItem.Post, resolver, basePath); shouldStopOnError(err, resolver.options) { |
| 966 | return err |
| 967 | } |
| 968 | if err := expandOperation(pathItem.Patch, resolver, basePath); shouldStopOnError(err, resolver.options) { |
| 969 | return err |
| 970 | } |
| 971 | if err := expandOperation(pathItem.Delete, resolver, basePath); shouldStopOnError(err, resolver.options) { |
| 972 | return err |
| 973 | } |
| 974 | return nil |
| 975 | } |
| 976 | |
| 977 | func expandOperation(op *Operation, resolver *schemaLoader, basePath string) error { |
| 978 | if op == nil { |
| 979 | return nil |
| 980 | } |
| 981 | |
| 982 | for i, param := range op.Parameters { |
| 983 | if err := expandParameter(¶m, resolver, basePath); shouldStopOnError(err, resolver.options) { |
| 984 | return err |
| 985 | } |
| 986 | op.Parameters[i] = param |
| 987 | } |
| 988 | |
| 989 | if op.Responses != nil { |
| 990 | responses := op.Responses |
| 991 | if err := expandResponse(responses.Default, resolver, basePath); shouldStopOnError(err, resolver.options) { |
| 992 | return err |
| 993 | } |
| 994 | for code, response := range responses.StatusCodeResponses { |
| 995 | if err := expandResponse(&response, resolver, basePath); shouldStopOnError(err, resolver.options) { |
| 996 | return err |
| 997 | } |
| 998 | responses.StatusCodeResponses[code] = response |
| 999 | } |
| 1000 | } |
| 1001 | return nil |
| 1002 | } |
| 1003 | |
| 1004 | func transitiveResolver(basePath string, ref Ref, resolver *schemaLoader) (*schemaLoader, error) { |
| 1005 | if ref.IsRoot() || ref.HasFragmentOnly { |
| 1006 | return resolver, nil |
| 1007 | } |
| 1008 | |
| 1009 | baseRef, _ := NewRef(basePath) |
| 1010 | currentRef := normalizeFileRef(&ref, basePath) |
| 1011 | // Set a new root to resolve against |
| 1012 | if !strings.HasPrefix(currentRef.String(), baseRef.String()) { |
| 1013 | rootURL := currentRef.GetURL() |
| 1014 | rootURL.Fragment = "" |
| 1015 | root, _ := resolver.cache.Get(rootURL.String()) |
| 1016 | var err error |
| 1017 | |
| 1018 | // shallow copy of resolver options to set a new RelativeBase when |
| 1019 | // traversing multiple documents |
| 1020 | newOptions := resolver.options |
| 1021 | newOptions.RelativeBase = rootURL.String() |
| 1022 | debugLog("setting new root: %s", newOptions.RelativeBase) |
| 1023 | resolver, err = defaultSchemaLoader(root, newOptions, resolver.cache, resolver.context) |
| 1024 | if err != nil { |
| 1025 | return nil, err |
| 1026 | } |
| 1027 | } |
| 1028 | |
| 1029 | return resolver, nil |
| 1030 | } |
| 1031 | |
| 1032 | // ExpandResponseWithRoot expands a response based on a root document, not a fetchable document |
| 1033 | func ExpandResponseWithRoot(response *Response, root interface{}, cache ResolutionCache) error { |
| 1034 | opts := &ExpandOptions{ |
| 1035 | RelativeBase: baseForRoot(root, cache), |
| 1036 | SkipSchemas: false, |
| 1037 | ContinueOnError: false, |
| 1038 | // when no base path is specified, remaining $ref (circular) are rendered with an absolute path |
| 1039 | AbsoluteCircularRef: true, |
| 1040 | } |
| 1041 | resolver, err := defaultSchemaLoader(root, opts, nil, nil) |
| 1042 | if err != nil { |
| 1043 | return err |
| 1044 | } |
| 1045 | |
| 1046 | return expandResponse(response, resolver, opts.RelativeBase) |
| 1047 | } |
| 1048 | |
| 1049 | // ExpandResponse expands a response based on a basepath |
| 1050 | // This is the exported version of expandResponse |
| 1051 | // all refs inside response will be resolved relative to basePath |
| 1052 | func ExpandResponse(response *Response, basePath string) error { |
| 1053 | var specBasePath string |
| 1054 | if basePath != "" { |
| 1055 | specBasePath, _ = absPath(basePath) |
| 1056 | } |
| 1057 | opts := &ExpandOptions{ |
| 1058 | RelativeBase: specBasePath, |
| 1059 | } |
| 1060 | resolver, err := defaultSchemaLoader(nil, opts, nil, nil) |
| 1061 | if err != nil { |
| 1062 | return err |
| 1063 | } |
| 1064 | |
| 1065 | return expandResponse(response, resolver, opts.RelativeBase) |
| 1066 | } |
| 1067 | |
| 1068 | func derefResponse(response *Response, parentRefs []string, resolver *schemaLoader, basePath string) error { |
| 1069 | curRef := response.Ref.String() |
| 1070 | if curRef != "" { |
| 1071 | /* Here the resolution scope is changed because a $ref was encountered */ |
| 1072 | normalizedRef := normalizeFileRef(&response.Ref, basePath) |
| 1073 | normalizedBasePath := normalizedRef.RemoteURI() |
| 1074 | |
| 1075 | if resolver.isCircular(normalizedRef, basePath, parentRefs...) { |
| 1076 | return nil |
| 1077 | } |
| 1078 | |
| 1079 | if err := resolver.Resolve(&response.Ref, response, basePath); shouldStopOnError(err, resolver.options) { |
| 1080 | return err |
| 1081 | } |
| 1082 | |
| 1083 | if response.Ref.String() != "" && response.Ref.String() != curRef && basePath != normalizedBasePath { |
| 1084 | parentRefs = append(parentRefs, normalizedRef.String()) |
| 1085 | return derefResponse(response, parentRefs, resolver, normalizedBasePath) |
| 1086 | } |
| 1087 | } |
| 1088 | |
| 1089 | return nil |
| 1090 | } |
| 1091 | |
| 1092 | func expandResponse(response *Response, resolver *schemaLoader, basePath string) error { |
| 1093 | if response == nil { |
| 1094 | return nil |
| 1095 | } |
| 1096 | parentRefs := []string{} |
| 1097 | if err := derefResponse(response, parentRefs, resolver, basePath); shouldStopOnError(err, resolver.options) { |
| 1098 | return err |
| 1099 | } |
| 1100 | if response.Ref.String() != "" { |
| 1101 | transitiveResolver, err := transitiveResolver(basePath, response.Ref, resolver) |
| 1102 | if shouldStopOnError(err, transitiveResolver.options) { |
| 1103 | return err |
| 1104 | } |
| 1105 | basePath = updateBasePath(transitiveResolver, resolver, basePath) |
| 1106 | resolver = transitiveResolver |
| 1107 | } |
| 1108 | if response.Schema != nil && response.Schema.Ref.String() != "" { |
| 1109 | // schema expanded to a $ref in another root |
| 1110 | var ern error |
| 1111 | response.Schema.Ref, ern = NewRef(normalizePaths(response.Schema.Ref.String(), response.Ref.RemoteURI())) |
| 1112 | if ern != nil { |
| 1113 | return ern |
| 1114 | } |
| 1115 | } |
| 1116 | response.Ref = Ref{} |
| 1117 | |
| 1118 | parentRefs = parentRefs[0:] |
| 1119 | if !resolver.options.SkipSchemas && response.Schema != nil { |
| 1120 | // parentRefs = append(parentRefs, response.Schema.Ref.String()) |
| 1121 | s, err := expandSchema(*response.Schema, parentRefs, resolver, basePath) |
| 1122 | if shouldStopOnError(err, resolver.options) { |
| 1123 | return err |
| 1124 | } |
| 1125 | *response.Schema = *s |
| 1126 | } |
| 1127 | |
| 1128 | return nil |
| 1129 | } |
| 1130 | |
| 1131 | // ExpandParameterWithRoot expands a parameter based on a root document, not a fetchable document |
| 1132 | func ExpandParameterWithRoot(parameter *Parameter, root interface{}, cache ResolutionCache) error { |
| 1133 | opts := &ExpandOptions{ |
| 1134 | RelativeBase: baseForRoot(root, cache), |
| 1135 | SkipSchemas: false, |
| 1136 | ContinueOnError: false, |
| 1137 | // when no base path is specified, remaining $ref (circular) are rendered with an absolute path |
| 1138 | AbsoluteCircularRef: true, |
| 1139 | } |
| 1140 | resolver, err := defaultSchemaLoader(root, opts, nil, nil) |
| 1141 | if err != nil { |
| 1142 | return err |
| 1143 | } |
| 1144 | |
| 1145 | return expandParameter(parameter, resolver, opts.RelativeBase) |
| 1146 | } |
| 1147 | |
| 1148 | // ExpandParameter expands a parameter based on a basepath |
| 1149 | // This is the exported version of expandParameter |
| 1150 | // all refs inside parameter will be resolved relative to basePath |
| 1151 | func ExpandParameter(parameter *Parameter, basePath string) error { |
| 1152 | var specBasePath string |
| 1153 | if basePath != "" { |
| 1154 | specBasePath, _ = absPath(basePath) |
| 1155 | } |
| 1156 | opts := &ExpandOptions{ |
| 1157 | RelativeBase: specBasePath, |
| 1158 | } |
| 1159 | resolver, err := defaultSchemaLoader(nil, opts, nil, nil) |
| 1160 | if err != nil { |
| 1161 | return err |
| 1162 | } |
| 1163 | |
| 1164 | return expandParameter(parameter, resolver, opts.RelativeBase) |
| 1165 | } |
| 1166 | |
| 1167 | func derefParameter(parameter *Parameter, parentRefs []string, resolver *schemaLoader, basePath string) error { |
| 1168 | curRef := parameter.Ref.String() |
| 1169 | if curRef != "" { |
| 1170 | normalizedRef := normalizeFileRef(¶meter.Ref, basePath) |
| 1171 | normalizedBasePath := normalizedRef.RemoteURI() |
| 1172 | |
| 1173 | if resolver.isCircular(normalizedRef, basePath, parentRefs...) { |
| 1174 | return nil |
| 1175 | } |
| 1176 | |
| 1177 | if err := resolver.Resolve(¶meter.Ref, parameter, basePath); shouldStopOnError(err, resolver.options) { |
| 1178 | return err |
| 1179 | } |
| 1180 | |
| 1181 | if parameter.Ref.String() != "" && parameter.Ref.String() != curRef && basePath != normalizedBasePath { |
| 1182 | parentRefs = append(parentRefs, normalizedRef.String()) |
| 1183 | return derefParameter(parameter, parentRefs, resolver, normalizedBasePath) |
| 1184 | } |
| 1185 | } |
| 1186 | |
| 1187 | return nil |
| 1188 | } |
| 1189 | |
| 1190 | func expandParameter(parameter *Parameter, resolver *schemaLoader, basePath string) error { |
| 1191 | if parameter == nil { |
| 1192 | return nil |
| 1193 | } |
| 1194 | |
| 1195 | parentRefs := []string{} |
| 1196 | if err := derefParameter(parameter, parentRefs, resolver, basePath); shouldStopOnError(err, resolver.options) { |
| 1197 | return err |
| 1198 | } |
| 1199 | if parameter.Ref.String() != "" { |
| 1200 | transitiveResolver, err := transitiveResolver(basePath, parameter.Ref, resolver) |
| 1201 | if shouldStopOnError(err, transitiveResolver.options) { |
| 1202 | return err |
| 1203 | } |
| 1204 | basePath = updateBasePath(transitiveResolver, resolver, basePath) |
| 1205 | resolver = transitiveResolver |
| 1206 | } |
| 1207 | |
| 1208 | if parameter.Schema != nil && parameter.Schema.Ref.String() != "" { |
| 1209 | // schema expanded to a $ref in another root |
| 1210 | var ern error |
| 1211 | parameter.Schema.Ref, ern = NewRef(normalizePaths(parameter.Schema.Ref.String(), parameter.Ref.RemoteURI())) |
| 1212 | if ern != nil { |
| 1213 | return ern |
| 1214 | } |
| 1215 | } |
| 1216 | parameter.Ref = Ref{} |
| 1217 | |
| 1218 | parentRefs = parentRefs[0:] |
| 1219 | if !resolver.options.SkipSchemas && parameter.Schema != nil { |
| 1220 | s, err := expandSchema(*parameter.Schema, parentRefs, resolver, basePath) |
| 1221 | if shouldStopOnError(err, resolver.options) { |
| 1222 | return err |
| 1223 | } |
| 1224 | *parameter.Schema = *s |
| 1225 | } |
| 1226 | return nil |
| 1227 | } |