309 lines
7.5 KiB
Go
309 lines
7.5 KiB
Go
package analysis
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/go-openapi/analysis/internal/flatten/operations"
|
|
"github.com/go-openapi/analysis/internal/flatten/replace"
|
|
"github.com/go-openapi/analysis/internal/flatten/schutils"
|
|
"github.com/go-openapi/analysis/internal/flatten/sortref"
|
|
"github.com/go-openapi/spec"
|
|
"github.com/go-openapi/swag"
|
|
)
|
|
|
|
// InlineSchemaNamer finds a new name for an inlined type
|
|
type InlineSchemaNamer struct {
|
|
Spec *spec.Swagger
|
|
Operations map[string]operations.OpRef
|
|
flattenContext *context
|
|
opts *FlattenOpts
|
|
}
|
|
|
|
// Name yields a new name for the inline schema
|
|
func (isn *InlineSchemaNamer) Name(key string, schema *spec.Schema, aschema *AnalyzedSchema) error {
|
|
debugLog("naming inlined schema at %s", key)
|
|
|
|
parts := sortref.KeyParts(key)
|
|
for _, name := range namesFromKey(parts, aschema, isn.Operations) {
|
|
if name == "" {
|
|
continue
|
|
}
|
|
|
|
// create unique name
|
|
mangle := mangler(isn.opts)
|
|
newName, isOAIGen := uniqifyName(isn.Spec.Definitions, mangle(name))
|
|
|
|
// clone schema
|
|
sch := schutils.Clone(schema)
|
|
|
|
// replace values on schema
|
|
debugLog("rewriting schema to ref: key=%s with new name: %s", key, newName)
|
|
if err := replace.RewriteSchemaToRef(isn.Spec, key,
|
|
spec.MustCreateRef(path.Join(definitionsPath, newName))); err != nil {
|
|
return fmt.Errorf("error while creating definition %q from inline schema: %w", 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 := replace.DeepestRef(isn.opts.Swagger(), isn.opts.ExpandOpts(false), v)
|
|
if erd != nil {
|
|
return fmt.Errorf("at %s, %w", k, erd)
|
|
}
|
|
|
|
if isn.opts.flattenContext != nil {
|
|
isn.opts.flattenContext.warnings = append(isn.opts.flattenContext.warnings, r.Warnings...)
|
|
}
|
|
|
|
if r.Ref.String() != key && (r.Ref.String() != path.Join(definitionsPath, newName) || path.Dir(v.String()) == definitionsPath) {
|
|
continue
|
|
}
|
|
|
|
debugLog("found a $ref to a rewritten schema: %s points to %s", k, v.String())
|
|
|
|
// rewrite $ref to the new target
|
|
if err := replace.UpdateRef(isn.Spec, k,
|
|
spec.MustCreateRef(path.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
|
|
schutils.Save(isn.Spec, newName, sch)
|
|
|
|
// keep track of created refs
|
|
if isn.flattenContext == nil {
|
|
continue
|
|
}
|
|
|
|
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: path.Join(definitionsPath, newName),
|
|
isOAIGen: isOAIGen,
|
|
resolved: resolved,
|
|
schema: sch,
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// uniqifyName yields a unique name for a definition
|
|
func uniqifyName(definitions spec.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 sortref.SplitKey, aschema *AnalyzedSchema, operations map[string]operations.OpRef) []string {
|
|
var (
|
|
baseNames [][]string
|
|
startIndex int
|
|
)
|
|
|
|
switch {
|
|
case parts.IsOperation():
|
|
baseNames, startIndex = namesForOperation(parts, operations)
|
|
case parts.IsDefinition():
|
|
baseNames, startIndex = namesForDefinition(parts)
|
|
default:
|
|
// this a non-standard pointer: build a name by concatenating its parts
|
|
baseNames = [][]string{parts}
|
|
startIndex = len(baseNames) + 1
|
|
}
|
|
|
|
result := make([]string, 0, len(baseNames))
|
|
for _, segments := range baseNames {
|
|
nm := parts.BuildName(segments, startIndex, partAdder(aschema))
|
|
if nm == "" {
|
|
continue
|
|
}
|
|
|
|
result = append(result, nm)
|
|
}
|
|
sort.Strings(result)
|
|
|
|
debugLog("names from parts: %v => %v", parts, result)
|
|
return result
|
|
}
|
|
|
|
func namesForParam(parts sortref.SplitKey, operations map[string]operations.OpRef) ([][]string, int) {
|
|
var (
|
|
baseNames [][]string
|
|
startIndex int
|
|
)
|
|
|
|
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"})
|
|
}
|
|
}
|
|
}
|
|
|
|
return baseNames, startIndex
|
|
}
|
|
|
|
func namesForOperation(parts sortref.SplitKey, operations map[string]operations.OpRef) ([][]string, int) {
|
|
var (
|
|
baseNames [][]string
|
|
startIndex int
|
|
)
|
|
|
|
// params
|
|
if parts.IsOperationParam() || parts.IsSharedOperationParam() {
|
|
baseNames, startIndex = namesForParam(parts, operations)
|
|
}
|
|
|
|
// 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"})
|
|
}
|
|
}
|
|
}
|
|
|
|
return baseNames, startIndex
|
|
}
|
|
|
|
func namesForDefinition(parts sortref.SplitKey) ([][]string, int) {
|
|
nm := parts.DefinitionName()
|
|
if nm != "" {
|
|
return [][]string{{parts.DefinitionName()}}, 2
|
|
}
|
|
|
|
return [][]string{}, 0
|
|
}
|
|
|
|
// partAdder knows how to interpret a schema when it comes to build a name from parts
|
|
func partAdder(aschema *AnalyzedSchema) sortref.PartAdder {
|
|
return func(part string) []string {
|
|
segments := make([]string, 0, 2)
|
|
|
|
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)
|
|
}
|
|
|
|
return segments
|
|
}
|
|
|
|
segments = append(segments, part)
|
|
|
|
return segments
|
|
}
|
|
}
|
|
|
|
func mangler(o *FlattenOpts) func(string) string {
|
|
if o.KeepNames {
|
|
return func(in string) string { return in }
|
|
}
|
|
|
|
return swag.ToJSONName
|
|
}
|
|
|
|
func nameFromRef(ref spec.Ref, o *FlattenOpts) string {
|
|
mangle := mangler(o)
|
|
|
|
u := ref.GetURL()
|
|
if u.Fragment != "" {
|
|
return mangle(path.Base(u.Fragment))
|
|
}
|
|
|
|
if u.Path != "" {
|
|
bn := path.Base(u.Path)
|
|
if bn != "" && bn != "/" {
|
|
ext := path.Ext(bn)
|
|
if ext != "" {
|
|
return mangle(bn[:len(bn)-len(ext)])
|
|
}
|
|
|
|
return mangle(bn)
|
|
}
|
|
}
|
|
|
|
return mangle(strings.ReplaceAll(u.Host, ".", " "))
|
|
}
|
|
|
|
// 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 provided
|
|
// for information only.
|
|
func GenLocation(parts sortref.SplitKey) string {
|
|
switch {
|
|
case parts.IsOperation():
|
|
return "operations"
|
|
case parts.IsDefinition():
|
|
return "models"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|