diff --git a/internal/apiserver/ffi2swagger.go b/internal/apiserver/ffi2swagger.go index 15b6dea8b..c6f63d1c2 100644 --- a/internal/apiserver/ffi2swagger.go +++ b/internal/apiserver/ffi2swagger.go @@ -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, }) @@ -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, @@ -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 @@ -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) diff --git a/internal/apiserver/ffi2swagger_test.go b/internal/apiserver/ffi2swagger_test.go index 3323e515b..4551c1c82 100644 --- a/internal/apiserver/ffi2swagger_test.go +++ b/internal/apiserver/ffi2swagger_test.go @@ -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{ @@ -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) } diff --git a/internal/coremsgs/en_struct_descriptions.go b/internal/coremsgs/en_struct_descriptions.go index d7b86913a..8ecb94618 100644 --- a/internal/coremsgs/en_struct_descriptions.go +++ b/internal/coremsgs/en_struct_descriptions.go @@ -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")