mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 03:18:24 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			1758 lines
		
	
	
		
			52 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
			
		
		
	
	
			1758 lines
		
	
	
		
			52 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
| // 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"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	slashpath "path"
 | |
| 	"path/filepath"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| 
 | |
| 	"strconv"
 | |
| 
 | |
| 	"github.com/go-openapi/analysis/internal"
 | |
| 	"github.com/go-openapi/jsonpointer"
 | |
| 	swspec "github.com/go-openapi/spec"
 | |
| 	"github.com/go-openapi/swag"
 | |
| )
 | |
| 
 | |
| // FlattenOpts configuration for flattening a swagger specification.
 | |
| type FlattenOpts struct {
 | |
| 	Spec           *Spec    // The analyzed spec to work with
 | |
| 	flattenContext *context // Internal context to track flattening activity
 | |
| 
 | |
| 	BasePath string
 | |
| 
 | |
| 	// Flattening options
 | |
| 	Expand          bool // If Expand is true, we skip flattening the spec and expand it instead
 | |
| 	Minimal         bool
 | |
| 	Verbose         bool
 | |
| 	RemoveUnused    bool
 | |
| 	ContinueOnError bool // Continues when facing some issues
 | |
| 
 | |
| 	/* Extra keys */
 | |
| 	_ struct{} // require keys
 | |
| }
 | |
| 
 | |
| // ExpandOpts creates a spec.ExpandOptions to configure expanding a specification document.
 | |
| func (f *FlattenOpts) ExpandOpts(skipSchemas bool) *swspec.ExpandOptions {
 | |
| 	return &swspec.ExpandOptions{RelativeBase: f.BasePath, SkipSchemas: skipSchemas}
 | |
| }
 | |
| 
 | |
| // Swagger gets the swagger specification for this flatten operation
 | |
| func (f *FlattenOpts) Swagger() *swspec.Swagger {
 | |
| 	return f.Spec.spec
 | |
| }
 | |
| 
 | |
| // newRef stores information about refs created during the flattening process
 | |
| type newRef struct {
 | |
| 	key      string
 | |
| 	newName  string
 | |
| 	path     string
 | |
| 	isOAIGen bool
 | |
| 	resolved bool
 | |
| 	schema   *swspec.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.
 | |
| //
 | |
| // 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)
 | |
| 	// Make sure opts.BasePath is an absolute path
 | |
| 	if !filepath.IsAbs(opts.BasePath) {
 | |
| 		cwd, _ := os.Getwd()
 | |
| 		opts.BasePath = filepath.Join(cwd, opts.BasePath)
 | |
| 	}
 | |
| 	// make sure drive letter on windows is normalized to lower case
 | |
| 	u, _ := url.Parse(opts.BasePath)
 | |
| 	opts.BasePath = u.String()
 | |
| 
 | |
| 	opts.flattenContext = newContext()
 | |
| 
 | |
| 	// recursively expand responses, parameters, path items and items in simple schemas.
 | |
| 	// This simplifies the spec and leaves $ref only into schema objects.
 | |
| 	expandOpts := opts.ExpandOpts(!opts.Expand)
 | |
| 	expandOpts.ContinueOnError = opts.ContinueOnError
 | |
| 	if err := swspec.ExpandSpec(opts.Swagger(), expandOpts); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// strip current file from $ref's, so we can recognize them as proper definitions
 | |
| 	// In particular, this works around for issue go-openapi/spec#76: leading absolute file in $ref is stripped
 | |
| 	if err := normalizeRef(&opts); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if opts.RemoveUnused {
 | |
| 		// optionally removes shared parameters and responses already expanded (now unused)
 | |
| 		// default parameters (i.e. under paths) remain.
 | |
| 		opts.Swagger().Parameters = nil
 | |
| 		opts.Swagger().Responses = nil
 | |
| 	}
 | |
| 
 | |
| 	opts.Spec.reload() // re-analyze
 | |
| 
 | |
| 	// at this point there are no references left but in schemas
 | |
| 
 | |
| 	for imported := false; !imported; {
 | |
| 		// iteratively import remote references until none left.
 | |
| 		// This inlining deals with name conflicts by introducing auto-generated names ("OAIGen")
 | |
| 		var err error
 | |
| 		if imported, err = importExternalReferences(&opts); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		opts.Spec.reload() // re-analyze
 | |
| 	}
 | |
| 
 | |
| 	if !opts.Minimal && !opts.Expand {
 | |
| 		// full flattening: rewrite inline schemas (schemas that aren't simple types or arrays or maps)
 | |
| 		if err := nameInlinedSchemas(&opts); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		opts.Spec.reload() // re-analyze
 | |
| 	}
 | |
| 
 | |
| 	// 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
 | |
| 	}
 | |
| 
 | |
| 	if opts.RemoveUnused {
 | |
| 		// remove unused definitions
 | |
| 		expected := make(map[string]struct{})
 | |
| 		for k := range opts.Swagger().Definitions {
 | |
| 			expected[slashpath.Join(definitionsPath, jsonpointer.Escape(k))] = struct{}{}
 | |
| 		}
 | |
| 		for _, k := range opts.Spec.AllDefinitionReferences() {
 | |
| 			delete(expected, k)
 | |
| 		}
 | |
| 		for k := range expected {
 | |
| 			debugLog("removing unused definition %s", slashpath.Base(k))
 | |
| 			if opts.Verbose {
 | |
| 				log.Printf("info: removing unused definition: %s", slashpath.Base(k))
 | |
| 			}
 | |
| 			delete(opts.Swagger().Definitions, slashpath.Base(k))
 | |
| 		}
 | |
| 		opts.Spec.reload() // re-analyze
 | |
| 	}
 | |
| 
 | |
| 	// 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
 | |
| 
 | |
| 	if opts.Verbose {
 | |
| 		// issue notifications
 | |
| 		croak(&opts)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // isAnalyzedAsComplex determines if an analyzed schema is eligible to flattening (i.e. it is "complex").
 | |
| //
 | |
| // Complex means the schema is any of:
 | |
| //  - a simple type (primitive)
 | |
| //  - an array of something (items are possibly complex ; if this is the case, items will generate a definition)
 | |
| //  - a map of something (additionalProperties are possibly complex ; if this is the case, additionalProperties will
 | |
| //    generate a definition)
 | |
| func isAnalyzedAsComplex(asch *AnalyzedSchema) bool {
 | |
| 	if !asch.IsSimpleSchema && !asch.IsArray && !asch.IsMap {
 | |
| 		return true
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // nameInlinedSchemas replaces every complex inline construct by a named definition.
 | |
| func nameInlinedSchemas(opts *FlattenOpts) error {
 | |
| 	debugLog("nameInlinedSchemas")
 | |
| 	namer := &inlineSchemaNamer{
 | |
| 		Spec:           opts.Swagger(),
 | |
| 		Operations:     opRefsByRef(gatherOperations(opts.Spec, nil)),
 | |
| 		flattenContext: opts.flattenContext,
 | |
| 		opts:           opts,
 | |
| 	}
 | |
| 	depthFirst := sortDepthFirst(opts.Spec.allSchemas)
 | |
| 	for _, key := range depthFirst {
 | |
| 		sch := opts.Spec.allSchemas[key]
 | |
| 		if sch.Schema != nil && sch.Schema.Ref.String() == "" && !sch.TopLevel { // inline schema
 | |
| 			asch, err := Schema(SchemaOpts{Schema: sch.Schema, Root: opts.Swagger(), BasePath: opts.BasePath})
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("schema analysis [%s]: %v", key, err)
 | |
| 			}
 | |
| 
 | |
| 			if isAnalyzedAsComplex(asch) { // move complex schemas to definitions
 | |
| 				if err := namer.Name(key, sch.Schema, asch); err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| var depthGroupOrder = []string{
 | |
| 	"sharedParam", "sharedResponse", "sharedOpParam", "opParam", "codeResponse", "defaultResponse", "definition",
 | |
| }
 | |
| 
 | |
| func sortDepthFirst(data map[string]SchemaRef) []string {
 | |
| 	// group by category (shared params, op param, statuscode response, default response, definitions)
 | |
| 	// sort groups internally by number of parts in the key and lexical names
 | |
| 	// flatten groups into a single list of keys
 | |
| 	sorted := make([]string, 0, len(data))
 | |
| 	grouped := make(map[string]keys, len(data))
 | |
| 	for k := range data {
 | |
| 		split := keyParts(k)
 | |
| 		var pk string
 | |
| 		if split.IsSharedOperationParam() {
 | |
| 			pk = "sharedOpParam"
 | |
| 		}
 | |
| 		if split.IsOperationParam() {
 | |
| 			pk = "opParam"
 | |
| 		}
 | |
| 		if split.IsStatusCodeResponse() {
 | |
| 			pk = "codeResponse"
 | |
| 		}
 | |
| 		if split.IsDefaultResponse() {
 | |
| 			pk = "defaultResponse"
 | |
| 		}
 | |
| 		if split.IsDefinition() {
 | |
| 			pk = "definition"
 | |
| 		}
 | |
| 		if split.IsSharedParam() {
 | |
| 			pk = "sharedParam"
 | |
| 		}
 | |
| 		if split.IsSharedResponse() {
 | |
| 			pk = "sharedResponse"
 | |
| 		}
 | |
| 		grouped[pk] = append(grouped[pk], key{Segments: len(split), Key: k})
 | |
| 	}
 | |
| 
 | |
| 	for _, pk := range depthGroupOrder {
 | |
| 		res := grouped[pk]
 | |
| 		sort.Sort(res)
 | |
| 		for _, v := range res {
 | |
| 			sorted = append(sorted, v.Key)
 | |
| 		}
 | |
| 	}
 | |
| 	return sorted
 | |
| }
 | |
| 
 | |
| type key struct {
 | |
| 	Segments int
 | |
| 	Key      string
 | |
| }
 | |
| type keys []key
 | |
| 
 | |
| func (k keys) Len() int      { return len(k) }
 | |
| func (k keys) Swap(i, j int) { k[i], k[j] = k[j], k[i] }
 | |
| func (k keys) Less(i, j int) bool {
 | |
| 	return k[i].Segments > k[j].Segments || (k[i].Segments == k[j].Segments && k[i].Key < k[j].Key)
 | |
| }
 | |
| 
 | |
| type inlineSchemaNamer struct {
 | |
| 	Spec           *swspec.Swagger
 | |
| 	Operations     map[string]opRef
 | |
| 	flattenContext *context
 | |
| 	opts           *FlattenOpts
 | |
| }
 | |
| 
 | |
| func opRefsByRef(oprefs map[string]opRef) map[string]opRef {
 | |
| 	result := make(map[string]opRef, len(oprefs))
 | |
| 	for _, v := range oprefs {
 | |
| 		result[v.Ref.String()] = v
 | |
| 	}
 | |
| 	return result
 | |
| }
 | |
| 
 | |
| func (isn *inlineSchemaNamer) Name(key string, schema *swspec.Schema, aschema *AnalyzedSchema) error {
 | |
| 	debugLog("naming inlined schema at %s", key)
 | |
| 
 | |
| 	parts := keyParts(key)
 | |
| 	for _, name := range namesFromKey(parts, aschema, isn.Operations) {
 | |
| 		if name != "" {
 | |
| 			// create unique name
 | |
| 			newName, isOAIGen := uniqifyName(isn.Spec.Definitions, swag.ToJSONName(name))
 | |
| 
 | |
| 			// clone schema
 | |
| 			sch, err := cloneSchema(schema)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			// replace values on schema
 | |
| 			if err := rewriteSchemaToRef(isn.Spec, key,
 | |
| 				swspec.MustCreateRef(slashpath.Join(definitionsPath, newName))); err != nil {
 | |
| 				return fmt.Errorf("error while creating definition %q from inline schema: %v", newName, err)
 | |
| 			}
 | |
| 
 | |
| 			// rewrite any dependent $ref pointing to this place,
 | |
| 			// when not already pointing to a top-level definition.
 | |
| 			//
 | |
| 			// NOTE: this is important if such referers use arbitrary JSON pointers.
 | |
| 			an := New(isn.Spec)
 | |
| 			for k, v := range an.references.allRefs {
 | |
| 				r, _, erd := deepestRef(isn.opts, v)
 | |
| 				if erd != nil {
 | |
| 					return fmt.Errorf("at %s, %v", k, erd)
 | |
| 				}
 | |
| 				if r.String() == key ||
 | |
| 					r.String() == slashpath.Join(definitionsPath, newName) &&
 | |
| 						slashpath.Dir(v.String()) != definitionsPath {
 | |
| 					debugLog("found a $ref to a rewritten schema: %s points to %s", k, v.String())
 | |
| 
 | |
| 					// rewrite $ref to the new target
 | |
| 					if err := updateRef(isn.Spec, k,
 | |
| 						swspec.MustCreateRef(slashpath.Join(definitionsPath, newName))); err != nil {
 | |
| 						return err
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			// NOTE: this extension is currently not used by go-swagger (provided for information only)
 | |
| 			sch.AddExtension("x-go-gen-location", genLocation(parts))
 | |
| 
 | |
| 			// save cloned schema to definitions
 | |
| 			saveSchema(isn.Spec, newName, sch)
 | |
| 
 | |
| 			// keep track of created refs
 | |
| 			if isn.flattenContext != nil {
 | |
| 				debugLog("track created ref: key=%s, newName=%s, isOAIGen=%t", key, newName, isOAIGen)
 | |
| 				resolved := false
 | |
| 				if _, ok := isn.flattenContext.newRefs[key]; ok {
 | |
| 					resolved = isn.flattenContext.newRefs[key].resolved
 | |
| 				}
 | |
| 				isn.flattenContext.newRefs[key] = &newRef{
 | |
| 					key:      key,
 | |
| 					newName:  newName,
 | |
| 					path:     slashpath.Join(definitionsPath, newName),
 | |
| 					isOAIGen: isOAIGen,
 | |
| 					resolved: resolved,
 | |
| 					schema:   sch,
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // genLocation indicates from which section of the specification (models or operations) a definition has been created.
 | |
| //
 | |
| // This is reflected in the output spec with a "x-go-gen-location" extension. At the moment, this is is provided
 | |
| // for information only.
 | |
| func genLocation(parts splitKey) string {
 | |
| 	if parts.IsOperation() {
 | |
| 		return "operations"
 | |
| 	}
 | |
| 	if parts.IsDefinition() {
 | |
| 		return "models"
 | |
| 	}
 | |
| 	return ""
 | |
| }
 | |
| 
 | |
| // uniqifyName yields a unique name for a definition
 | |
| func uniqifyName(definitions swspec.Definitions, name string) (string, bool) {
 | |
| 	isOAIGen := false
 | |
| 	if name == "" {
 | |
| 		name = "oaiGen"
 | |
| 		isOAIGen = true
 | |
| 	}
 | |
| 	if len(definitions) == 0 {
 | |
| 		return name, isOAIGen
 | |
| 	}
 | |
| 
 | |
| 	unq := true
 | |
| 	for k := range definitions {
 | |
| 		if strings.EqualFold(k, name) {
 | |
| 			unq = false
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if unq {
 | |
| 		return name, isOAIGen
 | |
| 	}
 | |
| 
 | |
| 	name += "OAIGen"
 | |
| 	isOAIGen = true
 | |
| 	var idx int
 | |
| 	unique := name
 | |
| 	_, known := definitions[unique]
 | |
| 	for known {
 | |
| 		idx++
 | |
| 		unique = fmt.Sprintf("%s%d", name, idx)
 | |
| 		_, known = definitions[unique]
 | |
| 	}
 | |
| 	return unique, isOAIGen
 | |
| }
 | |
| 
 | |
| func namesFromKey(parts splitKey, aschema *AnalyzedSchema, operations map[string]opRef) []string {
 | |
| 	var baseNames [][]string
 | |
| 	var startIndex int
 | |
| 	if parts.IsOperation() {
 | |
| 		// params
 | |
| 		if parts.IsOperationParam() || parts.IsSharedOperationParam() {
 | |
| 			piref := parts.PathItemRef()
 | |
| 			if piref.String() != "" && parts.IsOperationParam() {
 | |
| 				if op, ok := operations[piref.String()]; ok {
 | |
| 					startIndex = 5
 | |
| 					baseNames = append(baseNames, []string{op.ID, "params", "body"})
 | |
| 				}
 | |
| 			} else if parts.IsSharedOperationParam() {
 | |
| 				pref := parts.PathRef()
 | |
| 				for k, v := range operations {
 | |
| 					if strings.HasPrefix(k, pref.String()) {
 | |
| 						startIndex = 4
 | |
| 						baseNames = append(baseNames, []string{v.ID, "params", "body"})
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		// responses
 | |
| 		if parts.IsOperationResponse() {
 | |
| 			piref := parts.PathItemRef()
 | |
| 			if piref.String() != "" {
 | |
| 				if op, ok := operations[piref.String()]; ok {
 | |
| 					startIndex = 6
 | |
| 					baseNames = append(baseNames, []string{op.ID, parts.ResponseName(), "body"})
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// definitions
 | |
| 	if parts.IsDefinition() {
 | |
| 		nm := parts.DefinitionName()
 | |
| 		if nm != "" {
 | |
| 			startIndex = 2
 | |
| 			baseNames = append(baseNames, []string{parts.DefinitionName()})
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	var result []string
 | |
| 	for _, segments := range baseNames {
 | |
| 		nm := parts.BuildName(segments, startIndex, aschema)
 | |
| 		if nm != "" {
 | |
| 			result = append(result, nm)
 | |
| 		}
 | |
| 	}
 | |
| 	sort.Strings(result)
 | |
| 	return result
 | |
| }
 | |
| 
 | |
| const (
 | |
| 	paths           = "paths"
 | |
| 	responses       = "responses"
 | |
| 	parameters      = "parameters"
 | |
| 	definitions     = "definitions"
 | |
| 	definitionsPath = "#/definitions"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	ignoredKeys  map[string]struct{}
 | |
| 	validMethods map[string]struct{}
 | |
| )
 | |
| 
 | |
| func init() {
 | |
| 	ignoredKeys = map[string]struct{}{
 | |
| 		"schema":     {},
 | |
| 		"properties": {},
 | |
| 		"not":        {},
 | |
| 		"anyOf":      {},
 | |
| 		"oneOf":      {},
 | |
| 	}
 | |
| 
 | |
| 	validMethods = map[string]struct{}{
 | |
| 		"GET":     {},
 | |
| 		"HEAD":    {},
 | |
| 		"OPTIONS": {},
 | |
| 		"PATCH":   {},
 | |
| 		"POST":    {},
 | |
| 		"PUT":     {},
 | |
| 		"DELETE":  {},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type splitKey []string
 | |
| 
 | |
| func (s splitKey) IsDefinition() bool {
 | |
| 	return len(s) > 1 && s[0] == definitions
 | |
| }
 | |
| 
 | |
| func (s splitKey) DefinitionName() string {
 | |
| 	if !s.IsDefinition() {
 | |
| 		return ""
 | |
| 	}
 | |
| 	return s[1]
 | |
| }
 | |
| 
 | |
| func (s splitKey) isKeyName(i int) bool {
 | |
| 	if i <= 0 {
 | |
| 		return false
 | |
| 	}
 | |
| 	count := 0
 | |
| 	for idx := i - 1; idx > 0; idx-- {
 | |
| 		if s[idx] != "properties" {
 | |
| 			break
 | |
| 		}
 | |
| 		count++
 | |
| 	}
 | |
| 
 | |
| 	return count%2 != 0
 | |
| }
 | |
| 
 | |
| func (s splitKey) BuildName(segments []string, startIndex int, aschema *AnalyzedSchema) string {
 | |
| 	for i, part := range s[startIndex:] {
 | |
| 		if _, ignored := ignoredKeys[part]; !ignored || s.isKeyName(startIndex+i) {
 | |
| 			if part == "items" || part == "additionalItems" {
 | |
| 				if aschema.IsTuple || aschema.IsTupleWithExtra {
 | |
| 					segments = append(segments, "tuple")
 | |
| 				} else {
 | |
| 					segments = append(segments, "items")
 | |
| 				}
 | |
| 				if part == "additionalItems" {
 | |
| 					segments = append(segments, part)
 | |
| 				}
 | |
| 				continue
 | |
| 			}
 | |
| 			segments = append(segments, part)
 | |
| 		}
 | |
| 	}
 | |
| 	return strings.Join(segments, " ")
 | |
| }
 | |
| 
 | |
| func (s splitKey) IsOperation() bool {
 | |
| 	return len(s) > 1 && s[0] == paths
 | |
| }
 | |
| 
 | |
| func (s splitKey) IsSharedOperationParam() bool {
 | |
| 	return len(s) > 2 && s[0] == paths && s[2] == parameters
 | |
| }
 | |
| 
 | |
| func (s splitKey) IsSharedParam() bool {
 | |
| 	return len(s) > 1 && s[0] == parameters
 | |
| }
 | |
| 
 | |
| func (s splitKey) IsOperationParam() bool {
 | |
| 	return len(s) > 3 && s[0] == paths && s[3] == parameters
 | |
| }
 | |
| 
 | |
| func (s splitKey) IsOperationResponse() bool {
 | |
| 	return len(s) > 3 && s[0] == paths && s[3] == responses
 | |
| }
 | |
| 
 | |
| func (s splitKey) IsSharedResponse() bool {
 | |
| 	return len(s) > 1 && s[0] == responses
 | |
| }
 | |
| 
 | |
| func (s splitKey) IsDefaultResponse() bool {
 | |
| 	return len(s) > 4 && s[0] == paths && s[3] == responses && s[4] == "default"
 | |
| }
 | |
| 
 | |
| func (s splitKey) IsStatusCodeResponse() bool {
 | |
| 	isInt := func() bool {
 | |
| 		_, err := strconv.Atoi(s[4])
 | |
| 		return err == nil
 | |
| 	}
 | |
| 	return len(s) > 4 && s[0] == paths && s[3] == responses && isInt()
 | |
| }
 | |
| 
 | |
| func (s splitKey) ResponseName() string {
 | |
| 	if s.IsStatusCodeResponse() {
 | |
| 		code, _ := strconv.Atoi(s[4])
 | |
| 		return http.StatusText(code)
 | |
| 	}
 | |
| 	if s.IsDefaultResponse() {
 | |
| 		return "Default"
 | |
| 	}
 | |
| 	return ""
 | |
| }
 | |
| 
 | |
| func (s splitKey) PathItemRef() swspec.Ref {
 | |
| 	if len(s) < 3 {
 | |
| 		return swspec.Ref{}
 | |
| 	}
 | |
| 	pth, method := s[1], s[2]
 | |
| 	if _, isValidMethod := validMethods[strings.ToUpper(method)]; !isValidMethod && !strings.HasPrefix(method, "x-") {
 | |
| 		return swspec.Ref{}
 | |
| 	}
 | |
| 	return swspec.MustCreateRef("#" + slashpath.Join("/", paths, jsonpointer.Escape(pth), strings.ToUpper(method)))
 | |
| }
 | |
| 
 | |
| func (s splitKey) PathRef() swspec.Ref {
 | |
| 	if !s.IsOperation() {
 | |
| 		return swspec.Ref{}
 | |
| 	}
 | |
| 	return swspec.MustCreateRef("#" + slashpath.Join("/", paths, jsonpointer.Escape(s[1])))
 | |
| }
 | |
| 
 | |
| func keyParts(key string) splitKey {
 | |
| 	var res []string
 | |
| 	for _, part := range strings.Split(key[1:], "/") {
 | |
| 		if part != "" {
 | |
| 			res = append(res, jsonpointer.Unescape(part))
 | |
| 		}
 | |
| 	}
 | |
| 	return res
 | |
| }
 | |
| 
 | |
| func rewriteSchemaToRef(spec *swspec.Swagger, key string, ref swspec.Ref) error {
 | |
| 	debugLog("rewriting schema to ref for %s with %s", key, ref.String())
 | |
| 	_, value, err := getPointerFromKey(spec, key)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	switch refable := value.(type) {
 | |
| 	case *swspec.Schema:
 | |
| 		return rewriteParentRef(spec, key, ref)
 | |
| 
 | |
| 	case swspec.Schema:
 | |
| 		return rewriteParentRef(spec, key, ref)
 | |
| 
 | |
| 	case *swspec.SchemaOrArray:
 | |
| 		if refable.Schema != nil {
 | |
| 			refable.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
 | |
| 		}
 | |
| 
 | |
| 	case *swspec.SchemaOrBool:
 | |
| 		if refable.Schema != nil {
 | |
| 			refable.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
 | |
| 		}
 | |
| 	default:
 | |
| 		return fmt.Errorf("no schema with ref found at %s for %T", key, value)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func rewriteParentRef(spec *swspec.Swagger, key string, ref swspec.Ref) error {
 | |
| 	parent, entry, pvalue, err := getParentFromKey(spec, key)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	debugLog("rewriting holder for %T", pvalue)
 | |
| 	switch container := pvalue.(type) {
 | |
| 	case swspec.Response:
 | |
| 		if err := rewriteParentRef(spec, "#"+parent, ref); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 	case *swspec.Response:
 | |
| 		container.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
 | |
| 
 | |
| 	case *swspec.Responses:
 | |
| 		statusCode, err := strconv.Atoi(entry)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("%s not a number: %v", key[1:], err)
 | |
| 		}
 | |
| 		resp := container.StatusCodeResponses[statusCode]
 | |
| 		resp.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
 | |
| 		container.StatusCodeResponses[statusCode] = resp
 | |
| 
 | |
| 	case map[string]swspec.Response:
 | |
| 		resp := container[entry]
 | |
| 		resp.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
 | |
| 		container[entry] = resp
 | |
| 
 | |
| 	case swspec.Parameter:
 | |
| 		if err := rewriteParentRef(spec, "#"+parent, ref); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 	case map[string]swspec.Parameter:
 | |
| 		param := container[entry]
 | |
| 		param.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
 | |
| 		container[entry] = param
 | |
| 
 | |
| 	case []swspec.Parameter:
 | |
| 		idx, err := strconv.Atoi(entry)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("%s not a number: %v", key[1:], err)
 | |
| 		}
 | |
| 		param := container[idx]
 | |
| 		param.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
 | |
| 		container[idx] = param
 | |
| 
 | |
| 	case swspec.Definitions:
 | |
| 		container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
 | |
| 
 | |
| 	case map[string]swspec.Schema:
 | |
| 		container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
 | |
| 
 | |
| 	case []swspec.Schema:
 | |
| 		idx, err := strconv.Atoi(entry)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("%s not a number: %v", key[1:], err)
 | |
| 		}
 | |
| 		container[idx] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
 | |
| 
 | |
| 	case *swspec.SchemaOrArray:
 | |
| 		// NOTE: this is necessarily an array - otherwise, the parent would be *Schema
 | |
| 		idx, err := strconv.Atoi(entry)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("%s not a number: %v", key[1:], err)
 | |
| 		}
 | |
| 		container.Schemas[idx] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
 | |
| 
 | |
| 	// NOTE: can't have case *swspec.SchemaOrBool = parent in this case is *Schema
 | |
| 
 | |
| 	default:
 | |
| 		return fmt.Errorf("unhandled parent schema rewrite %s (%T)", key, pvalue)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func cloneSchema(schema *swspec.Schema) (*swspec.Schema, error) {
 | |
| 	var sch swspec.Schema
 | |
| 	if err := swag.FromDynamicJSON(schema, &sch); err != nil {
 | |
| 		return nil, fmt.Errorf("cannot clone schema: %v", err)
 | |
| 	}
 | |
| 	return &sch, 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 := reverseIndexForSchemaRefs(opts)
 | |
| 	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
 | |
| 		var isOAIGen bool
 | |
| 
 | |
| 		newName := opts.flattenContext.resolved[refStr]
 | |
| 		if newName != "" {
 | |
| 			// 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 := updateRef(opts.Swagger(), key,
 | |
| 					swspec.MustCreateRef(slashpath.Join(definitionsPath, newName))); err != nil {
 | |
| 					return false, err
 | |
| 				}
 | |
| 			}
 | |
| 		} else {
 | |
| 			// resolve schemas
 | |
| 			debugLog("resolving schema from remote $ref [%s]", refStr)
 | |
| 			sch, err := swspec.ResolveRefWithBase(opts.Swagger(), &entry.Ref, opts.ExpandOpts(false))
 | |
| 			if err != nil {
 | |
| 				return false, fmt.Errorf("could not resolve schema: %v", 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 := updateRef(sch, key, swspec.MustCreateRef(rebaseRef(entry.Ref.String(), ref.String()))); err != nil {
 | |
| 					return false, fmt.Errorf("failed to rewrite ref for key %q at %s: %v", 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 := updateRef(opts.Swagger(), key,
 | |
| 					swspec.MustCreateRef(slashpath.Join(definitionsPath, newName))); err != nil {
 | |
| 					return false, 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:     slashpath.Join(definitionsPath, newName),
 | |
| 					isOAIGen: isOAIGen,
 | |
| 					resolved: resolved,
 | |
| 					schema:   sch,
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			// add the resolved schema to the definitions
 | |
| 			saveSchema(opts.Swagger(), newName, sch)
 | |
| 		}
 | |
| 	}
 | |
| 	// 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 := swspec.MustCreateRef(r.path)
 | |
| 			sch, err := swspec.ResolveRefWithBase(opts.Swagger(), &ref, opts.ExpandOpts(false))
 | |
| 			if err != nil {
 | |
| 				return false, fmt.Errorf("could not resolve schema: %v", err)
 | |
| 			}
 | |
| 			r.schema = sch
 | |
| 		}
 | |
| 		// update tracking with renamed keys: got a cascade of refs
 | |
| 		if r.path != k {
 | |
| 			renamed := *r
 | |
| 			renamed.key = r.path
 | |
| 			opts.flattenContext.newRefs[renamed.path] = &renamed
 | |
| 
 | |
| 			// indirect ref
 | |
| 			r.newName = slashpath.Base(k)
 | |
| 			r.schema = swspec.RefSchema(r.path)
 | |
| 			r.path = k
 | |
| 			r.isOAIGen = strings.Contains(k, "OAIGen")
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return complete, nil
 | |
| }
 | |
| 
 | |
| type refRevIdx struct {
 | |
| 	Ref  swspec.Ref
 | |
| 	Keys []string
 | |
| }
 | |
| 
 | |
| // rebaseRef rebase a remote ref relative to a base ref.
 | |
| //
 | |
| // NOTE: does not support JSONschema ID for $ref (we assume we are working with swagger specs here).
 | |
| //
 | |
| // NOTE(windows):
 | |
| // * refs are assumed to have been normalized with drive letter lower cased (from go-openapi/spec)
 | |
| // * "/ in paths may appear as escape sequences
 | |
| func rebaseRef(baseRef string, ref string) string {
 | |
| 	debugLog("rebasing ref: %s onto %s", ref, baseRef)
 | |
| 	baseRef, _ = url.PathUnescape(baseRef)
 | |
| 	ref, _ = url.PathUnescape(ref)
 | |
| 	if baseRef == "" || baseRef == "." || strings.HasPrefix(baseRef, "#") {
 | |
| 		return ref
 | |
| 	}
 | |
| 
 | |
| 	parts := strings.Split(ref, "#")
 | |
| 
 | |
| 	baseParts := strings.Split(baseRef, "#")
 | |
| 	baseURL, _ := url.Parse(baseParts[0])
 | |
| 	if strings.HasPrefix(ref, "#") {
 | |
| 		if baseURL.Host == "" {
 | |
| 			return strings.Join([]string{baseParts[0], parts[1]}, "#")
 | |
| 		}
 | |
| 		return strings.Join([]string{baseParts[0], parts[1]}, "#")
 | |
| 	}
 | |
| 
 | |
| 	refURL, _ := url.Parse(parts[0])
 | |
| 	if refURL.Host != "" || filepath.IsAbs(parts[0]) {
 | |
| 		// not rebasing an absolute path
 | |
| 		return ref
 | |
| 	}
 | |
| 
 | |
| 	// there is a relative path
 | |
| 	var basePath string
 | |
| 	if baseURL.Host != "" {
 | |
| 		// when there is a host, standard URI rules apply (with "/")
 | |
| 		baseURL.Path = slashpath.Dir(baseURL.Path)
 | |
| 		baseURL.Path = slashpath.Join(baseURL.Path, "/"+parts[0])
 | |
| 		return baseURL.String()
 | |
| 	}
 | |
| 
 | |
| 	// this is a local relative path
 | |
| 	// basePart[0] and parts[0] are local filesystem directories/files
 | |
| 	basePath = filepath.Dir(baseParts[0])
 | |
| 	relPath := filepath.Join(basePath, string(filepath.Separator)+parts[0])
 | |
| 	if len(parts) > 1 {
 | |
| 		return strings.Join([]string{relPath, parts[1]}, "#")
 | |
| 	}
 | |
| 	return relPath
 | |
| }
 | |
| 
 | |
| // normalizePath renders absolute path on remote file refs
 | |
| //
 | |
| // NOTE(windows):
 | |
| // * refs are assumed to have been normalized with drive letter lower cased (from go-openapi/spec)
 | |
| // * "/ in paths may appear as escape sequences
 | |
| func normalizePath(ref swspec.Ref, opts *FlattenOpts) (normalizedPath string) {
 | |
| 	uri, _ := url.PathUnescape(ref.String())
 | |
| 	if ref.HasFragmentOnly || filepath.IsAbs(uri) {
 | |
| 		normalizedPath = uri
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	refURL, _ := url.Parse(uri)
 | |
| 	if refURL.Host != "" {
 | |
| 		normalizedPath = uri
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	parts := strings.Split(uri, "#")
 | |
| 	// BasePath, parts[0] are local filesystem directories, guaranteed to be absolute at this stage
 | |
| 	parts[0] = filepath.Join(filepath.Dir(opts.BasePath), parts[0])
 | |
| 	normalizedPath = strings.Join(parts, "#")
 | |
| 	return
 | |
| }
 | |
| 
 | |
| func reverseIndexForSchemaRefs(opts *FlattenOpts) map[string]refRevIdx {
 | |
| 	collected := make(map[string]refRevIdx)
 | |
| 	for key, schRef := range opts.Spec.references.schemas {
 | |
| 		// normalize paths before sorting,
 | |
| 		// so we get together keys in same external file
 | |
| 		normalizedPath := normalizePath(schRef, opts)
 | |
| 		if entry, ok := collected[normalizedPath]; ok {
 | |
| 			entry.Keys = append(entry.Keys, key)
 | |
| 			collected[normalizedPath] = entry
 | |
| 		} else {
 | |
| 			collected[normalizedPath] = refRevIdx{
 | |
| 				Ref:  schRef,
 | |
| 				Keys: []string{key},
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return collected
 | |
| }
 | |
| 
 | |
| func nameFromRef(ref swspec.Ref) string {
 | |
| 	u := ref.GetURL()
 | |
| 	if u.Fragment != "" {
 | |
| 		return swag.ToJSONName(slashpath.Base(u.Fragment))
 | |
| 	}
 | |
| 	if u.Path != "" {
 | |
| 		bn := slashpath.Base(u.Path)
 | |
| 		if bn != "" && bn != "/" {
 | |
| 			ext := slashpath.Ext(bn)
 | |
| 			if ext != "" {
 | |
| 				return swag.ToJSONName(bn[:len(bn)-len(ext)])
 | |
| 			}
 | |
| 			return swag.ToJSONName(bn)
 | |
| 		}
 | |
| 	}
 | |
| 	return swag.ToJSONName(strings.Replace(u.Host, ".", " ", -1))
 | |
| }
 | |
| 
 | |
| func saveSchema(spec *swspec.Swagger, name string, schema *swspec.Schema) {
 | |
| 	if schema == nil {
 | |
| 		return
 | |
| 	}
 | |
| 	if spec.Definitions == nil {
 | |
| 		spec.Definitions = make(map[string]swspec.Schema, 150)
 | |
| 	}
 | |
| 	spec.Definitions[name] = *schema
 | |
| }
 | |
| 
 | |
| // getPointerFromKey retrieves the content of the JSON pointer "key"
 | |
| func getPointerFromKey(spec interface{}, key string) (string, interface{}, error) {
 | |
| 	switch spec.(type) {
 | |
| 	case *swspec.Schema:
 | |
| 	case *swspec.Swagger:
 | |
| 	default:
 | |
| 		panic("unexpected type used in getPointerFromKey")
 | |
| 	}
 | |
| 	if key == "#/" {
 | |
| 		return "", spec, nil
 | |
| 	}
 | |
| 	// unescape chars in key, e.g. "{}" from path params
 | |
| 	pth, _ := internal.PathUnescape(key[1:])
 | |
| 	ptr, err := jsonpointer.New(pth)
 | |
| 	if err != nil {
 | |
| 		return "", nil, err
 | |
| 	}
 | |
| 
 | |
| 	value, _, err := ptr.Get(spec)
 | |
| 	if err != nil {
 | |
| 		debugLog("error when getting key: %s with path: %s", key, pth)
 | |
| 		return "", nil, err
 | |
| 	}
 | |
| 	return pth, value, nil
 | |
| }
 | |
| 
 | |
| // getParentFromKey retrieves the container of the JSON pointer "key"
 | |
| func getParentFromKey(spec interface{}, key string) (string, string, interface{}, error) {
 | |
| 	switch spec.(type) {
 | |
| 	case *swspec.Schema:
 | |
| 	case *swspec.Swagger:
 | |
| 	default:
 | |
| 		panic("unexpected type used in getPointerFromKey")
 | |
| 	}
 | |
| 	// unescape chars in key, e.g. "{}" from path params
 | |
| 	pth, _ := internal.PathUnescape(key[1:])
 | |
| 
 | |
| 	parent, entry := slashpath.Dir(pth), slashpath.Base(pth)
 | |
| 	debugLog("getting schema holder at: %s, with entry: %s", parent, entry)
 | |
| 
 | |
| 	pptr, err := jsonpointer.New(parent)
 | |
| 	if err != nil {
 | |
| 		return "", "", nil, err
 | |
| 	}
 | |
| 	pvalue, _, err := pptr.Get(spec)
 | |
| 	if err != nil {
 | |
| 		return "", "", nil, fmt.Errorf("can't get parent for %s: %v", parent, err)
 | |
| 	}
 | |
| 	return parent, entry, pvalue, nil
 | |
| }
 | |
| 
 | |
| // updateRef replaces a ref by another one
 | |
| func updateRef(spec interface{}, key string, ref swspec.Ref) error {
 | |
| 	switch spec.(type) {
 | |
| 	case *swspec.Schema:
 | |
| 	case *swspec.Swagger:
 | |
| 	default:
 | |
| 		panic("unexpected type used in getPointerFromKey")
 | |
| 	}
 | |
| 	debugLog("updating ref for %s with %s", key, ref.String())
 | |
| 	pth, value, err := getPointerFromKey(spec, key)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	switch refable := value.(type) {
 | |
| 	case *swspec.Schema:
 | |
| 		refable.Ref = ref
 | |
| 	case *swspec.SchemaOrArray:
 | |
| 		if refable.Schema != nil {
 | |
| 			refable.Schema.Ref = ref
 | |
| 		}
 | |
| 	case *swspec.SchemaOrBool:
 | |
| 		if refable.Schema != nil {
 | |
| 			refable.Schema.Ref = ref
 | |
| 		}
 | |
| 	case swspec.Schema:
 | |
| 		debugLog("rewriting holder for %T", refable)
 | |
| 		_, entry, pvalue, erp := getParentFromKey(spec, key)
 | |
| 		if erp != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		switch container := pvalue.(type) {
 | |
| 		case swspec.Definitions:
 | |
| 			container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
 | |
| 
 | |
| 		case map[string]swspec.Schema:
 | |
| 			container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
 | |
| 
 | |
| 		case []swspec.Schema:
 | |
| 			idx, err := strconv.Atoi(entry)
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("%s not a number: %v", pth, err)
 | |
| 			}
 | |
| 			container[idx] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
 | |
| 
 | |
| 		case *swspec.SchemaOrArray:
 | |
| 			// NOTE: this is necessarily an array - otherwise, the parent would be *Schema
 | |
| 			idx, err := strconv.Atoi(entry)
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("%s not a number: %v", pth, err)
 | |
| 			}
 | |
| 			container.Schemas[idx] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
 | |
| 
 | |
| 		// NOTE: can't have case *swspec.SchemaOrBool = parent in this case is *Schema
 | |
| 
 | |
| 		default:
 | |
| 			return fmt.Errorf("unhandled container type at %s: %T", key, value)
 | |
| 		}
 | |
| 
 | |
| 	default:
 | |
| 		return fmt.Errorf("no schema with ref found at %s for %T", key, value)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // updateRefWithSchema replaces a ref with a schema (i.e. re-inline schema)
 | |
| func updateRefWithSchema(spec *swspec.Swagger, key string, sch *swspec.Schema) error {
 | |
| 	debugLog("updating ref for %s with schema", key)
 | |
| 	pth, value, err := getPointerFromKey(spec, key)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	switch refable := value.(type) {
 | |
| 	case *swspec.Schema:
 | |
| 		*refable = *sch
 | |
| 	case swspec.Schema:
 | |
| 		_, entry, pvalue, erp := getParentFromKey(spec, key)
 | |
| 		if erp != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		switch container := pvalue.(type) {
 | |
| 		case swspec.Definitions:
 | |
| 			container[entry] = *sch
 | |
| 
 | |
| 		case map[string]swspec.Schema:
 | |
| 			container[entry] = *sch
 | |
| 
 | |
| 		case []swspec.Schema:
 | |
| 			idx, err := strconv.Atoi(entry)
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("%s not a number: %v", pth, err)
 | |
| 			}
 | |
| 			container[idx] = *sch
 | |
| 
 | |
| 		case *swspec.SchemaOrArray:
 | |
| 			// NOTE: this is necessarily an array - otherwise, the parent would be *Schema
 | |
| 			idx, err := strconv.Atoi(entry)
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("%s not a number: %v", pth, err)
 | |
| 			}
 | |
| 			container.Schemas[idx] = *sch
 | |
| 
 | |
| 		// NOTE: can't have case *swspec.SchemaOrBool = parent in this case is *Schema
 | |
| 
 | |
| 		default:
 | |
| 			return fmt.Errorf("unhandled type for parent of [%s]: %T", key, value)
 | |
| 		}
 | |
| 	case *swspec.SchemaOrArray:
 | |
| 		*refable.Schema = *sch
 | |
| 	// NOTE: can't have case *swspec.SchemaOrBool = parent in this case is *Schema
 | |
| 	case *swspec.SchemaOrBool:
 | |
| 		*refable.Schema = *sch
 | |
| 	default:
 | |
| 		return fmt.Errorf("no schema with ref found at %s for %T", key, value)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func containsString(names []string, name string) bool {
 | |
| 	for _, nm := range names {
 | |
| 		if nm == name {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| type opRef struct {
 | |
| 	Method string
 | |
| 	Path   string
 | |
| 	Key    string
 | |
| 	ID     string
 | |
| 	Op     *swspec.Operation
 | |
| 	Ref    swspec.Ref
 | |
| }
 | |
| 
 | |
| type opRefs []opRef
 | |
| 
 | |
| func (o opRefs) Len() int           { return len(o) }
 | |
| func (o opRefs) Swap(i, j int)      { o[i], o[j] = o[j], o[i] }
 | |
| func (o opRefs) Less(i, j int) bool { return o[i].Key < o[j].Key }
 | |
| 
 | |
| func gatherOperations(specDoc *Spec, operationIDs []string) map[string]opRef {
 | |
| 	var oprefs opRefs
 | |
| 
 | |
| 	for method, pathItem := range specDoc.Operations() {
 | |
| 		for pth, operation := range pathItem {
 | |
| 			vv := *operation
 | |
| 			oprefs = append(oprefs, opRef{
 | |
| 				Key:    swag.ToGoName(strings.ToLower(method) + " " + pth),
 | |
| 				Method: method,
 | |
| 				Path:   pth,
 | |
| 				ID:     vv.ID,
 | |
| 				Op:     &vv,
 | |
| 				Ref:    swspec.MustCreateRef("#" + slashpath.Join("/paths", jsonpointer.Escape(pth), method)),
 | |
| 			})
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	sort.Sort(oprefs)
 | |
| 
 | |
| 	operations := make(map[string]opRef)
 | |
| 	for _, opr := range oprefs {
 | |
| 		nm := opr.ID
 | |
| 		if nm == "" {
 | |
| 			nm = opr.Key
 | |
| 		}
 | |
| 
 | |
| 		oo, found := operations[nm]
 | |
| 		if found && oo.Method != opr.Method && oo.Path != opr.Path {
 | |
| 			nm = opr.Key
 | |
| 		}
 | |
| 		if len(operationIDs) == 0 || containsString(operationIDs, opr.ID) || containsString(operationIDs, nm) {
 | |
| 			opr.ID = nm
 | |
| 			opr.Op.ID = nm
 | |
| 			operations[nm] = opr
 | |
| 		}
 | |
| 	}
 | |
| 	return operations
 | |
| }
 | |
| 
 | |
| // 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
 | |
| 		if hasIntroducedPointerOrInline, ers = stripOAIGen(opts); ers != nil {
 | |
| 			return ers
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func updateRefParents(opts *FlattenOpts, r *newRef) {
 | |
| 	if !r.isOAIGen || r.resolved { // bail on already resolved entries (avoid looping)
 | |
| 		return
 | |
| 	}
 | |
| 	for k, v := range opts.Spec.references.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)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // topMostRefs is able to sort refs by hierarchical then lexicographic order,
 | |
| // yielding refs ordered breadth-first.
 | |
| type topmostRefs []string
 | |
| 
 | |
| func (k topmostRefs) Len() int      { return len(k) }
 | |
| func (k topmostRefs) Swap(i, j int) { k[i], k[j] = k[j], k[i] }
 | |
| func (k topmostRefs) Less(i, j int) bool {
 | |
| 	li, lj := len(strings.Split(k[i], "/")), len(strings.Split(k[j], "/"))
 | |
| 	if li == lj {
 | |
| 		return k[i] < k[j]
 | |
| 	}
 | |
| 	return li < lj
 | |
| }
 | |
| 
 | |
| func topmostFirst(refs []string) []string {
 | |
| 	res := topmostRefs(refs)
 | |
| 	sort.Sort(res)
 | |
| 	return res
 | |
| }
 | |
| 
 | |
| // 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, 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) >= 1 {
 | |
| 			pr := topmostFirst(r.parents)
 | |
| 
 | |
| 			// rewrite first parent schema in hierarchical then lexicographical order
 | |
| 			debugLog("rewrite first parent %s with schema", pr[0])
 | |
| 			if err := 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 := swspec.MustCreateRef(pr[0])
 | |
| 
 | |
| 					// set complex when replacing ref is an anonymous jsonpointer: further processing may be required
 | |
| 					replacedWithComplex = replacedWithComplex ||
 | |
| 						slashpath.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 := 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", slashpath.Base(r.path))
 | |
| 			delete(opts.Swagger().Definitions, slashpath.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 = slashpath.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], isAnalyzedAsComplex(asch))
 | |
| 				replacedWithComplex = replacedWithComplex ||
 | |
| 					!(slashpath.Dir(pr[0]) == definitionsPath) && isAnalyzedAsComplex(asch)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	debugLog("replacedWithComplex: %t", replacedWithComplex)
 | |
| 	opts.Spec.reload() // re-analyze
 | |
| 	return replacedWithComplex, nil
 | |
| }
 | |
| 
 | |
| // croak logs notifications and warnings about valid, but possibly unwanted constructs resulting
 | |
| // from flattening a spec
 | |
| func croak(opts *FlattenOpts) {
 | |
| 	reported := make(map[string]bool, len(opts.flattenContext.newRefs))
 | |
| 	for _, v := range opts.Spec.references.allRefs {
 | |
| 		// warns about duplicate handling
 | |
| 		for _, r := range opts.flattenContext.newRefs {
 | |
| 			if r.isOAIGen && r.path == v.String() {
 | |
| 				reported[r.newName] = true
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	for k := range reported {
 | |
| 		log.Printf("warning: duplicate flattened definition name resolved as %s", k)
 | |
| 	}
 | |
| 	// warns about possible type mismatches
 | |
| 	uniqueMsg := make(map[string]bool)
 | |
| 	for _, msg := range opts.flattenContext.warnings {
 | |
| 		if _, ok := uniqueMsg[msg]; ok {
 | |
| 			continue
 | |
| 		}
 | |
| 		log.Printf("warning: %s", msg)
 | |
| 		uniqueMsg[msg] = true
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // 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 slashpath.Dir(ref.String()) == definitionsPath {
 | |
| 			// this a ref to a top-level definition: ok
 | |
| 			continue
 | |
| 		}
 | |
| 		replacingRef, sch, erd := deepestRef(opts, ref)
 | |
| 		if erd != nil {
 | |
| 			return fmt.Errorf("at %s, %v", k, erd)
 | |
| 		}
 | |
| 		debugLog("planning pointer to replace at %s: %s, resolved to: %s", k, ref.String(), replacingRef.String())
 | |
| 		refsToReplace[k] = SchemaRef{
 | |
| 			Name:     k,            // caller
 | |
| 			Ref:      replacingRef, // callee
 | |
| 			Schema:   sch,
 | |
| 			TopLevel: slashpath.Dir(replacingRef.String()) == definitionsPath,
 | |
| 		}
 | |
| 	}
 | |
| 	depthFirst := sortDepthFirst(refsToReplace)
 | |
| 	namer := &inlineSchemaNamer{
 | |
| 		Spec:           opts.Swagger(),
 | |
| 		Operations:     opRefsByRef(gatherOperations(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
 | |
| 		replacingRef, sch, erd := deepestRef(opts, v.Ref)
 | |
| 		if erd != nil {
 | |
| 			return fmt.Errorf("at %s, %v", key, erd)
 | |
| 		}
 | |
| 		v.Ref = replacingRef
 | |
| 		v.Schema = sch
 | |
| 		v.TopLevel = slashpath.Dir(replacingRef.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 := updateRef(opts.Swagger(), key, v.Ref); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		} else {
 | |
| 			// 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
 | |
| 			/*
 | |
| 				if key == "#/paths/~1some~1where~1{id}/get/parameters/1/items" {
 | |
| 					// DEBUG
 | |
| 					//func getPointerFromKey(spec interface{}, key string) (string, interface{}, error) {
 | |
| 					k, res, err := getPointerFromKey(namer.Spec, key)
 | |
| 					debugLog("k = %s, res=%#v, err=%v", k, res, err)
 | |
| 				}
 | |
| 			*/
 | |
| 			asch, ers := Schema(SchemaOpts{Schema: v.Schema, Root: opts.Swagger(), BasePath: opts.BasePath})
 | |
| 			if ers != nil {
 | |
| 				return fmt.Errorf("schema analysis [%s]: %v", key, ers)
 | |
| 			}
 | |
| 			callers := make([]string, 0, 64)
 | |
| 
 | |
| 			debugLog("looking for callers")
 | |
| 			an := New(opts.Swagger())
 | |
| 			for k, w := range an.references.allRefs {
 | |
| 				r, _, erd := deepestRef(opts, w)
 | |
| 				if erd != nil {
 | |
| 					return fmt.Errorf("at %s, %v", key, erd)
 | |
| 				}
 | |
| 				if r.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
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			parts := 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 {
 | |
| 						// 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
 | |
| 					}
 | |
| 				}
 | |
| 			} else {
 | |
| 				debugLog("expand JSON pointer for key=%s", key)
 | |
| 				if err := updateRefWithSchema(opts.Swagger(), key, v.Schema); err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 				// NOTE: there is no other caller to update
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	opts.Spec.reload() // re-analyze
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // deepestRef finds the first definition ref, from a cascade of nested refs which are not definitions.
 | |
| //  - if no definition is found, returns the deepest ref.
 | |
| //  - pointers to external files are expanded
 | |
| //
 | |
| // NOTE: all external $ref's are assumed to be already expanded at this stage.
 | |
| func deepestRef(opts *FlattenOpts, ref swspec.Ref) (swspec.Ref, *swspec.Schema, error) {
 | |
| 	if !ref.HasFragmentOnly {
 | |
| 		// we found an external $ref, which is odd
 | |
| 		// does nothing on external $refs
 | |
| 		return ref, nil, nil
 | |
| 	}
 | |
| 	currentRef := ref
 | |
| 	visited := make(map[string]bool, 64)
 | |
| DOWNREF:
 | |
| 	for currentRef.String() != "" {
 | |
| 		if slashpath.Dir(currentRef.String()) == definitionsPath {
 | |
| 			// this is a top-level definition: stop here and return this ref
 | |
| 			return currentRef, nil, nil
 | |
| 		}
 | |
| 		if _, beenThere := visited[currentRef.String()]; beenThere {
 | |
| 			return swspec.Ref{}, nil,
 | |
| 				fmt.Errorf("cannot resolve cyclic chain of pointers under %s", currentRef.String())
 | |
| 		}
 | |
| 		visited[currentRef.String()] = true
 | |
| 		value, _, err := currentRef.GetPointer().Get(opts.Swagger())
 | |
| 		if err != nil {
 | |
| 			return swspec.Ref{}, nil, err
 | |
| 		}
 | |
| 		switch refable := value.(type) {
 | |
| 		case *swspec.Schema:
 | |
| 			if refable.Ref.String() == "" {
 | |
| 				break DOWNREF
 | |
| 			}
 | |
| 			currentRef = refable.Ref
 | |
| 
 | |
| 		case swspec.Schema:
 | |
| 			if refable.Ref.String() == "" {
 | |
| 				break DOWNREF
 | |
| 			}
 | |
| 			currentRef = refable.Ref
 | |
| 
 | |
| 		case *swspec.SchemaOrArray:
 | |
| 			if refable.Schema == nil || refable.Schema != nil && refable.Schema.Ref.String() == "" {
 | |
| 				break DOWNREF
 | |
| 			}
 | |
| 			currentRef = refable.Schema.Ref
 | |
| 
 | |
| 		case *swspec.SchemaOrBool:
 | |
| 			if refable.Schema == nil || refable.Schema != nil && refable.Schema.Ref.String() == "" {
 | |
| 				break DOWNREF
 | |
| 			}
 | |
| 			currentRef = refable.Schema.Ref
 | |
| 
 | |
| 		case swspec.Response:
 | |
| 			// a pointer points to a schema initially marshalled in responses section...
 | |
| 			// Attempt to convert this to a schema. If this fails, the spec is invalid
 | |
| 			asJSON, _ := refable.MarshalJSON()
 | |
| 			var asSchema swspec.Schema
 | |
| 			err := asSchema.UnmarshalJSON(asJSON)
 | |
| 			if err != nil {
 | |
| 				return swspec.Ref{}, nil,
 | |
| 					fmt.Errorf("invalid type for resolved JSON pointer %s. Expected a schema a, got: %T",
 | |
| 						currentRef.String(), value)
 | |
| 
 | |
| 			}
 | |
| 			opts.flattenContext.warnings = append(opts.flattenContext.warnings,
 | |
| 				fmt.Sprintf("found $ref %q (response) interpreted as schema", currentRef.String()))
 | |
| 
 | |
| 			if asSchema.Ref.String() == "" {
 | |
| 				break DOWNREF
 | |
| 			}
 | |
| 			currentRef = asSchema.Ref
 | |
| 
 | |
| 		case swspec.Parameter:
 | |
| 			// a pointer points to a schema initially marshalled in parameters section...
 | |
| 			// Attempt to convert this to a schema. If this fails, the spec is invalid
 | |
| 			asJSON, _ := refable.MarshalJSON()
 | |
| 			var asSchema swspec.Schema
 | |
| 			err := asSchema.UnmarshalJSON(asJSON)
 | |
| 			if err != nil {
 | |
| 				return swspec.Ref{}, nil,
 | |
| 					fmt.Errorf("invalid type for resolved JSON pointer %s. Expected a schema a, got: %T",
 | |
| 						currentRef.String(), value)
 | |
| 
 | |
| 			}
 | |
| 			opts.flattenContext.warnings = append(opts.flattenContext.warnings,
 | |
| 				fmt.Sprintf("found $ref %q (parameter) interpreted as schema", currentRef.String()))
 | |
| 
 | |
| 			if asSchema.Ref.String() == "" {
 | |
| 				break DOWNREF
 | |
| 			}
 | |
| 			currentRef = asSchema.Ref
 | |
| 
 | |
| 		default:
 | |
| 			return swspec.Ref{}, nil,
 | |
| 				fmt.Errorf("unhandled type to resolve JSON pointer %s. Expected a Schema, got: %T",
 | |
| 					currentRef.String(), value)
 | |
| 
 | |
| 		}
 | |
| 	}
 | |
| 	// assess what schema we're ending with
 | |
| 	sch, erv := swspec.ResolveRefWithBase(opts.Swagger(), ¤tRef, opts.ExpandOpts(false))
 | |
| 	if erv != nil {
 | |
| 		return swspec.Ref{}, nil, erv
 | |
| 	}
 | |
| 	if sch == nil {
 | |
| 		return swspec.Ref{}, nil, fmt.Errorf("no schema found at %s", currentRef.String())
 | |
| 	}
 | |
| 	return currentRef, sch, nil
 | |
| }
 | |
| 
 | |
| // normalizeRef strips the current file from any $ref. This works around issue go-openapi/spec#76:
 | |
| // leading absolute file in $ref is stripped
 | |
| func normalizeRef(opts *FlattenOpts) error {
 | |
| 	debugLog("normalizeRef")
 | |
| 	opts.Spec.reload() // re-analyze
 | |
| 	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
 | |
| 			// strip base path from definition
 | |
| 			debugLog("stripping absolute path for: %s", w.String())
 | |
| 			if err := updateRef(opts.Swagger(), k,
 | |
| 				swspec.MustCreateRef(slashpath.Join(definitionsPath, slashpath.Base(w.String())))); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	opts.Spec.reload() // re-analyze
 | |
| 	return nil
 | |
| }
 |