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