From c1a4639664f4007356b6dfd4f2788fb494ed2e19 Mon Sep 17 00:00:00 2001 From: Taichi Sasaki Date: Thu, 25 Jul 2024 22:15:03 +0900 Subject: [PATCH] Handle Any type as a typeless schema in OpenAPI (#3561) * Add test cases about Any type * Add Any type attributes to test of openapi v3 * Handle Any type as a typeless schema in OpenAPI * Combine types of function parameters --------- Co-authored-by: Raphael Simon --- http/codegen/openapi/json_schema.go | 7 +- http/codegen/openapi/v2/files_test.go | 1 + .../TestSections/with-any_file0.golden | 1 + .../TestSections/with-any_file1.golden | 89 +++++++++++++++++++ http/codegen/openapi/v3/files_test.go | 1 + .../codegen/openapi/v3/testdata/dsls/types.go | 1 + .../v3/testdata/golden/with-any_file0.golden | 1 + .../v3/testdata/golden/with-any_file1.golden | 74 +++++++++++++++ http/codegen/openapi/v3/types.go | 6 +- http/codegen/openapi/v3/types_test.go | 4 +- http/codegen/testdata/openapi_dsls.go | 34 +++++++ 11 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 http/codegen/openapi/v2/testdata/TestSections/with-any_file0.golden create mode 100644 http/codegen/openapi/v2/testdata/TestSections/with-any_file1.golden create mode 100644 http/codegen/openapi/v3/testdata/golden/with-any_file0.golden create mode 100644 http/codegen/openapi/v3/testdata/golden/with-any_file1.golden diff --git a/http/codegen/openapi/json_schema.go b/http/codegen/openapi/json_schema.go index 72c5567500..8c431ed2a7 100644 --- a/http/codegen/openapi/json_schema.go +++ b/http/codegen/openapi/json_schema.go @@ -230,7 +230,7 @@ func ResultTypeRef(api *expr.APIExpr, mt *expr.ResultTypeExpr, view string) stri // ResultTypeRefWithPrefix produces the JSON reference to the media type definition with // the given view and adds the provided prefix to the type name -func ResultTypeRefWithPrefix(api *expr.APIExpr, mt *expr.ResultTypeExpr, view string, prefix string) string { +func ResultTypeRefWithPrefix(api *expr.APIExpr, mt *expr.ResultTypeExpr, view, prefix string) string { projected, err := expr.Project(mt, view) if err != nil { panic(fmt.Sprintf("failed to project media type %#v: %s", mt.Identifier, err)) // bug @@ -318,8 +318,9 @@ func TypeSchemaWithPrefix(api *expr.APIExpr, t expr.DataType, prefix string) *Sc s.Type = Type(actual.Name()) switch actual.Kind() { case expr.AnyKind: - s.Type = Type("string") - s.Format = "binary" + // A schema without a type matches any data type. + // See https://swagger.io/docs/specification/data-models/data-types/#any. + s.Type = Type("") case expr.IntKind, expr.Int64Kind, expr.UIntKind, expr.UInt64Kind: // Use int64 format for IntKind and UIntKind because the OpenAPI diff --git a/http/codegen/openapi/v2/files_test.go b/http/codegen/openapi/v2/files_test.go index f77f28c443..7e9ecab503 100644 --- a/http/codegen/openapi/v2/files_test.go +++ b/http/codegen/openapi/v2/files_test.go @@ -39,6 +39,7 @@ func TestSections(t *testing.T) { {"server-host-with-variables", testdata.ServerHostWithVariablesDSL}, {"with-spaces", testdata.WithSpacesDSL}, {"with-map", testdata.WithMapDSL}, + {"with-any", testdata.WithAnyDSL}, {"path-with-wildcards", testdata.PathWithWildcardDSL}, {"path-with-multiple-wildcards", testdata.PathWithMultipleWildcardDSL}, {"path-with-multiple-explicit-wildcards", testdata.PathWithMultipleExplicitWildcardDSL}, diff --git a/http/codegen/openapi/v2/testdata/TestSections/with-any_file0.golden b/http/codegen/openapi/v2/testdata/TestSections/with-any_file0.golden new file mode 100644 index 0000000000..d29609f3e9 --- /dev/null +++ b/http/codegen/openapi/v2/testdata/TestSections/with-any_file0.golden @@ -0,0 +1 @@ +{"swagger":"2.0","info":{"title":"","version":"0.0.1"},"host":"localhost:80","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"post":{"tags":["testService"],"summary":"testEndpoint testService","operationId":"testService#testEndpoint","parameters":[{"name":"TestEndpointRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/TestServiceTestEndpointRequestBody"}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/TestServiceTestEndpointResponseBody"}}},"schemes":["http"]}}},"definitions":{"TestServiceTestEndpointRequestBody":{"title":"TestServiceTestEndpointRequestBody","type":"object","properties":{"any":{"example":""},"any_array":{"type":"array","items":{"example":""},"example":["","","",""]},"any_map":{"type":"object","example":{"":""},"additionalProperties":true}},"example":{"any":"","any_array":["","",""],"any_map":{"":""}}},"TestServiceTestEndpointResponseBody":{"title":"TestServiceTestEndpointResponseBody","type":"object","properties":{"any":{"example":""},"any_array":{"type":"array","items":{"example":""},"example":["","","",""]},"any_map":{"type":"object","example":{"":""},"additionalProperties":true}},"example":{"any":"","any_array":["",""],"any_map":{"":""}}}}} \ No newline at end of file diff --git a/http/codegen/openapi/v2/testdata/TestSections/with-any_file1.golden b/http/codegen/openapi/v2/testdata/TestSections/with-any_file1.golden new file mode 100644 index 0000000000..5ed077f4b9 --- /dev/null +++ b/http/codegen/openapi/v2/testdata/TestSections/with-any_file1.golden @@ -0,0 +1,89 @@ +swagger: "2.0" +info: + title: "" + version: 0.0.1 +host: localhost:80 +consumes: + - application/json + - application/xml + - application/gob +produces: + - application/json + - application/xml + - application/gob +paths: + /: + post: + tags: + - testService + summary: testEndpoint testService + operationId: testService#testEndpoint + parameters: + - name: TestEndpointRequestBody + in: body + required: true + schema: + $ref: '#/definitions/TestServiceTestEndpointRequestBody' + responses: + "200": + description: OK response. + schema: + $ref: '#/definitions/TestServiceTestEndpointResponseBody' + schemes: + - http +definitions: + TestServiceTestEndpointRequestBody: + title: TestServiceTestEndpointRequestBody + type: object + properties: + any: + example: "" + any_array: + type: array + items: + example: "" + example: + - "" + - "" + - "" + - "" + any_map: + type: object + example: + "": "" + additionalProperties: true + example: + any: "" + any_array: + - "" + - "" + - "" + any_map: + "": "" + TestServiceTestEndpointResponseBody: + title: TestServiceTestEndpointResponseBody + type: object + properties: + any: + example: "" + any_array: + type: array + items: + example: "" + example: + - "" + - "" + - "" + - "" + any_map: + type: object + example: + "": "" + additionalProperties: true + example: + any: "" + any_array: + - "" + - "" + any_map: + "": "" diff --git a/http/codegen/openapi/v3/files_test.go b/http/codegen/openapi/v3/files_test.go index f68bbd3cf6..59350af756 100644 --- a/http/codegen/openapi/v3/files_test.go +++ b/http/codegen/openapi/v3/files_test.go @@ -41,6 +41,7 @@ func TestFiles(t *testing.T) { {"server-host-with-variables", testdata.ServerHostWithVariablesDSL}, {"with-spaces", testdata.WithSpacesDSL}, {"with-map", testdata.WithMapDSL}, + {"with-any", testdata.WithAnyDSL}, {"path-with-wildcards", testdata.PathWithWildcardDSL}, {"path-with-multiple-wildcards", testdata.PathWithMultipleWildcardDSL}, {"path-with-multiple-explicit-wildcards", testdata.PathWithMultipleExplicitWildcardDSL}, diff --git a/http/codegen/openapi/v3/testdata/dsls/types.go b/http/codegen/openapi/v3/testdata/dsls/types.go index 54eff0f886..8e13029bd1 100644 --- a/http/codegen/openapi/v3/testdata/dsls/types.go +++ b/http/codegen/openapi/v3/testdata/dsls/types.go @@ -82,6 +82,7 @@ func ObjectResponseBodyDSL(svcName, metName string) func() { Result(func() { Attribute("name") Attribute("age", Int) + Attribute("misc", Any) }) HTTP(func() { POST("/") diff --git a/http/codegen/openapi/v3/testdata/golden/with-any_file0.golden b/http/codegen/openapi/v3/testdata/golden/with-any_file0.golden new file mode 100644 index 0000000000..5811ba8634 --- /dev/null +++ b/http/codegen/openapi/v3/testdata/golden/with-any_file0.golden @@ -0,0 +1 @@ +{"openapi":"3.0.3","info":{"title":"Goa API","version":"0.0.1"},"servers":[{"url":"http://localhost:80","description":"Default server for test api"}],"paths":{"/":{"post":{"tags":["testService"],"summary":"testEndpoint testService","operationId":"testService#testEndpoint","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestEndpointRequestBody"},"example":{"any":"","any_array":["","","",""],"any_map":{"":""}}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestEndpointRequestBody"},"example":{"any":"","any_array":["","",""],"any_map":{"":""}}}}}}}}},"components":{"schemas":{"TestEndpointRequestBody":{"type":"object","properties":{"any":{"example":""},"any_array":{"type":"array","items":{"example":""},"example":["","","",""]},"any_map":{"type":"object","example":{"":""},"additionalProperties":true}},"example":{"any":"","any_array":["",""],"any_map":{"":""}}}}},"tags":[{"name":"testService"}]} \ No newline at end of file diff --git a/http/codegen/openapi/v3/testdata/golden/with-any_file1.golden b/http/codegen/openapi/v3/testdata/golden/with-any_file1.golden new file mode 100644 index 0000000000..6417a5c6a5 --- /dev/null +++ b/http/codegen/openapi/v3/testdata/golden/with-any_file1.golden @@ -0,0 +1,74 @@ +openapi: 3.0.3 +info: + title: Goa API + version: 0.0.1 +servers: + - url: http://localhost:80 + description: Default server for test api +paths: + /: + post: + tags: + - testService + summary: testEndpoint testService + operationId: testService#testEndpoint + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TestEndpointRequestBody' + example: + any: "" + any_array: + - "" + - "" + - "" + - "" + any_map: + "": "" + responses: + "200": + description: OK response. + content: + application/json: + schema: + $ref: '#/components/schemas/TestEndpointRequestBody' + example: + any: "" + any_array: + - "" + - "" + - "" + any_map: + "": "" +components: + schemas: + TestEndpointRequestBody: + type: object + properties: + any: + example: "" + any_array: + type: array + items: + example: "" + example: + - "" + - "" + - "" + - "" + any_map: + type: object + example: + "": "" + additionalProperties: true + example: + any: "" + any_array: + - "" + - "" + any_map: + "": "" +tags: + - name: testService diff --git a/http/codegen/openapi/v3/types.go b/http/codegen/openapi/v3/types.go index 29df754dbd..8a4fe51fd0 100644 --- a/http/codegen/openapi/v3/types.go +++ b/http/codegen/openapi/v3/types.go @@ -166,7 +166,7 @@ func (sf *schemafier) schemafy(attr *expr.AttributeExpr, noref ...bool) *openapi case expr.Float64Kind: s.Type = openapi.Type("number") s.Format = "double" - case expr.BytesKind, expr.AnyKind: + case expr.BytesKind: if bases := attr.Bases; len(bases) > 0 { for _, b := range bases { // Union type @@ -177,6 +177,10 @@ func (sf *schemafier) schemafy(attr *expr.AttributeExpr, noref ...bool) *openapi s.Type = openapi.Type("string") s.Format = "binary" } + case expr.AnyKind: + // A schema without a type matches any data type. + // See https://swagger.io/docs/specification/data-models/data-types/#any. + s.Type = openapi.Type("") default: s.Type = openapi.Type(t.Name()) } diff --git a/http/codegen/openapi/v3/types_test.go b/http/codegen/openapi/v3/types_test.go index f61bffc838..3d641256ff 100644 --- a/http/codegen/openapi/v3/types_test.go +++ b/http/codegen/openapi/v3/types_test.go @@ -119,7 +119,7 @@ func TestBuildBodyTypes(t *testing.T) { DSL: dsls.ObjectResponseBodyDSL(svcName, "object_response_body"), ExpectedType: tempty, - ExpectedResponseTypes: rt{200: tobj("name", tstring, "age", tint)}, + ExpectedResponseTypes: rt{200: tobj("name", tstring, "age", tint, "misc", tempty)}, }, { Name: "multi_cookie_response_body", DSL: dsls.MultiCookieResponseBodyDSL(svcName, "multi_cookie_response_body"), @@ -137,7 +137,7 @@ func TestBuildBodyTypes(t *testing.T) { DSL: dsls.ObjectResponseBodyDSL(svcName, "object_streaming_response_body"), ExpectedType: tempty, - ExpectedResponseTypes: rt{200: tobj("name", tstring, "age", tint)}, + ExpectedResponseTypes: rt{200: tobj("name", tstring, "age", tint, "misc", tempty)}, }, { Name: "string_error_response", DSL: dsls.StringErrorResponseBodyDSL(svcName, "string_error_response"), diff --git a/http/codegen/testdata/openapi_dsls.go b/http/codegen/testdata/openapi_dsls.go index 3619a09cd6..105071416b 100644 --- a/http/codegen/testdata/openapi_dsls.go +++ b/http/codegen/testdata/openapi_dsls.go @@ -522,6 +522,40 @@ var WithMapDSL = func() { }) } +var WithAnyDSL = func() { + Service("testService", func() { + Method("testEndpoint", func() { + Payload(func() { + Attribute("any", Any, func() { + Example("") + }) + Attribute("any_array", ArrayOf(Any, func() { + Example("") + })) + Attribute("any_map", MapOf(String, Any), func() { + Key(func() { Example("") }) + Elem(func() { Example("") }) + }) + }) + Result(func() { + Attribute("any", Any, func() { + Example("") + }) + Attribute("any_array", ArrayOf(Any, func() { + Example("") + })) + Attribute("any_map", MapOf(String, Any), func() { + Key(func() { Example("") }) + Elem(func() { Example("") }) + }) + }) + HTTP(func() { + POST("/") + }) + }) + }) +} + var PathWithWildcardDSL = func() { Service("test service", func() { Method("test endpoint", func() {