Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix JSON schema output for custom contracts #1427

Merged
merged 2 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading