Skip to content

Commit

Permalink
Merge pull request #20 from hyperledger/anyof
Browse files Browse the repository at this point in the history
Allow use of FF object schema generator in custom schema defs for anyOf
  • Loading branch information
nguyer authored Jun 22, 2022
2 parents f598736 + a825551 commit a4927eb
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 69 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ coverage.txt
**/debug.test
.DS_Store
__debug*
.vscode/*.log
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"go.lintTool": "golangci-lint",
"cSpell.words": [
"codecov",
"Customizer",
"Debugf",
"FCAPI",
"ffapi",
Expand Down
9 changes: 6 additions & 3 deletions pkg/ffapi/openapi3.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package ffapi

import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
Expand Down Expand Up @@ -192,7 +191,9 @@ func (sg *SwaggerGen) addInput(ctx context.Context, doc *openapi3.T, route *Rout
}
switch {
case route.JSONInputSchema != nil:
err = json.Unmarshal([]byte(route.JSONInputSchema(ctx)), &schemaRef)
schemaRef, err = route.JSONInputSchema(ctx, func(obj interface{}) (*openapi3.SchemaRef, error) {
return openapi3gen.NewSchemaRefForValue(obj, doc.Components.Schemas, openapi3gen.SchemaCustomizer(schemaCustomizer))
})
if err != nil {
panic(fmt.Sprintf("invalid schema: %s", err))
}
Expand Down Expand Up @@ -245,7 +246,9 @@ func (sg *SwaggerGen) addOutput(ctx context.Context, doc *openapi3.T, route *Rou
}
switch {
case route.JSONOutputSchema != nil:
err := json.Unmarshal([]byte(route.JSONOutputSchema(ctx)), &schemaRef)
schemaRef, err = route.JSONOutputSchema(ctx, func(obj interface{}) (*openapi3.SchemaRef, error) {
return openapi3gen.NewSchemaRefForValue(obj, doc.Components.Schemas, openapi3gen.SchemaCustomizer(schemaCustomizer))
})
if err != nil {
panic(fmt.Sprintf("invalid schema: %s", err))
}
Expand Down
95 changes: 31 additions & 64 deletions pkg/ffapi/openapi3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ var testRoutes = []*Route{
{
Name: "op1",
Path: "namespaces/{ns}/example1/{id}",
Method: http.MethodPatch,
Method: http.MethodGet,
PathParams: []*PathParam{
{Name: "lang", ExampleFromConf: config.Lang, Description: ExampleDesc},
{Name: "id", Example: "id12345", Description: ExampleDesc},
Expand All @@ -82,30 +82,20 @@ var testRoutes = []*Route{
{
Name: "op2",
Path: "example2",
Method: http.MethodGet,
Method: http.MethodPatch,
PathParams: nil,
QueryParams: nil,
Description: ExampleDesc,
JSONInputValue: func() interface{} { return nil },
JSONInputSchema: func(ctx context.Context) string {
return `{
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
}`
JSONInputSchema: func(ctx context.Context, schemaGen SchemaGenerator) (*openapi3.SchemaRef, error) {
s1, _ := schemaGen(&TestStruct1{})
s2, _ := schemaGen(&TestStruct2{})
return &openapi3.SchemaRef{
Value: openapi3.NewAnyOfSchema(s1.Value, s2.Value),
}, nil
},
JSONOutputSchema: func(ctx context.Context) string {
return `{
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
}`
JSONOutputSchema: func(ctx context.Context, schemaGen SchemaGenerator) (*openapi3.SchemaRef, error) {
return schemaGen(&TestStruct1{})
},
JSONOutputCodes: []int{http.StatusOK},
},
Expand Down Expand Up @@ -184,21 +174,22 @@ func TestOpenAPI3SwaggerGen(t *testing.T) {
fmt.Print(string(b))
}

func TestBadCustomInputSchema(t *testing.T) {
func TestBadCustomInputSchemaFail(t *testing.T) {

routes := []*Route{
{
Name: "op6",
Path: "namespaces/{ns}/example1/{id}",
Method: http.MethodPost,
JSONInputValue: func() interface{} { return &TestStruct1{} },
JSONInputMask: []string{"id"},
JSONOutputCodes: []int{http.StatusOK},
JSONInputSchema: func(ctx context.Context) string { return `!json` },
JSONOutputSchema: func(ctx context.Context) string { return `!json` },
Name: "op6",
Path: "namespaces/{ns}/example1/{id}",
Method: http.MethodPost,
JSONInputValue: func() interface{} { return &TestStruct1{} },
JSONInputMask: []string{"id"},
JSONOutputCodes: []int{http.StatusOK},
JSONInputSchema: func(ctx context.Context, schemaGen SchemaGenerator) (*openapi3.SchemaRef, error) {
return nil, fmt.Errorf("pop")
},
},
}
assert.PanicsWithValue(t, "invalid schema: invalid character '!' looking for beginning of value", func() {
assert.Panics(t, func() {
_ = NewSwaggerGen(&Options{
Title: "UnitTest",
Version: "1.0",
Expand All @@ -207,19 +198,20 @@ func TestBadCustomInputSchema(t *testing.T) {
})
}

func TestBadCustomOutputSchema(t *testing.T) {
func TestBadCustomOutputSchemaFail(t *testing.T) {
routes := []*Route{
{
Name: "op7",
Path: "namespaces/{ns}/example1/{id}",
Method: http.MethodGet,
JSONInputValue: func() interface{} { return &TestStruct1{} },
JSONInputMask: []string{"id"},
JSONOutputCodes: []int{http.StatusOK}, JSONInputSchema: func(ctx context.Context) string { return `!json` },
JSONOutputSchema: func(ctx context.Context) string { return `!json` },
Name: "op7",
Path: "namespaces/{ns}/example1/{id}",
Method: http.MethodGet,
JSONInputValue: func() interface{} { return &TestStruct1{} },
JSONInputMask: []string{"id"},
JSONOutputSchema: func(ctx context.Context, schemaGen SchemaGenerator) (*openapi3.SchemaRef, error) {
return nil, fmt.Errorf("pop")
},
},
}
assert.PanicsWithValue(t, "invalid schema: invalid character '!' looking for beginning of value", func() {
assert.Panics(t, func() {
_ = NewSwaggerGen(&Options{
Title: "UnitTest",
Version: "1.0",
Expand Down Expand Up @@ -290,31 +282,6 @@ func TestFFExcludeTag(t *testing.T) {
assert.Regexp(t, "object has no field", err)
}

func TestCustomSchema(t *testing.T) {
routes := []*Route{
{
Name: "PostCustomSchema",
Path: "namespaces/{ns}/example1/test",
Method: http.MethodPost,
JSONInputSchema: func(ctx context.Context) string {
return `{"properties": {"foo": {"type": "string", "description": "a custom foo"}}}`
},
JSONOutputSchema: func(ctx context.Context) string {
return `{"properties": {"bar": {"type": "string", "description": "a custom bar"}}}`
},
},
}
swagger := NewSwaggerGen(&Options{
Title: "UnitTest",
Version: "1.0",
BaseURL: "http://localhost:12345/api/v1",
}).Generate(context.Background(), routes)
assert.NotNil(t, swagger.Paths["/namespaces/{ns}/example1/test"].Post.RequestBody.Value)
length, err := swagger.Paths["/namespaces/{ns}/example1/test"].Post.RequestBody.Value.Content.Get("application/json").Schema.Value.Properties.JSONLookup("foo")
assert.NoError(t, err)
assert.NotNil(t, length)
}

func TestPanicOnMissingDescription(t *testing.T) {
routes := []*Route{
{
Expand Down
9 changes: 7 additions & 2 deletions pkg/ffapi/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ package ffapi
import (
"context"

"github.com/getkin/kin-openapi/openapi3"
"github.com/hyperledger/firefly-common/pkg/config"
"github.com/hyperledger/firefly-common/pkg/i18n"
)

// SchemaGenerator is passed into the JSONInputSchema advanced customization function, to give
// access to tools like object schema generation, for an anyOf schema for example
type SchemaGenerator func(o interface{}) (*openapi3.SchemaRef, error)

// Route defines each API operation on the REST API of Firefly
// Having a standard pluggable layer here on top of Gorilla allows us to autmoatically
// maintain the OpenAPI specification in-line with the code, while retaining the
Expand All @@ -49,9 +54,9 @@ type Route struct {
// JSONInputMask are fields that aren't available for users to supply on input
JSONInputMask []string
// JSONInputSchema is a custom schema definition, for the case where the auto-gen + mask isn't good enough
JSONInputSchema func(ctx context.Context) string
JSONInputSchema func(ctx context.Context, schemaGen SchemaGenerator) (*openapi3.SchemaRef, error)
// JSONOutputSchema is a custom schema definition, for the case where the auto-gen + mask isn't good enough
JSONOutputSchema func(ctx context.Context) string
JSONOutputSchema func(ctx context.Context, schemaGen SchemaGenerator) (*openapi3.SchemaRef, error)
// JSONOutputValue is a function that returns a pointer to a structure to take JSON output
JSONOutputValue func() interface{}
// JSONOutputCodes is the success response code
Expand Down

0 comments on commit a4927eb

Please sign in to comment.