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(), &currentRef, 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