Skip to content

Commit 5b198a8

Browse files
authored
Add webhooks support (#149)
1 parent ba6524e commit 5b198a8

File tree

3 files changed

+123
-10
lines changed

3 files changed

+123
-10
lines changed

openapi31/helper.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,41 @@ func (s *Spec) AddOperation(method, path string, operation Operation) error {
182182
})
183183
}
184184

185+
// AddWebhook validates and sets webhook by name and method.
186+
//
187+
// It will fail if webhook with same name already exists.
188+
func (s *Spec) AddWebhook(method, name string, operation Operation) error {
189+
if _, found := s.Webhooks[name]; found {
190+
return fmt.Errorf("duplicate webhook name: %s", name)
191+
}
192+
193+
method = strings.ToLower(method)
194+
195+
// Add "No Content" response if there are no responses configured.
196+
if len(operation.ResponsesEns().MapOfResponseOrReferenceValues) == 0 && operation.Responses.Default == nil {
197+
operation.Responses.WithMapOfResponseOrReferenceValuesItem(strconv.Itoa(http.StatusNoContent), ResponseOrReference{
198+
Response: &Response{
199+
Description: http.StatusText(http.StatusNoContent),
200+
},
201+
})
202+
}
203+
204+
method, _, _, err := openapi.SanitizeMethodPath(method, "/")
205+
if err != nil {
206+
return err
207+
}
208+
209+
pathItem := PathItem{}
210+
211+
if err := pathItem.SetOperation(method, &operation); err != nil {
212+
return err
213+
}
214+
215+
s.WithWebhooksItem(name, pathItem.PathItemOrReference())
216+
217+
return nil
218+
}
219+
185220
// UnknownParamIsForbidden indicates forbidden unknown parameters.
186221
func (o Operation) UnknownParamIsForbidden(in ParameterIn) bool {
187222
f, ok := o.MapOfAnything[xForbidUnknown+string(in)].(bool)
@@ -281,3 +316,10 @@ func (r *RequestBodyOrReference) SetReference(ref string) {
281316
r.ReferenceEns().Ref = ref
282317
r.RequestBody = nil
283318
}
319+
320+
// PathItemOrReference exposes PathItem as union type.
321+
func (p *PathItem) PathItemOrReference() PathItemOrReference {
322+
return PathItemOrReference{
323+
PathItem: p,
324+
}
325+
}

openapi31/reflect.go

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func NewReflector() *Reflector {
2828

2929
r.DefaultOptions = append(r.DefaultOptions, jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (stop bool, err error) {
3030
// See https://spec.openapis.org/oas/v3.1.0.html#data-types.
31-
switch params.Value.Kind() { //nolint:exhaustive // Not all kinds have formats defined.
31+
switch params.Value.Kind() { //nolint // Not all kinds have formats defined.
3232
case reflect.Int64:
3333
params.Schema.WithFormat("int64")
3434
case reflect.Int32:
@@ -46,9 +46,9 @@ func NewReflector() *Reflector {
4646
}
4747

4848
// NewOperationContext initializes openapi.OperationContext to be prepared
49-
// and added later with Reflector.AddOperation.
50-
func (r *Reflector) NewOperationContext(method, pathPattern string) (openapi.OperationContext, error) {
51-
method, pathPattern, pathParams, err := openapi.SanitizeMethodPath(method, pathPattern)
49+
// and added later with Reflector.AddOperation or Reflector.AddWebhook.
50+
func (r *Reflector) NewOperationContext(method, pathPatternOrWebhookName string) (openapi.OperationContext, error) {
51+
method, pathPattern, pathParams, err := openapi.SanitizeMethodPath(method, pathPatternOrWebhookName)
5252
if err != nil {
5353
return nil, err
5454
}
@@ -61,7 +61,7 @@ func (r *Reflector) NewOperationContext(method, pathPattern string) (openapi.Ope
6161
}
6262

6363
if operation != nil {
64-
return nil, fmt.Errorf("operation already exists: %s %s", method, pathPattern)
64+
return nil, fmt.Errorf("operation already exists: %s %s", method, pathPatternOrWebhookName)
6565
}
6666

6767
operation = &Operation{}
@@ -211,24 +211,43 @@ func (o operationContext) Operation() *Operation {
211211

212212
// AddOperation configures operation request and response schema.
213213
func (r *Reflector) AddOperation(oc openapi.OperationContext) error {
214+
c, err := r.setupOC(oc)
215+
if err != nil {
216+
return err
217+
}
218+
219+
return r.SpecEns().AddOperation(oc.Method(), oc.PathPattern(), *c.op)
220+
}
221+
222+
// AddWebhook configures webhook request and response schema.
223+
func (r *Reflector) AddWebhook(oc openapi.OperationContext) error {
224+
c, err := r.setupOC(oc)
225+
if err != nil {
226+
return err
227+
}
228+
229+
return r.SpecEns().AddWebhook(oc.Method(), c.PathPattern(), *c.op)
230+
}
231+
232+
func (r *Reflector) setupOC(oc openapi.OperationContext) (operationContext, error) {
214233
c, ok := oc.(operationContext)
215234
if !ok {
216-
return fmt.Errorf("wrong operation context %T received, %T expected", oc, operationContext{})
235+
return c, fmt.Errorf("wrong operation context %T received, %T expected", oc, operationContext{})
217236
}
218237

219238
if err := r.setupRequest(c.op, oc); err != nil {
220-
return fmt.Errorf("setup request %s %s: %w", oc.Method(), oc.PathPattern(), err)
239+
return c, fmt.Errorf("setup request %s %s: %w", oc.Method(), oc.PathPattern(), err)
221240
}
222241

223242
if err := c.op.validatePathParams(c.pathParams); err != nil {
224-
return fmt.Errorf("validate path params %s %s: %w", oc.Method(), oc.PathPattern(), err)
243+
return c, fmt.Errorf("validate path params %s %s: %w", oc.Method(), oc.PathPattern(), err)
225244
}
226245

227246
if err := r.setupResponse(c.op, oc); err != nil {
228-
return fmt.Errorf("setup response %s %s: %w", oc.Method(), oc.PathPattern(), err)
247+
return c, fmt.Errorf("setup response %s %s: %w", oc.Method(), oc.PathPattern(), err)
229248
}
230249

231-
return r.SpecEns().AddOperation(oc.Method(), oc.PathPattern(), *c.op)
250+
return c, nil
232251
}
233252

234253
func (r *Reflector) setupRequest(o *Operation, oc openapi.OperationContext) error {

openapi31/reflect_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1594,3 +1594,55 @@ func TestSelfReference(t *testing.T) {
15941594
}
15951595
}`, reflector.SpecSchema())
15961596
}
1597+
1598+
func TestReflector_AddWebhook(t *testing.T) {
1599+
r := openapi31.NewReflector()
1600+
1601+
oc, err := r.NewOperationContext(http.MethodPost, "newPet")
1602+
require.NoError(t, err)
1603+
1604+
type Pet struct {
1605+
Name string `json:"name"`
1606+
Breed string `json:"breed"`
1607+
}
1608+
1609+
oc.AddReqStructure(Pet{}, func(cu *openapi.ContentUnit) {
1610+
cu.Description = "Information about a new pet in the system"
1611+
})
1612+
1613+
oc.AddRespStructure(nil, func(cu *openapi.ContentUnit) {
1614+
cu.Description = "Return a 200 status to indicate that the data was received successfully"
1615+
cu.HTTPStatus = http.StatusOK
1616+
})
1617+
1618+
require.NoError(t, r.AddWebhook(oc))
1619+
1620+
assertjson.EqMarshal(t, `{
1621+
"openapi":"3.1.0","info":{"title":"","version":""},"paths":{},
1622+
"webhooks":{
1623+
"newPet":{
1624+
"post":{
1625+
"requestBody":{
1626+
"description":"Information about a new pet in the system",
1627+
"content":{
1628+
"application/json":{"schema":{"$ref":"#/components/schemas/Openapi31TestPet"}}
1629+
}
1630+
},
1631+
"responses":{
1632+
"200":{
1633+
"description":"Return a 200 status to indicate that the data was received successfully"
1634+
}
1635+
}
1636+
}
1637+
}
1638+
},
1639+
"components":{
1640+
"schemas":{
1641+
"Openapi31TestPet":{
1642+
"properties":{"breed":{"type":"string"},"name":{"type":"string"}},
1643+
"type":"object"
1644+
}
1645+
}
1646+
}
1647+
}`, r.SpecEns())
1648+
}

0 commit comments

Comments
 (0)