Skip to content

Commit

Permalink
Merge pull request #94 from kaleido-io/enterprise-ffi2swagger-output
Browse files Browse the repository at this point in the history
Backport FFI Swagger Fixes
  • Loading branch information
nguyer authored Jan 9, 2024
2 parents b88b1f6 + a934ae8 commit 6f81ef0
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 13 deletions.
6 changes: 6 additions & 0 deletions docs/swagger/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26737,6 +26737,9 @@ paths:
description: The time the operation was created
format: date-time
type: string
detail:
description: Additional detailed information about an operation
provided by the connector
error:
description: Any error reported back from the plugin for this
operation
Expand Down Expand Up @@ -35719,6 +35722,9 @@ paths:
description: The time the operation was created
format: date-time
type: string
detail:
description: Additional detailed information about an operation
provided by the connector
error:
description: Any error reported back from the plugin for this
operation
Expand Down
50 changes: 41 additions & 9 deletions internal/apiserver/ffi2swagger.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,9 @@ func addFFIMethod(ctx context.Context, routes []*ffapi.Route, method *fftypes.FF
Path: fmt.Sprintf("invoke/%s", method.Pathname), // must match a route defined in apiserver routes!
Method: http.MethodPost,
JSONInputSchema: func(ctx context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) {
return contractJSONSchema(ctx, &method.Params, hasLocation)
},
JSONOutputSchema: func(ctx context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) {
return contractJSONSchema(ctx, &method.Returns, true)
return contractRequestJSONSchema(ctx, &method.Params, hasLocation)
},
JSONOutputValue: func() interface{} { return &core.OperationWithDetail{} },
JSONOutputCodes: []int{http.StatusOK},
PreTranslatedDescription: description,
})
Expand All @@ -103,10 +101,10 @@ func addFFIMethod(ctx context.Context, routes []*ffapi.Route, method *fftypes.FF
Path: fmt.Sprintf("query/%s", method.Pathname), // must match a route defined in apiserver routes!
Method: http.MethodPost,
JSONInputSchema: func(ctx context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) {
return contractJSONSchema(ctx, &method.Params, hasLocation)
return contractRequestJSONSchema(ctx, &method.Params, hasLocation)
},
JSONOutputSchema: func(ctx context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) {
return contractJSONSchema(ctx, &method.Returns, true)
return contractQueryResponseJSONSchema(ctx, &method.Returns)
},
JSONOutputCodes: []int{http.StatusOK},
PreTranslatedDescription: description,
Expand Down Expand Up @@ -146,10 +144,10 @@ func addFFIEvent(ctx context.Context, routes []*ffapi.Route, event *fftypes.FFIE
}

/**
* Parse the FFI and build a corresponding JSON Schema to describe the request body for "invoke".
* Returns the JSON Schema as an `fftypes.JSONObject`.
* Parse the FFI and build a corresponding JSON Schema to describe the request body for "invoke" or "query" requests
* Returns the JSON Schema as an `fftypes.JSONObject`
*/
func contractJSONSchema(ctx context.Context, params *fftypes.FFIParams, hasLocation bool) (*openapi3.SchemaRef, error) {
func contractRequestJSONSchema(ctx context.Context, params *fftypes.FFIParams, hasLocation bool) (*openapi3.SchemaRef, error) {
paramSchema := make(fftypes.JSONObject, len(*params))
for _, param := range *params {
paramSchema[param.Name] = param.Schema
Expand Down Expand Up @@ -193,6 +191,40 @@ func contractJSONSchema(ctx context.Context, params *fftypes.FFIParams, hasLocat
return openapi3.NewSchemaRef("", s), nil
}

/**
* Parse the FFI and build a corresponding JSON Schema to describe the response body for "query" requests
* Returns the JSON Schema as an `fftypes.JSONObject`
*/
func contractQueryResponseJSONSchema(ctx context.Context, params *fftypes.FFIParams) (*openapi3.SchemaRef, error) {
paramSchema := make(fftypes.JSONObject, len(*params))
for i, param := range *params {
paramName := param.Name
if paramName == "" {
if i > 0 {
paramName = fmt.Sprintf("output%v", i)
} else {
paramName = "output"
}
}
paramSchema[paramName] = param.Schema
}
outputSchema := fftypes.JSONObject{
"type": "object",
"description": i18n.Expand(ctx, coremsgs.ContractCallRequestOutput),
"properties": paramSchema,
}
b, err := json.Marshal(outputSchema)
if err != nil {
return nil, err
}
s := openapi3.NewSchema()
err = s.UnmarshalJSON(b)
if err != nil {
return nil, err
}
return openapi3.NewSchemaRef("", s), nil
}

func buildDetailsTable(ctx context.Context, details map[string]interface{}) string {
keyHeader := i18n.Expand(ctx, coremsgs.APISmartContractDetailsKey)
valueHeader := i18n.Expand(ctx, coremsgs.APISmartContractDetailsKey)
Expand Down
54 changes: 52 additions & 2 deletions internal/apiserver/ffi2swagger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ func TestFFIParamBadSchema(t *testing.T) {
Schema: fftypes.JSONAnyPtr(`{`),
},
}
_, err := contractJSONSchema(ctx, params, true)
_, err := contractRequestJSONSchema(ctx, params, true)
assert.Error(t, err)

params = &fftypes.FFIParams{
Expand All @@ -205,6 +205,56 @@ func TestFFIParamBadSchema(t *testing.T) {
Schema: fftypes.JSONAnyPtr(`{"type": false}`),
},
}
_, err = contractJSONSchema(ctx, params, true)
_, err = contractRequestJSONSchema(ctx, params, true)
assert.Error(t, err)
}

func TestUnnamedOutputs(t *testing.T) {
ctx := context.Background()
params := &fftypes.FFIParams{
{
Name: "",
Schema: fftypes.JSONAnyPtr(`{}`),
},
{
Name: "",
Schema: fftypes.JSONAnyPtr(`{}`),
},
}

expectedJSON := `{
"description": "A map of named outputs",
"properties": {
"output": {},
"output1": {}
},
"type": "object"
}`

ref, err := contractQueryResponseJSONSchema(ctx, params)
assert.NoError(t, err)
b, err := ref.MarshalJSON()
assert.JSONEq(t, expectedJSON, string(b))
}

func TestBadSchema(t *testing.T) {
ctx := context.Background()
params := &fftypes.FFIParams{
{
Name: "",
Schema: fftypes.JSONAnyPtr(`{`),
},
}
_, err := contractQueryResponseJSONSchema(ctx, params)
assert.Error(t, err)

ctx = context.Background()
params = &fftypes.FFIParams{
{
Name: "",
Schema: fftypes.JSONAnyPtr(`{"type": false}`),
},
}
_, err = contractQueryResponseJSONSchema(ctx, params)
assert.Error(t, err)
}
4 changes: 2 additions & 2 deletions internal/apiserver/route_get_op_by_id.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2022 Kaleido, Inc.
// Copyright © 2023 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand Down Expand Up @@ -37,7 +37,7 @@ var getOpByID = &ffapi.Route{
},
Description: coremsgs.APIEndpointsGetOpByID,
JSONInputValue: nil,
JSONOutputValue: func() interface{} { return &core.Operation{} },
JSONOutputValue: func() interface{} { return &core.OperationWithDetail{} },
JSONOutputCodes: []int{http.StatusOK},
Extensions: &coreExtensions{
CoreJSONHandler: func(r *ffapi.APIRequest, cr *coreRequest) (output interface{}, err error) {
Expand Down
1 change: 1 addition & 0 deletions internal/coremsgs/en_struct_descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@ var (
ContractCallRequestMethodPath = ffm("ContractCallRequest.methodPath", "The pathname of the method on the specified FFI")
ContractCallRequestErrors = ffm("ContractCallRequest.errors", "An in-line FFI errors definition for the method to invoke. Alternative to specifying FFI")
ContractCallRequestInput = ffm("ContractCallRequest.input", "A map of named inputs. The name and type of each input must be compatible with the FFI description of the method, so that FireFly knows how to serialize it to the blockchain via the connector")
ContractCallRequestOutput = ffm("ContractCallRequest.output", "A map of named outputs")
ContractCallRequestOptions = ffm("ContractCallRequest.options", "A map of named inputs that will be passed through to the blockchain connector")
ContractCallMessage = ffm("ContractCallRequest.message", "You can specify a message to correlate with the invocation, which can be of type broadcast or private. Your specified method must support on-chain/off-chain correlation by taking a data input on the call")
ContractCallIdempotencyKey = ffm("ContractCallRequest.idempotencyKey", "An optional identifier to allow idempotent submission of requests. Stored on the transaction uniquely within a namespace")
Expand Down

0 comments on commit 6f81ef0

Please sign in to comment.