podman

Форк
0
802 строки · 24.1 Кб
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

15
package analysis
16

17
import (
18
	"fmt"
19
	"log"
20
	"path"
21
	"sort"
22
	"strings"
23

24
	"github.com/go-openapi/analysis/internal/flatten/normalize"
25
	"github.com/go-openapi/analysis/internal/flatten/operations"
26
	"github.com/go-openapi/analysis/internal/flatten/replace"
27
	"github.com/go-openapi/analysis/internal/flatten/schutils"
28
	"github.com/go-openapi/analysis/internal/flatten/sortref"
29
	"github.com/go-openapi/jsonpointer"
30
	"github.com/go-openapi/spec"
31
)
32

33
const definitionsPath = "#/definitions"
34

35
// newRef stores information about refs created during the flattening process
36
type newRef struct {
37
	key      string
38
	newName  string
39
	path     string
40
	isOAIGen bool
41
	resolved bool
42
	schema   *spec.Schema
43
	parents  []string
44
}
45

46
// context stores intermediary results from flatten
47
type context struct {
48
	newRefs  map[string]*newRef
49
	warnings []string
50
	resolved map[string]string
51
}
52

53
func newContext() *context {
54
	return &context{
55
		newRefs:  make(map[string]*newRef, 150),
56
		warnings: make([]string, 0),
57
		resolved: make(map[string]string, 50),
58
	}
59
}
60

61
// Flatten an analyzed spec and produce a self-contained spec bundle.
62
//
63
// There is a minimal and a full flattening mode.
64
//
65
//
66
// Minimally flattening a spec means:
67
//  - Expanding parameters, responses, path items, parameter items and header items (references to schemas are left
68
//    unscathed)
69
//  - Importing external (http, file) references so they become internal to the document
70
//  - Moving every JSON pointer to a $ref to a named definition (i.e. the reworked spec does not contain pointers
71
//    like "$ref": "#/definitions/myObject/allOfs/1")
72
//
73
// A minimally flattened spec thus guarantees the following properties:
74
//  - all $refs point to a local definition (i.e. '#/definitions/...')
75
//  - definitions are unique
76
//
77
// NOTE: arbitrary JSON pointers (other than $refs to top level definitions) are rewritten as definitions if they
78
// represent a complex schema or express commonality in the spec.
79
// Otherwise, they are simply expanded.
80
// Self-referencing JSON pointers cannot resolve to a type and trigger an error.
81
//
82
//
83
// Minimal flattening is necessary and sufficient for codegen rendering using go-swagger.
84
//
85
// Fully flattening a spec means:
86
//  - Moving every complex inline schema to be a definition with an auto-generated name in a depth-first fashion.
87
//
88
// By complex, we mean every JSON object with some properties.
89
// Arrays, when they do not define a tuple,
90
// or empty objects with or without additionalProperties, are not considered complex and remain inline.
91
//
92
// NOTE: rewritten schemas get a vendor extension x-go-gen-location so we know from which part of the spec definitions
93
// have been created.
94
//
95
// Available flattening options:
96
//  - Minimal: stops flattening after minimal $ref processing, leaving schema constructs untouched
97
//  - Expand: expand all $ref's in the document (inoperant if Minimal set to true)
98
//  - Verbose: croaks about name conflicts detected
99
//  - RemoveUnused: removes unused parameters, responses and definitions after expansion/flattening
100
//
101
// NOTE: expansion removes all $ref save circular $ref, which remain in place
102
//
103
// TODO: additional options
104
//  - ProgagateNameExtensions: ensure that created entries properly follow naming rules when their parent have set a
105
//    x-go-name extension
106
//  - LiftAllOfs:
107
//     - limit the flattening of allOf members when simple objects
108
//     - merge allOf with validation only
109
//     - merge allOf with extensions only
110
//     - ...
111
//
112
func Flatten(opts FlattenOpts) error {
113
	debugLog("FlattenOpts: %#v", opts)
114

115
	opts.flattenContext = newContext()
116

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

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

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

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

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

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

157
	// 7. Strip the spec from unused definitions
158
	if opts.RemoveUnused {
159
		removeUnused(&opts)
160
	}
161

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

165
	// TODO: simplify known schema patterns to flat objects with properties
166
	// examples:
167
	//  - lift simple allOf object,
168
	//  - empty allOf with validation only or extensions only
169
	//  - rework allOf arrays
170
	//  - rework allOf additionalProperties
171

172
	return nil
173
}
174

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

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

182
	return nil
183
}
184

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

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

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

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

206
	if altered {
207
		opts.Spec.reload() // re-analyze
208
	}
209

210
	return nil
211
}
212

213
func removeUnusedShared(opts *FlattenOpts) {
214
	opts.Swagger().Parameters = nil
215
	opts.Swagger().Responses = nil
216

217
	opts.Spec.reload() // re-analyze
218
}
219

220
func importReferences(opts *FlattenOpts) error {
221
	var (
222
		imported bool
223
		err      error
224
	)
225

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

231
		opts.Spec.reload() // re-analyze
232
	}
233

234
	return err
235
}
236

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

241
	namer := &InlineSchemaNamer{
242
		Spec:           opts.Swagger(),
243
		Operations:     operations.AllOpRefsByRef(opts.Spec, nil),
244
		flattenContext: opts.flattenContext,
245
		opts:           opts,
246
	}
247

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

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

260
		if asch.isAnalyzedAsComplex() { // move complex schemas to definitions
261
			if err := namer.Name(key, sch.Schema, asch); err != nil {
262
				return err
263
			}
264
		}
265
	}
266

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

269
	return nil
270
}
271

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

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

282
	for k := range expected {
283
		debugLog("removing unused definition %s", path.Base(k))
284
		if opts.Verbose {
285
			log.Printf("info: removing unused definition: %s", path.Base(k))
286
		}
287
		delete(opts.Swagger().Definitions, path.Base(k))
288
	}
289

290
	opts.Spec.reload() // re-analyze
291
}
292

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

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

304
	return nil
305
}
306

307
func importNewRef(entry sortref.RefRevIdx, refStr string, opts *FlattenOpts) error {
308
	var (
309
		isOAIGen bool
310
		newName  string
311
	)
312

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

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

320
	// at this stage only $ref analysis matters
321
	partialAnalyzer := &Spec{
322
		references: referenceAnalysis{},
323
		patterns:   patternAnalysis{},
324
		enums:      enumAnalysis{},
325
	}
326
	partialAnalyzer.reset()
327
	partialAnalyzer.analyzeSchema("", sch, "/")
328

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

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

340
	opts.flattenContext.resolved[refStr] = newName
341

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

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

355
		debugLog("keeping track of ref: %s (%s), resolved: %t", key, newName, resolved)
356
		opts.flattenContext.newRefs[key] = &newRef{
357
			key:      key,
358
			newName:  newName,
359
			path:     path.Join(definitionsPath, newName),
360
			isOAIGen: isOAIGen,
361
			resolved: resolved,
362
			schema:   sch,
363
		}
364
	}
365

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

369
	return nil
370
}
371

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

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

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

392
	complete := true
393

394
	for _, refStr := range sortedRefStr {
395
		entry := groupedRefs[refStr]
396
		if entry.Ref.HasFragmentOnly {
397
			continue
398
		}
399

400
		complete = false
401

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

408
			continue
409
		}
410

411
		// resolve schemas
412
		if err := importNewRef(entry, refStr, opts); err != nil {
413
			return false, err
414
		}
415
	}
416

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

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

429
			r.schema = sch
430
		}
431

432
		if r.path == k {
433
			continue
434
		}
435

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

441
		// indirect ref
442
		r.newName = path.Base(k)
443
		r.schema = spec.RefSchema(r.path)
444
		r.path = k
445
		r.isOAIGen = strings.Contains(k, "OAIGen")
446
	}
447

448
	return complete, nil
449
}
450

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

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

465
	// iterate as pointer or OAIGen resolution may introduce inline schemas or pointers
466
	for hasIntroducedPointerOrInline {
467
		if !opts.Minimal {
468
			opts.Spec.reload() // re-analyze
469
			if err := nameInlinedSchemas(opts); err != nil {
470
				return err
471
			}
472
		}
473

474
		if err := namePointers(opts); err != nil {
475
			return err
476
		}
477

478
		// restrip and re-analyze
479
		var err error
480
		if hasIntroducedPointerOrInline, err = stripOAIGen(opts); err != nil {
481
			return err
482
		}
483
	}
484

485
	return nil
486
}
487

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

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

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

511
		if !r.isOAIGen || len(r.parents) == 0 {
512
			continue
513
		}
514

515
		hasReplacedWithComplex, err := stripOAIGenForRef(opts, k, r)
516
		if err != nil {
517
			return replacedWithComplex, err
518
		}
519

520
		replacedWithComplex = replacedWithComplex || hasReplacedWithComplex
521
	}
522

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

526
	return replacedWithComplex, nil
527
}
528

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

539
		found := false
540
		for _, p := range r.parents {
541
			if p == k {
542
				found = true
543

544
				break
545
			}
546
		}
547
		if !found {
548
			r.parents = append(r.parents, k)
549
		}
550
	}
551
}
552

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

556
	pr := sortref.TopmostFirst(r.parents)
557

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

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

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

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

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

587
			if pa, ok := opts.flattenContext.newRefs[p]; ok && pa.isOAIGen {
588
				// update parent in ref index
589
				debugLog("update parent entry: %s", p)
590
				pa.schema = r.schema
591
				pa.resolved = false
592
				replacedWithComplex = true
593
			}
594
		}
595
	}
596

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

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

607
		found := false
608
		newParents := make([]string, 0, len(value.parents))
609
		for _, parent := range value.parents {
610
			switch {
611
			case parent == r.path:
612
				found = true
613
				parent = pr[0]
614
			case strings.HasPrefix(parent, r.path+"/"):
615
				found = true
616
				parent = path.Join(pr[0], strings.TrimPrefix(parent, r.path))
617
			}
618

619
			newParents = append(newParents, parent)
620
		}
621

622
		if found {
623
			value.parents = newParents
624
		}
625
	}
626

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

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

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

643
	return replacedWithComplex, nil
644
}
645

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

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

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

665
		replacingRef := result.Ref
666
		sch := result.Schema
667
		if opts.flattenContext != nil {
668
			opts.flattenContext.warnings = append(opts.flattenContext.warnings, result.Warnings...)
669
		}
670

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

680
	depthFirst := sortref.DepthFirst(refsToReplace)
681
	namer := &InlineSchemaNamer{
682
		Spec:           opts.Swagger(),
683
		Operations:     operations.AllOpRefsByRef(opts.Spec, nil),
684
		flattenContext: opts.flattenContext,
685
		opts:           opts,
686
	}
687

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

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

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

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

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

713
			continue
714
		}
715

716
		if err := flattenAnonPointer(key, v, refsToReplace, namer, opts); err != nil {
717
			return err
718
		}
719
	}
720

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

723
	return nil
724
}
725

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

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

743
	debugLog("looking for callers")
744

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

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

756
		if r.Ref.String() == v.Ref.String() {
757
			callers = append(callers, k)
758
		}
759
	}
760

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

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

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

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

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

791
		return nil
792
	}
793

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

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

801
	return nil
802
}
803

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.