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 15package spec 16 17import ( 18 "encoding/json" 19 "fmt" 20) 21 22// ExpandOptions provides options for the spec expander. 23// 24// RelativeBase is the path to the root document. This can be a remote URL or a path to a local file. 25// 26// If left empty, the root document is assumed to be located in the current working directory: 27// all relative $ref's will be resolved from there. 28// 29// PathLoader injects a document loading method. By default, this resolves to the function provided by the SpecLoader package variable. 30// 31type ExpandOptions struct { 32 RelativeBase string // the path to the root document to expand. This is a file, not a directory 33 SkipSchemas bool // do not expand schemas, just paths, parameters and responses 34 ContinueOnError bool // continue expanding even after and error is found 35 PathLoader func(string) (json.RawMessage, error) `json:"-"` // the document loading method that takes a path as input and yields a json document 36 AbsoluteCircularRef bool // circular $ref remaining after expansion remain absolute URLs 37} 38 39func optionsOrDefault(opts *ExpandOptions) *ExpandOptions { 40 if opts != nil { 41 clone := *opts // shallow clone to avoid internal changes to be propagated to the caller 42 if clone.RelativeBase != "" { 43 clone.RelativeBase = normalizeBase(clone.RelativeBase) 44 } 45 // if the relative base is empty, let the schema loader choose a pseudo root document 46 return &clone 47 } 48 return &ExpandOptions{} 49} 50 51// ExpandSpec expands the references in a swagger spec 52func ExpandSpec(spec *Swagger, options *ExpandOptions) error { 53 options = optionsOrDefault(options) 54 resolver := defaultSchemaLoader(spec, options, nil, nil) 55 56 specBasePath := options.RelativeBase 57 58 if !options.SkipSchemas { 59 for key, definition := range spec.Definitions { 60 parentRefs := make([]string, 0, 10) 61 parentRefs = append(parentRefs, fmt.Sprintf("#/definitions/%s", key)) 62 63 def, err := expandSchema(definition, parentRefs, resolver, specBasePath) 64 if resolver.shouldStopOnError(err) { 65 return err 66 } 67 if def != nil { 68 spec.Definitions[key] = *def 69 } 70 } 71 } 72 73 for key := range spec.Parameters { 74 parameter := spec.Parameters[key] 75 if err := expandParameterOrResponse(¶meter, resolver, specBasePath); resolver.shouldStopOnError(err) { 76 return err 77 } 78 spec.Parameters[key] = parameter 79 } 80 81 for key := range spec.Responses { 82 response := spec.Responses[key] 83 if err := expandParameterOrResponse(&response, resolver, specBasePath); resolver.shouldStopOnError(err) { 84 return err 85 } 86 spec.Responses[key] = response 87 } 88 89 if spec.Paths != nil { 90 for key := range spec.Paths.Paths { 91 pth := spec.Paths.Paths[key] 92 if err := expandPathItem(&pth, resolver, specBasePath); resolver.shouldStopOnError(err) { 93 return err 94 } 95 spec.Paths.Paths[key] = pth 96 } 97 } 98 99 return nil 100} 101 102const rootBase = ".root" 103 104// baseForRoot loads in the cache the root document and produces a fake ".root" base path entry 105// for further $ref resolution 106// 107// Setting the cache is optional and this parameter may safely be left to nil. 108func baseForRoot(root interface{}, cache ResolutionCache) string { 109 if root == nil { 110 return "" 111 } 112 113 // cache the root document to resolve $ref's 114 normalizedBase := normalizeBase(rootBase) 115 cache.Set(normalizedBase, root) 116 117 return normalizedBase 118} 119 120// ExpandSchema expands the refs in the schema object with reference to the root object. 121// 122// go-openapi/validate uses this function. 123// 124// Notice that it is impossible to reference a json schema in a different document other than root 125// (use ExpandSchemaWithBasePath to resolve external references). 126// 127// Setting the cache is optional and this parameter may safely be left to nil. 128func ExpandSchema(schema *Schema, root interface{}, cache ResolutionCache) error { 129 cache = cacheOrDefault(cache) 130 if root == nil { 131 root = schema 132 } 133 134 opts := &ExpandOptions{ 135 // when a root is specified, cache the root as an in-memory document for $ref retrieval 136 RelativeBase: baseForRoot(root, cache), 137 SkipSchemas: false, 138 ContinueOnError: false, 139 } 140 141 return ExpandSchemaWithBasePath(schema, cache, opts) 142} 143 144// ExpandSchemaWithBasePath expands the refs in the schema object, base path configured through expand options. 145// 146// Setting the cache is optional and this parameter may safely be left to nil. 147func ExpandSchemaWithBasePath(schema *Schema, cache ResolutionCache, opts *ExpandOptions) error { 148 if schema == nil { 149 return nil 150 } 151 152 cache = cacheOrDefault(cache) 153 154 opts = optionsOrDefault(opts) 155 156 resolver := defaultSchemaLoader(nil, opts, cache, nil) 157 158 parentRefs := make([]string, 0, 10) 159 s, err := expandSchema(*schema, parentRefs, resolver, opts.RelativeBase) 160 if err != nil { 161 return err 162 } 163 if s != nil { 164 // guard for when continuing on error 165 *schema = *s 166 } 167 168 return nil 169} 170 171func expandItems(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) { 172 if target.Items == nil { 173 return &target, nil 174 } 175 176 // array 177 if target.Items.Schema != nil { 178 t, err := expandSchema(*target.Items.Schema, parentRefs, resolver, basePath) 179 if err != nil { 180 return nil, err 181 } 182 *target.Items.Schema = *t 183 } 184 185 // tuple 186 for i := range target.Items.Schemas { 187 t, err := expandSchema(target.Items.Schemas[i], parentRefs, resolver, basePath) 188 if err != nil { 189 return nil, err 190 } 191 target.Items.Schemas[i] = *t 192 } 193 194 return &target, nil 195} 196 197func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) { 198 if target.Ref.String() == "" && target.Ref.IsRoot() { 199 newRef := normalizeRef(&target.Ref, basePath) 200 target.Ref = *newRef 201 return &target, nil 202 } 203 204 // change the base path of resolution when an ID is encountered 205 // otherwise the basePath should inherit the parent's 206 if target.ID != "" { 207 basePath, _ = resolver.setSchemaID(target, target.ID, basePath) 208 } 209 210 if target.Ref.String() != "" { 211 return expandSchemaRef(target, parentRefs, resolver, basePath) 212 } 213 214 for k := range target.Definitions { 215 tt, err := expandSchema(target.Definitions[k], parentRefs, resolver, basePath) 216 if resolver.shouldStopOnError(err) { 217 return &target, err 218 } 219 if tt != nil { 220 target.Definitions[k] = *tt 221 } 222 } 223 224 t, err := expandItems(target, parentRefs, resolver, basePath) 225 if resolver.shouldStopOnError(err) { 226 return &target, err 227 } 228 if t != nil { 229 target = *t 230 } 231 232 for i := range target.AllOf { 233 t, err := expandSchema(target.AllOf[i], parentRefs, resolver, basePath) 234 if resolver.shouldStopOnError(err) { 235 return &target, err 236 } 237 if t != nil { 238 target.AllOf[i] = *t 239 } 240 } 241 242 for i := range target.AnyOf { 243 t, err := expandSchema(target.AnyOf[i], parentRefs, resolver, basePath) 244 if resolver.shouldStopOnError(err) { 245 return &target, err 246 } 247 if t != nil { 248 target.AnyOf[i] = *t 249 } 250 } 251 252 for i := range target.OneOf { 253 t, err := expandSchema(target.OneOf[i], parentRefs, resolver, basePath) 254 if resolver.shouldStopOnError(err) { 255 return &target, err 256 } 257 if t != nil { 258 target.OneOf[i] = *t 259 } 260 } 261 262 if target.Not != nil { 263 t, err := expandSchema(*target.Not, parentRefs, resolver, basePath) 264 if resolver.shouldStopOnError(err) { 265 return &target, err 266 } 267 if t != nil { 268 *target.Not = *t 269 } 270 } 271 272 for k := range target.Properties { 273 t, err := expandSchema(target.Properties[k], parentRefs, resolver, basePath) 274 if resolver.shouldStopOnError(err) { 275 return &target, err 276 } 277 if t != nil { 278 target.Properties[k] = *t 279 } 280 } 281 282 if target.AdditionalProperties != nil && target.AdditionalProperties.Schema != nil { 283 t, err := expandSchema(*target.AdditionalProperties.Schema, parentRefs, resolver, basePath) 284 if resolver.shouldStopOnError(err) { 285 return &target, err 286 } 287 if t != nil { 288 *target.AdditionalProperties.Schema = *t 289 } 290 } 291 292 for k := range target.PatternProperties { 293 t, err := expandSchema(target.PatternProperties[k], parentRefs, resolver, basePath) 294 if resolver.shouldStopOnError(err) { 295 return &target, err 296 } 297 if t != nil { 298 target.PatternProperties[k] = *t 299 } 300 } 301 302 for k := range target.Dependencies { 303 if target.Dependencies[k].Schema != nil { 304 t, err := expandSchema(*target.Dependencies[k].Schema, parentRefs, resolver, basePath) 305 if resolver.shouldStopOnError(err) { 306 return &target, err 307 } 308 if t != nil { 309 *target.Dependencies[k].Schema = *t 310 } 311 } 312 } 313 314 if target.AdditionalItems != nil && target.AdditionalItems.Schema != nil { 315 t, err := expandSchema(*target.AdditionalItems.Schema, parentRefs, resolver, basePath) 316 if resolver.shouldStopOnError(err) { 317 return &target, err 318 } 319 if t != nil { 320 *target.AdditionalItems.Schema = *t 321 } 322 } 323 return &target, nil 324} 325 326func expandSchemaRef(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) { 327 // if a Ref is found, all sibling fields are skipped 328 // Ref also changes the resolution scope of children expandSchema 329 330 // here the resolution scope is changed because a $ref was encountered 331 normalizedRef := normalizeRef(&target.Ref, basePath) 332 normalizedBasePath := normalizedRef.RemoteURI() 333 334 if resolver.isCircular(normalizedRef, basePath, parentRefs...) { 335 // this means there is a cycle in the recursion tree: return the Ref 336 // - circular refs cannot be expanded. We leave them as ref. 337 // - denormalization means that a new local file ref is set relative to the original basePath 338 debugLog("short circuit circular ref: basePath: %s, normalizedPath: %s, normalized ref: %s", 339 basePath, normalizedBasePath, normalizedRef.String()) 340 if !resolver.options.AbsoluteCircularRef { 341 target.Ref = denormalizeRef(normalizedRef, resolver.context.basePath, resolver.context.rootID) 342 } else { 343 target.Ref = *normalizedRef 344 } 345 return &target, nil 346 } 347 348 var t *Schema 349 err := resolver.Resolve(&target.Ref, &t, basePath) 350 if resolver.shouldStopOnError(err) { 351 return nil, err 352 } 353 354 if t == nil { 355 // guard for when continuing on error 356 return &target, nil 357 } 358 359 parentRefs = append(parentRefs, normalizedRef.String()) 360 transitiveResolver := resolver.transitiveResolver(basePath, target.Ref) 361 362 basePath = resolver.updateBasePath(transitiveResolver, normalizedBasePath) 363 364 return expandSchema(*t, parentRefs, transitiveResolver, basePath) 365} 366 367func expandPathItem(pathItem *PathItem, resolver *schemaLoader, basePath string) error { 368 if pathItem == nil { 369 return nil 370 } 371 372 parentRefs := make([]string, 0, 10) 373 if err := resolver.deref(pathItem, parentRefs, basePath); resolver.shouldStopOnError(err) { 374 return err 375 } 376 377 if pathItem.Ref.String() != "" { 378 transitiveResolver := resolver.transitiveResolver(basePath, pathItem.Ref) 379 basePath = transitiveResolver.updateBasePath(resolver, basePath) 380 resolver = transitiveResolver 381 } 382 383 pathItem.Ref = Ref{} 384 for i := range pathItem.Parameters { 385 if err := expandParameterOrResponse(&(pathItem.Parameters[i]), resolver, basePath); resolver.shouldStopOnError(err) { 386 return err 387 } 388 } 389 390 ops := []*Operation{ 391 pathItem.Get, 392 pathItem.Head, 393 pathItem.Options, 394 pathItem.Put, 395 pathItem.Post, 396 pathItem.Patch, 397 pathItem.Delete, 398 } 399 for _, op := range ops { 400 if err := expandOperation(op, resolver, basePath); resolver.shouldStopOnError(err) { 401 return err 402 } 403 } 404 405 return nil 406} 407 408func expandOperation(op *Operation, resolver *schemaLoader, basePath string) error { 409 if op == nil { 410 return nil 411 } 412 413 for i := range op.Parameters { 414 param := op.Parameters[i] 415 if err := expandParameterOrResponse(¶m, resolver, basePath); resolver.shouldStopOnError(err) { 416 return err 417 } 418 op.Parameters[i] = param 419 } 420 421 if op.Responses == nil { 422 return nil 423 } 424 425 responses := op.Responses 426 if err := expandParameterOrResponse(responses.Default, resolver, basePath); resolver.shouldStopOnError(err) { 427 return err 428 } 429 430 for code := range responses.StatusCodeResponses { 431 response := responses.StatusCodeResponses[code] 432 if err := expandParameterOrResponse(&response, resolver, basePath); resolver.shouldStopOnError(err) { 433 return err 434 } 435 responses.StatusCodeResponses[code] = response 436 } 437 438 return nil 439} 440 441// ExpandResponseWithRoot expands a response based on a root document, not a fetchable document 442// 443// Notice that it is impossible to reference a json schema in a different document other than root 444// (use ExpandResponse to resolve external references). 445// 446// Setting the cache is optional and this parameter may safely be left to nil. 447func ExpandResponseWithRoot(response *Response, root interface{}, cache ResolutionCache) error { 448 cache = cacheOrDefault(cache) 449 opts := &ExpandOptions{ 450 RelativeBase: baseForRoot(root, cache), 451 } 452 resolver := defaultSchemaLoader(root, opts, cache, nil) 453 454 return expandParameterOrResponse(response, resolver, opts.RelativeBase) 455} 456 457// ExpandResponse expands a response based on a basepath 458// 459// All refs inside response will be resolved relative to basePath 460func ExpandResponse(response *Response, basePath string) error { 461 opts := optionsOrDefault(&ExpandOptions{ 462 RelativeBase: basePath, 463 }) 464 resolver := defaultSchemaLoader(nil, opts, nil, nil) 465 466 return expandParameterOrResponse(response, resolver, opts.RelativeBase) 467} 468 469// ExpandParameterWithRoot expands a parameter based on a root document, not a fetchable document. 470// 471// Notice that it is impossible to reference a json schema in a different document other than root 472// (use ExpandParameter to resolve external references). 473func ExpandParameterWithRoot(parameter *Parameter, root interface{}, cache ResolutionCache) error { 474 cache = cacheOrDefault(cache) 475 476 opts := &ExpandOptions{ 477 RelativeBase: baseForRoot(root, cache), 478 } 479 resolver := defaultSchemaLoader(root, opts, cache, nil) 480 481 return expandParameterOrResponse(parameter, resolver, opts.RelativeBase) 482} 483 484// ExpandParameter expands a parameter based on a basepath. 485// This is the exported version of expandParameter 486// all refs inside parameter will be resolved relative to basePath 487func ExpandParameter(parameter *Parameter, basePath string) error { 488 opts := optionsOrDefault(&ExpandOptions{ 489 RelativeBase: basePath, 490 }) 491 resolver := defaultSchemaLoader(nil, opts, nil, nil) 492 493 return expandParameterOrResponse(parameter, resolver, opts.RelativeBase) 494} 495 496func getRefAndSchema(input interface{}) (*Ref, *Schema, error) { 497 var ( 498 ref *Ref 499 sch *Schema 500 ) 501 502 switch refable := input.(type) { 503 case *Parameter: 504 if refable == nil { 505 return nil, nil, nil 506 } 507 ref = &refable.Ref 508 sch = refable.Schema 509 case *Response: 510 if refable == nil { 511 return nil, nil, nil 512 } 513 ref = &refable.Ref 514 sch = refable.Schema 515 default: 516 return nil, nil, fmt.Errorf("unsupported type: %T: %w", input, ErrExpandUnsupportedType) 517 } 518 519 return ref, sch, nil 520} 521 522func expandParameterOrResponse(input interface{}, resolver *schemaLoader, basePath string) error { 523 ref, _, err := getRefAndSchema(input) 524 if err != nil { 525 return err 526 } 527 528 if ref == nil { 529 return nil 530 } 531 532 parentRefs := make([]string, 0, 10) 533 if err = resolver.deref(input, parentRefs, basePath); resolver.shouldStopOnError(err) { 534 return err 535 } 536 537 ref, sch, _ := getRefAndSchema(input) 538 if ref.String() != "" { 539 transitiveResolver := resolver.transitiveResolver(basePath, *ref) 540 basePath = resolver.updateBasePath(transitiveResolver, basePath) 541 resolver = transitiveResolver 542 } 543 544 if sch == nil { 545 // nothing to be expanded 546 if ref != nil { 547 *ref = Ref{} 548 } 549 return nil 550 } 551 552 if sch.Ref.String() != "" { 553 rebasedRef, ern := NewRef(normalizeURI(sch.Ref.String(), basePath)) 554 if ern != nil { 555 return ern 556 } 557 558 switch { 559 case resolver.isCircular(&rebasedRef, basePath, parentRefs...): 560 // this is a circular $ref: stop expansion 561 if !resolver.options.AbsoluteCircularRef { 562 sch.Ref = denormalizeRef(&rebasedRef, resolver.context.basePath, resolver.context.rootID) 563 } else { 564 sch.Ref = rebasedRef 565 } 566 case !resolver.options.SkipSchemas: 567 // schema expanded to a $ref in another root 568 sch.Ref = rebasedRef 569 debugLog("rebased to: %s", sch.Ref.String()) 570 default: 571 // skip schema expansion but rebase $ref to schema 572 sch.Ref = denormalizeRef(&rebasedRef, resolver.context.basePath, resolver.context.rootID) 573 } 574 } 575 576 if ref != nil { 577 *ref = Ref{} 578 } 579 580 // expand schema 581 if !resolver.options.SkipSchemas { 582 s, err := expandSchema(*sch, parentRefs, resolver, basePath) 583 if resolver.shouldStopOnError(err) { 584 return err 585 } 586 if s == nil { 587 // guard for when continuing on error 588 return nil 589 } 590 *sch = *s 591 } 592 593 return nil 594} 595