Skip to content

Commit

Permalink
Merge branch 'main' into implement-ergonomics
Browse files Browse the repository at this point in the history
  • Loading branch information
987Nabil authored Jun 8, 2024
2 parents d35d4c1 + d0c8684 commit 5cc6b94
Show file tree
Hide file tree
Showing 11 changed files with 374 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
18 changes: 18 additions & 0 deletions zio-http-gen/src/test/resources/EndpointsWithOverlappingPath.scala
Original file line number Diff line number Diff line change
@@ -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)

}
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
161 changes: 161 additions & 0 deletions zio-http-gen/src/test/scala/zio/http/gen/scala/CodeGenSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package zio.http

import java.io.{File, FileNotFoundException}
import java.nio.charset.Charset
import java.util.zip.ZipFile

import zio.{Trace, ZIO}
Expand All @@ -13,21 +14,24 @@ trait HandlerPlatformSpecific {
/**
* Creates a handler from a resource path
*/
def fromResource(path: String)(implicit trace: Trace): Handler[Any, Throwable, Any, Response] =
def fromResource(path: String, charset: Charset = Charsets.Utf8)(implicit
trace: Trace,
): Handler[Any, Throwable, Any, Response] =
Handler.fromZIO {
ZIO
.attemptBlocking(getClass.getClassLoader.getResource(path))
.map { resource =>
if (resource == null) Handler.fail(new FileNotFoundException(s"Resource $path not found"))
else fromResourceWithURL(resource)
else fromResourceWithURL(resource, charset)
}
}.flatten

private[zio] def fromResourceWithURL(
url: java.net.URL,
charset: Charset,
)(implicit trace: Trace): Handler[Any, Throwable, Any, Response] = {
url.getProtocol match {
case "file" => Handler.fromFile(new File(url.getPath))
case "file" => Handler.fromFile(new File(url.getPath), charset)
case "jar" =>
val path = new java.net.URI(url.getPath).getPath // remove "file:" prefix and normalize whitespace
val bangIndex = path.indexOf('!')
Expand Down Expand Up @@ -56,8 +60,9 @@ trait HandlerPlatformSpecific {
.flatMap { case (entry, jar) => ZStream.fromInputStream(jar.getInputStream(entry)) }
response = Response(body = Body.fromStream(inZStream, contentLength))
} yield mediaType.fold(response) { t =>
val charset0 = if (t.mainType == "text" || !t.binary) Some(charset) else None
response
.addHeader(Header.ContentType(t))
.addHeader(Header.ContentType(t, charset = charset0))
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions zio-http/jvm/src/test/scala/zio/http/ContentTypeSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ object ContentTypeSpec extends HttpRunnableSpec {
test("js") {
val res =
Handler.fromResource("TestFile3.js").sandbox.toRoutes.deploy(Request()).map(_.header(Header.ContentType))
assertZIO(res)(isSome(equalTo(Header.ContentType(MediaType.application.`javascript`))))
assertZIO(res)(
isSome(equalTo(Header.ContentType(MediaType.application.`javascript`, charset = Some(Charsets.Utf8)))),
)
},
test("no extension") {
val res = Handler.fromResource("TestFile4").sandbox.toRoutes.deploy(Request()).map(_.header(Header.ContentType))
Expand All @@ -43,7 +45,7 @@ object ContentTypeSpec extends HttpRunnableSpec {
test("css") {
val res =
Handler.fromResource("TestFile5.css").sandbox.toRoutes.deploy(Request()).map(_.header(Header.ContentType))
assertZIO(res)(isSome(equalTo(Header.ContentType(MediaType.text.`css`))))
assertZIO(res)(isSome(equalTo(Header.ContentType(MediaType.text.`css`, charset = Some(Charsets.Utf8)))))
},
test("mp3") {
val res =
Expand Down
Loading

0 comments on commit 5cc6b94

Please sign in to comment.