forgejo/vendor/github.com/go-openapi/analysis/flatten.go
2020-09-01 10:01:23 -04:00

1758 lines
52 KiB
Go
Vendored

// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package analysis
import (
"fmt"
"log"
"net/http"
"net/url"
"os"
slashpath "path"
"path/filepath"
"sort"
"strings"
"strconv"
"github.com/go-openapi/analysis/internal"
"github.com/go-openapi/jsonpointer"
swspec "github.com/go-openapi/spec"
"github.com/go-openapi/swag"
)
// FlattenOpts configuration for flattening a swagger specification.
type FlattenOpts struct {
Spec *Spec // The analyzed spec to work with
flattenContext *context // Internal context to track flattening activity
BasePath string
// Flattening options
Expand bool // If Expand is true, we skip flattening the spec and expand it instead
Minimal bool
Verbose bool
RemoveUnused bool
ContinueOnError bool // Continues when facing some issues
/* Extra keys */
_ struct{} // require keys
}
// ExpandOpts creates a spec.ExpandOptions to configure expanding a specification document.
func (f *FlattenOpts) ExpandOpts(skipSchemas bool) *swspec.ExpandOptions {
return &swspec.ExpandOptions{RelativeBase: f.BasePath, SkipSchemas: skipSchemas}
}
// Swagger gets the swagger specification for this flatten operation
func (f *FlattenOpts) Swagger() *swspec.Swagger {
return f.Spec.spec
}
// newRef stores information about refs created during the flattening process
type newRef struct {
key string
newName string
path string
isOAIGen bool
resolved bool
schema *swspec.Schema
parents []string
}
// context stores intermediary results from flatten
type context struct {
newRefs map[string]*newRef
warnings []string
resolved map[string]string
}
func newContext() *context {
return &context{
newRefs: make(map[string]*newRef, 150),
warnings: make([]string, 0),
resolved: make(map[string]string, 50),
}
}
// Flatten an analyzed spec and produce a self-contained spec bundle.
//
// There is a minimal and a full flattening mode.
//
// Minimally flattening a spec means:
// - Expanding parameters, responses, path items, parameter items and header items (references to schemas are left
// unscathed)
// - Importing external (http, file) references so they become internal to the document
// - Moving every JSON pointer to a $ref to a named definition (i.e. the reworked spec does not contain pointers
// like "$ref": "#/definitions/myObject/allOfs/1")
//
// A minimally flattened spec thus guarantees the following properties:
// - all $refs point to a local definition (i.e. '#/definitions/...')
// - definitions are unique
//
// NOTE: arbitrary JSON pointers (other than $refs to top level definitions) are rewritten as definitions if they
// represent a complex schema or express commonality in the spec.
// Otherwise, they are simply expanded.
//
// Minimal flattening is necessary and sufficient for codegen rendering using go-swagger.
//
// Fully flattening a spec means:
// - Moving every complex inline schema to be a definition with an auto-generated name in a depth-first fashion.
//
// By complex, we mean every JSON object with some properties.
// Arrays, when they do not define a tuple,
// or empty objects with or without additionalProperties, are not considered complex and remain inline.
//
// NOTE: rewritten schemas get a vendor extension x-go-gen-location so we know from which part of the spec definitions
// have been created.
//
// Available flattening options:
// - Minimal: stops flattening after minimal $ref processing, leaving schema constructs untouched
// - Expand: expand all $ref's in the document (inoperant if Minimal set to true)
// - Verbose: croaks about name conflicts detected
// - RemoveUnused: removes unused parameters, responses and definitions after expansion/flattening
//
// NOTE: expansion removes all $ref save circular $ref, which remain in place
//
// TODO: additional options
// - ProgagateNameExtensions: ensure that created entries properly follow naming rules when their parent have set a
// x-go-name extension
// - LiftAllOfs:
// - limit the flattening of allOf members when simple objects
// - merge allOf with validation only
// - merge allOf with extensions only
// - ...
//
func Flatten(opts FlattenOpts) error {
debugLog("FlattenOpts: %#v", opts)
// Make sure opts.BasePath is an absolute path
if !filepath.IsAbs(opts.BasePath) {
cwd, _ := os.Getwd()
opts.BasePath = filepath.Join(cwd, opts.BasePath)
}
// make sure drive letter on windows is normalized to lower case
u, _ := url.Parse(opts.BasePath)
opts.BasePath = u.String()
opts.flattenContext = newContext()
// recursively expand responses, parameters, path items and items in simple schemas.
// This simplifies the spec and leaves $ref only into schema objects.
expandOpts := opts.ExpandOpts(!opts.Expand)
expandOpts.ContinueOnError = opts.ContinueOnError
if err := swspec.ExpandSpec(opts.Swagger(), expandOpts); err != nil {
return err
}
// strip current file from $ref's, so we can recognize them as proper definitions
// In particular, this works around for issue go-openapi/spec#76: leading absolute file in $ref is stripped
if err := normalizeRef(&opts); err != nil {
return err
}
if opts.RemoveUnused {
// optionally removes shared parameters and responses already expanded (now unused)
// default parameters (i.e. under paths) remain.
opts.Swagger().Parameters = nil
opts.Swagger().Responses = nil
}
opts.Spec.reload() // re-analyze
// at this point there are no references left but in schemas
for imported := false; !imported; {
// iteratively import remote references until none left.
// This inlining deals with name conflicts by introducing auto-generated names ("OAIGen")
var err error
if imported, err = importExternalReferences(&opts); err != nil {
return err
}
opts.Spec.reload() // re-analyze
}
if !opts.Minimal && !opts.Expand {
// full flattening: rewrite inline schemas (schemas that aren't simple types or arrays or maps)
if err := nameInlinedSchemas(&opts); err != nil {
return err
}
opts.Spec.reload() // re-analyze
}
// rewrite JSON pointers other than $ref to named definitions
// and attempt to resolve conflicting names whenever possible.
if err := stripPointersAndOAIGen(&opts); err != nil {
return err
}
if opts.RemoveUnused {
// remove unused definitions
expected := make(map[string]struct{})
for k := range opts.Swagger().Definitions {
expected[slashpath.Join(definitionsPath, jsonpointer.Escape(k))] = struct{}{}
}
for _, k := range opts.Spec.AllDefinitionReferences() {
delete(expected, k)
}
for k := range expected {
debugLog("removing unused definition %s", slashpath.Base(k))
if opts.Verbose {
log.Printf("info: removing unused definition: %s", slashpath.Base(k))
}
delete(opts.Swagger().Definitions, slashpath.Base(k))
}
opts.Spec.reload() // re-analyze
}
// TODO: simplify known schema patterns to flat objects with properties
// examples:
// - lift simple allOf object,
// - empty allOf with validation only or extensions only
// - rework allOf arrays
// - rework allOf additionalProperties
if opts.Verbose {
// issue notifications
croak(&opts)
}
return nil
}
// isAnalyzedAsComplex determines if an analyzed schema is eligible to flattening (i.e. it is "complex").
//
// Complex means the schema is any of:
// - a simple type (primitive)
// - an array of something (items are possibly complex ; if this is the case, items will generate a definition)
// - a map of something (additionalProperties are possibly complex ; if this is the case, additionalProperties will
// generate a definition)
func isAnalyzedAsComplex(asch *AnalyzedSchema) bool {
if !asch.IsSimpleSchema && !asch.IsArray && !asch.IsMap {
return true
}
return false
}
// nameInlinedSchemas replaces every complex inline construct by a named definition.
func nameInlinedSchemas(opts *FlattenOpts) error {
debugLog("nameInlinedSchemas")
namer := &inlineSchemaNamer{
Spec: opts.Swagger(),
Operations: opRefsByRef(gatherOperations(opts.Spec, nil)),
flattenContext: opts.flattenContext,
opts: opts,
}
depthFirst := sortDepthFirst(opts.Spec.allSchemas)
for _, key := range depthFirst {
sch := opts.Spec.allSchemas[key]
if sch.Schema != nil && sch.Schema.Ref.String() == "" && !sch.TopLevel { // inline schema
asch, err := Schema(SchemaOpts{Schema: sch.Schema, Root: opts.Swagger(), BasePath: opts.BasePath})
if err != nil {
return fmt.Errorf("schema analysis [%s]: %v", key, err)
}
if isAnalyzedAsComplex(asch) { // move complex schemas to definitions
if err := namer.Name(key, sch.Schema, asch); err != nil {
return err
}
}
}
}
return nil
}
var depthGroupOrder = []string{
"sharedParam", "sharedResponse", "sharedOpParam", "opParam", "codeResponse", "defaultResponse", "definition",
}
func sortDepthFirst(data map[string]SchemaRef) []string {
// group by category (shared params, op param, statuscode response, default response, definitions)
// sort groups internally by number of parts in the key and lexical names
// flatten groups into a single list of keys
sorted := make([]string, 0, len(data))
grouped := make(map[string]keys, len(data))
for k := range data {
split := keyParts(k)
var pk string
if split.IsSharedOperationParam() {
pk = "sharedOpParam"
}
if split.IsOperationParam() {
pk = "opParam"
}
if split.IsStatusCodeResponse() {
pk = "codeResponse"
}
if split.IsDefaultResponse() {
pk = "defaultResponse"
}
if split.IsDefinition() {
pk = "definition"
}
if split.IsSharedParam() {
pk = "sharedParam"
}
if split.IsSharedResponse() {
pk = "sharedResponse"
}
grouped[pk] = append(grouped[pk], key{Segments: len(split), Key: k})
}
for _, pk := range depthGroupOrder {
res := grouped[pk]
sort.Sort(res)
for _, v := range res {
sorted = append(sorted, v.Key)
}
}
return sorted
}
type key struct {
Segments int
Key string
}
type keys []key
func (k keys) Len() int { return len(k) }
func (k keys) Swap(i, j int) { k[i], k[j] = k[j], k[i] }
func (k keys) Less(i, j int) bool {
return k[i].Segments > k[j].Segments || (k[i].Segments == k[j].Segments && k[i].Key < k[j].Key)
}
type inlineSchemaNamer struct {
Spec *swspec.Swagger
Operations map[string]opRef
flattenContext *context
opts *FlattenOpts
}
func opRefsByRef(oprefs map[string]opRef) map[string]opRef {
result := make(map[string]opRef, len(oprefs))
for _, v := range oprefs {
result[v.Ref.String()] = v
}
return result
}
func (isn *inlineSchemaNamer) Name(key string, schema *swspec.Schema, aschema *AnalyzedSchema) error {
debugLog("naming inlined schema at %s", key)
parts := keyParts(key)
for _, name := range namesFromKey(parts, aschema, isn.Operations) {
if name != "" {
// create unique name
newName, isOAIGen := uniqifyName(isn.Spec.Definitions, swag.ToJSONName(name))
// clone schema
sch, err := cloneSchema(schema)
if err != nil {
return err
}
// replace values on schema
if err := rewriteSchemaToRef(isn.Spec, key,
swspec.MustCreateRef(slashpath.Join(definitionsPath, newName))); err != nil {
return fmt.Errorf("error while creating definition %q from inline schema: %v", newName, err)
}
// rewrite any dependent $ref pointing to this place,
// when not already pointing to a top-level definition.
//
// NOTE: this is important if such referers use arbitrary JSON pointers.
an := New(isn.Spec)
for k, v := range an.references.allRefs {
r, _, erd := deepestRef(isn.opts, v)
if erd != nil {
return fmt.Errorf("at %s, %v", k, erd)
}
if r.String() == key ||
r.String() == slashpath.Join(definitionsPath, newName) &&
slashpath.Dir(v.String()) != definitionsPath {
debugLog("found a $ref to a rewritten schema: %s points to %s", k, v.String())
// rewrite $ref to the new target
if err := updateRef(isn.Spec, k,
swspec.MustCreateRef(slashpath.Join(definitionsPath, newName))); err != nil {
return err
}
}
}
// NOTE: this extension is currently not used by go-swagger (provided for information only)
sch.AddExtension("x-go-gen-location", genLocation(parts))
// save cloned schema to definitions
saveSchema(isn.Spec, newName, sch)
// keep track of created refs
if isn.flattenContext != nil {
debugLog("track created ref: key=%s, newName=%s, isOAIGen=%t", key, newName, isOAIGen)
resolved := false
if _, ok := isn.flattenContext.newRefs[key]; ok {
resolved = isn.flattenContext.newRefs[key].resolved
}
isn.flattenContext.newRefs[key] = &newRef{
key: key,
newName: newName,
path: slashpath.Join(definitionsPath, newName),
isOAIGen: isOAIGen,
resolved: resolved,
schema: sch,
}
}
}
}
return nil
}
// genLocation indicates from which section of the specification (models or operations) a definition has been created.
//
// This is reflected in the output spec with a "x-go-gen-location" extension. At the moment, this is is provided
// for information only.
func genLocation(parts splitKey) string {
if parts.IsOperation() {
return "operations"
}
if parts.IsDefinition() {
return "models"
}
return ""
}
// uniqifyName yields a unique name for a definition
func uniqifyName(definitions swspec.Definitions, name string) (string, bool) {
isOAIGen := false
if name == "" {
name = "oaiGen"
isOAIGen = true
}
if len(definitions) == 0 {
return name, isOAIGen
}
unq := true
for k := range definitions {
if strings.EqualFold(k, name) {
unq = false
break
}
}
if unq {
return name, isOAIGen
}
name += "OAIGen"
isOAIGen = true
var idx int
unique := name
_, known := definitions[unique]
for known {
idx++
unique = fmt.Sprintf("%s%d", name, idx)
_, known = definitions[unique]
}
return unique, isOAIGen
}
func namesFromKey(parts splitKey, aschema *AnalyzedSchema, operations map[string]opRef) []string {
var baseNames [][]string
var startIndex int
if parts.IsOperation() {
// params
if parts.IsOperationParam() || parts.IsSharedOperationParam() {
piref := parts.PathItemRef()
if piref.String() != "" && parts.IsOperationParam() {
if op, ok := operations[piref.String()]; ok {
startIndex = 5
baseNames = append(baseNames, []string{op.ID, "params", "body"})
}
} else if parts.IsSharedOperationParam() {
pref := parts.PathRef()
for k, v := range operations {
if strings.HasPrefix(k, pref.String()) {
startIndex = 4
baseNames = append(baseNames, []string{v.ID, "params", "body"})
}
}
}
}
// responses
if parts.IsOperationResponse() {
piref := parts.PathItemRef()
if piref.String() != "" {
if op, ok := operations[piref.String()]; ok {
startIndex = 6
baseNames = append(baseNames, []string{op.ID, parts.ResponseName(), "body"})
}
}
}
}
// definitions
if parts.IsDefinition() {
nm := parts.DefinitionName()
if nm != "" {
startIndex = 2
baseNames = append(baseNames, []string{parts.DefinitionName()})
}
}
var result []string
for _, segments := range baseNames {
nm := parts.BuildName(segments, startIndex, aschema)
if nm != "" {
result = append(result, nm)
}
}
sort.Strings(result)
return result
}
const (
paths = "paths"
responses = "responses"
parameters = "parameters"
definitions = "definitions"
definitionsPath = "#/definitions"
)
var (
ignoredKeys map[string]struct{}
validMethods map[string]struct{}
)
func init() {
ignoredKeys = map[string]struct{}{
"schema": {},
"properties": {},
"not": {},
"anyOf": {},
"oneOf": {},
}
validMethods = map[string]struct{}{
"GET": {},
"HEAD": {},
"OPTIONS": {},
"PATCH": {},
"POST": {},
"PUT": {},
"DELETE": {},
}
}
type splitKey []string
func (s splitKey) IsDefinition() bool {
return len(s) > 1 && s[0] == definitions
}
func (s splitKey) DefinitionName() string {
if !s.IsDefinition() {
return ""
}
return s[1]
}
func (s splitKey) isKeyName(i int) bool {
if i <= 0 {
return false
}
count := 0
for idx := i - 1; idx > 0; idx-- {
if s[idx] != "properties" {
break
}
count++
}
return count%2 != 0
}
func (s splitKey) BuildName(segments []string, startIndex int, aschema *AnalyzedSchema) string {
for i, part := range s[startIndex:] {
if _, ignored := ignoredKeys[part]; !ignored || s.isKeyName(startIndex+i) {
if part == "items" || part == "additionalItems" {
if aschema.IsTuple || aschema.IsTupleWithExtra {
segments = append(segments, "tuple")
} else {
segments = append(segments, "items")
}
if part == "additionalItems" {
segments = append(segments, part)
}
continue
}
segments = append(segments, part)
}
}
return strings.Join(segments, " ")
}
func (s splitKey) IsOperation() bool {
return len(s) > 1 && s[0] == paths
}
func (s splitKey) IsSharedOperationParam() bool {
return len(s) > 2 && s[0] == paths && s[2] == parameters
}
func (s splitKey) IsSharedParam() bool {
return len(s) > 1 && s[0] == parameters
}
func (s splitKey) IsOperationParam() bool {
return len(s) > 3 && s[0] == paths && s[3] == parameters
}
func (s splitKey) IsOperationResponse() bool {
return len(s) > 3 && s[0] == paths && s[3] == responses
}
func (s splitKey) IsSharedResponse() bool {
return len(s) > 1 && s[0] == responses
}
func (s splitKey) IsDefaultResponse() bool {
return len(s) > 4 && s[0] == paths && s[3] == responses && s[4] == "default"
}
func (s splitKey) IsStatusCodeResponse() bool {
isInt := func() bool {
_, err := strconv.Atoi(s[4])
return err == nil
}
return len(s) > 4 && s[0] == paths && s[3] == responses && isInt()
}
func (s splitKey) ResponseName() string {
if s.IsStatusCodeResponse() {
code, _ := strconv.Atoi(s[4])
return http.StatusText(code)
}
if s.IsDefaultResponse() {
return "Default"
}
return ""
}
func (s splitKey) PathItemRef() swspec.Ref {
if len(s) < 3 {
return swspec.Ref{}
}
pth, method := s[1], s[2]
if _, isValidMethod := validMethods[strings.ToUpper(method)]; !isValidMethod && !strings.HasPrefix(method, "x-") {
return swspec.Ref{}
}
return swspec.MustCreateRef("#" + slashpath.Join("/", paths, jsonpointer.Escape(pth), strings.ToUpper(method)))
}
func (s splitKey) PathRef() swspec.Ref {
if !s.IsOperation() {
return swspec.Ref{}
}
return swspec.MustCreateRef("#" + slashpath.Join("/", paths, jsonpointer.Escape(s[1])))
}
func keyParts(key string) splitKey {
var res []string
for _, part := range strings.Split(key[1:], "/") {
if part != "" {
res = append(res, jsonpointer.Unescape(part))
}
}
return res
}
func rewriteSchemaToRef(spec *swspec.Swagger, key string, ref swspec.Ref) error {
debugLog("rewriting schema to ref for %s with %s", key, ref.String())
_, value, err := getPointerFromKey(spec, key)
if err != nil {
return err
}
switch refable := value.(type) {
case *swspec.Schema:
return rewriteParentRef(spec, key, ref)
case swspec.Schema:
return rewriteParentRef(spec, key, ref)
case *swspec.SchemaOrArray:
if refable.Schema != nil {
refable.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
}
case *swspec.SchemaOrBool:
if refable.Schema != nil {
refable.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
}
default:
return fmt.Errorf("no schema with ref found at %s for %T", key, value)
}
return nil
}
func rewriteParentRef(spec *swspec.Swagger, key string, ref swspec.Ref) error {
parent, entry, pvalue, err := getParentFromKey(spec, key)
if err != nil {
return err
}
debugLog("rewriting holder for %T", pvalue)
switch container := pvalue.(type) {
case swspec.Response:
if err := rewriteParentRef(spec, "#"+parent, ref); err != nil {
return err
}
case *swspec.Response:
container.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
case *swspec.Responses:
statusCode, err := strconv.Atoi(entry)
if err != nil {
return fmt.Errorf("%s not a number: %v", key[1:], err)
}
resp := container.StatusCodeResponses[statusCode]
resp.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
container.StatusCodeResponses[statusCode] = resp
case map[string]swspec.Response:
resp := container[entry]
resp.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
container[entry] = resp
case swspec.Parameter:
if err := rewriteParentRef(spec, "#"+parent, ref); err != nil {
return err
}
case map[string]swspec.Parameter:
param := container[entry]
param.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
container[entry] = param
case []swspec.Parameter:
idx, err := strconv.Atoi(entry)
if err != nil {
return fmt.Errorf("%s not a number: %v", key[1:], err)
}
param := container[idx]
param.Schema = &swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
container[idx] = param
case swspec.Definitions:
container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
case map[string]swspec.Schema:
container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
case []swspec.Schema:
idx, err := strconv.Atoi(entry)
if err != nil {
return fmt.Errorf("%s not a number: %v", key[1:], err)
}
container[idx] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
case *swspec.SchemaOrArray:
// NOTE: this is necessarily an array - otherwise, the parent would be *Schema
idx, err := strconv.Atoi(entry)
if err != nil {
return fmt.Errorf("%s not a number: %v", key[1:], err)
}
container.Schemas[idx] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
// NOTE: can't have case *swspec.SchemaOrBool = parent in this case is *Schema
default:
return fmt.Errorf("unhandled parent schema rewrite %s (%T)", key, pvalue)
}
return nil
}
func cloneSchema(schema *swspec.Schema) (*swspec.Schema, error) {
var sch swspec.Schema
if err := swag.FromDynamicJSON(schema, &sch); err != nil {
return nil, fmt.Errorf("cannot clone schema: %v", err)
}
return &sch, nil
}
// importExternalReferences iteratively digs remote references and imports them into the main schema.
//
// At every iteration, new remotes may be found when digging deeper: they are rebased to the current schema before being imported.
//
// This returns true when no more remote references can be found.
func importExternalReferences(opts *FlattenOpts) (bool, error) {
debugLog("importExternalReferences")
groupedRefs := reverseIndexForSchemaRefs(opts)
sortedRefStr := make([]string, 0, len(groupedRefs))
if opts.flattenContext == nil {
opts.flattenContext = newContext()
}
// sort $ref resolution to ensure deterministic name conflict resolution
for refStr := range groupedRefs {
sortedRefStr = append(sortedRefStr, refStr)
}
sort.Strings(sortedRefStr)
complete := true
for _, refStr := range sortedRefStr {
entry := groupedRefs[refStr]
if entry.Ref.HasFragmentOnly {
continue
}
complete = false
var isOAIGen bool
newName := opts.flattenContext.resolved[refStr]
if newName != "" {
// rewrite ref with already resolved external ref (useful for cyclical refs):
// rewrite external refs to local ones
debugLog("resolving known ref [%s] to %s", refStr, newName)
for _, key := range entry.Keys {
if err := updateRef(opts.Swagger(), key,
swspec.MustCreateRef(slashpath.Join(definitionsPath, newName))); err != nil {
return false, err
}
}
} else {
// resolve schemas
debugLog("resolving schema from remote $ref [%s]", refStr)
sch, err := swspec.ResolveRefWithBase(opts.Swagger(), &entry.Ref, opts.ExpandOpts(false))
if err != nil {
return false, fmt.Errorf("could not resolve schema: %v", err)
}
// at this stage only $ref analysis matters
partialAnalyzer := &Spec{
references: referenceAnalysis{},
patterns: patternAnalysis{},
enums: enumAnalysis{},
}
partialAnalyzer.reset()
partialAnalyzer.analyzeSchema("", sch, "/")
// now rewrite those refs with rebase
for key, ref := range partialAnalyzer.references.allRefs {
if err := updateRef(sch, key, swspec.MustCreateRef(rebaseRef(entry.Ref.String(), ref.String()))); err != nil {
return false, fmt.Errorf("failed to rewrite ref for key %q at %s: %v", key, entry.Ref.String(), err)
}
}
// generate a unique name - isOAIGen means that a naming conflict was resolved by changing the name
newName, isOAIGen = uniqifyName(opts.Swagger().Definitions, nameFromRef(entry.Ref))
debugLog("new name for [%s]: %s - with name conflict:%t",
strings.Join(entry.Keys, ", "), newName, isOAIGen)
opts.flattenContext.resolved[refStr] = newName
// rewrite the external refs to local ones
for _, key := range entry.Keys {
if err := updateRef(opts.Swagger(), key,
swspec.MustCreateRef(slashpath.Join(definitionsPath, newName))); err != nil {
return false, err
}
// keep track of created refs
resolved := false
if _, ok := opts.flattenContext.newRefs[key]; ok {
resolved = opts.flattenContext.newRefs[key].resolved
}
debugLog("keeping track of ref: %s (%s), resolved: %t", key, newName, resolved)
opts.flattenContext.newRefs[key] = &newRef{
key: key,
newName: newName,
path: slashpath.Join(definitionsPath, newName),
isOAIGen: isOAIGen,
resolved: resolved,
schema: sch,
}
}
// add the resolved schema to the definitions
saveSchema(opts.Swagger(), newName, sch)
}
}
// maintains ref index entries
for k := range opts.flattenContext.newRefs {
r := opts.flattenContext.newRefs[k]
// update tracking with resolved schemas
if r.schema.Ref.String() != "" {
ref := swspec.MustCreateRef(r.path)
sch, err := swspec.ResolveRefWithBase(opts.Swagger(), &ref, opts.ExpandOpts(false))
if err != nil {
return false, fmt.Errorf("could not resolve schema: %v", err)
}
r.schema = sch
}
// update tracking with renamed keys: got a cascade of refs
if r.path != k {
renamed := *r
renamed.key = r.path
opts.flattenContext.newRefs[renamed.path] = &renamed
// indirect ref
r.newName = slashpath.Base(k)
r.schema = swspec.RefSchema(r.path)
r.path = k
r.isOAIGen = strings.Contains(k, "OAIGen")
}
}
return complete, nil
}
type refRevIdx struct {
Ref swspec.Ref
Keys []string
}
// rebaseRef rebase a remote ref relative to a base ref.
//
// NOTE: does not support JSONschema ID for $ref (we assume we are working with swagger specs here).
//
// NOTE(windows):
// * refs are assumed to have been normalized with drive letter lower cased (from go-openapi/spec)
// * "/ in paths may appear as escape sequences
func rebaseRef(baseRef string, ref string) string {
debugLog("rebasing ref: %s onto %s", ref, baseRef)
baseRef, _ = url.PathUnescape(baseRef)
ref, _ = url.PathUnescape(ref)
if baseRef == "" || baseRef == "." || strings.HasPrefix(baseRef, "#") {
return ref
}
parts := strings.Split(ref, "#")
baseParts := strings.Split(baseRef, "#")
baseURL, _ := url.Parse(baseParts[0])
if strings.HasPrefix(ref, "#") {
if baseURL.Host == "" {
return strings.Join([]string{baseParts[0], parts[1]}, "#")
}
return strings.Join([]string{baseParts[0], parts[1]}, "#")
}
refURL, _ := url.Parse(parts[0])
if refURL.Host != "" || filepath.IsAbs(parts[0]) {
// not rebasing an absolute path
return ref
}
// there is a relative path
var basePath string
if baseURL.Host != "" {
// when there is a host, standard URI rules apply (with "/")
baseURL.Path = slashpath.Dir(baseURL.Path)
baseURL.Path = slashpath.Join(baseURL.Path, "/"+parts[0])
return baseURL.String()
}
// this is a local relative path
// basePart[0] and parts[0] are local filesystem directories/files
basePath = filepath.Dir(baseParts[0])
relPath := filepath.Join(basePath, string(filepath.Separator)+parts[0])
if len(parts) > 1 {
return strings.Join([]string{relPath, parts[1]}, "#")
}
return relPath
}
// normalizePath renders absolute path on remote file refs
//
// NOTE(windows):
// * refs are assumed to have been normalized with drive letter lower cased (from go-openapi/spec)
// * "/ in paths may appear as escape sequences
func normalizePath(ref swspec.Ref, opts *FlattenOpts) (normalizedPath string) {
uri, _ := url.PathUnescape(ref.String())
if ref.HasFragmentOnly || filepath.IsAbs(uri) {
normalizedPath = uri
return
}
refURL, _ := url.Parse(uri)
if refURL.Host != "" {
normalizedPath = uri
return
}
parts := strings.Split(uri, "#")
// BasePath, parts[0] are local filesystem directories, guaranteed to be absolute at this stage
parts[0] = filepath.Join(filepath.Dir(opts.BasePath), parts[0])
normalizedPath = strings.Join(parts, "#")
return
}
func reverseIndexForSchemaRefs(opts *FlattenOpts) map[string]refRevIdx {
collected := make(map[string]refRevIdx)
for key, schRef := range opts.Spec.references.schemas {
// normalize paths before sorting,
// so we get together keys in same external file
normalizedPath := normalizePath(schRef, opts)
if entry, ok := collected[normalizedPath]; ok {
entry.Keys = append(entry.Keys, key)
collected[normalizedPath] = entry
} else {
collected[normalizedPath] = refRevIdx{
Ref: schRef,
Keys: []string{key},
}
}
}
return collected
}
func nameFromRef(ref swspec.Ref) string {
u := ref.GetURL()
if u.Fragment != "" {
return swag.ToJSONName(slashpath.Base(u.Fragment))
}
if u.Path != "" {
bn := slashpath.Base(u.Path)
if bn != "" && bn != "/" {
ext := slashpath.Ext(bn)
if ext != "" {
return swag.ToJSONName(bn[:len(bn)-len(ext)])
}
return swag.ToJSONName(bn)
}
}
return swag.ToJSONName(strings.Replace(u.Host, ".", " ", -1))
}
func saveSchema(spec *swspec.Swagger, name string, schema *swspec.Schema) {
if schema == nil {
return
}
if spec.Definitions == nil {
spec.Definitions = make(map[string]swspec.Schema, 150)
}
spec.Definitions[name] = *schema
}
// getPointerFromKey retrieves the content of the JSON pointer "key"
func getPointerFromKey(spec interface{}, key string) (string, interface{}, error) {
switch spec.(type) {
case *swspec.Schema:
case *swspec.Swagger:
default:
panic("unexpected type used in getPointerFromKey")
}
if key == "#/" {
return "", spec, nil
}
// unescape chars in key, e.g. "{}" from path params
pth, _ := internal.PathUnescape(key[1:])
ptr, err := jsonpointer.New(pth)
if err != nil {
return "", nil, err
}
value, _, err := ptr.Get(spec)
if err != nil {
debugLog("error when getting key: %s with path: %s", key, pth)
return "", nil, err
}
return pth, value, nil
}
// getParentFromKey retrieves the container of the JSON pointer "key"
func getParentFromKey(spec interface{}, key string) (string, string, interface{}, error) {
switch spec.(type) {
case *swspec.Schema:
case *swspec.Swagger:
default:
panic("unexpected type used in getPointerFromKey")
}
// unescape chars in key, e.g. "{}" from path params
pth, _ := internal.PathUnescape(key[1:])
parent, entry := slashpath.Dir(pth), slashpath.Base(pth)
debugLog("getting schema holder at: %s, with entry: %s", parent, entry)
pptr, err := jsonpointer.New(parent)
if err != nil {
return "", "", nil, err
}
pvalue, _, err := pptr.Get(spec)
if err != nil {
return "", "", nil, fmt.Errorf("can't get parent for %s: %v", parent, err)
}
return parent, entry, pvalue, nil
}
// updateRef replaces a ref by another one
func updateRef(spec interface{}, key string, ref swspec.Ref) error {
switch spec.(type) {
case *swspec.Schema:
case *swspec.Swagger:
default:
panic("unexpected type used in getPointerFromKey")
}
debugLog("updating ref for %s with %s", key, ref.String())
pth, value, err := getPointerFromKey(spec, key)
if err != nil {
return err
}
switch refable := value.(type) {
case *swspec.Schema:
refable.Ref = ref
case *swspec.SchemaOrArray:
if refable.Schema != nil {
refable.Schema.Ref = ref
}
case *swspec.SchemaOrBool:
if refable.Schema != nil {
refable.Schema.Ref = ref
}
case swspec.Schema:
debugLog("rewriting holder for %T", refable)
_, entry, pvalue, erp := getParentFromKey(spec, key)
if erp != nil {
return err
}
switch container := pvalue.(type) {
case swspec.Definitions:
container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
case map[string]swspec.Schema:
container[entry] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
case []swspec.Schema:
idx, err := strconv.Atoi(entry)
if err != nil {
return fmt.Errorf("%s not a number: %v", pth, err)
}
container[idx] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
case *swspec.SchemaOrArray:
// NOTE: this is necessarily an array - otherwise, the parent would be *Schema
idx, err := strconv.Atoi(entry)
if err != nil {
return fmt.Errorf("%s not a number: %v", pth, err)
}
container.Schemas[idx] = swspec.Schema{SchemaProps: swspec.SchemaProps{Ref: ref}}
// NOTE: can't have case *swspec.SchemaOrBool = parent in this case is *Schema
default:
return fmt.Errorf("unhandled container type at %s: %T", key, value)
}
default:
return fmt.Errorf("no schema with ref found at %s for %T", key, value)
}
return nil
}
// updateRefWithSchema replaces a ref with a schema (i.e. re-inline schema)
func updateRefWithSchema(spec *swspec.Swagger, key string, sch *swspec.Schema) error {
debugLog("updating ref for %s with schema", key)
pth, value, err := getPointerFromKey(spec, key)
if err != nil {
return err
}
switch refable := value.(type) {
case *swspec.Schema:
*refable = *sch
case swspec.Schema:
_, entry, pvalue, erp := getParentFromKey(spec, key)
if erp != nil {
return err
}
switch container := pvalue.(type) {
case swspec.Definitions:
container[entry] = *sch
case map[string]swspec.Schema:
container[entry] = *sch
case []swspec.Schema:
idx, err := strconv.Atoi(entry)
if err != nil {
return fmt.Errorf("%s not a number: %v", pth, err)
}
container[idx] = *sch
case *swspec.SchemaOrArray:
// NOTE: this is necessarily an array - otherwise, the parent would be *Schema
idx, err := strconv.Atoi(entry)
if err != nil {
return fmt.Errorf("%s not a number: %v", pth, err)
}
container.Schemas[idx] = *sch
// NOTE: can't have case *swspec.SchemaOrBool = parent in this case is *Schema
default:
return fmt.Errorf("unhandled type for parent of [%s]: %T", key, value)
}
case *swspec.SchemaOrArray:
*refable.Schema = *sch
// NOTE: can't have case *swspec.SchemaOrBool = parent in this case is *Schema
case *swspec.SchemaOrBool:
*refable.Schema = *sch
default:
return fmt.Errorf("no schema with ref found at %s for %T", key, value)
}
return nil
}
func containsString(names []string, name string) bool {
for _, nm := range names {
if nm == name {
return true
}
}
return false
}
type opRef struct {
Method string
Path string
Key string
ID string
Op *swspec.Operation
Ref swspec.Ref
}
type opRefs []opRef
func (o opRefs) Len() int { return len(o) }
func (o opRefs) Swap(i, j int) { o[i], o[j] = o[j], o[i] }
func (o opRefs) Less(i, j int) bool { return o[i].Key < o[j].Key }
func gatherOperations(specDoc *Spec, operationIDs []string) map[string]opRef {
var oprefs opRefs
for method, pathItem := range specDoc.Operations() {
for pth, operation := range pathItem {
vv := *operation
oprefs = append(oprefs, opRef{
Key: swag.ToGoName(strings.ToLower(method) + " " + pth),
Method: method,
Path: pth,
ID: vv.ID,
Op: &vv,
Ref: swspec.MustCreateRef("#" + slashpath.Join("/paths", jsonpointer.Escape(pth), method)),
})
}
}
sort.Sort(oprefs)
operations := make(map[string]opRef)
for _, opr := range oprefs {
nm := opr.ID
if nm == "" {
nm = opr.Key
}
oo, found := operations[nm]
if found && oo.Method != opr.Method && oo.Path != opr.Path {
nm = opr.Key
}
if len(operationIDs) == 0 || containsString(operationIDs, opr.ID) || containsString(operationIDs, nm) {
opr.ID = nm
opr.Op.ID = nm
operations[nm] = opr
}
}
return operations
}
// stripPointersAndOAIGen removes anonymous JSON pointers from spec and chain with name conflicts handler.
// This loops until the spec has no such pointer and all name conflicts have been reduced as much as possible.
func stripPointersAndOAIGen(opts *FlattenOpts) error {
// name all JSON pointers to anonymous documents
if err := namePointers(opts); err != nil {
return err
}
// remove unnecessary OAIGen ref (created when flattening external refs creates name conflicts)
hasIntroducedPointerOrInline, ers := stripOAIGen(opts)
if ers != nil {
return ers
}
// iterate as pointer or OAIGen resolution may introduce inline schemas or pointers
for hasIntroducedPointerOrInline {
if !opts.Minimal {
opts.Spec.reload() // re-analyze
if err := nameInlinedSchemas(opts); err != nil {
return err
}
}
if err := namePointers(opts); err != nil {
return err
}
// restrip and re-analyze
if hasIntroducedPointerOrInline, ers = stripOAIGen(opts); ers != nil {
return ers
}
}
return nil
}
func updateRefParents(opts *FlattenOpts, r *newRef) {
if !r.isOAIGen || r.resolved { // bail on already resolved entries (avoid looping)
return
}
for k, v := range opts.Spec.references.allRefs {
if r.path != v.String() {
continue
}
found := false
for _, p := range r.parents {
if p == k {
found = true
break
}
}
if !found {
r.parents = append(r.parents, k)
}
}
}
// topMostRefs is able to sort refs by hierarchical then lexicographic order,
// yielding refs ordered breadth-first.
type topmostRefs []string
func (k topmostRefs) Len() int { return len(k) }
func (k topmostRefs) Swap(i, j int) { k[i], k[j] = k[j], k[i] }
func (k topmostRefs) Less(i, j int) bool {
li, lj := len(strings.Split(k[i], "/")), len(strings.Split(k[j], "/"))
if li == lj {
return k[i] < k[j]
}
return li < lj
}
func topmostFirst(refs []string) []string {
res := topmostRefs(refs)
sort.Sort(res)
return res
}
// stripOAIGen strips the spec from unnecessary OAIGen constructs, initially created to dedupe flattened definitions.
//
// A dedupe is deemed unnecessary whenever:
// - the only conflict is with its (single) parent: OAIGen is merged into its parent (reinlining)
// - there is a conflict with multiple parents: merge OAIGen in first parent, the rewrite other parents to point to
// the first parent.
//
// This function returns true whenever it re-inlined a complex schema, so the caller may chose to iterate
// pointer and name resolution again.
func stripOAIGen(opts *FlattenOpts) (bool, error) {
debugLog("stripOAIGen")
replacedWithComplex := false
// figure out referers of OAIGen definitions (doing it before the ref start mutating)
for _, r := range opts.flattenContext.newRefs {
updateRefParents(opts, r)
}
for k := range opts.flattenContext.newRefs {
r := opts.flattenContext.newRefs[k]
debugLog("newRefs[%s]: isOAIGen: %t, resolved: %t, name: %s, path:%s, #parents: %d, parents: %v, ref: %s",
k, r.isOAIGen, r.resolved, r.newName, r.path, len(r.parents), r.parents, r.schema.Ref.String())
if r.isOAIGen && len(r.parents) >= 1 {
pr := topmostFirst(r.parents)
// rewrite first parent schema in hierarchical then lexicographical order
debugLog("rewrite first parent %s with schema", pr[0])
if err := updateRefWithSchema(opts.Swagger(), pr[0], r.schema); err != nil {
return false, err
}
if pa, ok := opts.flattenContext.newRefs[pr[0]]; ok && pa.isOAIGen {
// update parent in ref index entry
debugLog("update parent entry: %s", pr[0])
pa.schema = r.schema
pa.resolved = false
replacedWithComplex = true
}
// rewrite other parents to point to first parent
if len(pr) > 1 {
for _, p := range pr[1:] {
replacingRef := swspec.MustCreateRef(pr[0])
// set complex when replacing ref is an anonymous jsonpointer: further processing may be required
replacedWithComplex = replacedWithComplex ||
slashpath.Dir(replacingRef.String()) != definitionsPath
debugLog("rewrite parent with ref: %s", replacingRef.String())
// NOTE: it is possible at this stage to introduce json pointers (to non-definitions places).
// Those are stripped later on.
if err := updateRef(opts.Swagger(), p, replacingRef); err != nil {
return false, err
}
if pa, ok := opts.flattenContext.newRefs[p]; ok && pa.isOAIGen {
// update parent in ref index
debugLog("update parent entry: %s", p)
pa.schema = r.schema
pa.resolved = false
replacedWithComplex = true
}
}
}
// remove OAIGen definition
debugLog("removing definition %s", slashpath.Base(r.path))
delete(opts.Swagger().Definitions, slashpath.Base(r.path))
// propagate changes in ref index for keys which have this one as a parent
for kk, value := range opts.flattenContext.newRefs {
if kk == k || !value.isOAIGen || value.resolved {
continue
}
found := false
newParents := make([]string, 0, len(value.parents))
for _, parent := range value.parents {
switch {
case parent == r.path:
found = true
parent = pr[0]
case strings.HasPrefix(parent, r.path+"/"):
found = true
parent = slashpath.Join(pr[0], strings.TrimPrefix(parent, r.path))
}
newParents = append(newParents, parent)
}
if found {
value.parents = newParents
}
}
// mark naming conflict as resolved
debugLog("marking naming conflict resolved for key: %s", r.key)
opts.flattenContext.newRefs[r.key].isOAIGen = false
opts.flattenContext.newRefs[r.key].resolved = true
// determine if the previous substitution did inline a complex schema
if r.schema != nil && r.schema.Ref.String() == "" { // inline schema
asch, err := Schema(SchemaOpts{Schema: r.schema, Root: opts.Swagger(), BasePath: opts.BasePath})
if err != nil {
return false, err
}
debugLog("re-inlined schema: parent: %s, %t", pr[0], isAnalyzedAsComplex(asch))
replacedWithComplex = replacedWithComplex ||
!(slashpath.Dir(pr[0]) == definitionsPath) && isAnalyzedAsComplex(asch)
}
}
}
debugLog("replacedWithComplex: %t", replacedWithComplex)
opts.Spec.reload() // re-analyze
return replacedWithComplex, nil
}
// croak logs notifications and warnings about valid, but possibly unwanted constructs resulting
// from flattening a spec
func croak(opts *FlattenOpts) {
reported := make(map[string]bool, len(opts.flattenContext.newRefs))
for _, v := range opts.Spec.references.allRefs {
// warns about duplicate handling
for _, r := range opts.flattenContext.newRefs {
if r.isOAIGen && r.path == v.String() {
reported[r.newName] = true
}
}
}
for k := range reported {
log.Printf("warning: duplicate flattened definition name resolved as %s", k)
}
// warns about possible type mismatches
uniqueMsg := make(map[string]bool)
for _, msg := range opts.flattenContext.warnings {
if _, ok := uniqueMsg[msg]; ok {
continue
}
log.Printf("warning: %s", msg)
uniqueMsg[msg] = true
}
}
// namePointers replaces all JSON pointers to anonymous documents by a $ref to a new named definitions.
//
// This is carried on depth-first. Pointers to $refs which are top level definitions are replaced by the $ref itself.
// Pointers to simple types are expanded, unless they express commonality (i.e. several such $ref are used).
func namePointers(opts *FlattenOpts) error {
debugLog("name pointers")
refsToReplace := make(map[string]SchemaRef, len(opts.Spec.references.schemas))
for k, ref := range opts.Spec.references.allRefs {
if slashpath.Dir(ref.String()) == definitionsPath {
// this a ref to a top-level definition: ok
continue
}
replacingRef, sch, erd := deepestRef(opts, ref)
if erd != nil {
return fmt.Errorf("at %s, %v", k, erd)
}
debugLog("planning pointer to replace at %s: %s, resolved to: %s", k, ref.String(), replacingRef.String())
refsToReplace[k] = SchemaRef{
Name: k, // caller
Ref: replacingRef, // callee
Schema: sch,
TopLevel: slashpath.Dir(replacingRef.String()) == definitionsPath,
}
}
depthFirst := sortDepthFirst(refsToReplace)
namer := &inlineSchemaNamer{
Spec: opts.Swagger(),
Operations: opRefsByRef(gatherOperations(opts.Spec, nil)),
flattenContext: opts.flattenContext,
opts: opts,
}
for _, key := range depthFirst {
v := refsToReplace[key]
// update current replacement, which may have been updated by previous changes of deeper elements
replacingRef, sch, erd := deepestRef(opts, v.Ref)
if erd != nil {
return fmt.Errorf("at %s, %v", key, erd)
}
v.Ref = replacingRef
v.Schema = sch
v.TopLevel = slashpath.Dir(replacingRef.String()) == definitionsPath
debugLog("replacing pointer at %s: resolved to: %s", key, v.Ref.String())
if v.TopLevel {
debugLog("replace pointer %s by canonical definition: %s", key, v.Ref.String())
// if the schema is a $ref to a top level definition, just rewrite the pointer to this $ref
if err := updateRef(opts.Swagger(), key, v.Ref); err != nil {
return err
}
} else {
// this is a JSON pointer to an anonymous document (internal or external):
// create a definition for this schema when:
// - it is a complex schema
// - or it is pointed by more than one $ref (i.e. expresses commonality)
// otherwise, expand the pointer (single reference to a simple type)
//
// The named definition for this follows the target's key, not the caller's
debugLog("namePointers at %s for %s", key, v.Ref.String())
// qualify the expanded schema
/*
if key == "#/paths/~1some~1where~1{id}/get/parameters/1/items" {
// DEBUG
//func getPointerFromKey(spec interface{}, key string) (string, interface{}, error) {
k, res, err := getPointerFromKey(namer.Spec, key)
debugLog("k = %s, res=%#v, err=%v", k, res, err)
}
*/
asch, ers := Schema(SchemaOpts{Schema: v.Schema, Root: opts.Swagger(), BasePath: opts.BasePath})
if ers != nil {
return fmt.Errorf("schema analysis [%s]: %v", key, ers)
}
callers := make([]string, 0, 64)
debugLog("looking for callers")
an := New(opts.Swagger())
for k, w := range an.references.allRefs {
r, _, erd := deepestRef(opts, w)
if erd != nil {
return fmt.Errorf("at %s, %v", key, erd)
}
if r.String() == v.Ref.String() {
callers = append(callers, k)
}
}
debugLog("callers for %s: %d", v.Ref.String(), len(callers))
if len(callers) == 0 {
// has already been updated and resolved
continue
}
parts := keyParts(v.Ref.String())
debugLog("number of callers for %s: %d", v.Ref.String(), len(callers))
// identifying edge case when the namer did nothing because we point to a non-schema object
// no definition is created and we expand the $ref for all callers
if (!asch.IsSimpleSchema || len(callers) > 1) && !parts.IsSharedParam() && !parts.IsSharedResponse() {
debugLog("replace JSON pointer at [%s] by definition: %s", key, v.Ref.String())
if err := namer.Name(v.Ref.String(), v.Schema, asch); err != nil {
return err
}
// regular case: we named the $ref as a definition, and we move all callers to this new $ref
for _, caller := range callers {
if caller != key {
// move $ref for next to resolve
debugLog("identified caller of %s at [%s]", v.Ref.String(), caller)
c := refsToReplace[caller]
c.Ref = v.Ref
refsToReplace[caller] = c
}
}
} else {
debugLog("expand JSON pointer for key=%s", key)
if err := updateRefWithSchema(opts.Swagger(), key, v.Schema); err != nil {
return err
}
// NOTE: there is no other caller to update
}
}
}
opts.Spec.reload() // re-analyze
return nil
}
// deepestRef finds the first definition ref, from a cascade of nested refs which are not definitions.
// - if no definition is found, returns the deepest ref.
// - pointers to external files are expanded
//
// NOTE: all external $ref's are assumed to be already expanded at this stage.
func deepestRef(opts *FlattenOpts, ref swspec.Ref) (swspec.Ref, *swspec.Schema, error) {
if !ref.HasFragmentOnly {
// we found an external $ref, which is odd
// does nothing on external $refs
return ref, nil, nil
}
currentRef := ref
visited := make(map[string]bool, 64)
DOWNREF:
for currentRef.String() != "" {
if slashpath.Dir(currentRef.String()) == definitionsPath {
// this is a top-level definition: stop here and return this ref
return currentRef, nil, nil
}
if _, beenThere := visited[currentRef.String()]; beenThere {
return swspec.Ref{}, nil,
fmt.Errorf("cannot resolve cyclic chain of pointers under %s", currentRef.String())
}
visited[currentRef.String()] = true
value, _, err := currentRef.GetPointer().Get(opts.Swagger())
if err != nil {
return swspec.Ref{}, nil, err
}
switch refable := value.(type) {
case *swspec.Schema:
if refable.Ref.String() == "" {
break DOWNREF
}
currentRef = refable.Ref
case swspec.Schema:
if refable.Ref.String() == "" {
break DOWNREF
}
currentRef = refable.Ref
case *swspec.SchemaOrArray:
if refable.Schema == nil || refable.Schema != nil && refable.Schema.Ref.String() == "" {
break DOWNREF
}
currentRef = refable.Schema.Ref
case *swspec.SchemaOrBool:
if refable.Schema == nil || refable.Schema != nil && refable.Schema.Ref.String() == "" {
break DOWNREF
}
currentRef = refable.Schema.Ref
case swspec.Response:
// a pointer points to a schema initially marshalled in responses section...
// Attempt to convert this to a schema. If this fails, the spec is invalid
asJSON, _ := refable.MarshalJSON()
var asSchema swspec.Schema
err := asSchema.UnmarshalJSON(asJSON)
if err != nil {
return swspec.Ref{}, nil,
fmt.Errorf("invalid type for resolved JSON pointer %s. Expected a schema a, got: %T",
currentRef.String(), value)
}
opts.flattenContext.warnings = append(opts.flattenContext.warnings,
fmt.Sprintf("found $ref %q (response) interpreted as schema", currentRef.String()))
if asSchema.Ref.String() == "" {
break DOWNREF
}
currentRef = asSchema.Ref
case swspec.Parameter:
// a pointer points to a schema initially marshalled in parameters section...
// Attempt to convert this to a schema. If this fails, the spec is invalid
asJSON, _ := refable.MarshalJSON()
var asSchema swspec.Schema
err := asSchema.UnmarshalJSON(asJSON)
if err != nil {
return swspec.Ref{}, nil,
fmt.Errorf("invalid type for resolved JSON pointer %s. Expected a schema a, got: %T",
currentRef.String(), value)
}
opts.flattenContext.warnings = append(opts.flattenContext.warnings,
fmt.Sprintf("found $ref %q (parameter) interpreted as schema", currentRef.String()))
if asSchema.Ref.String() == "" {
break DOWNREF
}
currentRef = asSchema.Ref
default:
return swspec.Ref{}, nil,
fmt.Errorf("unhandled type to resolve JSON pointer %s. Expected a Schema, got: %T",
currentRef.String(), value)
}
}
// assess what schema we're ending with
sch, erv := swspec.ResolveRefWithBase(opts.Swagger(), &currentRef, 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
}