summaryrefslogtreecommitdiff
path: root/vendor/github.com/go-openapi/analysis/internal/flatten/replace/replace.go
blob: 26c2a05a3101ec601546c81cfceed0f2b2a0beb8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
package replace

import (
	"fmt"
	"net/url"
	"os"
	"path"
	"strconv"

	"github.com/go-openapi/analysis/internal/debug"
	"github.com/go-openapi/jsonpointer"
	"github.com/go-openapi/spec"
)

const definitionsPath = "#/definitions"

var debugLog = debug.GetLogger("analysis/flatten/replace", os.Getenv("SWAGGER_DEBUG") != "")

// RewriteSchemaToRef replaces a schema with a Ref
func RewriteSchemaToRef(sp *spec.Swagger, key string, ref spec.Ref) error {
	debugLog("rewriting schema to ref for %s with %s", key, ref.String())
	_, value, err := getPointerFromKey(sp, key)
	if err != nil {
		return err
	}

	switch refable := value.(type) {
	case *spec.Schema:
		return rewriteParentRef(sp, key, ref)

	case spec.Schema:
		return rewriteParentRef(sp, key, ref)

	case *spec.SchemaOrArray:
		if refable.Schema != nil {
			refable.Schema = &spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}
		}

	case *spec.SchemaOrBool:
		if refable.Schema != nil {
			refable.Schema = &spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}
		}
	default:
		return fmt.Errorf("no schema with ref found at %s for %T", key, value)
	}

	return nil
}

func rewriteParentRef(sp *spec.Swagger, key string, ref spec.Ref) error {
	parent, entry, pvalue, err := getParentFromKey(sp, key)
	if err != nil {
		return err
	}

	debugLog("rewriting holder for %T", pvalue)
	switch container := pvalue.(type) {
	case spec.Response:
		if err := rewriteParentRef(sp, "#"+parent, ref); err != nil {
			return err
		}

	case *spec.Response:
		container.Schema = &spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}

	case *spec.Responses:
		statusCode, err := strconv.Atoi(entry)
		if err != nil {
			return fmt.Errorf("%s not a number: %w", key[1:], err)
		}
		resp := container.StatusCodeResponses[statusCode]
		resp.Schema = &spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}
		container.StatusCodeResponses[statusCode] = resp

	case map[string]spec.Response:
		resp := container[entry]
		resp.Schema = &spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}
		container[entry] = resp

	case spec.Parameter:
		if err := rewriteParentRef(sp, "#"+parent, ref); err != nil {
			return err
		}

	case map[string]spec.Parameter:
		param := container[entry]
		param.Schema = &spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}
		container[entry] = param

	case []spec.Parameter:
		idx, err := strconv.Atoi(entry)
		if err != nil {
			return fmt.Errorf("%s not a number: %w", key[1:], err)
		}
		param := container[idx]
		param.Schema = &spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}
		container[idx] = param

	case spec.Definitions:
		container[entry] = spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}

	case map[string]spec.Schema:
		container[entry] = spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}

	case []spec.Schema:
		idx, err := strconv.Atoi(entry)
		if err != nil {
			return fmt.Errorf("%s not a number: %w", key[1:], err)
		}
		container[idx] = spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}

	case *spec.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: %w", key[1:], err)
		}
		container.Schemas[idx] = spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}

	case spec.SchemaProperties:
		container[entry] = spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}

	// NOTE: can't have case *spec.SchemaOrBool = parent in this case is *Schema

	default:
		return fmt.Errorf("unhandled parent schema rewrite %s (%T)", key, pvalue)
	}

	return nil
}

// getPointerFromKey retrieves the content of the JSON pointer "key"
func getPointerFromKey(sp interface{}, key string) (string, interface{}, error) {
	switch sp.(type) {
	case *spec.Schema:
	case *spec.Swagger:
	default:
		panic("unexpected type used in getPointerFromKey")
	}
	if key == "#/" {
		return "", sp, nil
	}
	// unescape chars in key, e.g. "{}" from path params
	pth, _ := url.PathUnescape(key[1:])
	ptr, err := jsonpointer.New(pth)
	if err != nil {
		return "", nil, err
	}

	value, _, err := ptr.Get(sp)
	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(sp interface{}, key string) (string, string, interface{}, error) {
	switch sp.(type) {
	case *spec.Schema:
	case *spec.Swagger:
	default:
		panic("unexpected type used in getPointerFromKey")
	}
	// unescape chars in key, e.g. "{}" from path params
	pth, _ := url.PathUnescape(key[1:])

	parent, entry := path.Dir(pth), path.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(sp)
	if err != nil {
		return "", "", nil, fmt.Errorf("can't get parent for %s: %w", parent, err)
	}

	return parent, entry, pvalue, nil
}

// UpdateRef replaces a ref by another one
func UpdateRef(sp interface{}, key string, ref spec.Ref) error {
	switch sp.(type) {
	case *spec.Schema:
	case *spec.Swagger:
	default:
		panic("unexpected type used in getPointerFromKey")
	}
	debugLog("updating ref for %s with %s", key, ref.String())
	pth, value, err := getPointerFromKey(sp, key)
	if err != nil {
		return err
	}

	switch refable := value.(type) {
	case *spec.Schema:
		refable.Ref = ref
	case *spec.SchemaOrArray:
		if refable.Schema != nil {
			refable.Schema.Ref = ref
		}
	case *spec.SchemaOrBool:
		if refable.Schema != nil {
			refable.Schema.Ref = ref
		}
	case spec.Schema:
		debugLog("rewriting holder for %T", refable)
		_, entry, pvalue, erp := getParentFromKey(sp, key)
		if erp != nil {
			return err
		}
		switch container := pvalue.(type) {
		case spec.Definitions:
			container[entry] = spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}

		case map[string]spec.Schema:
			container[entry] = spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}

		case []spec.Schema:
			idx, err := strconv.Atoi(entry)
			if err != nil {
				return fmt.Errorf("%s not a number: %w", pth, err)
			}
			container[idx] = spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}

		case *spec.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: %w", pth, err)
			}
			container.Schemas[idx] = spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}

		case spec.SchemaProperties:
			container[entry] = spec.Schema{SchemaProps: spec.SchemaProps{Ref: ref}}

		// NOTE: can't have case *spec.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(sp *spec.Swagger, key string, sch *spec.Schema) error {
	debugLog("updating ref for %s with schema", key)
	pth, value, err := getPointerFromKey(sp, key)
	if err != nil {
		return err
	}

	switch refable := value.(type) {
	case *spec.Schema:
		*refable = *sch
	case spec.Schema:
		_, entry, pvalue, erp := getParentFromKey(sp, key)
		if erp != nil {
			return err
		}
		switch container := pvalue.(type) {
		case spec.Definitions:
			container[entry] = *sch

		case map[string]spec.Schema:
			container[entry] = *sch

		case []spec.Schema:
			idx, err := strconv.Atoi(entry)
			if err != nil {
				return fmt.Errorf("%s not a number: %w", pth, err)
			}
			container[idx] = *sch

		case *spec.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: %w", pth, err)
			}
			container.Schemas[idx] = *sch

		case spec.SchemaProperties:
			container[entry] = *sch

		// NOTE: can't have case *spec.SchemaOrBool = parent in this case is *Schema

		default:
			return fmt.Errorf("unhandled type for parent of [%s]: %T", key, value)
		}
	case *spec.SchemaOrArray:
		*refable.Schema = *sch
	// NOTE: can't have case *spec.SchemaOrBool = parent in this case is *Schema
	case *spec.SchemaOrBool:
		*refable.Schema = *sch
	default:
		return fmt.Errorf("no schema with ref found at %s for %T", key, value)
	}

	return nil
}

// DeepestRefResult holds the results from DeepestRef analysis
type DeepestRefResult struct {
	Ref      spec.Ref
	Schema   *spec.Schema
	Warnings []string
}

// 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(sp *spec.Swagger, opts *spec.ExpandOptions, ref spec.Ref) (*DeepestRefResult, error) {
	if !ref.HasFragmentOnly {
		// we found an external $ref, which is odd at this stage:
		// do nothing on external $refs
		return &DeepestRefResult{Ref: ref}, nil
	}

	currentRef := ref
	visited := make(map[string]bool, 64)
	warnings := make([]string, 0, 2)

DOWNREF:
	for currentRef.String() != "" {
		if path.Dir(currentRef.String()) == definitionsPath {
			// this is a top-level definition: stop here and return this ref
			return &DeepestRefResult{Ref: currentRef}, nil
		}

		if _, beenThere := visited[currentRef.String()]; beenThere {
			return nil,
				fmt.Errorf("cannot resolve cyclic chain of pointers under %s", currentRef.String())
		}

		visited[currentRef.String()] = true
		value, _, err := currentRef.GetPointer().Get(sp)
		if err != nil {
			return nil, err
		}

		switch refable := value.(type) {
		case *spec.Schema:
			if refable.Ref.String() == "" {
				break DOWNREF
			}
			currentRef = refable.Ref

		case spec.Schema:
			if refable.Ref.String() == "" {
				break DOWNREF
			}
			currentRef = refable.Ref

		case *spec.SchemaOrArray:
			if refable.Schema == nil || refable.Schema != nil && refable.Schema.Ref.String() == "" {
				break DOWNREF
			}
			currentRef = refable.Schema.Ref

		case *spec.SchemaOrBool:
			if refable.Schema == nil || refable.Schema != nil && refable.Schema.Ref.String() == "" {
				break DOWNREF
			}
			currentRef = refable.Schema.Ref

		case spec.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 spec.Schema

			err := asSchema.UnmarshalJSON(asJSON)
			if err != nil {
				return nil,
					fmt.Errorf("invalid type for resolved JSON pointer %s. Expected a schema a, got: %T",
						currentRef.String(), value)
			}
			warnings = append(warnings, fmt.Sprintf("found $ref %q (response) interpreted as schema", currentRef.String()))

			if asSchema.Ref.String() == "" {
				break DOWNREF
			}
			currentRef = asSchema.Ref

		case spec.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 spec.Schema
			if err := asSchema.UnmarshalJSON(asJSON); err != nil {
				return nil,
					fmt.Errorf("invalid type for resolved JSON pointer %s. Expected a schema a, got: %T",
						currentRef.String(), value)
			}

			warnings = append(warnings, fmt.Sprintf("found $ref %q (parameter) interpreted as schema", currentRef.String()))

			if asSchema.Ref.String() == "" {
				break DOWNREF
			}
			currentRef = asSchema.Ref

		default:
			return 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 := spec.ResolveRefWithBase(sp, &currentRef, opts)
	if erv != nil {
		return nil, erv
	}

	if sch == nil {
		return nil, fmt.Errorf("no schema found at %s", currentRef.String())
	}

	return &DeepestRefResult{Ref: currentRef, Schema: sch, Warnings: warnings}, nil
}