1package analysis
2
3import (
4	"fmt"
5	"path"
6	"sort"
7	"strings"
8
9	"github.com/go-openapi/analysis/internal/flatten/operations"
10	"github.com/go-openapi/analysis/internal/flatten/replace"
11	"github.com/go-openapi/analysis/internal/flatten/schutils"
12	"github.com/go-openapi/analysis/internal/flatten/sortref"
13	"github.com/go-openapi/spec"
14	"github.com/go-openapi/swag"
15)
16
17// InlineSchemaNamer finds a new name for an inlined type
18type InlineSchemaNamer struct {
19	Spec           *spec.Swagger
20	Operations     map[string]operations.OpRef
21	flattenContext *context
22	opts           *FlattenOpts
23}
24
25// Name yields a new name for the inline schema
26func (isn *InlineSchemaNamer) Name(key string, schema *spec.Schema, aschema *AnalyzedSchema) error {
27	debugLog("naming inlined schema at %s", key)
28
29	parts := sortref.KeyParts(key)
30	for _, name := range namesFromKey(parts, aschema, isn.Operations) {
31		if name == "" {
32			continue
33		}
34
35		// create unique name
36		newName, isOAIGen := uniqifyName(isn.Spec.Definitions, swag.ToJSONName(name))
37
38		// clone schema
39		sch := schutils.Clone(schema)
40
41		// replace values on schema
42		if err := replace.RewriteSchemaToRef(isn.Spec, key,
43			spec.MustCreateRef(path.Join(definitionsPath, newName))); err != nil {
44			return fmt.Errorf("error while creating definition %q from inline schema: %w", newName, err)
45		}
46
47		// rewrite any dependent $ref pointing to this place,
48		// when not already pointing to a top-level definition.
49		//
50		// NOTE: this is important if such referers use arbitrary JSON pointers.
51		an := New(isn.Spec)
52		for k, v := range an.references.allRefs {
53			r, erd := replace.DeepestRef(isn.opts.Swagger(), isn.opts.ExpandOpts(false), v)
54			if erd != nil {
55				return fmt.Errorf("at %s, %w", k, erd)
56			}
57
58			if isn.opts.flattenContext != nil {
59				isn.opts.flattenContext.warnings = append(isn.opts.flattenContext.warnings, r.Warnings...)
60			}
61
62			if r.Ref.String() != key && (r.Ref.String() != path.Join(definitionsPath, newName) || path.Dir(v.String()) == definitionsPath) {
63				continue
64			}
65
66			debugLog("found a $ref to a rewritten schema: %s points to %s", k, v.String())
67
68			// rewrite $ref to the new target
69			if err := replace.UpdateRef(isn.Spec, k,
70				spec.MustCreateRef(path.Join(definitionsPath, newName))); err != nil {
71				return err
72			}
73		}
74
75		// NOTE: this extension is currently not used by go-swagger (provided for information only)
76		sch.AddExtension("x-go-gen-location", GenLocation(parts))
77
78		// save cloned schema to definitions
79		schutils.Save(isn.Spec, newName, sch)
80
81		// keep track of created refs
82		if isn.flattenContext == nil {
83			continue
84		}
85
86		debugLog("track created ref: key=%s, newName=%s, isOAIGen=%t", key, newName, isOAIGen)
87		resolved := false
88
89		if _, ok := isn.flattenContext.newRefs[key]; ok {
90			resolved = isn.flattenContext.newRefs[key].resolved
91		}
92
93		isn.flattenContext.newRefs[key] = &newRef{
94			key:      key,
95			newName:  newName,
96			path:     path.Join(definitionsPath, newName),
97			isOAIGen: isOAIGen,
98			resolved: resolved,
99			schema:   sch,
100		}
101	}
102
103	return nil
104}
105
106// uniqifyName yields a unique name for a definition
107func uniqifyName(definitions spec.Definitions, name string) (string, bool) {
108	isOAIGen := false
109	if name == "" {
110		name = "oaiGen"
111		isOAIGen = true
112	}
113
114	if len(definitions) == 0 {
115		return name, isOAIGen
116	}
117
118	unq := true
119	for k := range definitions {
120		if strings.EqualFold(k, name) {
121			unq = false
122
123			break
124		}
125	}
126
127	if unq {
128		return name, isOAIGen
129	}
130
131	name += "OAIGen"
132	isOAIGen = true
133	var idx int
134	unique := name
135	_, known := definitions[unique]
136
137	for known {
138		idx++
139		unique = fmt.Sprintf("%s%d", name, idx)
140		_, known = definitions[unique]
141	}
142
143	return unique, isOAIGen
144}
145
146func namesFromKey(parts sortref.SplitKey, aschema *AnalyzedSchema, operations map[string]operations.OpRef) []string {
147	var (
148		baseNames  [][]string
149		startIndex int
150	)
151
152	if parts.IsOperation() {
153		baseNames, startIndex = namesForOperation(parts, operations)
154	}
155
156	// definitions
157	if parts.IsDefinition() {
158		baseNames, startIndex = namesForDefinition(parts)
159	}
160
161	result := make([]string, 0, len(baseNames))
162	for _, segments := range baseNames {
163		nm := parts.BuildName(segments, startIndex, partAdder(aschema))
164		if nm == "" {
165			continue
166		}
167
168		result = append(result, nm)
169	}
170	sort.Strings(result)
171
172	return result
173}
174
175func namesForParam(parts sortref.SplitKey, operations map[string]operations.OpRef) ([][]string, int) {
176	var (
177		baseNames  [][]string
178		startIndex int
179	)
180
181	piref := parts.PathItemRef()
182	if piref.String() != "" && parts.IsOperationParam() {
183		if op, ok := operations[piref.String()]; ok {
184			startIndex = 5
185			baseNames = append(baseNames, []string{op.ID, "params", "body"})
186		}
187	} else if parts.IsSharedOperationParam() {
188		pref := parts.PathRef()
189		for k, v := range operations {
190			if strings.HasPrefix(k, pref.String()) {
191				startIndex = 4
192				baseNames = append(baseNames, []string{v.ID, "params", "body"})
193			}
194		}
195	}
196
197	return baseNames, startIndex
198}
199
200func namesForOperation(parts sortref.SplitKey, operations map[string]operations.OpRef) ([][]string, int) {
201	var (
202		baseNames  [][]string
203		startIndex int
204	)
205
206	// params
207	if parts.IsOperationParam() || parts.IsSharedOperationParam() {
208		baseNames, startIndex = namesForParam(parts, operations)
209	}
210
211	// responses
212	if parts.IsOperationResponse() {
213		piref := parts.PathItemRef()
214		if piref.String() != "" {
215			if op, ok := operations[piref.String()]; ok {
216				startIndex = 6
217				baseNames = append(baseNames, []string{op.ID, parts.ResponseName(), "body"})
218			}
219		}
220	}
221
222	return baseNames, startIndex
223}
224
225func namesForDefinition(parts sortref.SplitKey) ([][]string, int) {
226	nm := parts.DefinitionName()
227	if nm != "" {
228		return [][]string{{parts.DefinitionName()}}, 2
229	}
230
231	return [][]string{}, 0
232}
233
234// partAdder knows how to interpret a schema when it comes to build a name from parts
235func partAdder(aschema *AnalyzedSchema) sortref.PartAdder {
236	return func(part string) []string {
237		segments := make([]string, 0, 2)
238
239		if part == "items" || part == "additionalItems" {
240			if aschema.IsTuple || aschema.IsTupleWithExtra {
241				segments = append(segments, "tuple")
242			} else {
243				segments = append(segments, "items")
244			}
245
246			if part == "additionalItems" {
247				segments = append(segments, part)
248			}
249
250			return segments
251		}
252
253		segments = append(segments, part)
254
255		return segments
256	}
257}
258
259func nameFromRef(ref spec.Ref) string {
260	u := ref.GetURL()
261	if u.Fragment != "" {
262		return swag.ToJSONName(path.Base(u.Fragment))
263	}
264
265	if u.Path != "" {
266		bn := path.Base(u.Path)
267		if bn != "" && bn != "/" {
268			ext := path.Ext(bn)
269			if ext != "" {
270				return swag.ToJSONName(bn[:len(bn)-len(ext)])
271			}
272
273			return swag.ToJSONName(bn)
274		}
275	}
276
277	return swag.ToJSONName(strings.ReplaceAll(u.Host, ".", " "))
278}
279
280// GenLocation indicates from which section of the specification (models or operations) a definition has been created.
281//
282// This is reflected in the output spec with a "x-go-gen-location" extension. At the moment, this is is provided
283// for information only.
284func GenLocation(parts sortref.SplitKey) string {
285	switch {
286	case parts.IsOperation():
287		return "operations"
288	case parts.IsDefinition():
289		return "models"
290	default:
291		return ""
292	}
293}
294