| // 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 |
| |
| /* 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 |
| } |
| |
| func newContext() *context { |
| return &context{ |
| newRefs: make(map[string]*newRef, 150), |
| warnings: make([]string, 0), |
| } |
| } |
| |
| // 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 { |
| // Make sure opts.BasePath is an absolute path |
| if !filepath.IsAbs(opts.BasePath) { |
| cwd, _ := os.Getwd() |
| opts.BasePath = filepath.Join(cwd, opts.BasePath) |
| } |
| |
| opts.flattenContext = newContext() |
| |
| // recursively expand responses, parameters, path items and items in simple schemas |
| // TODO: we should not expand discriminated types |
| if err := swspec.ExpandSpec(opts.Swagger(), opts.ExpandOpts(!opts.Expand)); 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 other references left but schemas |
| if 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 attempts to resolve conflicting names |
| 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() { |
| if _, ok := expected[k]; ok { |
| 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 "" |
| } |
| |
| 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.ToLower(k) == strings.ToLower(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{} |
| |
| func init() { |
| ignoredKeys = map[string]struct{}{ |
| "schema": {}, |
| "properties": {}, |
| "not": {}, |
| "anyOf": {}, |
| "oneOf": {}, |
| } |
| } |
| |
| 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 "" |
| } |
| |
| var validMethods map[string]struct{} |
| |
| func init() { |
| validMethods = map[string]struct{}{ |
| "GET": {}, |
| "HEAD": {}, |
| "OPTIONS": {}, |
| "PATCH": {}, |
| "POST": {}, |
| "PUT": {}, |
| "DELETE": {}, |
| } |
| } |
| |
| func (s splitKey) PathItemRef() swspec.Ref { |
| if len(s) < 3 { |
| return swspec.Ref{} |
| } |
| pth, method := s[1], s[2] |
| if _, validMethod := validMethods[strings.ToUpper(method)]; !validMethod && !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 |
| } |
| |
| func importExternalReferences(opts *FlattenOpts) error { |
| groupedRefs := reverseIndexForSchemaRefs(opts) |
| sortedRefStr := make([]string, 0, len(groupedRefs)) |
| |
| // sort $ref resolution to ensure deterministic name conflict resolution |
| for refStr := range groupedRefs { |
| sortedRefStr = append(sortedRefStr, refStr) |
| } |
| sort.Strings(sortedRefStr) |
| |
| for _, refStr := range sortedRefStr { |
| entry := groupedRefs[refStr] |
| if !entry.Ref.HasFragmentOnly { |
| debugLog("importing external schema for [%s] from %s", strings.Join(entry.Keys, ", "), refStr) |
| // resolve to actual schema |
| sch := new(swspec.Schema) |
| sch.Ref = entry.Ref |
| if err := swspec.ExpandSchemaWithBasePath(sch, nil, opts.ExpandOpts(false)); err != nil { |
| return err |
| } |
| if sch == nil { |
| return fmt.Errorf("no schema found at %s for [%s]", refStr, strings.Join(entry.Keys, ", ")) |
| } |
| debugLog("importing external schema for [%s] from %s", strings.Join(entry.Keys, ", "), refStr) |
| |
| // 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) |
| |
| // 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 err |
| } |
| |
| // keep track of created refs |
| if opts.flattenContext != nil { |
| resolved := false |
| if _, ok := opts.flattenContext.newRefs[key]; ok { |
| resolved = opts.flattenContext.newRefs[key].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) |
| } |
| } |
| return nil |
| } |
| |
| type refRevIdx struct { |
| Ref swspec.Ref |
| Keys []string |
| } |
| |
| // normalizePath renders absolute path on remote file refs |
| func normalizePath(ref swspec.Ref, opts *FlattenOpts) (normalizedPath string) { |
| if ref.HasFragmentOnly || filepath.IsAbs(ref.String()) { |
| normalizedPath = ref.String() |
| return |
| } |
| |
| refURL, _ := url.Parse(ref.String()) |
| if refURL.Host != "" { |
| normalizedPath = ref.String() |
| return |
| } |
| |
| parts := strings.Split(ref.String(), "#") |
| 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 *swspec.Swagger, key string) (string, interface{}, error) { |
| // 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 *swspec.Swagger, key string) (string, string, interface{}, error) { |
| // 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 *swspec.Swagger, key string, ref swspec.Ref) error { |
| 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 { |
| if err := nameInlinedSchemas(opts); err != nil { |
| return err |
| } |
| } |
| |
| if err := namePointers(opts); err != nil { |
| return err |
| } |
| |
| // restrip |
| if hasIntroducedPointerOrInline, ers = stripOAIGen(opts); ers != nil { |
| return ers |
| } |
| } |
| return nil |
| } |
| |
| // stripOAIGen strips the spec from unnecessary OAIGen constructs, initially created to dedupe flattened definitions. |
| // A dedupe is deemed unnecessary whenever: |
| // - the only conflict is with its (single) parent: OAIGen is merged into its parent |
| // - 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 a true bool whenever it re-inlined a complex schema, so the caller may chose to iterate |
| // flattening again. |
| // |
| // NOTE: the OAIGen definition cannot be itself a $ref. |
| func stripOAIGen(opts *FlattenOpts) (bool, error) { |
| debugLog("stripOAIGen") |
| replacedWithComplex := false |
| for k, v := range opts.Spec.references.allRefs { |
| // figure out referers of OAIGen definitions |
| for _, r := range opts.flattenContext.newRefs { |
| if r.isOAIGen && !r.resolved && r.path == v.String() { // bail on already resolved entries (avoid looping) |
| r.parents = append(r.parents, k) |
| } |
| } |
| } |
| |
| for _, r := range opts.flattenContext.newRefs { |
| if r.isOAIGen && len(r.parents) >= 1 && r.schema.Ref.String() == "" { |
| pr := r.parents |
| sort.Strings(pr) |
| // rewrite first parent schema in 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 |
| } |
| // 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 |
| } |
| } |
| } |
| // remove OAIGen definition |
| debugLog("removing definition %s", slashpath.Base(r.path)) |
| delete(opts.Swagger().Definitions, slashpath.Base(r.path)) |
| // mark naming conflict as resolved |
| 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-inline schema: parent: %s, %t", pr[0], isAnalyzedAsComplex(asch)) |
| replacedWithComplex = replacedWithComplex || |
| !(slashpath.Dir(pr[0]) == definitionsPath) && isAnalyzedAsComplex(asch) |
| } |
| } |
| } |
| 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.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 |
| 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 { |
| // 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 |
| } |