Codebase list golang-github-go-openapi-analysis / HEAD flatten.go
HEAD

Tree @HEAD (Download .tar.gz)

flatten.go @HEADraw · history · blame

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package analysis

import (
	"fmt"
	"log"
	"path"
	"sort"
	"strings"

	"github.com/go-openapi/analysis/internal/flatten/normalize"
	"github.com/go-openapi/analysis/internal/flatten/operations"
	"github.com/go-openapi/analysis/internal/flatten/replace"
	"github.com/go-openapi/analysis/internal/flatten/schutils"
	"github.com/go-openapi/analysis/internal/flatten/sortref"
	"github.com/go-openapi/jsonpointer"
	"github.com/go-openapi/spec"
)

const definitionsPath = "#/definitions"

// newRef stores information about refs created during the flattening process
type newRef struct {
	key      string
	newName  string
	path     string
	isOAIGen bool
	resolved bool
	schema   *spec.Schema
	parents  []string
}

// context stores intermediary results from flatten
type context struct {
	newRefs  map[string]*newRef
	warnings []string
	resolved map[string]string
}

func newContext() *context {
	return &context{
		newRefs:  make(map[string]*newRef, 150),
		warnings: make([]string, 0),
		resolved: make(map[string]string, 50),
	}
}

// Flatten an analyzed spec and produce a self-contained spec bundle.
//
// There is a minimal and a full flattening mode.
//
//
// Minimally flattening a spec means:
//  - Expanding parameters, responses, path items, parameter items and header items (references to schemas are left
//    unscathed)
//  - Importing external (http, file) references so they become internal to the document
//  - Moving every JSON pointer to a $ref to a named definition (i.e. the reworked spec does not contain pointers
//    like "$ref": "#/definitions/myObject/allOfs/1")
//
// A minimally flattened spec thus guarantees the following properties:
//  - all $refs point to a local definition (i.e. '#/definitions/...')
//  - definitions are unique
//
// NOTE: arbitrary JSON pointers (other than $refs to top level definitions) are rewritten as definitions if they
// represent a complex schema or express commonality in the spec.
// Otherwise, they are simply expanded.
// Self-referencing JSON pointers cannot resolve to a type and trigger an error.
//
//
// Minimal flattening is necessary and sufficient for codegen rendering using go-swagger.
//
// Fully flattening a spec means:
//  - Moving every complex inline schema to be a definition with an auto-generated name in a depth-first fashion.
//
// By complex, we mean every JSON object with some properties.
// Arrays, when they do not define a tuple,
// or empty objects with or without additionalProperties, are not considered complex and remain inline.
//
// NOTE: rewritten schemas get a vendor extension x-go-gen-location so we know from which part of the spec definitions
// have been created.
//
// Available flattening options:
//  - Minimal: stops flattening after minimal $ref processing, leaving schema constructs untouched
//  - Expand: expand all $ref's in the document (inoperant if Minimal set to true)
//  - Verbose: croaks about name conflicts detected
//  - RemoveUnused: removes unused parameters, responses and definitions after expansion/flattening
//
// NOTE: expansion removes all $ref save circular $ref, which remain in place
//
// TODO: additional options
//  - ProgagateNameExtensions: ensure that created entries properly follow naming rules when their parent have set a
//    x-go-name extension
//  - LiftAllOfs:
//     - limit the flattening of allOf members when simple objects
//     - merge allOf with validation only
//     - merge allOf with extensions only
//     - ...
//
func Flatten(opts FlattenOpts) error {
	debugLog("FlattenOpts: %#v", opts)

	opts.flattenContext = newContext()

	// 1. Recursively expand responses, parameters, path items and items in simple schemas.
	//
	// This simplifies the spec and leaves only the $ref's in schema objects.
	if err := expand(&opts); err != nil {
		return err
	}

	// 2. Strip the current document from absolute $ref's that actually a in the root,
	// so we can recognize them as proper definitions
	//
	// In particular, this works around issue go-openapi/spec#76: leading absolute file in $ref is stripped
	if err := normalizeRef(&opts); err != nil {
		return err
	}

	// 3. Optionally remove shared parameters and responses already expanded (now unused).
	//
	// Operation parameters (i.e. under paths) remain.
	if opts.RemoveUnused {
		removeUnusedShared(&opts)
	}

	// 4. Import all remote references.
	if err := importReferences(&opts); err != nil {
		return err
	}

	// 5. full flattening: rewrite inline schemas (schemas that aren't simple types or arrays or maps)
	if !opts.Minimal && !opts.Expand {
		if err := nameInlinedSchemas(&opts); err != nil {
			return err
		}
	}

	// 6. Rewrite JSON pointers other than $ref to named definitions
	// and attempt to resolve conflicting names whenever possible.
	if err := stripPointersAndOAIGen(&opts); err != nil {
		return err
	}

	// 7. Strip the spec from unused definitions
	if opts.RemoveUnused {
		removeUnused(&opts)
	}

	// 8. Issue warning notifications, if any
	opts.croak()

	// TODO: simplify known schema patterns to flat objects with properties
	// examples:
	//  - lift simple allOf object,
	//  - empty allOf with validation only or extensions only
	//  - rework allOf arrays
	//  - rework allOf additionalProperties

	return nil
}

func expand(opts *FlattenOpts) error {
	if err := spec.ExpandSpec(opts.Swagger(), opts.ExpandOpts(!opts.Expand)); err != nil {
		return err
	}

	opts.Spec.reload() // re-analyze

	return nil
}

// normalizeRef strips the current file from any absolute file $ref. This works around issue go-openapi/spec#76:
// leading absolute file in $ref is stripped
func normalizeRef(opts *FlattenOpts) error {
	debugLog("normalizeRef")

	altered := false
	for k, w := range opts.Spec.references.allRefs {
		if !strings.HasPrefix(w.String(), opts.BasePath+definitionsPath) { // may be a mix of / and \, depending on OS
			continue
		}

		altered = true
		debugLog("stripping absolute path for: %s", w.String())

		// strip the base path from definition
		if err := replace.UpdateRef(opts.Swagger(), k,
			spec.MustCreateRef(path.Join(definitionsPath, path.Base(w.String())))); err != nil {
			return err
		}
	}

	if altered {
		opts.Spec.reload() // re-analyze
	}

	return nil
}

func removeUnusedShared(opts *FlattenOpts) {
	opts.Swagger().Parameters = nil
	opts.Swagger().Responses = nil

	opts.Spec.reload() // re-analyze
}

func importReferences(opts *FlattenOpts) error {
	var (
		imported bool
		err      error
	)

	for !imported && err == nil {
		// iteratively import remote references until none left.
		// This inlining deals with name conflicts by introducing auto-generated names ("OAIGen")
		imported, err = importExternalReferences(opts)

		opts.Spec.reload() // re-analyze
	}

	return err
}

// nameInlinedSchemas replaces every complex inline construct by a named definition.
func nameInlinedSchemas(opts *FlattenOpts) error {
	debugLog("nameInlinedSchemas")

	namer := &InlineSchemaNamer{
		Spec:           opts.Swagger(),
		Operations:     operations.AllOpRefsByRef(opts.Spec, nil),
		flattenContext: opts.flattenContext,
		opts:           opts,
	}

	depthFirst := sortref.DepthFirst(opts.Spec.allSchemas)
	for _, key := range depthFirst {
		sch := opts.Spec.allSchemas[key]
		if sch.Schema == nil || sch.Schema.Ref.String() != "" || sch.TopLevel {
			continue
		}

		asch, err := Schema(SchemaOpts{Schema: sch.Schema, Root: opts.Swagger(), BasePath: opts.BasePath})
		if err != nil {
			return fmt.Errorf("schema analysis [%s]: %w", key, err)
		}

		if asch.isAnalyzedAsComplex() { // move complex schemas to definitions
			if err := namer.Name(key, sch.Schema, asch); err != nil {
				return err
			}
		}
	}

	opts.Spec.reload() // re-analyze

	return nil
}

func removeUnused(opts *FlattenOpts) {
	expected := make(map[string]struct{})
	for k := range opts.Swagger().Definitions {
		expected[path.Join(definitionsPath, jsonpointer.Escape(k))] = struct{}{}
	}

	for _, k := range opts.Spec.AllDefinitionReferences() {
		delete(expected, k)
	}

	for k := range expected {
		debugLog("removing unused definition %s", path.Base(k))
		if opts.Verbose {
			log.Printf("info: removing unused definition: %s", path.Base(k))
		}
		delete(opts.Swagger().Definitions, path.Base(k))
	}

	opts.Spec.reload() // re-analyze
}

func importKnownRef(entry sortref.RefRevIdx, refStr, newName string, opts *FlattenOpts) error {
	// rewrite ref with already resolved external ref (useful for cyclical refs):
	// rewrite external refs to local ones
	debugLog("resolving known ref [%s] to %s", refStr, newName)

	for _, key := range entry.Keys {
		if err := replace.UpdateRef(opts.Swagger(), key, spec.MustCreateRef(path.Join(definitionsPath, newName))); err != nil {
			return err
		}
	}

	return nil
}

func importNewRef(entry sortref.RefRevIdx, refStr string, opts *FlattenOpts) error {
	var (
		isOAIGen bool
		newName  string
	)

	debugLog("resolving schema from remote $ref [%s]", refStr)

	sch, err := spec.ResolveRefWithBase(opts.Swagger(), &entry.Ref, opts.ExpandOpts(false))
	if err != nil {
		return fmt.Errorf("could not resolve schema: %w", err)
	}

	// at this stage only $ref analysis matters
	partialAnalyzer := &Spec{
		references: referenceAnalysis{},
		patterns:   patternAnalysis{},
		enums:      enumAnalysis{},
	}
	partialAnalyzer.reset()
	partialAnalyzer.analyzeSchema("", sch, "/")

	// now rewrite those refs with rebase
	for key, ref := range partialAnalyzer.references.allRefs {
		if err := replace.UpdateRef(sch, key, spec.MustCreateRef(normalize.RebaseRef(entry.Ref.String(), ref.String()))); err != nil {
			return fmt.Errorf("failed to rewrite ref for key %q at %s: %w", key, entry.Ref.String(), err)
		}
	}

	// generate a unique name - isOAIGen means that a naming conflict was resolved by changing the name
	newName, isOAIGen = uniqifyName(opts.Swagger().Definitions, nameFromRef(entry.Ref))
	debugLog("new name for [%s]: %s - with name conflict:%t", strings.Join(entry.Keys, ", "), newName, isOAIGen)

	opts.flattenContext.resolved[refStr] = newName

	// rewrite the external refs to local ones
	for _, key := range entry.Keys {
		if err := replace.UpdateRef(opts.Swagger(), key,
			spec.MustCreateRef(path.Join(definitionsPath, newName))); err != nil {
			return err
		}

		// keep track of created refs
		resolved := false
		if _, ok := opts.flattenContext.newRefs[key]; ok {
			resolved = opts.flattenContext.newRefs[key].resolved
		}

		debugLog("keeping track of ref: %s (%s), resolved: %t", key, newName, resolved)
		opts.flattenContext.newRefs[key] = &newRef{
			key:      key,
			newName:  newName,
			path:     path.Join(definitionsPath, newName),
			isOAIGen: isOAIGen,
			resolved: resolved,
			schema:   sch,
		}
	}

	// add the resolved schema to the definitions
	schutils.Save(opts.Swagger(), newName, sch)

	return nil
}

// importExternalReferences iteratively digs remote references and imports them into the main schema.
//
// At every iteration, new remotes may be found when digging deeper: they are rebased to the current schema before being imported.
//
// This returns true when no more remote references can be found.
func importExternalReferences(opts *FlattenOpts) (bool, error) {
	debugLog("importExternalReferences")

	groupedRefs := sortref.ReverseIndex(opts.Spec.references.schemas, opts.BasePath)
	sortedRefStr := make([]string, 0, len(groupedRefs))
	if opts.flattenContext == nil {
		opts.flattenContext = newContext()
	}

	// sort $ref resolution to ensure deterministic name conflict resolution
	for refStr := range groupedRefs {
		sortedRefStr = append(sortedRefStr, refStr)
	}
	sort.Strings(sortedRefStr)

	complete := true

	for _, refStr := range sortedRefStr {
		entry := groupedRefs[refStr]
		if entry.Ref.HasFragmentOnly {
			continue
		}

		complete = false

		newName := opts.flattenContext.resolved[refStr]
		if newName != "" {
			if err := importKnownRef(entry, refStr, newName, opts); err != nil {
				return false, err
			}

			continue
		}

		// resolve schemas
		if err := importNewRef(entry, refStr, opts); err != nil {
			return false, err
		}
	}

	// maintains ref index entries
	for k := range opts.flattenContext.newRefs {
		r := opts.flattenContext.newRefs[k]

		// update tracking with resolved schemas
		if r.schema.Ref.String() != "" {
			ref := spec.MustCreateRef(r.path)
			sch, err := spec.ResolveRefWithBase(opts.Swagger(), &ref, opts.ExpandOpts(false))
			if err != nil {
				return false, fmt.Errorf("could not resolve schema: %w", err)
			}

			r.schema = sch
		}

		if r.path == k {
			continue
		}

		// update tracking with renamed keys: got a cascade of refs
		renamed := *r
		renamed.key = r.path
		opts.flattenContext.newRefs[renamed.path] = &renamed

		// indirect ref
		r.newName = path.Base(k)
		r.schema = spec.RefSchema(r.path)
		r.path = k
		r.isOAIGen = strings.Contains(k, "OAIGen")
	}

	return complete, nil
}

// stripPointersAndOAIGen removes anonymous JSON pointers from spec and chain with name conflicts handler.
// This loops until the spec has no such pointer and all name conflicts have been reduced as much as possible.
func stripPointersAndOAIGen(opts *FlattenOpts) error {
	// name all JSON pointers to anonymous documents
	if err := namePointers(opts); err != nil {
		return err
	}

	// remove unnecessary OAIGen ref (created when flattening external refs creates name conflicts)
	hasIntroducedPointerOrInline, ers := stripOAIGen(opts)
	if ers != nil {
		return ers
	}

	// iterate as pointer or OAIGen resolution may introduce inline schemas or pointers
	for hasIntroducedPointerOrInline {
		if !opts.Minimal {
			opts.Spec.reload() // re-analyze
			if err := nameInlinedSchemas(opts); err != nil {
				return err
			}
		}

		if err := namePointers(opts); err != nil {
			return err
		}

		// restrip and re-analyze
		var err error
		if hasIntroducedPointerOrInline, err = stripOAIGen(opts); err != nil {
			return err
		}
	}

	return nil
}

// stripOAIGen strips the spec from unnecessary OAIGen constructs, initially created to dedupe flattened definitions.
//
// A dedupe is deemed unnecessary whenever:
//  - the only conflict is with its (single) parent: OAIGen is merged into its parent (reinlining)
//  - there is a conflict with multiple parents: merge OAIGen in first parent, the rewrite other parents to point to
//    the first parent.
//
// This function returns true whenever it re-inlined a complex schema, so the caller may chose to iterate
// pointer and name resolution again.
func stripOAIGen(opts *FlattenOpts) (bool, error) {
	debugLog("stripOAIGen")
	replacedWithComplex := false

	// figure out referers of OAIGen definitions (doing it before the ref start mutating)
	for _, r := range opts.flattenContext.newRefs {
		updateRefParents(opts.Spec.references.allRefs, r)
	}

	for k := range opts.flattenContext.newRefs {
		r := opts.flattenContext.newRefs[k]
		debugLog("newRefs[%s]: isOAIGen: %t, resolved: %t, name: %s, path:%s, #parents: %d, parents: %v,  ref: %s",
			k, r.isOAIGen, r.resolved, r.newName, r.path, len(r.parents), r.parents, r.schema.Ref.String())

		if !r.isOAIGen || len(r.parents) == 0 {
			continue
		}

		hasReplacedWithComplex, err := stripOAIGenForRef(opts, k, r)
		if err != nil {
			return replacedWithComplex, err
		}

		replacedWithComplex = replacedWithComplex || hasReplacedWithComplex
	}

	debugLog("replacedWithComplex: %t", replacedWithComplex)
	opts.Spec.reload() // re-analyze

	return replacedWithComplex, nil
}

// updateRefParents updates all parents of an updated $ref
func updateRefParents(allRefs map[string]spec.Ref, r *newRef) {
	if !r.isOAIGen || r.resolved { // bail on already resolved entries (avoid looping)
		return
	}
	for k, v := range allRefs {
		if r.path != v.String() {
			continue
		}

		found := false
		for _, p := range r.parents {
			if p == k {
				found = true

				break
			}
		}
		if !found {
			r.parents = append(r.parents, k)
		}
	}
}

func stripOAIGenForRef(opts *FlattenOpts, k string, r *newRef) (bool, error) {
	replacedWithComplex := false

	pr := sortref.TopmostFirst(r.parents)

	// rewrite first parent schema in hierarchical then lexicographical order
	debugLog("rewrite first parent %s with schema", pr[0])
	if err := replace.UpdateRefWithSchema(opts.Swagger(), pr[0], r.schema); err != nil {
		return false, err
	}

	if pa, ok := opts.flattenContext.newRefs[pr[0]]; ok && pa.isOAIGen {
		// update parent in ref index entry
		debugLog("update parent entry: %s", pr[0])
		pa.schema = r.schema
		pa.resolved = false
		replacedWithComplex = true
	}

	// rewrite other parents to point to first parent
	if len(pr) > 1 {
		for _, p := range pr[1:] {
			replacingRef := spec.MustCreateRef(pr[0])

			// set complex when replacing ref is an anonymous jsonpointer: further processing may be required
			replacedWithComplex = replacedWithComplex || path.Dir(replacingRef.String()) != definitionsPath
			debugLog("rewrite parent with ref: %s", replacingRef.String())

			// NOTE: it is possible at this stage to introduce json pointers (to non-definitions places).
			// Those are stripped later on.
			if err := replace.UpdateRef(opts.Swagger(), p, replacingRef); err != nil {
				return false, err
			}

			if pa, ok := opts.flattenContext.newRefs[p]; ok && pa.isOAIGen {
				// update parent in ref index
				debugLog("update parent entry: %s", p)
				pa.schema = r.schema
				pa.resolved = false
				replacedWithComplex = true
			}
		}
	}

	// remove OAIGen definition
	debugLog("removing definition %s", path.Base(r.path))
	delete(opts.Swagger().Definitions, path.Base(r.path))

	// propagate changes in ref index for keys which have this one as a parent
	for kk, value := range opts.flattenContext.newRefs {
		if kk == k || !value.isOAIGen || value.resolved {
			continue
		}

		found := false
		newParents := make([]string, 0, len(value.parents))
		for _, parent := range value.parents {
			switch {
			case parent == r.path:
				found = true
				parent = pr[0]
			case strings.HasPrefix(parent, r.path+"/"):
				found = true
				parent = path.Join(pr[0], strings.TrimPrefix(parent, r.path))
			}

			newParents = append(newParents, parent)
		}

		if found {
			value.parents = newParents
		}
	}

	// mark naming conflict as resolved
	debugLog("marking naming conflict resolved for key: %s", r.key)
	opts.flattenContext.newRefs[r.key].isOAIGen = false
	opts.flattenContext.newRefs[r.key].resolved = true

	// determine if the previous substitution did inline a complex schema
	if r.schema != nil && r.schema.Ref.String() == "" { // inline schema
		asch, err := Schema(SchemaOpts{Schema: r.schema, Root: opts.Swagger(), BasePath: opts.BasePath})
		if err != nil {
			return false, err
		}

		debugLog("re-inlined schema: parent: %s, %t", pr[0], asch.isAnalyzedAsComplex())
		replacedWithComplex = replacedWithComplex || !(path.Dir(pr[0]) == definitionsPath) && asch.isAnalyzedAsComplex()
	}

	return replacedWithComplex, nil
}

// namePointers replaces all JSON pointers to anonymous documents by a $ref to a new named definitions.
//
// This is carried on depth-first. Pointers to $refs which are top level definitions are replaced by the $ref itself.
// Pointers to simple types are expanded, unless they express commonality (i.e. several such $ref are used).
func namePointers(opts *FlattenOpts) error {
	debugLog("name pointers")

	refsToReplace := make(map[string]SchemaRef, len(opts.Spec.references.schemas))
	for k, ref := range opts.Spec.references.allRefs {
		if path.Dir(ref.String()) == definitionsPath {
			// this a ref to a top-level definition: ok
			continue
		}

		result, err := replace.DeepestRef(opts.Swagger(), opts.ExpandOpts(false), ref)
		if err != nil {
			return fmt.Errorf("at %s, %w", k, err)
		}

		replacingRef := result.Ref
		sch := result.Schema
		if opts.flattenContext != nil {
			opts.flattenContext.warnings = append(opts.flattenContext.warnings, result.Warnings...)
		}

		debugLog("planning pointer to replace at %s: %s, resolved to: %s", k, ref.String(), replacingRef.String())
		refsToReplace[k] = SchemaRef{
			Name:     k,            // caller
			Ref:      replacingRef, // called
			Schema:   sch,
			TopLevel: path.Dir(replacingRef.String()) == definitionsPath,
		}
	}

	depthFirst := sortref.DepthFirst(refsToReplace)
	namer := &InlineSchemaNamer{
		Spec:           opts.Swagger(),
		Operations:     operations.AllOpRefsByRef(opts.Spec, nil),
		flattenContext: opts.flattenContext,
		opts:           opts,
	}

	for _, key := range depthFirst {
		v := refsToReplace[key]
		// update current replacement, which may have been updated by previous changes of deeper elements
		result, erd := replace.DeepestRef(opts.Swagger(), opts.ExpandOpts(false), v.Ref)
		if erd != nil {
			return fmt.Errorf("at %s, %w", key, erd)
		}

		if opts.flattenContext != nil {
			opts.flattenContext.warnings = append(opts.flattenContext.warnings, result.Warnings...)
		}

		v.Ref = result.Ref
		v.Schema = result.Schema
		v.TopLevel = path.Dir(result.Ref.String()) == definitionsPath
		debugLog("replacing pointer at %s: resolved to: %s", key, v.Ref.String())

		if v.TopLevel {
			debugLog("replace pointer %s by canonical definition: %s", key, v.Ref.String())

			// if the schema is a $ref to a top level definition, just rewrite the pointer to this $ref
			if err := replace.UpdateRef(opts.Swagger(), key, v.Ref); err != nil {
				return err
			}

			continue
		}

		if err := flattenAnonPointer(key, v, refsToReplace, namer, opts); err != nil {
			return err
		}
	}

	opts.Spec.reload() // re-analyze

	return nil
}

func flattenAnonPointer(key string, v SchemaRef, refsToReplace map[string]SchemaRef, namer *InlineSchemaNamer, opts *FlattenOpts) error {
	// this is a JSON pointer to an anonymous document (internal or external):
	// create a definition for this schema when:
	// - it is a complex schema
	// - or it is pointed by more than one $ref (i.e. expresses commonality)
	// otherwise, expand the pointer (single reference to a simple type)
	//
	// The named definition for this follows the target's key, not the caller's
	debugLog("namePointers at %s for %s", key, v.Ref.String())

	// qualify the expanded schema
	asch, ers := Schema(SchemaOpts{Schema: v.Schema, Root: opts.Swagger(), BasePath: opts.BasePath})
	if ers != nil {
		return fmt.Errorf("schema analysis [%s]: %w", key, ers)
	}
	callers := make([]string, 0, 64)

	debugLog("looking for callers")

	an := New(opts.Swagger())
	for k, w := range an.references.allRefs {
		r, err := replace.DeepestRef(opts.Swagger(), opts.ExpandOpts(false), w)
		if err != nil {
			return fmt.Errorf("at %s, %w", key, err)
		}

		if opts.flattenContext != nil {
			opts.flattenContext.warnings = append(opts.flattenContext.warnings, r.Warnings...)
		}

		if r.Ref.String() == v.Ref.String() {
			callers = append(callers, k)
		}
	}

	debugLog("callers for %s: %d", v.Ref.String(), len(callers))
	if len(callers) == 0 {
		// has already been updated and resolved
		return nil
	}

	parts := sortref.KeyParts(v.Ref.String())
	debugLog("number of callers for %s: %d", v.Ref.String(), len(callers))

	// identifying edge case when the namer did nothing because we point to a non-schema object
	// no definition is created and we expand the $ref for all callers
	if (!asch.IsSimpleSchema || len(callers) > 1) && !parts.IsSharedParam() && !parts.IsSharedResponse() {
		debugLog("replace JSON pointer at [%s] by definition: %s", key, v.Ref.String())
		if err := namer.Name(v.Ref.String(), v.Schema, asch); err != nil {
			return err
		}

		// regular case: we named the $ref as a definition, and we move all callers to this new $ref
		for _, caller := range callers {
			if caller == key {
				continue
			}

			// move $ref for next to resolve
			debugLog("identified caller of %s at [%s]", v.Ref.String(), caller)
			c := refsToReplace[caller]
			c.Ref = v.Ref
			refsToReplace[caller] = c
		}

		return nil
	}

	debugLog("expand JSON pointer for key=%s", key)

	if err := replace.UpdateRefWithSchema(opts.Swagger(), key, v.Schema); err != nil {
		return err
	}
	// NOTE: there is no other caller to update

	return nil
}