Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1773 lines
53 KiB
1773 lines
53 KiB
// 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" |
|
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. |
|
// |
|
// The BasePath parameter is used to locate remote relative $ref found in the specification. |
|
// This path is a file: it points to the location of the root document and may be either a local |
|
// file path or a URL. |
|
// |
|
// If none specified, relative references (e.g. "$ref": "folder/schema.yaml#/definitions/...") |
|
// found in the spec are searched from the current working directory. |
|
type FlattenOpts struct { |
|
Spec *Spec // The analyzed spec to work with |
|
flattenContext *context // Internal context to track flattening activity |
|
|
|
BasePath string // The location of the root document for this spec to resolve relative $ref |
|
|
|
// Flattening options |
|
Expand bool // When true, skip flattening the spec and expand it instead (if Minimal is false) |
|
Minimal bool // When true, do not decompose complex structures such as allOf |
|
Verbose bool // enable some reporting on possible name conflicts detected |
|
RemoveUnused bool // When true, remove unused parameters, responses and definitions after expansion/flattening |
|
ContinueOnError bool // Continue when spec expansion issues are found |
|
|
|
/* 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. |
|
// Self-referencing JSON pointers cannot resolve to a type and trigger an error. |
|
// |
|
// |
|
// Minimal flattening is necessary and sufficient for codegen rendering using go-swagger. |
|
// |
|
// Fully flattening a spec means: |
|
// - Moving every complex inline schema to be a definition with an auto-generated name in a depth-first fashion. |
|
// |
|
// By complex, we mean every JSON object with some properties. |
|
// Arrays, when they do not define a tuple, |
|
// or empty objects with or without additionalProperties, are not considered complex and remain inline. |
|
// |
|
// NOTE: rewritten schemas get a vendor extension x-go-gen-location so we know from which part of the spec definitions |
|
// have been created. |
|
// |
|
// Available flattening options: |
|
// - Minimal: stops flattening after minimal $ref processing, leaving schema constructs untouched |
|
// - Expand: expand all $ref's in the document (inoperant if Minimal set to true) |
|
// - Verbose: croaks about name conflicts detected |
|
// - RemoveUnused: removes unused parameters, responses and definitions after expansion/flattening |
|
// |
|
// NOTE: expansion removes all $ref save circular $ref, which remain in place |
|
// |
|
// TODO: additional options |
|
// - ProgagateNameExtensions: ensure that created entries properly follow naming rules when their parent have set a |
|
// x-go-name extension |
|
// - LiftAllOfs: |
|
// - limit the flattening of allOf members when simple objects |
|
// - merge allOf with validation only |
|
// - merge allOf with extensions only |
|
// - ... |
|
// |
|
func Flatten(opts FlattenOpts) error { |
|
debugLog("FlattenOpts: %#v", opts) |
|
opts.flattenContext = newContext() |
|
|
|
// 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 |
|
} |
|
|
|
opts.Spec.reload() // re-analyze |
|
|
|
// 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 { |
|
debugLog("error in importExternalReferences: %v", err) |
|
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}} |
|
|
|
case swspec.SchemaProperties: |
|
container[entry] = 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.ReplaceAll(u.Host, ".", " ")) |
|
} |
|
|
|
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}} |
|
|
|
case swspec.SchemaProperties: |
|
container[entry] = 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 |
|
|
|
case swspec.SchemaProperties: |
|
container[entry] = *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") |
|
altered := false |
|
for k, w := range opts.Spec.references.allRefs { |
|
if !strings.HasPrefix(w.String(), opts.BasePath+definitionsPath) { // may be a mix of / and \, depending on OS |
|
continue |
|
} |
|
altered = true |
|
// 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 |
|
} |
|
} |
|
if altered { |
|
opts.Spec.reload() // re-analyze |
|
} |
|
return nil |
|
}
|
|
|