From e28bcb3640547e5d449bb2ff49f1ffb3b140a312 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Sat, 8 Jun 2024 18:32:17 +0200 Subject: [PATCH] Endpoints as list in CodeGen to avoid duplicate key elimination (#2836) (#2892) Key is imports. And imports can be the same. We don't need here map semantics, but just a list. --- .../scala/zio/http/gen/scala/CodeGen.scala | 2 +- .../EndpointsWithOverlappingPath.scala | 18 ++ .../http/gen/openapi/EndpointGenSpec.scala | 62 +++++++ .../zio/http/gen/scala/CodeGenSpec.scala | 161 ++++++++++++++++++ 4 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 zio-http-gen/src/test/resources/EndpointsWithOverlappingPath.scala diff --git a/zio-http-gen/src/main/scala/zio/http/gen/scala/CodeGen.scala b/zio-http-gen/src/main/scala/zio/http/gen/scala/CodeGen.scala index da11489647..94ed8a3c0c 100644 --- a/zio-http-gen/src/main/scala/zio/http/gen/scala/CodeGen.scala +++ b/zio-http-gen/src/main/scala/zio/http/gen/scala/CodeGen.scala @@ -65,7 +65,7 @@ object CodeGen { case Code.Object(name, schema, endpoints, objects, caseClasses, enums) => val baseImports = if (endpoints.nonEmpty) EndpointImports else Nil - val (epImports, epContent) = endpoints.map { case (k, v) => + val (epImports, epContent) = endpoints.toList.map { case (k, v) => val (kImports, kContent) = render(basePackage)(k) val (vImports, vContent) = render(basePackage)(v) (kImports ++ vImports, s"$kContent=$vContent") diff --git a/zio-http-gen/src/test/resources/EndpointsWithOverlappingPath.scala b/zio-http-gen/src/test/resources/EndpointsWithOverlappingPath.scala new file mode 100644 index 0000000000..a265b4cbd2 --- /dev/null +++ b/zio-http-gen/src/test/resources/EndpointsWithOverlappingPath.scala @@ -0,0 +1,18 @@ +package test + +import test.component._ + +object Pets { + import zio.http._ + import zio.http.endpoint._ + import zio.http.codec._ + val listPets = Endpoint(Method.GET / "pets") + .query(QueryCodec.queryTo[Int]("limit")) + .in[Unit] + .out[Pets](status = Status.Ok) + + val createPets = Endpoint(Method.POST / "pets") + .in[Pet] + .out[Unit](status = Status.Created) + +} diff --git a/zio-http-gen/src/test/scala/zio/http/gen/openapi/EndpointGenSpec.scala b/zio-http-gen/src/test/scala/zio/http/gen/openapi/EndpointGenSpec.scala index b28cadd39b..a30bde7e70 100644 --- a/zio-http-gen/src/test/scala/zio/http/gen/openapi/EndpointGenSpec.scala +++ b/zio-http-gen/src/test/scala/zio/http/gen/openapi/EndpointGenSpec.scala @@ -657,6 +657,68 @@ object EndpointGenSpec extends ZIOSpecDefault { ) assertTrue(scala.files.head == expected) }, + test("endpoints with overlapping prefix") { + val endpoint1 = Endpoint(Method.GET / "api" / "v1" / "users") + val endpoint2 = Endpoint(Method.GET / "api" / "v1" / "users" / "info") + val openAPI = OpenAPIGen.fromEndpoints(endpoint1, endpoint2) + val scala = EndpointGen.fromOpenAPI(openAPI) + val expected1 = Code.File( + List("api", "v1", "Users.scala"), + pkgPath = List("api", "v1"), + imports = List(Code.Import.FromBase(path = "component._")), + objects = List( + Code.Object( + "Users", + Map( + Code.Field("get") -> Code.EndpointCode( + Method.GET, + Code.PathPatternCode(segments = + List(Code.PathSegmentCode("api"), Code.PathSegmentCode("v1"), Code.PathSegmentCode("users")), + ), + queryParamsCode = Set.empty, + headersCode = Code.HeadersCode.empty, + inCode = Code.InCode("Unit"), + outCodes = Nil, + errorsCode = Nil, + ), + ), + ), + ), + caseClasses = Nil, + enums = Nil, + ) + val expected2 = Code.File( + List("api", "v1", "users", "Info.scala"), + pkgPath = List("api", "v1", "users"), + imports = List(Code.Import.FromBase(path = "component._")), + objects = List( + Code.Object( + "Info", + Map( + Code.Field("get") -> Code.EndpointCode( + Method.GET, + Code.PathPatternCode(segments = + List( + Code.PathSegmentCode("api"), + Code.PathSegmentCode("v1"), + Code.PathSegmentCode("users"), + Code.PathSegmentCode("info"), + ), + ), + queryParamsCode = Set.empty, + headersCode = Code.HeadersCode.empty, + inCode = Code.InCode("Unit"), + outCodes = Nil, + errorsCode = Nil, + ), + ), + ), + ), + caseClasses = Nil, + enums = Nil, + ) + assertTrue(scala.files.toSet == Set(expected1, expected2)) + }, ), suite("data gen spec")( test("generates case class, companion object and schema") { diff --git a/zio-http-gen/src/test/scala/zio/http/gen/scala/CodeGenSpec.scala b/zio-http-gen/src/test/scala/zio/http/gen/scala/CodeGenSpec.scala index e139209b77..c3b70da766 100644 --- a/zio-http-gen/src/test/scala/zio/http/gen/scala/CodeGenSpec.scala +++ b/zio-http-gen/src/test/scala/zio/http/gen/scala/CodeGenSpec.scala @@ -556,5 +556,166 @@ object CodeGenSpec extends ZIOSpecDefault { "/GeneratedUserNameArray.scala", ) }, + test("Endpoints with common prefix") { + val json = """{ + | "openapi": "3.0.0", + | "info": { + | "version": "1.0.0", + | "title": "Swagger Petstore", + | "license": { + | "name": "MIT" + | } + | }, + | "servers": [ + | { + | "url": "http://petstore.swagger.io/v1" + | } + | ], + | "paths": { + | "/pets": { + | "get": { + | "summary": "List all pets", + | "operationId": "listPets", + | "tags": [ + | "pets" + | ], + | "parameters": [ + | { + | "name": "limit", + | "in": "query", + | "description": "How many items to return at one time (max 100)", + | "required": false, + | "schema": { + | "type": "integer", + | "maximum": 100, + | "format": "int32" + | } + | } + | ], + | "responses": { + | "200": { + | "description": "A paged array of pets", + | "headers": { + | "x-next": { + | "description": "A link to the next page of responses", + | "schema": { + | "type": "string" + | } + | } + | }, + | "content": { + | "application/json": { + | "schema": { + | "$ref": "#/components/schemas/Pets" + | } + | } + | } + | }, + | "default": { + | "description": "unexpected error", + | "content": { + | "application/json": { + | "schema": { + | "$ref": "#/components/schemas/Error" + | } + | } + | } + | } + | } + | }, + | "post": { + | "summary": "Create a pet", + | "operationId": "createPets", + | "tags": [ + | "pets" + | ], + | "requestBody": { + | "content": { + | "application/json": { + | "schema": { + | "$ref": "#/components/schemas/Pet" + | } + | } + | }, + | "required": true + | }, + | "responses": { + | "201": { + | "description": "Null response" + | }, + | "default": { + | "description": "unexpected error", + | "content": { + | "application/json": { + | "schema": { + | "$ref": "#/components/schemas/Error" + | } + | } + | } + | } + | } + | } + | } + | }, + | "components": { + | "schemas": { + | "Pet": { + | "type": "object", + | "required": [ + | "id", + | "name" + | ], + | "properties": { + | "id": { + | "type": "integer", + | "format": "int64" + | }, + | "name": { + | "type": "string", + | "minLength": 3 + | }, + | "tag": { + | "type": "string" + | } + | } + | }, + | "Pets": { + | "type": "array", + | "maxItems": 100, + | "items": { + | "$ref": "#/components/schemas/Pet" + | } + | }, + | "Error": { + | "type": "object", + | "required": [ + | "code", + | "message" + | ], + | "properties": { + | "code": { + | "type": "integer", + | "format": "int32" + | }, + | "message": { + | "type": "string" + | } + | } + | } + | } + | } + |}""".stripMargin + val openAPI = OpenAPI.fromJson(json).toOption.get + val code = EndpointGen.fromOpenAPI(openAPI) + val tempDir = Files.createTempDirectory("codegen") + + CodeGen.writeFiles(code, java.nio.file.Paths.get(tempDir.toString, "test"), "test", Some(scalaFmtPath)) + + fileShouldBe( + tempDir, + "test/Pets.scala", + "/EndpointsWithOverlappingPath.scala", + ) + }, ) @@ java11OrNewer @@ flaky @@ blocking // Downloading scalafmt on CI is flaky }