Skip to content

Commit

Permalink
Merge pull request #1427 from kaleido-io/ffi2swagger-output
Browse files Browse the repository at this point in the history
Fix JSON schema output for custom contracts
  • Loading branch information
nguyer authored Nov 29, 2023
2 parents e0e9e34 + 4d87450 commit 50d2acd
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 11 deletions.
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)
}
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 50d2acd

Please sign in to comment.