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 analysis 16 17import ( 18 "fmt" 19 "log" 20 "net/http" 21 "net/url" 22 slashpath "path" 23 "path/filepath" 24 "sort" 25 "strings" 26 27 "strconv" 28 29 "github.com/go-openapi/analysis/internal" 30 "github.com/go-openapi/jsonpointer" 31 swspec "github.com/go-openapi/spec" 32 "github.com/go-openapi/swag" 33) 34 35// FlattenOpts configuration for flattening a swagger specification. 36// 37// The BasePath parameter is used to locate remote relative $ref found in the specification. 38// This path is a file: it points to the location of the root document and may be either a local 39// file path or a URL. 40// 41// If none specified, relative references (e.g. "$ref": "folder/schema.yaml#/definitions/...") 42// found in the spec are searched from the current working directory. 43type FlattenOpts struct { 44 Spec *Spec // The analyzed spec to work with 45 flattenContext *context // Internal context to track flattening activity 46 47 BasePath string // The location of the root document for this spec to resolve relative $ref 48 49 // Flattening options 50 Expand bool // When true, skip flattening the spec and expand it instead (if Minimal is false) 51 Minimal bool // When true, do not decompose complex structures such as allOf 52 Verbose bool // enable some reporting on possible name conflicts detected 53 RemoveUnused bool // When true, remove unused parameters, responses and definitions after expansion/flattening 54 ContinueOnError bool // Continue when spec expansion issues are found 55 56 /* Extra keys */ 57 _ struct{} // require keys 58} 59 60// ExpandOpts creates a spec.ExpandOptions to configure expanding a specification document. 61func (f *FlattenOpts) ExpandOpts(skipSchemas bool) *swspec.ExpandOptions { 62 return &swspec.ExpandOptions{RelativeBase: f.BasePath, SkipSchemas: skipSchemas} 63} 64 65// Swagger gets the swagger specification for this flatten operation 66func (f *FlattenOpts) Swagger() *swspec.Swagger { 67 return f.Spec.spec 68} 69 70// newRef stores information about refs created during the flattening process 71type newRef struct { 72 key string 73 newName string 74 path string 75 isOAIGen bool 76 resolved bool 77 schema *swspec.Schema 78 parents []string 79} 80 81// context stores intermediary results from flatten 82type context struct { 83 newRefs map[string]*newRef 84 warnings []string 85 resolved map[string]string 86} 87 88func newContext() *context { 89 return &context{ 90 newRefs: make(map[string]*newRef, 150), 91 warnings: make([]string, 0), 92 resolved: make(map[string]string, 50), 93 } 94} 95 96// Flatten an analyzed spec and produce a self-contained spec bundle. 97// 98// There is a minimal and a full flattening mode. 99// 100// 101// Minimally flattening a spec means: 102// - Expanding parameters, responses, path items, parameter items and header items (references to schemas are left 103// unscathed) 104// - Importing external (http, file) references so they become internal to the document 105// - Moving every JSON pointer to a $ref to a named definition (i.e. the reworked spec does not contain pointers 106// like "$ref": "#/definitions/myObject/allOfs/1") 107// 108// A minimally flattened spec thus guarantees the following properties: 109// - all $refs point to a local definition (i.e. '#/definitions/...') 110// - definitions are unique 111// 112// NOTE: arbitrary JSON pointers (other than $refs to top level definitions) are rewritten as definitions if they 113// represent a complex schema or express commonality in the spec. 114// Otherwise, they are simply expanded. 115// Self-referencing JSON pointers cannot resolve to a type and trigger an error. 116// 117// 118// Minimal flattening is necessary and sufficient for codegen rendering using go-swagger. 119// 120// Fully flattening a spec means: 121// - Moving every complex inline schema to be a definition with an auto-generated name in a depth-first fashion. 122// 123// By complex, we mean every JSON object with some properties. 124// Arrays, when they do not define a tuple, 125// or empty objects with or without additionalProperties, are not considered complex and remain inline. 126// 127// NOTE: rewritten schemas get a vendor extension x-go-gen-location so we know from which part of the spec definitions 128// have been created. 129// 130// Available flattening options: 131// - Minimal: stops flattening after minimal $ref processing, leaving schema constructs untouched 132// - Expand: expand all $ref's in the document (inoperant if Minimal set to true) 133// - Verbose: croaks about name conflicts detected 134// - RemoveUnused: removes unused parameters, responses and definitions after expansion/flattening 135// 136// NOTE: expansion removes all $ref save circular $ref, which remain in place 137// 138// TODO: additional options 139// - ProgagateNameExtensions: ensure that created entries properly follow naming rules when their parent have set a 140// x-go-name extension 141// - LiftAllOfs: 142// - limit the flattening of allOf members when simple objects 143// - merge allOf with validation only 144// - merge allOf with extensions only 145// - ... 146// 147func Flatten(opts FlattenOpts) error { 148 debugLog("FlattenOpts: %#v", opts) 149 opts.flattenContext = newContext() 150 151 // recursively expand responses, parameters, path items and items in simple schemas. 152 // This simplifies the spec and leaves $ref only into schema objects. 153 expandOpts := opts.ExpandOpts(!opts.Expand) 154 expandOpts.ContinueOnError = opts.ContinueOnError 155 if err := swspec.ExpandSpec(opts.Swagger(), expandOpts); err != nil { 156 return err 157 } 158 159 opts.Spec.reload() // re-analyze 160 161 // strip current file from $ref's, so we can recognize them as proper definitions 162 // In particular, this works around for issue go-openapi/spec#76: leading absolute file in $ref is stripped 163 if err := normalizeRef(&opts); err != nil { 164 return err 165 } 166 167 if opts.RemoveUnused { 168 // optionally removes shared parameters and responses already expanded (now unused) 169 // default parameters (i.e. under paths) remain. 170 opts.Swagger().Parameters = nil 171 opts.Swagger().Responses = nil 172 } 173 174 opts.Spec.reload() // re-analyze 175 176 // at this point there are no references left but in schemas 177 178 for imported := false; !imported; { 179 // iteratively import remote references until none left. 180 // This inlining deals with name conflicts by introducing auto-generated names ("OAIGen") 181 var err error 182 if imported, err = importExternalReferences(&opts); err != nil { 183 debugLog("error in importExternalReferences: %v", err) 184 return err 185 } 186 opts.Spec.reload() // re-analyze 187 } 188 189 if !opts.Minimal && !opts.Expand { 190 // full flattening: rewrite inline schemas (schemas that aren't simple types or arrays or maps) 191 if err := nameInlinedSchemas(&opts); err != nil { 192 return err 193 } 194 195 opts.Spec.reload() // re-analyze 196 } 197 198 // rewrite JSON pointers other than $ref to named definitions 199 // and attempt to resolve conflicting names whenever possible. 200 if err := stripPointersAndOAIGen(&opts); err != nil { 201 return err 202 } 203 204 if opts.RemoveUnused { 205 // remove unused definitions 206 expected := make(map[string]struct{}) 207 for k := range opts.Swagger().Definitions { 208 expected[slashpath.Join(definitionsPath, jsonpointer.Escape(k))] = struct{}{} 209 } 210 for _, k := range opts.Spec.AllDefinitionReferences() { 211 delete(expected, k) 212 } 213 for k := range expected { 214 debugLog("removing unused definition %s", slashpath.Base(k)) 215 if opts.Verbose { 216 log.Printf("info: removing unused definition: %s", slashpath.Base(k)) 217 } 218 delete(opts.Swagger().Definitions, slashpath.Base(k)) 219 } 220 opts.Spec.reload() // re-analyze 221 } 222 223 // TODO: simplify known schema patterns to flat objects with properties 224 // examples: 225 // - lift simple allOf object, 226 // - empty allOf with validation only or extensions only 227 // - rework allOf arrays 228 // - rework allOf additionalProperties 229 230 if opts.Verbose { 231 // issue notifications 232 croak(&opts) 233 } 234 return nil 235} 236 237// isAnalyzedAsComplex determines if an analyzed schema is eligible to flattening (i.e. it is "complex"). 238// 239// Complex means the schema is any of: 240// - a simple type (primitive) 241// - an array of something (items are possibly complex ; if this is the case, items will generate a definition) 242// - a map of something (additionalProperties are possibly complex ; if this is the case, additionalProperties will 243// generate a definition) 244func isAnalyzedAsComplex(asch *AnalyzedSchema) bool { 245 if !asch.IsSimpleSchema && !asch.IsArray && !asch.IsMap { 246 return true 247 } 248 return false 249} 250 251// nameInlinedSchemas replaces every complex inline construct by a named definition. 252func nameInlinedSchemas(opts *FlattenOpts) error { 253 debugLog("nameInlinedSchemas") 254 namer := &inlineSchemaNamer{ 255 Spec: opts.Swagger(), 256 Operations: opRefsByRef(gatherOperations(opts.Spec, nil)), 257 flattenContext: opts.flattenContext, 258 opts: opts, 259 } 260 depthFirst := sortDepthFirst(opts.Spec.allSchemas) 261 for _, key := range depthFirst { 262 sch := opts.Spec.allSchemas[key] 263 if sch.Schema != nil && sch.Schema.Ref.String() == "" && !sch.TopLevel { // inline schema 264 asch, err := Schema(SchemaOpts{Schema: sch.Schema, Root: opts.Swagger(), BasePath: opts.BasePath}) 265 if err != nil { 266 return fmt.Errorf("schema analysis [%s]: %v", key, err) 267 } 268 269 if isAnalyzedAsComplex(asch) { // move complex schemas to definitions 270 if err := namer.Name(key, sch.Schema, asch); err != nil { 271 return err 272 } 273 } 274 } 275 } 276 return nil 277} 278 279var depthGroupOrder = []string{ 280 "sharedParam", "sharedResponse", "sharedOpParam", "opParam", "codeResponse", "defaultResponse", "definition", 281} 282 283func sortDepthFirst(data map[string]SchemaRef) []string { 284 // group by category (shared params, op param, statuscode response, default response, definitions) 285 // sort groups internally by number of parts in the key and lexical names 286 // flatten groups into a single list of keys 287 sorted := make([]string, 0, len(data)) 288 grouped := make(map[string]keys, len(data)) 289 for k := range data { 290 split := keyParts(k) 291 var pk string 292 if split.IsSharedOperationParam() { 293 pk = "sharedOpParam" 294 } 295 if split.IsOperationParam() { 296 pk = "opParam" 297 } 298 if split.IsStatusCodeResponse() { 299 pk = "codeResponse" 300 } 301 if split.IsDefaultResponse() { 302 pk = "defaultResponse" 303 } 304 if split.IsDefinition() { 305 pk = "definition" 306 } 307 if split.IsSharedParam() { 308 pk = "sharedParam" 309 } 310 if split.IsSharedResponse() { 311 pk = "sharedResponse" 312 } 313 grouped[pk] = append(grouped[pk], key{Segments: len(split), Key: k}) 314 } 315 316 for _, pk := range depthGroupOrder { 317 res := grouped[pk] 318 sort.Sort(res) 319 for _, v := range res { 320 sorted = append(sorted, v.Key) 321 } 322 } 323 return sorted 324} 325 326type key struct { 327 Segments int 328 Key string 329} 330type keys []key 331 332func (k keys) Len() int { return len(k) } 333func (k keys) Swap(i, j int) { k[i], k[j] = k[j], k[i] } 334func (k keys) Less(i, j int) bool { 335 return k[i].Segments > k[j].Segments || (k[i].Segments == k[j].Segments && k[i].Key < k[j].Key) 336} 337 338type inlineSchemaNamer struct { 339 Spec *swspec.Swagger 340 Operations map[string]opRef 341 flattenContext *context 342 opts *FlattenOpts 343} 344 345func opRefsByRef(oprefs map[string]opRef) map[string]opRef { 346 result := make(map[string]opRef, len(oprefs)) 347 for _, v := range oprefs { 348 result[v.Ref.String()] = v 349 } 350 return result 351} 352 353func (isn *inlineSchemaNamer) Name(key string, schema *swspec.Schema, aschema *AnalyzedSchema) error { 354 debugLog("naming inlined schema at %s", key) 355 356 parts := keyParts(key) 357 for _, name := range namesFromKey(parts, aschema, isn.Operations) { 358 if name != "" { 359 // create unique name 360 newName, isOAIGen := uniqifyName(isn.Spec.Definitions, swag.ToJSONName(name)) 361 362 // clone schema 363 sch, err := cloneSchema(schema) 364 if err != nil { 365 return err 366 } 367 368 // replace values on schema 369 if err := rewriteSchemaToRef(isn.Spec, key, 370 swspec.MustCreateRef(slashpath.Join(definitionsPath, newName))); err != nil { 371 return fmt.Errorf("error while creating definition %q from inline schema: %v", newName, err) 372 } 373 374 // rewrite any dependent $ref pointing to this place, 375 // when not already pointing to a top-level definition. 376 // 377 // NOTE: this is important if such referers use arbitrary JSON pointers. 378 an := New(isn.Spec) 379 for k, v := range an.references.allRefs { 380 r, _, erd := deepestRef(isn.opts, v) 381 if erd != nil { 382 return fmt.Errorf("at %s, %v", k, erd) 383 } 384 if r.String() == key || 385 r.String() == slashpath.Join(definitionsPath, newName) && 386 slashpath.Dir(v.String()) != definitionsPath { 387 debugLog("found a $ref to a rewritten schema: %s points to %s", k, v.String()) 388 389 // rewrite $ref to the new target 390 if err := updateRef(isn.Spec, k, 391 swspec.MustCreateRef(slashpath.Join(definitionsPath, newName))); err != nil { 392 return err 393 } 394 } 395 } 396 397 // NOTE: this extension is currently not used by go-swagger (provided for information only) 398 sch.AddExtension("x-go-gen-location", genLocation(parts)) 399 400 // save cloned schema to definitions 401 saveSchema(isn.Spec, newName, sch) 402 403 // keep track of created refs 404 if isn.flattenContext != nil { 405 debugLog("track created ref: key=%s, newName=%s, isOAIGen=%t", key, newName, isOAIGen) 406 resolved := false 407 if _, ok := isn.flattenContext.newRefs[key]; ok { 408 resolved = isn.flattenContext.newRefs[key].resolved 409 } 410 isn.flattenContext.newRefs[key] = &newRef{ 411 key: key, 412 newName: newName, 413 path: slashpath.Join(definitionsPath, newName), 414 isOAIGen: isOAIGen, 415 resolved: resolved, 416 schema: sch, 417 } 418 } 419 } 420 } 421 return nil 422} 423 424// genLocation indicates from which section of the specification (models or operations) a definition has been created. 425// 426// This is reflected in the output spec with a "x-go-gen-location" extension. At the moment, this is is provided 427// for information only. 428func genLocation(parts splitKey) string { 429 if parts.IsOperation() { 430 return "operations" 431 } 432 if parts.IsDefinition() { 433 return "models" 434 } 435 return "" 436} 437 438// uniqifyName yields a unique name for a definition 439func uniqifyName(definitions swspec.Definitions, name string) (string, bool) { 440 isOAIGen := false 441 if name == "" { 442 name = "oaiGen" 443 isOAIGen = true 444 } 445 if len(definitions) == 0 { 446 return name, isOAIGen 447 } 448 449 unq := true 450 for k := range definitions { 451 if strings.EqualFold(k, name) { 452 unq = false 453 break 454 } 455 } 456 457 if unq { 458 return name, isOAIGen 459 } 460 461 name += "OAIGen" 462 isOAIGen = true 463 var idx int 464 unique := name 465 _, known := definitions[unique] 466 for known { 467 idx++ 468 unique = fmt.Sprintf("%s%d", name, idx) 469 _, known = definitions[unique] 470 } 471 return unique, isOAIGen 472} 473 474func namesFromKey(parts splitKey, aschema *AnalyzedSchema, operations map[string]opRef) []string { 475 var baseNames [][]string 476 var startIndex int 477 if parts.IsOperation() { 478 // params 479 if parts.IsOperationParam() || parts.IsSharedOperationParam() { 480 piref := parts.PathItemRef() 481 if piref.String() != "" && parts.IsOperationParam() { 482 if op, ok := operations[piref.String()]; ok { 483 startIndex = 5 484 baseNames = append(baseNames, []string{op.ID, "params", "body"}) 485 } 486 } else if parts.IsSharedOperationParam() { 487 pref := parts.PathRef() 488 for k, v := range operations { 489 if strings.HasPrefix(k, pref.String()) { 490 startIndex = 4 491 baseNames = append(baseNames, []string{v.ID, "params", "body"}) 492 } 493 } 494 } 495 } 496 // responses 497 if parts.IsOperationResponse() { 498 piref := parts.PathItemRef() 499 if piref.String() != "" { 500 if op, ok := operations[piref.String()]; ok { 501 startIndex = 6 502 baseNames = append(baseNames, []string{op.ID, parts.ResponseName(), "body"}) 503 } 504 } 505 } 506 } 507 508 // definitions 509 if parts.IsDefinition() { 510 nm := parts.DefinitionName() 511 if nm != "" { 512 startIndex = 2 513 baseNames = append(baseNames, []string{parts.DefinitionName()}) 514 } 515 } 516 517 var result []string 518 for _, segments := range baseNames { 519 nm := parts.BuildName(segments, startIndex, aschema) 520 if nm != "" { 521 result = append(result, nm) 522 } 523 } 524 sort.Strings(result) 525 return result 526} 527 528const ( 529 paths = "paths" 530 responses = "responses" 531 parameters = "parameters" 532 definitions = "definitions" 533 definitionsPath = "#/definitions" 534) 535 536var ( 537 ignoredKeys map[string]struct{} 538 validMethods map[string]struct{} 539) 540 541func init() { 542 ignoredKeys = map[string]struct{}{ 543 "schema": {}, 544 "properties": {}, 545 "not": {}, 546 "anyOf": {}, 547 "oneOf": {}, 548 } 549 550 validMethods = map[string]struct{}{ 551 "GET": {}, 552 "HEAD": {}, 553 "OPTIONS": {}, 554 "PATCH": {}, 555 "POST": {}, 556 "PUT": {}, 557 "DELETE": {}, 558 } 559} 560 561type splitKey []string 562 563func (s splitKey) IsDefinition() bool { 564 return len(s) > 1 && s[0] == definitions 565} 566 567func (s splitKey) DefinitionName() string { 568 if !s.IsDefinition() { 569 return "" 570 } 571 return s[1] 572} 573 574func (s splitKey) isKeyName(i int) bool { 575 if i <= 0 { 576 return false 577 } 578 count := 0 579 for idx := i - 1; idx > 0; idx-- { 580 if s[idx] != "properties" { 581 break 582 } 583 count++ 584 } 585 586 return count%2 != 0 587} 588 589func (s splitKey) BuildName(segments []string, startIndex int, aschema *AnalyzedSchema) string { 590 for i, part := range s[startIndex:] { 591 if _, ignored := ignoredKeys[part]; !ignored || s.isKeyName(startIndex+i) { 592 if part == "items" || part == "additionalItems" { 593 if aschema.IsTuple || aschema.IsTupleWithExtra { 594 segments = append(segments, "tuple") 595 } else { 596 segments = append(segments, "items") 597 } 598 if part == "additionalItems" { 599 segments = append(segments, part) 600 } 601 continue 602 } 603 segments = append(segments, part) 604 } 605 } 606 return strings.Join(segments, " ") 607} 608 609func (s splitKey) IsOperation() bool { 610 return len(s) > 1 && s[0] == paths 611} 612 613func (s splitKey) IsSharedOperationParam() bool { 614 return len(s) > 2 && s[0] == paths && s[2] == parameters 615} 616 617func (s splitKey) IsSharedParam() bool { 618 return len(s) > 1 && s[0] == parameters 619} 620 621func (s splitKey) IsOperationParam() bool { 622 return len(s) > 3 && s[0] == paths && s[3] == parameters 623} 624 625func (s splitKey) IsOperationResponse() bool { 626 return len(s) > 3 && s[0] == paths && s[3] == responses 627} 628 629func (s splitKey) IsSharedResponse() bool { 630 return len(s) > 1 && s[0] == responses 631} 632 633func (s splitKey) IsDefaultResponse() bool { 634 return len(s) > 4 && s[0] == paths && s[3] == responses && s[4] == "default" 635} 636 637func (s splitKey) IsStatusCodeResponse() bool { 638 isInt := func() bool { 639 _, err := strconv.Atoi(s[4]) 640 return err == nil 641 } 642 return len(s) > 4 && s[0] == paths && s[3] == responses && isInt() 643} 644 645func (s splitKey) ResponseName() string { 646 if s.IsStatusCodeResponse() { 647 code, _ := strconv.Atoi(s[4]) 648 return http.StatusText(code) 649 } 650 if s.IsDefaultResponse() { 651 return "Default" 652 } 653 return "" 654} 655 656func (s splitKey) PathItemRef() swspec.Ref { 657 if len(s) < 3 { 658 return swspec.Ref{} 659 } 660 pth, method := s[1], s[2] 661 if _, isValidMethod := validMethods[strings.ToUpper(method)]; !isValidMethod && !strings.HasPrefix(method, "x-") { 662 return swspec.Ref{} 663 } 664 return swspec.MustCreateRef("#" + slashpath.Join("/", paths, jsonpointer.Escape(pth), strings.ToUpper(method))) 665} 666 667func (s splitKey) PathRef() swspec.Ref { 668 if !s.IsOperation() { 669 return swspec.Ref{} 670 } 671 return swspec.MustCreateRef("#" + slashpath.Join("/", paths, jsonpointer.Escape(s[1]))) 672} 673 674func keyParts(key string) splitKey { 675 var res []string 676 for _, part := range strings.Split(key[1:], "/") { 677 if part != "" { 678 res = append(res, jsonpointer.Unescape(part)) 679 } 680 } 681 return res 682} 683 684func rewriteSchemaToRef(spec *swspec.Swagger, key string, ref swspec.Ref) error { 685 debugLog("rewriting schema to ref for %s with %s", key, ref.String()) 686 _, value, err := getPointerFromKey(spec, key) 687 if err != nil { 688 return err 689 } 690 691 switch refable := value.(type) { 692 case *swspec.Schema: 693 return rewriteParentRef(spec, key, ref) 694 695 case swspec.Schema: 696 return rewriteParentRef(spec, key, ref) 697 698 case *swspec.SchemaOrArray: 699 if refable.Schema != nil { 700 refable.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 701 } 702 703 case *swspec.SchemaOrBool: 704 if refable.Schema != nil { 705 refable.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 706 } 707 default: 708 return fmt.Errorf("no schema with ref found at %s for %T", key, value) 709 } 710 711 return nil 712} 713 714func rewriteParentRef(spec *swspec.Swagger, key string, ref swspec.Ref) error { 715 parent, entry, pvalue, err := getParentFromKey(spec, key) 716 if err != nil { 717 return err 718 } 719 720 debugLog("rewriting holder for %T", pvalue) 721 switch container := pvalue.(type) { 722 case swspec.Response: 723 if err := rewriteParentRef(spec, "#"+parent, ref); err != nil { 724 return err 725 } 726 727 case *swspec.Response: 728 container.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 729 730 case *swspec.Responses: 731 statusCode, err := strconv.Atoi(entry) 732 if err != nil { 733 return fmt.Errorf("%s not a number: %v", key[1:], err) 734 } 735 resp := container.StatusCodeResponses[statusCode] 736 resp.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 737 container.StatusCodeResponses[statusCode] = resp 738 739 case map[string]swspec.Response: 740 resp := container[entry] 741 resp.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 742 container[entry] = resp 743 744 case swspec.Parameter: 745 if err := rewriteParentRef(spec, "#"+parent, ref); err != nil { 746 return err 747 } 748 749 case map[string]swspec.Parameter: 750 param := container[entry] 751 param.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 752 container[entry] = param 753 754 case []swspec.Parameter: 755 idx, err := strconv.Atoi(entry) 756 if err != nil { 757 return fmt.Errorf("%s not a number: %v", key[1:], err) 758 } 759 param := container[idx] 760 param.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 761 container[idx] = param 762 763 case swspec.Definitions: 764 container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 765 766 case map[string]swspec.Schema: 767 container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 768 769 case []swspec.Schema: 770 idx, err := strconv.Atoi(entry) 771 if err != nil { 772 return fmt.Errorf("%s not a number: %v", key[1:], err) 773 } 774 container[idx] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 775 776 case *swspec.SchemaOrArray: 777 // NOTE: this is necessarily an array - otherwise, the parent would be *Schema 778 idx, err := strconv.Atoi(entry) 779 if err != nil { 780 return fmt.Errorf("%s not a number: %v", key[1:], err) 781 } 782 container.Schemas[idx] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 783 784 case swspec.SchemaProperties: 785 container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 786 787 // NOTE: can't have case *swspec.SchemaOrBool = parent in this case is *Schema 788 789 default: 790 return fmt.Errorf("unhandled parent schema rewrite %s (%T)", key, pvalue) 791 } 792 return nil 793} 794 795func cloneSchema(schema *swspec.Schema) (*swspec.Schema, error) { 796 var sch swspec.Schema 797 if err := swag.FromDynamicJSON(schema, &sch); err != nil { 798 return nil, fmt.Errorf("cannot clone schema: %v", err) 799 } 800 return &sch, nil 801} 802 803// importExternalReferences iteratively digs remote references and imports them into the main schema. 804// 805// At every iteration, new remotes may be found when digging deeper: they are rebased to the current schema before being imported. 806// 807// This returns true when no more remote references can be found. 808func importExternalReferences(opts *FlattenOpts) (bool, error) { 809 debugLog("importExternalReferences") 810 811 groupedRefs := reverseIndexForSchemaRefs(opts) 812 sortedRefStr := make([]string, 0, len(groupedRefs)) 813 if opts.flattenContext == nil { 814 opts.flattenContext = newContext() 815 } 816 817 // sort $ref resolution to ensure deterministic name conflict resolution 818 for refStr := range groupedRefs { 819 sortedRefStr = append(sortedRefStr, refStr) 820 } 821 sort.Strings(sortedRefStr) 822 823 complete := true 824 825 for _, refStr := range sortedRefStr { 826 entry := groupedRefs[refStr] 827 if entry.Ref.HasFragmentOnly { 828 continue 829 } 830 complete = false 831 var isOAIGen bool 832 833 newName := opts.flattenContext.resolved[refStr] 834 if newName != "" { 835 // rewrite ref with already resolved external ref (useful for cyclical refs): 836 // rewrite external refs to local ones 837 debugLog("resolving known ref [%s] to %s", refStr, newName) 838 for _, key := range entry.Keys { 839 if err := updateRef(opts.Swagger(), key, 840 swspec.MustCreateRef(slashpath.Join(definitionsPath, newName))); err != nil { 841 return false, err 842 } 843 } 844 } else { 845 // resolve schemas 846 debugLog("resolving schema from remote $ref [%s]", refStr) 847 sch, err := swspec.ResolveRefWithBase(opts.Swagger(), &entry.Ref, opts.ExpandOpts(false)) 848 if err != nil { 849 return false, fmt.Errorf("could not resolve schema: %v", err) 850 } 851 852 // at this stage only $ref analysis matters 853 partialAnalyzer := &Spec{ 854 references: referenceAnalysis{}, 855 patterns: patternAnalysis{}, 856 enums: enumAnalysis{}, 857 } 858 partialAnalyzer.reset() 859 partialAnalyzer.analyzeSchema("", sch, "/") 860 861 // now rewrite those refs with rebase 862 for key, ref := range partialAnalyzer.references.allRefs { 863 if err := updateRef(sch, key, swspec.MustCreateRef(rebaseRef(entry.Ref.String(), ref.String()))); err != nil { 864 return false, fmt.Errorf("failed to rewrite ref for key %q at %s: %v", key, entry.Ref.String(), err) 865 } 866 } 867 868 // generate a unique name - isOAIGen means that a naming conflict was resolved by changing the name 869 newName, isOAIGen = uniqifyName(opts.Swagger().Definitions, nameFromRef(entry.Ref)) 870 debugLog("new name for [%s]: %s - with name conflict:%t", 871 strings.Join(entry.Keys, ", "), newName, isOAIGen) 872 873 opts.flattenContext.resolved[refStr] = newName 874 875 // rewrite the external refs to local ones 876 for _, key := range entry.Keys { 877 if err := updateRef(opts.Swagger(), key, 878 swspec.MustCreateRef(slashpath.Join(definitionsPath, newName))); err != nil { 879 return false, err 880 } 881 882 // keep track of created refs 883 resolved := false 884 if _, ok := opts.flattenContext.newRefs[key]; ok { 885 resolved = opts.flattenContext.newRefs[key].resolved 886 } 887 debugLog("keeping track of ref: %s (%s), resolved: %t", key, newName, resolved) 888 opts.flattenContext.newRefs[key] = &newRef{ 889 key: key, 890 newName: newName, 891 path: slashpath.Join(definitionsPath, newName), 892 isOAIGen: isOAIGen, 893 resolved: resolved, 894 schema: sch, 895 } 896 } 897 898 // add the resolved schema to the definitions 899 saveSchema(opts.Swagger(), newName, sch) 900 } 901 } 902 // maintains ref index entries 903 for k := range opts.flattenContext.newRefs { 904 r := opts.flattenContext.newRefs[k] 905 906 // update tracking with resolved schemas 907 if r.schema.Ref.String() != "" { 908 ref := swspec.MustCreateRef(r.path) 909 sch, err := swspec.ResolveRefWithBase(opts.Swagger(), &ref, opts.ExpandOpts(false)) 910 if err != nil { 911 return false, fmt.Errorf("could not resolve schema: %v", err) 912 } 913 r.schema = sch 914 } 915 // update tracking with renamed keys: got a cascade of refs 916 if r.path != k { 917 renamed := *r 918 renamed.key = r.path 919 opts.flattenContext.newRefs[renamed.path] = &renamed 920 921 // indirect ref 922 r.newName = slashpath.Base(k) 923 r.schema = swspec.RefSchema(r.path) 924 r.path = k 925 r.isOAIGen = strings.Contains(k, "OAIGen") 926 } 927 } 928 929 return complete, nil 930} 931 932type refRevIdx struct { 933 Ref swspec.Ref 934 Keys []string 935} 936 937// rebaseRef rebase a remote ref relative to a base ref. 938// 939// NOTE: does not support JSONschema ID for $ref (we assume we are working with swagger specs here). 940// 941// NOTE(windows): 942// * refs are assumed to have been normalized with drive letter lower cased (from go-openapi/spec) 943// * "/ in paths may appear as escape sequences 944func rebaseRef(baseRef string, ref string) string { 945 debugLog("rebasing ref: %s onto %s", ref, baseRef) 946 baseRef, _ = url.PathUnescape(baseRef) 947 ref, _ = url.PathUnescape(ref) 948 if baseRef == "" || baseRef == "." || strings.HasPrefix(baseRef, "#") { 949 return ref 950 } 951 952 parts := strings.Split(ref, "#") 953 954 baseParts := strings.Split(baseRef, "#") 955 baseURL, _ := url.Parse(baseParts[0]) 956 if strings.HasPrefix(ref, "#") { 957 if baseURL.Host == "" { 958 return strings.Join([]string{baseParts[0], parts[1]}, "#") 959 } 960 return strings.Join([]string{baseParts[0], parts[1]}, "#") 961 } 962 963 refURL, _ := url.Parse(parts[0]) 964 if refURL.Host != "" || filepath.IsAbs(parts[0]) { 965 // not rebasing an absolute path 966 return ref 967 } 968 969 // there is a relative path 970 var basePath string 971 if baseURL.Host != "" { 972 // when there is a host, standard URI rules apply (with "/") 973 baseURL.Path = slashpath.Dir(baseURL.Path) 974 baseURL.Path = slashpath.Join(baseURL.Path, "/"+parts[0]) 975 return baseURL.String() 976 } 977 978 // this is a local relative path 979 // basePart[0] and parts[0] are local filesystem directories/files 980 basePath = filepath.Dir(baseParts[0]) 981 relPath := filepath.Join(basePath, string(filepath.Separator)+parts[0]) 982 if len(parts) > 1 { 983 return strings.Join([]string{relPath, parts[1]}, "#") 984 } 985 return relPath 986} 987 988// normalizePath renders absolute path on remote file refs 989// 990// NOTE(windows): 991// * refs are assumed to have been normalized with drive letter lower cased (from go-openapi/spec) 992// * "/ in paths may appear as escape sequences 993func normalizePath(ref swspec.Ref, opts *FlattenOpts) (normalizedPath string) { 994 uri, _ := url.PathUnescape(ref.String()) 995 if ref.HasFragmentOnly || filepath.IsAbs(uri) { 996 normalizedPath = uri 997 return 998 } 999 1000 refURL, _ := url.Parse(uri) 1001 if refURL.Host != "" { 1002 normalizedPath = uri 1003 return 1004 } 1005 1006 parts := strings.Split(uri, "#") 1007 // BasePath, parts[0] are local filesystem directories, guaranteed to be absolute at this stage 1008 parts[0] = filepath.Join(filepath.Dir(opts.BasePath), parts[0]) 1009 normalizedPath = strings.Join(parts, "#") 1010 return 1011} 1012 1013func reverseIndexForSchemaRefs(opts *FlattenOpts) map[string]refRevIdx { 1014 collected := make(map[string]refRevIdx) 1015 for key, schRef := range opts.Spec.references.schemas { 1016 // normalize paths before sorting, 1017 // so we get together keys in same external file 1018 normalizedPath := normalizePath(schRef, opts) 1019 if entry, ok := collected[normalizedPath]; ok { 1020 entry.Keys = append(entry.Keys, key) 1021 collected[normalizedPath] = entry 1022 } else { 1023 collected[normalizedPath] = refRevIdx{ 1024 Ref: schRef, 1025 Keys: []string{key}, 1026 } 1027 } 1028 } 1029 return collected 1030} 1031 1032func nameFromRef(ref swspec.Ref) string { 1033 u := ref.GetURL() 1034 if u.Fragment != "" { 1035 return swag.ToJSONName(slashpath.Base(u.Fragment)) 1036 } 1037 if u.Path != "" { 1038 bn := slashpath.Base(u.Path) 1039 if bn != "" && bn != "/" { 1040 ext := slashpath.Ext(bn) 1041 if ext != "" { 1042 return swag.ToJSONName(bn[:len(bn)-len(ext)]) 1043 } 1044 return swag.ToJSONName(bn) 1045 } 1046 } 1047 return swag.ToJSONName(strings.ReplaceAll(u.Host, ".", " ")) 1048} 1049 1050func saveSchema(spec *swspec.Swagger, name string, schema *swspec.Schema) { 1051 if schema == nil { 1052 return 1053 } 1054 if spec.Definitions == nil { 1055 spec.Definitions = make(map[string]swspec.Schema, 150) 1056 } 1057 spec.Definitions[name] = *schema 1058} 1059 1060// getPointerFromKey retrieves the content of the JSON pointer "key" 1061func getPointerFromKey(spec interface{}, key string) (string, interface{}, error) { 1062 switch spec.(type) { 1063 case *swspec.Schema: 1064 case *swspec.Swagger: 1065 default: 1066 panic("unexpected type used in getPointerFromKey") 1067 } 1068 if key == "#/" { 1069 return "", spec, nil 1070 } 1071 // unescape chars in key, e.g. "{}" from path params 1072 pth, _ := internal.PathUnescape(key[1:]) 1073 ptr, err := jsonpointer.New(pth) 1074 if err != nil { 1075 return "", nil, err 1076 } 1077 1078 value, _, err := ptr.Get(spec) 1079 if err != nil { 1080 debugLog("error when getting key: %s with path: %s", key, pth) 1081 return "", nil, err 1082 } 1083 return pth, value, nil 1084} 1085 1086// getParentFromKey retrieves the container of the JSON pointer "key" 1087func getParentFromKey(spec interface{}, key string) (string, string, interface{}, error) { 1088 switch spec.(type) { 1089 case *swspec.Schema: 1090 case *swspec.Swagger: 1091 default: 1092 panic("unexpected type used in getPointerFromKey") 1093 } 1094 // unescape chars in key, e.g. "{}" from path params 1095 pth, _ := internal.PathUnescape(key[1:]) 1096 1097 parent, entry := slashpath.Dir(pth), slashpath.Base(pth) 1098 debugLog("getting schema holder at: %s, with entry: %s", parent, entry) 1099 1100 pptr, err := jsonpointer.New(parent) 1101 if err != nil { 1102 return "", "", nil, err 1103 } 1104 pvalue, _, err := pptr.Get(spec) 1105 if err != nil { 1106 return "", "", nil, fmt.Errorf("can't get parent for %s: %v", parent, err) 1107 } 1108 return parent, entry, pvalue, nil 1109} 1110 1111// updateRef replaces a ref by another one 1112func updateRef(spec interface{}, key string, ref swspec.Ref) error { 1113 switch spec.(type) { 1114 case *swspec.Schema: 1115 case *swspec.Swagger: 1116 default: 1117 panic("unexpected type used in getPointerFromKey") 1118 } 1119 debugLog("updating ref for %s with %s", key, ref.String()) 1120 pth, value, err := getPointerFromKey(spec, key) 1121 if err != nil { 1122 return err 1123 } 1124 1125 switch refable := value.(type) { 1126 case *swspec.Schema: 1127 refable.Ref = ref 1128 case *swspec.SchemaOrArray: 1129 if refable.Schema != nil { 1130 refable.Schema.Ref = ref 1131 } 1132 case *swspec.SchemaOrBool: 1133 if refable.Schema != nil { 1134 refable.Schema.Ref = ref 1135 } 1136 case swspec.Schema: 1137 debugLog("rewriting holder for %T", refable) 1138 _, entry, pvalue, erp := getParentFromKey(spec, key) 1139 if erp != nil { 1140 return err 1141 } 1142 switch container := pvalue.(type) { 1143 case swspec.Definitions: 1144 container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 1145 1146 case map[string]swspec.Schema: 1147 container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 1148 1149 case []swspec.Schema: 1150 idx, err := strconv.Atoi(entry) 1151 if err != nil { 1152 return fmt.Errorf("%s not a number: %v", pth, err) 1153 } 1154 container[idx] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 1155 1156 case *swspec.SchemaOrArray: 1157 // NOTE: this is necessarily an array - otherwise, the parent would be *Schema 1158 idx, err := strconv.Atoi(entry) 1159 if err != nil { 1160 return fmt.Errorf("%s not a number: %v", pth, err) 1161 } 1162 container.Schemas[idx] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 1163 1164 case swspec.SchemaProperties: 1165 container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}} 1166 1167 // NOTE: can't have case *swspec.SchemaOrBool = parent in this case is *Schema 1168 1169 default: 1170 return fmt.Errorf("unhandled container type at %s: %T", key, value) 1171 } 1172 1173 default: 1174 return fmt.Errorf("no schema with ref found at %s for %T", key, value) 1175 } 1176 1177 return nil 1178} 1179 1180// updateRefWithSchema replaces a ref with a schema (i.e. re-inline schema) 1181func updateRefWithSchema(spec *swspec.Swagger, key string, sch *swspec.Schema) error { 1182 debugLog("updating ref for %s with schema", key) 1183 pth, value, err := getPointerFromKey(spec, key) 1184 if err != nil { 1185 return err 1186 } 1187 1188 switch refable := value.(type) { 1189 case *swspec.Schema: 1190 *refable = *sch 1191 case swspec.Schema: 1192 _, entry, pvalue, erp := getParentFromKey(spec, key) 1193 if erp != nil { 1194 return err 1195 } 1196 switch container := pvalue.(type) { 1197 case swspec.Definitions: 1198 container[entry] = *sch 1199 1200 case map[string]swspec.Schema: 1201 container[entry] = *sch 1202 1203 case []swspec.Schema: 1204 idx, err := strconv.Atoi(entry) 1205 if err != nil { 1206 return fmt.Errorf("%s not a number: %v", pth, err) 1207 } 1208 container[idx] = *sch 1209 1210 case *swspec.SchemaOrArray: 1211 // NOTE: this is necessarily an array - otherwise, the parent would be *Schema 1212 idx, err := strconv.Atoi(entry) 1213 if err != nil { 1214 return fmt.Errorf("%s not a number: %v", pth, err) 1215 } 1216 container.Schemas[idx] = *sch 1217 1218 case swspec.SchemaProperties: 1219 container[entry] = *sch 1220 1221 // NOTE: can't have case *swspec.SchemaOrBool = parent in this case is *Schema 1222 1223 default: 1224 return fmt.Errorf("unhandled type for parent of [%s]: %T", key, value) 1225 } 1226 case *swspec.SchemaOrArray: 1227 *refable.Schema = *sch 1228 // NOTE: can't have case *swspec.SchemaOrBool = parent in this case is *Schema 1229 case *swspec.SchemaOrBool: 1230 *refable.Schema = *sch 1231 default: 1232 return fmt.Errorf("no schema with ref found at %s for %T", key, value) 1233 } 1234 1235 return nil 1236} 1237 1238func containsString(names []string, name string) bool { 1239 for _, nm := range names { 1240 if nm == name { 1241 return true 1242 } 1243 } 1244 return false 1245} 1246 1247type opRef struct { 1248 Method string 1249 Path string 1250 Key string 1251 ID string 1252 Op *swspec.Operation 1253 Ref swspec.Ref 1254} 1255 1256type opRefs []opRef 1257 1258func (o opRefs) Len() int { return len(o) } 1259func (o opRefs) Swap(i, j int) { o[i], o[j] = o[j], o[i] } 1260func (o opRefs) Less(i, j int) bool { return o[i].Key < o[j].Key } 1261 1262func gatherOperations(specDoc *Spec, operationIDs []string) map[string]opRef { 1263 var oprefs opRefs 1264 1265 for method, pathItem := range specDoc.Operations() { 1266 for pth, operation := range pathItem { 1267 vv := *operation 1268 oprefs = append(oprefs, opRef{ 1269 Key: swag.ToGoName(strings.ToLower(method) + " " + pth), 1270 Method: method, 1271 Path: pth, 1272 ID: vv.ID, 1273 Op: &vv, 1274 Ref: swspec.MustCreateRef("#" + slashpath.Join("/paths", jsonpointer.Escape(pth), method)), 1275 }) 1276 } 1277 } 1278 1279 sort.Sort(oprefs) 1280 1281 operations := make(map[string]opRef) 1282 for _, opr := range oprefs { 1283 nm := opr.ID 1284 if nm == "" { 1285 nm = opr.Key 1286 } 1287 1288 oo, found := operations[nm] 1289 if found && oo.Method != opr.Method && oo.Path != opr.Path { 1290 nm = opr.Key 1291 } 1292 if len(operationIDs) == 0 || containsString(operationIDs, opr.ID) || containsString(operationIDs, nm) { 1293 opr.ID = nm 1294 opr.Op.ID = nm 1295 operations[nm] = opr 1296 } 1297 } 1298 return operations 1299} 1300 1301// stripPointersAndOAIGen removes anonymous JSON pointers from spec and chain with name conflicts handler. 1302// This loops until the spec has no such pointer and all name conflicts have been reduced as much as possible. 1303func stripPointersAndOAIGen(opts *FlattenOpts) error { 1304 // name all JSON pointers to anonymous documents 1305 if err := namePointers(opts); err != nil { 1306 return err 1307 } 1308 1309 // remove unnecessary OAIGen ref (created when flattening external refs creates name conflicts) 1310 hasIntroducedPointerOrInline, ers := stripOAIGen(opts) 1311 if ers != nil { 1312 return ers 1313 } 1314 1315 // iterate as pointer or OAIGen resolution may introduce inline schemas or pointers 1316 for hasIntroducedPointerOrInline { 1317 if !opts.Minimal { 1318 opts.Spec.reload() // re-analyze 1319 if err := nameInlinedSchemas(opts); err != nil { 1320 return err 1321 } 1322 } 1323 1324 if err := namePointers(opts); err != nil { 1325 return err 1326 } 1327 1328 // restrip and re-analyze 1329 if hasIntroducedPointerOrInline, ers = stripOAIGen(opts); ers != nil { 1330 return ers 1331 } 1332 } 1333 return nil 1334} 1335 1336func updateRefParents(opts *FlattenOpts, r *newRef) { 1337 if !r.isOAIGen || r.resolved { // bail on already resolved entries (avoid looping) 1338 return 1339 } 1340 for k, v := range opts.Spec.references.allRefs { 1341 if r.path != v.String() { 1342 continue 1343 } 1344 found := false 1345 for _, p := range r.parents { 1346 if p == k { 1347 found = true 1348 break 1349 } 1350 } 1351 if !found { 1352 r.parents = append(r.parents, k) 1353 } 1354 } 1355} 1356 1357// topMostRefs is able to sort refs by hierarchical then lexicographic order, 1358// yielding refs ordered breadth-first. 1359type topmostRefs []string 1360 1361func (k topmostRefs) Len() int { return len(k) } 1362func (k topmostRefs) Swap(i, j int) { k[i], k[j] = k[j], k[i] } 1363func (k topmostRefs) Less(i, j int) bool { 1364 li, lj := len(strings.Split(k[i], "/")), len(strings.Split(k[j], "/")) 1365 if li == lj { 1366 return k[i] < k[j] 1367 } 1368 return li < lj 1369} 1370 1371func topmostFirst(refs []string) []string { 1372 res := topmostRefs(refs) 1373 sort.Sort(res) 1374 return res 1375} 1376 1377// stripOAIGen strips the spec from unnecessary OAIGen constructs, initially created to dedupe flattened definitions. 1378// 1379// A dedupe is deemed unnecessary whenever: 1380// - the only conflict is with its (single) parent: OAIGen is merged into its parent (reinlining) 1381// - there is a conflict with multiple parents: merge OAIGen in first parent, the rewrite other parents to point to 1382// the first parent. 1383// 1384// This function returns true whenever it re-inlined a complex schema, so the caller may chose to iterate 1385// pointer and name resolution again. 1386func stripOAIGen(opts *FlattenOpts) (bool, error) { 1387 debugLog("stripOAIGen") 1388 replacedWithComplex := false 1389 1390 // figure out referers of OAIGen definitions (doing it before the ref start mutating) 1391 for _, r := range opts.flattenContext.newRefs { 1392 updateRefParents(opts, r) 1393 } 1394 for k := range opts.flattenContext.newRefs { 1395 r := opts.flattenContext.newRefs[k] 1396 debugLog("newRefs[%s]: isOAIGen: %t, resolved: %t, name: %s, path:%s, #parents: %d, parents: %v, ref: %s", 1397 k, r.isOAIGen, r.resolved, r.newName, r.path, len(r.parents), r.parents, r.schema.Ref.String()) 1398 if r.isOAIGen && len(r.parents) >= 1 { 1399 pr := topmostFirst(r.parents) 1400 1401 // rewrite first parent schema in hierarchical then lexicographical order 1402 debugLog("rewrite first parent %s with schema", pr[0]) 1403 if err := updateRefWithSchema(opts.Swagger(), pr[0], r.schema); err != nil { 1404 return false, err 1405 } 1406 if pa, ok := opts.flattenContext.newRefs[pr[0]]; ok && pa.isOAIGen { 1407 // update parent in ref index entry 1408 debugLog("update parent entry: %s", pr[0]) 1409 pa.schema = r.schema 1410 pa.resolved = false 1411 replacedWithComplex = true 1412 } 1413 1414 // rewrite other parents to point to first parent 1415 if len(pr) > 1 { 1416 for _, p := range pr[1:] { 1417 replacingRef := swspec.MustCreateRef(pr[0]) 1418 1419 // set complex when replacing ref is an anonymous jsonpointer: further processing may be required 1420 replacedWithComplex = replacedWithComplex || 1421 slashpath.Dir(replacingRef.String()) != definitionsPath 1422 debugLog("rewrite parent with ref: %s", replacingRef.String()) 1423 1424 // NOTE: it is possible at this stage to introduce json pointers (to non-definitions places). 1425 // Those are stripped later on. 1426 if err := updateRef(opts.Swagger(), p, replacingRef); err != nil { 1427 return false, err 1428 } 1429 1430 if pa, ok := opts.flattenContext.newRefs[p]; ok && pa.isOAIGen { 1431 // update parent in ref index 1432 debugLog("update parent entry: %s", p) 1433 pa.schema = r.schema 1434 pa.resolved = false 1435 replacedWithComplex = true 1436 } 1437 } 1438 } 1439 1440 // remove OAIGen definition 1441 debugLog("removing definition %s", slashpath.Base(r.path)) 1442 delete(opts.Swagger().Definitions, slashpath.Base(r.path)) 1443 1444 // propagate changes in ref index for keys which have this one as a parent 1445 for kk, value := range opts.flattenContext.newRefs { 1446 if kk == k || !value.isOAIGen || value.resolved { 1447 continue 1448 } 1449 found := false 1450 newParents := make([]string, 0, len(value.parents)) 1451 for _, parent := range value.parents { 1452 switch { 1453 case parent == r.path: 1454 found = true 1455 parent = pr[0] 1456 case strings.HasPrefix(parent, r.path+"/"): 1457 found = true 1458 parent = slashpath.Join(pr[0], strings.TrimPrefix(parent, r.path)) 1459 } 1460 newParents = append(newParents, parent) 1461 } 1462 if found { 1463 value.parents = newParents 1464 } 1465 } 1466 1467 // mark naming conflict as resolved 1468 debugLog("marking naming conflict resolved for key: %s", r.key) 1469 opts.flattenContext.newRefs[r.key].isOAIGen = false 1470 opts.flattenContext.newRefs[r.key].resolved = true 1471 1472 // determine if the previous substitution did inline a complex schema 1473 if r.schema != nil && r.schema.Ref.String() == "" { // inline schema 1474 asch, err := Schema(SchemaOpts{Schema: r.schema, Root: opts.Swagger(), BasePath: opts.BasePath}) 1475 if err != nil { 1476 return false, err 1477 } 1478 debugLog("re-inlined schema: parent: %s, %t", pr[0], isAnalyzedAsComplex(asch)) 1479 replacedWithComplex = replacedWithComplex || 1480 !(slashpath.Dir(pr[0]) == definitionsPath) && isAnalyzedAsComplex(asch) 1481 } 1482 } 1483 } 1484 1485 debugLog("replacedWithComplex: %t", replacedWithComplex) 1486 opts.Spec.reload() // re-analyze 1487 return replacedWithComplex, nil 1488} 1489 1490// croak logs notifications and warnings about valid, but possibly unwanted constructs resulting 1491// from flattening a spec 1492func croak(opts *FlattenOpts) { 1493 reported := make(map[string]bool, len(opts.flattenContext.newRefs)) 1494 for _, v := range opts.Spec.references.allRefs { 1495 // warns about duplicate handling 1496 for _, r := range opts.flattenContext.newRefs { 1497 if r.isOAIGen && r.path == v.String() { 1498 reported[r.newName] = true 1499 } 1500 } 1501 } 1502 for k := range reported { 1503 log.Printf("warning: duplicate flattened definition name resolved as %s", k) 1504 } 1505 // warns about possible type mismatches 1506 uniqueMsg := make(map[string]bool) 1507 for _, msg := range opts.flattenContext.warnings { 1508 if _, ok := uniqueMsg[msg]; ok { 1509 continue 1510 } 1511 log.Printf("warning: %s", msg) 1512 uniqueMsg[msg] = true 1513 } 1514} 1515 1516// namePointers replaces all JSON pointers to anonymous documents by a $ref to a new named definitions. 1517// 1518// This is carried on depth-first. Pointers to $refs which are top level definitions are replaced by the $ref itself. 1519// Pointers to simple types are expanded, unless they express commonality (i.e. several such $ref are used). 1520func namePointers(opts *FlattenOpts) error { 1521 debugLog("name pointers") 1522 refsToReplace := make(map[string]SchemaRef, len(opts.Spec.references.schemas)) 1523 for k, ref := range opts.Spec.references.allRefs { 1524 if slashpath.Dir(ref.String()) == definitionsPath { 1525 // this a ref to a top-level definition: ok 1526 continue 1527 } 1528 replacingRef, sch, erd := deepestRef(opts, ref) 1529 if erd != nil { 1530 return fmt.Errorf("at %s, %v", k, erd) 1531 } 1532 debugLog("planning pointer to replace at %s: %s, resolved to: %s", k, ref.String(), replacingRef.String()) 1533 refsToReplace[k] = SchemaRef{ 1534 Name: k, // caller 1535 Ref: replacingRef, // callee 1536 Schema: sch, 1537 TopLevel: slashpath.Dir(replacingRef.String()) == definitionsPath, 1538 } 1539 } 1540 depthFirst := sortDepthFirst(refsToReplace) 1541 namer := &inlineSchemaNamer{ 1542 Spec: opts.Swagger(), 1543 Operations: opRefsByRef(gatherOperations(opts.Spec, nil)), 1544 flattenContext: opts.flattenContext, 1545 opts: opts, 1546 } 1547 1548 for _, key := range depthFirst { 1549 v := refsToReplace[key] 1550 // update current replacement, which may have been updated by previous changes of deeper elements 1551 replacingRef, sch, erd := deepestRef(opts, v.Ref) 1552 if erd != nil { 1553 return fmt.Errorf("at %s, %v", key, erd) 1554 } 1555 v.Ref = replacingRef 1556 v.Schema = sch 1557 v.TopLevel = slashpath.Dir(replacingRef.String()) == definitionsPath 1558 debugLog("replacing pointer at %s: resolved to: %s", key, v.Ref.String()) 1559 1560 if v.TopLevel { 1561 debugLog("replace pointer %s by canonical definition: %s", key, v.Ref.String()) 1562 // if the schema is a $ref to a top level definition, just rewrite the pointer to this $ref 1563 if err := updateRef(opts.Swagger(), key, v.Ref); err != nil { 1564 return err 1565 } 1566 } else { 1567 // this is a JSON pointer to an anonymous document (internal or external): 1568 // create a definition for this schema when: 1569 // - it is a complex schema 1570 // - or it is pointed by more than one $ref (i.e. expresses commonality) 1571 // otherwise, expand the pointer (single reference to a simple type) 1572 // 1573 // The named definition for this follows the target's key, not the caller's 1574 debugLog("namePointers at %s for %s", key, v.Ref.String()) 1575 1576 // qualify the expanded schema 1577 /* 1578 if key == "#/paths/~1some~1where~1{id}/get/parameters/1/items" { 1579 // DEBUG 1580 //func getPointerFromKey(spec interface{}, key string) (string, interface{}, error) { 1581 k, res, err := getPointerFromKey(namer.Spec, key) 1582 debugLog("k = %s, res=%#v, err=%v", k, res, err) 1583 } 1584 */ 1585 asch, ers := Schema(SchemaOpts{Schema: v.Schema, Root: opts.Swagger(), BasePath: opts.BasePath}) 1586 if ers != nil { 1587 return fmt.Errorf("schema analysis [%s]: %v", key, ers) 1588 } 1589 callers := make([]string, 0, 64) 1590 1591 debugLog("looking for callers") 1592 an := New(opts.Swagger()) 1593 for k, w := range an.references.allRefs { 1594 r, _, erd := deepestRef(opts, w) 1595 if erd != nil { 1596 return fmt.Errorf("at %s, %v", key, erd) 1597 } 1598 if r.String() == v.Ref.String() { 1599 callers = append(callers, k) 1600 } 1601 } 1602 debugLog("callers for %s: %d", v.Ref.String(), len(callers)) 1603 if len(callers) == 0 { 1604 // has already been updated and resolved 1605 continue 1606 } 1607 1608 parts := keyParts(v.Ref.String()) 1609 debugLog("number of callers for %s: %d", v.Ref.String(), len(callers)) 1610 // identifying edge case when the namer did nothing because we point to a non-schema object 1611 // no definition is created and we expand the $ref for all callers 1612 if (!asch.IsSimpleSchema || len(callers) > 1) && !parts.IsSharedParam() && !parts.IsSharedResponse() { 1613 debugLog("replace JSON pointer at [%s] by definition: %s", key, v.Ref.String()) 1614 if err := namer.Name(v.Ref.String(), v.Schema, asch); err != nil { 1615 return err 1616 } 1617 1618 // regular case: we named the $ref as a definition, and we move all callers to this new $ref 1619 for _, caller := range callers { 1620 if caller != key { 1621 // move $ref for next to resolve 1622 debugLog("identified caller of %s at [%s]", v.Ref.String(), caller) 1623 c := refsToReplace[caller] 1624 c.Ref = v.Ref 1625 refsToReplace[caller] = c 1626 } 1627 } 1628 } else { 1629 debugLog("expand JSON pointer for key=%s", key) 1630 if err := updateRefWithSchema(opts.Swagger(), key, v.Schema); err != nil { 1631 return err 1632 } 1633 // NOTE: there is no other caller to update 1634 } 1635 } 1636 } 1637 opts.Spec.reload() // re-analyze 1638 return nil 1639} 1640 1641// deepestRef finds the first definition ref, from a cascade of nested refs which are not definitions. 1642// - if no definition is found, returns the deepest ref. 1643// - pointers to external files are expanded 1644// 1645// NOTE: all external $ref's are assumed to be already expanded at this stage. 1646func deepestRef(opts *FlattenOpts, ref swspec.Ref) (swspec.Ref, *swspec.Schema, error) { 1647 if !ref.HasFragmentOnly { 1648 // we found an external $ref, which is odd 1649 // does nothing on external $refs 1650 return ref, nil, nil 1651 } 1652 currentRef := ref 1653 visited := make(map[string]bool, 64) 1654DOWNREF: 1655 for currentRef.String() != "" { 1656 if slashpath.Dir(currentRef.String()) == definitionsPath { 1657 // this is a top-level definition: stop here and return this ref 1658 return currentRef, nil, nil 1659 } 1660 if _, beenThere := visited[currentRef.String()]; beenThere { 1661 return swspec.Ref{}, nil, 1662 fmt.Errorf("cannot resolve cyclic chain of pointers under %s", currentRef.String()) 1663 } 1664 visited[currentRef.String()] = true 1665 value, _, err := currentRef.GetPointer().Get(opts.Swagger()) 1666 if err != nil { 1667 return swspec.Ref{}, nil, err 1668 } 1669 switch refable := value.(type) { 1670 case *swspec.Schema: 1671 if refable.Ref.String() == "" { 1672 break DOWNREF 1673 } 1674 currentRef = refable.Ref 1675 1676 case swspec.Schema: 1677 if refable.Ref.String() == "" { 1678 break DOWNREF 1679 } 1680 currentRef = refable.Ref 1681 1682 case *swspec.SchemaOrArray: 1683 if refable.Schema == nil || refable.Schema != nil && refable.Schema.Ref.String() == "" { 1684 break DOWNREF 1685 } 1686 currentRef = refable.Schema.Ref 1687 1688 case *swspec.SchemaOrBool: 1689 if refable.Schema == nil || refable.Schema != nil && refable.Schema.Ref.String() == "" { 1690 break DOWNREF 1691 } 1692 currentRef = refable.Schema.Ref 1693 1694 case swspec.Response: 1695 // a pointer points to a schema initially marshalled in responses section... 1696 // Attempt to convert this to a schema. If this fails, the spec is invalid 1697 asJSON, _ := refable.MarshalJSON() 1698 var asSchema swspec.Schema 1699 err := asSchema.UnmarshalJSON(asJSON) 1700 if err != nil { 1701 return swspec.Ref{}, nil, 1702 fmt.Errorf("invalid type for resolved JSON pointer %s. Expected a schema a, got: %T", 1703 currentRef.String(), value) 1704 1705 } 1706 opts.flattenContext.warnings = append(opts.flattenContext.warnings, 1707 fmt.Sprintf("found $ref %q (response) interpreted as schema", currentRef.String())) 1708 1709 if asSchema.Ref.String() == "" { 1710 break DOWNREF 1711 } 1712 currentRef = asSchema.Ref 1713 1714 case swspec.Parameter: 1715 // a pointer points to a schema initially marshalled in parameters section... 1716 // Attempt to convert this to a schema. If this fails, the spec is invalid 1717 asJSON, _ := refable.MarshalJSON() 1718 var asSchema swspec.Schema 1719 err := asSchema.UnmarshalJSON(asJSON) 1720 if err != nil { 1721 return swspec.Ref{}, nil, 1722 fmt.Errorf("invalid type for resolved JSON pointer %s. Expected a schema a, got: %T", 1723 currentRef.String(), value) 1724 1725 } 1726 opts.flattenContext.warnings = append(opts.flattenContext.warnings, 1727 fmt.Sprintf("found $ref %q (parameter) interpreted as schema", currentRef.String())) 1728 1729 if asSchema.Ref.String() == "" { 1730 break DOWNREF 1731 } 1732 currentRef = asSchema.Ref 1733 1734 default: 1735 return swspec.Ref{}, nil, 1736 fmt.Errorf("unhandled type to resolve JSON pointer %s. Expected a Schema, got: %T", 1737 currentRef.String(), value) 1738 1739 } 1740 } 1741 // assess what schema we're ending with 1742 sch, erv := swspec.ResolveRefWithBase(opts.Swagger(), ¤tRef, opts.ExpandOpts(false)) 1743 if erv != nil { 1744 return swspec.Ref{}, nil, erv 1745 } 1746 if sch == nil { 1747 return swspec.Ref{}, nil, fmt.Errorf("no schema found at %s", currentRef.String()) 1748 } 1749 return currentRef, sch, nil 1750} 1751 1752// normalizeRef strips the current file from any $ref. This works around issue go-openapi/spec#76: 1753// leading absolute file in $ref is stripped 1754func normalizeRef(opts *FlattenOpts) error { 1755 debugLog("normalizeRef") 1756 altered := false 1757 for k, w := range opts.Spec.references.allRefs { 1758 if !strings.HasPrefix(w.String(), opts.BasePath+definitionsPath) { // may be a mix of / and \, depending on OS 1759 continue 1760 } 1761 altered = true 1762 // strip base path from definition 1763 debugLog("stripping absolute path for: %s", w.String()) 1764 if err := updateRef(opts.Swagger(), k, 1765 swspec.MustCreateRef(slashpath.Join(definitionsPath, slashpath.Base(w.String())))); err != nil { 1766 return err 1767 } 1768 } 1769 if altered { 1770 opts.Spec.reload() // re-analyze 1771 } 1772 return nil 1773} 1774