Repository: hyperledger/firefly
Pull request #1119 (2022-12-15 20:44:32)

Add missing fields and descriptions to custom contract API swagger
Adds `key` and `idempotencyKey` as well as descriptions for those and `input` and `options`.


Patch:
diff --git a/internal/apiserver/ffi2swagger.go b/internal/apiserver/ffi2swagger.go
index cde43a112..83f1d8805 100644
--- a/internal/apiserver/ffi2swagger.go
+++ b/internal/apiserver/ffi2swagger.go
@@ -71,7 +71,7 @@ func (og *ffiSwaggerGen) Generate(ctx context.Context, baseURL string, api *core
 		},
 	}
 	for _, method := range ffi.Methods {
-		routes = og.addMethod(routes, method, hasLocation)
+		routes = og.addMethod(ctx, routes, method, hasLocation)
 	}
 	for _, event := range ffi.Events {
 		routes = og.addEvent(routes, event, hasLocation)
@@ -86,8 +86,7 @@ func (og *ffiSwaggerGen) Generate(ctx context.Context, baseURL string, api *core
 	}).Generate(ctx, routes)
 }
 
-func (og *ffiSwaggerGen) addMethod(routes []*ffapi.Route, method *fftypes.FFIMethod, hasLocation bool) []*ffapi.Route {
-	ctx := context.Background()
+func (og *ffiSwaggerGen) addMethod(ctx context.Context, routes []*ffapi.Route, method *fftypes.FFIMethod, hasLocation bool) []*ffapi.Route {
 	description := method.Description
 	if method.Details != nil && len(method.Details) > 0 {
 		additionalDetailsHeader := i18n.Expand(ctx, coremsgs.APISmartContractDetails)
@@ -98,10 +97,10 @@ func (og *ffiSwaggerGen) addMethod(routes []*ffapi.Route, method *fftypes.FFIMet
 		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(&method.Params, hasLocation)
+			return contractJSONSchema(ctx, &method.Params, hasLocation)
 		},
 		JSONOutputSchema: func(ctx context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) {
-			return contractJSONSchema(&method.Returns, true)
+			return contractJSONSchema(ctx, &method.Returns, true)
 		},
 		JSONOutputCodes:          []int{http.StatusOK},
 		PreTranslatedDescription: description,
@@ -111,10 +110,10 @@ func (og *ffiSwaggerGen) addMethod(routes []*ffapi.Route, method *fftypes.FFIMet
 		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(&method.Params, hasLocation)
+			return contractJSONSchema(ctx, &method.Params, hasLocation)
 		},
 		JSONOutputSchema: func(ctx context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) {
-			return contractJSONSchema(&method.Returns, true)
+			return contractJSONSchema(ctx, &method.Returns, true)
 		},
 		JSONOutputCodes:          []int{http.StatusOK},
 		PreTranslatedDescription: description,
@@ -158,19 +157,29 @@ func (og *ffiSwaggerGen) addEvent(routes []*ffapi.Route, event *fftypes.FFIEvent
  * Parse the FFI and build a corresponding JSON Schema to describe the request body for "invoke".
  * Returns the JSON Schema as an `fftypes.JSONObject`.
  */
-func contractJSONSchema(params *fftypes.FFIParams, hasLocation bool) (*openapi3.SchemaRef, error) {
+func contractJSONSchema(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
 	}
 	inputSchema := fftypes.JSONObject{
-		"type":       "object",
-		"properties": paramSchema,
+		"type":        "object",
+		"description": i18n.Expand(ctx, coremsgs.ContractCallRequestInput),
+		"properties":  paramSchema,
 	}
 	properties := fftypes.JSONObject{
 		"input": inputSchema,
 		"options": fftypes.JSONObject{
-			"type": "object",
+			"type":        "object",
+			"description": i18n.Expand(ctx, coremsgs.ContractCallRequestOptions),
+		},
+		"key": fftypes.JSONObject{
+			"type":        "string",
+			"description": i18n.Expand(ctx, coremsgs.ContractCallRequestKey),
+		},
+		"idempotencyKey": fftypes.JSONObject{
+			"type":        "string",
+			"description": i18n.Expand(ctx, coremsgs.ContractCallIdempotencyKey),
 		},
 	}
 	if !hasLocation {


Test patch:
diff --git a/internal/apiserver/ffi2swagger_test.go b/internal/apiserver/ffi2swagger_test.go
index 54dfd4e02..9f8624524 100644
--- a/internal/apiserver/ffi2swagger_test.go
+++ b/internal/apiserver/ffi2swagger_test.go
@@ -126,25 +126,25 @@ func TestGenerate(t *testing.T) {
 
 	invokeMethod1 := doc.Paths["/invoke/method1"].Post.RequestBody.Value.Content.Get("application/json").Schema.Value
 	assert.Equal(t, "object", invokeMethod1.Type)
-	assert.ElementsMatch(t, []string{"input", "location", "options"}, paramNames(invokeMethod1.Properties))
+	assert.ElementsMatch(t, []string{"input", "location", "options", "key", "idempotencyKey"}, paramNames(invokeMethod1.Properties))
 	assert.Equal(t, "object", invokeMethod1.Properties["input"].Value.Type)
 	assert.ElementsMatch(t, []string{"x", "y", "z"}, paramNames(invokeMethod1.Properties["input"].Value.Properties))
 
 	invokeMethod2 := doc.Paths["/invoke/method2"].Post.RequestBody.Value.Content.Get("application/json").Schema.Value
 	assert.Equal(t, "object", invokeMethod2.Type)
-	assert.ElementsMatch(t, []string{"input", "location", "options"}, paramNames(invokeMethod2.Properties))
+	assert.ElementsMatch(t, []string{"input", "location", "options", "key", "idempotencyKey"}, paramNames(invokeMethod2.Properties))
 	assert.Equal(t, "object", invokeMethod2.Properties["input"].Value.Type)
 	assert.ElementsMatch(t, []string{}, paramNames(invokeMethod2.Properties["input"].Value.Properties))
 
 	queryMethod1 := doc.Paths["/query/method1"].Post.RequestBody.Value.Content.Get("application/json").Schema.Value
 	assert.Equal(t, "object", queryMethod1.Type)
-	assert.ElementsMatch(t, []string{"input", "location", "options"}, paramNames(queryMethod1.Properties))
+	assert.ElementsMatch(t, []string{"input", "location", "options", "key", "idempotencyKey"}, paramNames(queryMethod1.Properties))
 	assert.Equal(t, "object", queryMethod1.Properties["input"].Value.Type)
 	assert.ElementsMatch(t, []string{"x", "y", "z"}, paramNames(queryMethod1.Properties["input"].Value.Properties))
 
 	queryMethod2 := doc.Paths["/query/method2"].Post.RequestBody.Value.Content.Get("application/json").Schema.Value
 	assert.Equal(t, "object", queryMethod2.Type)
-	assert.ElementsMatch(t, []string{"input", "location", "options"}, paramNames(queryMethod2.Properties))
+	assert.ElementsMatch(t, []string{"input", "location", "options", "key", "idempotencyKey"}, paramNames(queryMethod2.Properties))
 	assert.Equal(t, "object", queryMethod2.Properties["input"].Value.Type)
 	assert.ElementsMatch(t, []string{}, paramNames(queryMethod2.Properties["input"].Value.Properties))
 }
@@ -162,37 +162,38 @@ func TestGenerateWithLocation(t *testing.T) {
 
 	invokeMethod1 := doc.Paths["/invoke/method1"].Post.RequestBody.Value.Content.Get("application/json").Schema.Value
 	assert.Equal(t, "object", invokeMethod1.Type)
-	assert.ElementsMatch(t, []string{"input", "options"}, paramNames(invokeMethod1.Properties))
+	assert.ElementsMatch(t, []string{"input", "options", "key", "idempotencyKey"}, paramNames(invokeMethod1.Properties))
 	assert.Equal(t, "object", invokeMethod1.Properties["input"].Value.Type)
 	assert.ElementsMatch(t, []string{"x", "y", "z"}, paramNames(invokeMethod1.Properties["input"].Value.Properties))
 
 	invokeMethod2 := doc.Paths["/invoke/method2"].Post.RequestBody.Value.Content.Get("application/json").Schema.Value
 	assert.Equal(t, "object", invokeMethod2.Type)
-	assert.ElementsMatch(t, []string{"input", "options"}, paramNames(invokeMethod2.Properties))
+	assert.ElementsMatch(t, []string{"input", "options", "key", "idempotencyKey"}, paramNames(invokeMethod2.Properties))
 	assert.Equal(t, "object", invokeMethod2.Properties["input"].Value.Type)
 	assert.ElementsMatch(t, []string{}, paramNames(invokeMethod2.Properties["input"].Value.Properties))
 
 	queryMethod1 := doc.Paths["/query/method1"].Post.RequestBody.Value.Content.Get("application/json").Schema.Value
 	assert.Equal(t, "object", queryMethod1.Type)
-	assert.ElementsMatch(t, []string{"input", "options"}, paramNames(queryMethod1.Properties))
+	assert.ElementsMatch(t, []string{"input", "options", "key", "idempotencyKey"}, paramNames(queryMethod1.Properties))
 	assert.Equal(t, "object", queryMethod1.Properties["input"].Value.Type)
 	assert.ElementsMatch(t, []string{"x", "y", "z"}, paramNames(queryMethod1.Properties["input"].Value.Properties))
 
 	queryMethod2 := doc.Paths["/query/method2"].Post.RequestBody.Value.Content.Get("application/json").Schema.Value
 	assert.Equal(t, "object", queryMethod2.Type)
-	assert.ElementsMatch(t, []string{"input", "options"}, paramNames(queryMethod2.Properties))
+	assert.ElementsMatch(t, []string{"input", "options", "key", "idempotencyKey"}, paramNames(queryMethod2.Properties))
 	assert.Equal(t, "object", queryMethod2.Properties["input"].Value.Type)
 	assert.ElementsMatch(t, []string{}, paramNames(queryMethod2.Properties["input"].Value.Properties))
 }
 
 func TestFFIParamBadSchema(t *testing.T) {
+	ctx := context.Background()
 	params := &fftypes.FFIParams{
 		&fftypes.FFIParam{
 			Name:   "test",
 			Schema: fftypes.JSONAnyPtr(`{`),
 		},
 	}
-	_, err := contractJSONSchema(params, true)
+	_, err := contractJSONSchema(ctx, params, true)
 	assert.Error(t, err)
 
 	params = &fftypes.FFIParams{
@@ -201,6 +202,6 @@ func TestFFIParamBadSchema(t *testing.T) {
 			Schema: fftypes.JSONAnyPtr(`{"type": false}`),
 		},
 	}
-	_, err = contractJSONSchema(params, true)
+	_, err = contractJSONSchema(ctx, params, true)
 	assert.Error(t, err)
 }