diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 1aa9e191b3..ff978ec5b8 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -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 @@ -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 diff --git a/internal/apiserver/ffi2swagger.go b/internal/apiserver/ffi2swagger.go index 15b6dea8b5..c6f63d1c23 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 3323e515be..4551c1c824 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/apiserver/route_get_op_by_id.go b/internal/apiserver/route_get_op_by_id.go index 4c61d79305..46a6408afd 100644 --- a/internal/apiserver/route_get_op_by_id.go +++ b/internal/apiserver/route_get_op_by_id.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Kaleido, Inc. +// Copyright © 2023 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -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) { diff --git a/internal/coremsgs/en_struct_descriptions.go b/internal/coremsgs/en_struct_descriptions.go index d7b86913a4..8ecb94618b 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")