Skip to content

Commit

Permalink
Follow callbacks references (#347)
Browse files Browse the repository at this point in the history
  • Loading branch information
fenollp authored Apr 23, 2021
1 parent 1b47cce commit f6c20c3
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 1 deletion.
28 changes: 28 additions & 0 deletions openapi3/issue301_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package openapi3

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestIssue301(t *testing.T) {
sl := NewSwaggerLoader()
sl.IsExternalRefsAllowed = true

doc, err := sl.LoadSwaggerFromFile("testdata/callbacks.yml")
require.NoError(t, err)

err = doc.Validate(sl.Context)
require.NoError(t, err)

transCallbacks := doc.Paths["/trans"].Post.Callbacks["transactionCallback"].Value
require.Equal(t, "object", (*transCallbacks)["http://notificationServer.com?transactionId={$request.body#/id}&email={$request.body#/email}"].Post.RequestBody.
Value.Content["application/json"].Schema.
Value.Type)

otherCallbacks := doc.Paths["/other"].Post.Callbacks["myEvent"].Value
require.Equal(t, "boolean", (*otherCallbacks)["{$request.query.queryUrl}"].Post.RequestBody.
Value.Content["application/json"].Schema.
Value.Type)
}
103 changes: 102 additions & 1 deletion openapi3/swagger_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func (swaggerLoader *SwaggerLoader) allowsExternalRefs(ref string) (err error) {
}

// loadSingleElementFromURI reads the data from ref and unmarshals to the passed element.
func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPath *url.URL, element json.Unmarshaler) (*url.URL, error) {
func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPath *url.URL, element interface{}) (*url.URL, error) {
if err := swaggerLoader.allowsExternalRefs(ref); err != nil {
return nil, err
}
Expand Down Expand Up @@ -221,6 +221,11 @@ func (swaggerLoader *SwaggerLoader) ResolveRefsIn(swagger *Swagger, location *ur
return
}
}
for _, component := range components.Callbacks {
if err = swaggerLoader.resolveCallbackRef(swagger, component, location); err != nil {
return
}
}

// Visit all operations
for entrypoint, pathItem := range swagger.Paths {
Expand Down Expand Up @@ -799,6 +804,94 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen
return nil
}

func (swaggerLoader *SwaggerLoader) resolveCallbackRef(swagger *Swagger, component *CallbackRef, documentPath *url.URL) (err error) {

if component == nil {
return errors.New("invalid callback: value MUST be an object")
}
if ref := component.Ref; ref != "" {
if isSingleRefElement(ref) {
var resolved Callback
if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &resolved); err != nil {
return err
}
component.Value = &resolved
} else {
var resolved CallbackRef
componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath, &resolved)
if err != nil {
return err
}
if err := swaggerLoader.resolveCallbackRef(swagger, &resolved, componentPath); err != nil {
return err
}
component.Value = resolved.Value
}
}
value := component.Value
if value == nil {
return nil
}

for entrypoint, pathItem := range *value {
entrypoint, pathItem := entrypoint, pathItem
err = func() (err error) {
key := "-"
if documentPath != nil {
key = documentPath.EscapedPath()
}
key += entrypoint
if _, ok := swaggerLoader.visitedPathItemRefs[key]; ok {
return nil
}
swaggerLoader.visitedPathItemRefs[key] = struct{}{}

if pathItem == nil {
return errors.New("invalid path item: value MUST be an object")
}
ref := pathItem.Ref
if ref != "" {
if isSingleRefElement(ref) {
var p PathItem
if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &p); err != nil {
return err
}
*pathItem = p
} else {
if swagger, ref, documentPath, err = swaggerLoader.resolveRefSwagger(swagger, ref, documentPath); err != nil {
return
}

rest := strings.TrimPrefix(ref, "#/components/callbacks/")
if rest == ref {
return fmt.Errorf(`expected prefix "#/components/callbacks/" in URI %q`, ref)
}
id := unescapeRefString(rest)

definitions := swagger.Components.Callbacks
if definitions == nil {
return failedToResolveRefFragmentPart(ref, "callbacks")
}
resolved := definitions[id]
if resolved == nil {
return failedToResolveRefFragmentPart(ref, id)
}

for _, p := range *resolved.Value {
*pathItem = *p
break
}
}
}
return swaggerLoader.resolvePathItemRefContinued(swagger, pathItem, documentPath)
}()
if err != nil {
return err
}
}
return nil
}

func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component *LinkRef, documentPath *url.URL) (err error) {
if component != nil && component.Value != nil {
if swaggerLoader.visitedLink == nil {
Expand Down Expand Up @@ -880,7 +973,10 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo
*pathItem = *resolved
}
}
return swaggerLoader.resolvePathItemRefContinued(swagger, pathItem, documentPath)
}

func (swaggerLoader *SwaggerLoader) resolvePathItemRefContinued(swagger *Swagger, pathItem *PathItem, documentPath *url.URL) (err error) {
for _, parameter := range pathItem.Parameters {
if err = swaggerLoader.resolveParameterRef(swagger, parameter, documentPath); err != nil {
return
Expand All @@ -902,6 +998,11 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo
return
}
}
for _, callback := range operation.Callbacks {
if err = swaggerLoader.resolveCallbackRef(swagger, callback, documentPath); err != nil {
return
}
}
}
return
}
Expand Down
10 changes: 10 additions & 0 deletions openapi3/testdata/callback-transactioned.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
post:
requestBody:
description: Callback payload
content:
'application/json':
schema:
$ref: 'callbacks.yml#/components/schemas/SomePayload'
responses:
'200':
description: callback successfully processed
71 changes: 71 additions & 0 deletions openapi3/testdata/callbacks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
openapi: 3.1.0
info:
title: Callback refd
version: 1.2.3
paths:
/trans:
post:
description: ''
requestBody:
description: ''
content:
'application/json':
schema:
properties:
id: {type: string}
email: {format: email}
responses:
'201':
description: subscription successfully created
content:
application/json:
schema:
type: object
callbacks:
transactionCallback:
'http://notificationServer.com?transactionId={$request.body#/id}&email={$request.body#/email}':
$ref: callback-transactioned.yml

/other:
post:
description: ''
parameters:
- name: queryUrl
in: query
required: true
description: |
bla
bla
bla
schema:
type: string
format: uri
example: https://example.com
responses:
'201':
description: ''
content:
application/json:
schema:
type: object
callbacks:
myEvent:
$ref: '#/components/callbacks/MyCallbackEvent'

components:
schemas:
SomePayload: {type: object}
SomeOtherPayload: {type: boolean}
callbacks:
MyCallbackEvent:
'{$request.query.queryUrl}':
post:
requestBody:
description: Callback payload
content:
'application/json':
schema:
$ref: '#/components/schemas/SomeOtherPayload'
responses:
'200':
description: callback successfully processed

0 comments on commit f6c20c3

Please sign in to comment.