-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathref.go
More file actions
244 lines (222 loc) · 6.63 KB
/
ref.go
File metadata and controls
244 lines (222 loc) · 6.63 KB
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
package openapi
import (
"encoding/json"
"fmt"
"strings"
"go.yaml.in/yaml/v4"
)
// Ref is a simple object to allow referencing other components in the OpenAPI document, internally and externally.
// The $ref string value contains a URI [RFC3986], which identifies the location of the value being referenced.
// See the rules for resolving Relative References.
//
// https://spec.openapis.org/oas/v3.1.1#reference-object
//
// Example:
//
// $ref: '#/components/schemas/Pet'
type Ref struct {
// REQUIRED.
// The reference identifier.
// This MUST be in the form of a URI.
Ref string `json:"$ref" yaml:"$ref"`
// A short summary which by default SHOULD override that of the referenced component.
// If the referenced object-type does not allow a summary field, then this field has no effect.
Summary string `json:"summary,omitempty" yaml:"summary,omitempty"`
// A description which by default SHOULD override that of the referenced component.
// CommonMark syntax MAY be used for rich text representation.
// If the referenced object-type does not allow a description field, then this field has no effect.
Description string `json:"description,omitempty" yaml:"description,omitempty"`
}
// RefOrSpec holds either Ref or any OpenAPI spec type.
//
// NOTE: The Ref object takes precedent over Spec if using json or yaml Marshal and Unmarshal functions.
type RefOrSpec[T any] struct {
Ref *Ref `json:"-" yaml:"-"`
Spec *T `json:"-" yaml:"-"`
}
// NewRefOrSpec creates an object of RefOrSpec type from given Ref or string or any form of Spec.
func NewRefOrSpec[T any](v any) *RefOrSpec[T] {
o := RefOrSpec[T]{}
switch t := v.(type) {
case *Ref:
o.Ref = t
case Ref:
o.Ref = &t
case string:
o.Ref = &Ref{Ref: t}
case nil:
case *T:
o.Spec = t
case T:
o.Spec = &t
}
return &o
}
// NewRefOrExtSpec creates an object of RefOrSpec[Extendable[any]] type from given Ref or string or any form of Spec.
func NewRefOrExtSpec[T any](v any) *RefOrSpec[Extendable[T]] {
o := RefOrSpec[Extendable[T]]{}
switch t := v.(type) {
case *Ref:
o.Ref = t
case Ref:
o.Ref = &t
case string:
o.Ref = &Ref{Ref: t}
case nil:
case *T:
o.Spec = NewExtendable[T](t)
case T:
o.Spec = NewExtendable[T](&t)
}
return &o
}
func (o *RefOrSpec[T]) getLocationOrRef(location string) string {
if o.Ref != nil {
return o.Ref.Ref
}
return location
}
// GetSpec return a Spec if it is set or loads it from Components in case of Ref or an error
func (o *RefOrSpec[T]) GetSpec(c *Extendable[Components]) (*T, error) {
return o.getSpec(c, make(visitedObjects))
}
const specNotFoundPrefix = "spec not found: "
type SpecNotFoundError struct {
message string
visitedObjects string
}
func (e *SpecNotFoundError) Error() string {
return specNotFoundPrefix + e.message + "; visited refs: " + e.visitedObjects
}
func (e *SpecNotFoundError) Is(target error) bool {
return strings.HasPrefix(target.Error(), specNotFoundPrefix)
}
func NewSpecNotFoundError(message string, visitedObjects visitedObjects) error {
return &SpecNotFoundError{
message: message,
visitedObjects: visitedObjects.String(),
}
}
func (o *RefOrSpec[T]) getSpec(c *Extendable[Components], visited visitedObjects) (*T, error) {
// some guards
switch {
case o.Spec != nil:
return o.Spec, nil
case o.Ref == nil:
return nil, NewSpecNotFoundError("nil Ref", visited)
case visited[o.Ref.Ref]:
return nil, NewSpecNotFoundError(fmt.Sprintf("cycle ref %q detected", o.Ref.Ref), visited)
case !strings.HasPrefix(o.Ref.Ref, "#/components/"):
// TODO: support loading by url
return nil, NewSpecNotFoundError(fmt.Sprintf("loading outside of components is not implemented for the ref %q", o.Ref.Ref), visited)
case c == nil:
return nil, NewSpecNotFoundError("components is required, but got nil", visited)
}
visited[o.Ref.Ref] = true
parts := strings.SplitN(o.Ref.Ref[13:], "/", 2)
if len(parts) != 2 {
return nil, NewSpecNotFoundError(fmt.Sprintf("incorrect ref %q", o.Ref.Ref), visited)
}
objName := parts[1]
var ref any
switch parts[0] {
case "schemas":
ref = c.Spec.Schemas[objName]
case "responses":
ref = c.Spec.Responses[objName]
case "parameters":
ref = c.Spec.Parameters[objName]
case "examples":
ref = c.Spec.Examples[objName]
case "requestBodies":
ref = c.Spec.RequestBodies[objName]
case "headers":
ref = c.Spec.Headers[objName]
case "links":
ref = c.Spec.Links[objName]
case "callbacks":
ref = c.Spec.Callbacks[objName]
case "paths":
ref = c.Spec.Paths[objName]
default:
return nil, NewSpecNotFoundError(fmt.Sprintf("unexpected component name %q in ref %q", parts[0], o.Ref.Ref), visited)
}
obj, ok := ref.(*RefOrSpec[T])
if !ok {
return nil, NewSpecNotFoundError(fmt.Sprintf("expected spec of type %T, but got %T", RefOrSpec[T]{}, ref), visited)
}
if obj.Spec != nil {
return obj.Spec, nil
}
return obj.getSpec(c, visited)
}
// MarshalJSON implements json.Marshaler interface.
func (o *RefOrSpec[T]) MarshalJSON() ([]byte, error) {
var v any
if o.Ref != nil {
v = o.Ref
} else {
v = o.Spec
}
data, err := json.Marshal(&v)
if err != nil {
return nil, fmt.Errorf("%T: %w", o.Spec, err)
}
return data, nil
}
// UnmarshalJSON implements json.Unmarshaler interface.
func (o *RefOrSpec[T]) UnmarshalJSON(data []byte) error {
if json.Unmarshal(data, &o.Ref) == nil && o.Ref.Ref != "" {
o.Spec = nil
return nil
}
o.Ref = nil
if err := json.Unmarshal(data, &o.Spec); err != nil {
return fmt.Errorf("%T: %w", o.Spec, err)
}
return nil
}
// MarshalYAML implements yaml.Marshaler interface.
func (o *RefOrSpec[T]) MarshalYAML() (any, error) {
var v any
if o.Ref != nil {
v = o.Ref
} else {
v = o.Spec
}
return v, nil
}
// UnmarshalYAML implements yaml.Unmarshaler interface.
func (o *RefOrSpec[T]) UnmarshalYAML(node *yaml.Node) error {
if node.Decode(&o.Ref) == nil && o.Ref.Ref != "" {
return nil
}
o.Ref = nil
if err := node.Decode(&o.Spec); err != nil {
return fmt.Errorf("%T: %w", o.Spec, err)
}
return nil
}
func (o *RefOrSpec[T]) validateSpec(location string, validator *Validator) []*validationError {
var errs []*validationError
if o.Spec != nil {
if spec, ok := any(o.Spec).(validatable); ok {
errs = append(errs, spec.validateSpec(location, validator)...)
} else {
errs = append(errs, newValidationError(location, NewUnsupportedSpecTypeError(o.Spec)))
}
} else {
// do not validate already visited refs
if validator.visited[o.Ref.Ref] {
return errs
}
validator.visited[o.Ref.Ref] = true
spec, err := o.GetSpec(validator.spec.Spec.Components)
if err != nil {
errs = append(errs, newValidationError(location, err))
} else if spec != nil {
errs = append(errs, o.validateSpec(location, validator)...)
}
}
return errs
}