From 5e7f1473af66fc708e5934c686210227663fbc93 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Fri, 27 Sep 2024 20:36:13 +0200 Subject: [PATCH] Ignore empty appending to RoutePattern and render empty as slash (#3110) (#3121) --- .../scala/zio/http/codec/PathCodecSpec.scala | 7 ++---- .../endpoint/openapi/OpenAPIGenSpec.scala | 23 +++++++++++++++++-- .../src/main/scala/zio/http/Method.scala | 4 +++- .../main/scala/zio/http/RoutePattern.scala | 4 +++- .../main/scala/zio/http/codec/PathCodec.scala | 7 ++++-- .../scala/zio/http/codec/SegmentCodec.scala | 1 + 6 files changed, 35 insertions(+), 11 deletions(-) diff --git a/zio-http/jvm/src/test/scala/zio/http/codec/PathCodecSpec.scala b/zio-http/jvm/src/test/scala/zio/http/codec/PathCodecSpec.scala index 8c00e783ba..fc5026061b 100644 --- a/zio-http/jvm/src/test/scala/zio/http/codec/PathCodecSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/codec/PathCodecSpec.scala @@ -194,17 +194,14 @@ object PathCodecSpec extends ZIOHttpSpec { test("/users") { val codec = PathCodec.empty / PathCodec.literal("users") - assertTrue( - codec.segments == - Chunk(SegmentCodec.empty, SegmentCodec.literal("users")), - ) + assertTrue(codec.segments == Chunk(SegmentCodec.literal("users"))) }, ), suite("render")( test("empty") { val codec = PathCodec.empty - assertTrue(codec.render == "") + assertTrue(codec.render == "/") }, test("/users") { val codec = PathCodec.empty / PathCodec.literal("users") diff --git a/zio-http/jvm/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala b/zio-http/jvm/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala index 06e00af622..5b7368de2b 100644 --- a/zio-http/jvm/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala @@ -10,8 +10,8 @@ import zio.schema.{DeriveSchema, Schema} import zio.http.Method.{GET, POST} import zio.http._ -import zio.http.codec.PathCodec.string -import zio.http.codec.{ContentCodec, Doc, HttpCodec} +import zio.http.codec.PathCodec.{empty, string} +import zio.http.codec._ import zio.http.endpoint._ object OpenAPIGenSpec extends ZIOSpecDefault { @@ -211,6 +211,25 @@ object OpenAPIGenSpec extends ZIOSpecDefault { override def spec: Spec[TestEnvironment with Scope, Any] = suite("OpenAPIGenSpec")( + test("root endpoint to OpenAPI") { + val rootEndpoint = Endpoint(Method.GET / empty) + val generated = OpenAPIGen.fromEndpoints("Root Endpoint", "1.0", rootEndpoint) + val json = toJsonAst(generated) + val expectedJson = """|{ + | "openapi" : "3.1.0", + | "info" : { + | "title" : "Root Endpoint", + | "version" : "1.0" + | }, + | "paths" : { + | "/" : { + | "get" : {} + | } + | }, + | "components" : {} + |}""".stripMargin + assertTrue(json == toJsonAst(expectedJson)) + }, test("simple endpoint to OpenAPI") { val generated = OpenAPIGen.fromEndpoints("Simple Endpoint", "1.0", simpleEndpoint.tag("simple", "endpoint")) val json = toJsonAst(generated) diff --git a/zio-http/shared/src/main/scala/zio/http/Method.scala b/zio-http/shared/src/main/scala/zio/http/Method.scala index e367719e89..93083a86bf 100644 --- a/zio-http/shared/src/main/scala/zio/http/Method.scala +++ b/zio-http/shared/src/main/scala/zio/http/Method.scala @@ -31,7 +31,9 @@ sealed trait Method { self => if (that == Method.ANY) self else that - def /[A](that: PathCodec[A]): RoutePattern[A] = RoutePattern.fromMethod(self) / that + def /[A](that: PathCodec[A]): RoutePattern[A] = + if (that == PathCodec.empty) RoutePattern.fromMethod(self).asInstanceOf[RoutePattern[A]] + else RoutePattern.fromMethod(self) / that def matches(that: Method): Boolean = if (self == Method.ANY) true diff --git a/zio-http/shared/src/main/scala/zio/http/RoutePattern.scala b/zio-http/shared/src/main/scala/zio/http/RoutePattern.scala index b73f5b09b4..c42739408b 100644 --- a/zio-http/shared/src/main/scala/zio/http/RoutePattern.scala +++ b/zio-http/shared/src/main/scala/zio/http/RoutePattern.scala @@ -53,7 +53,9 @@ final case class RoutePattern[A](method: Method, pathCodec: PathCodec[A]) { self * Returns a new pattern that is extended with the specified segment pattern. */ def /[B](that: PathCodec[B])(implicit combiner: Combiner[A, B]): RoutePattern[combiner.Out] = - copy(pathCodec = pathCodec ++ that) + if (that == PathCodec.empty) self.asInstanceOf[RoutePattern[combiner.Out]] + else if (pathCodec == PathCodec.empty) copy(pathCodec = that.asInstanceOf[PathCodec[combiner.Out]]) + else copy(pathCodec = pathCodec ++ that) /** * Creates a route from this pattern and the specified handler. diff --git a/zio-http/shared/src/main/scala/zio/http/codec/PathCodec.scala b/zio-http/shared/src/main/scala/zio/http/codec/PathCodec.scala index ead97b5da7..75c7e34140 100644 --- a/zio-http/shared/src/main/scala/zio/http/codec/PathCodec.scala +++ b/zio-http/shared/src/main/scala/zio/http/codec/PathCodec.scala @@ -48,8 +48,11 @@ sealed trait PathCodec[A] extends codec.PathCodecPlatformSpecific { self => final def ++[B](that: PathCodec[B])(implicit combiner: Combiner[A, B]): PathCodec[combiner.Out] = PathCodec.Concat(self, that, combiner) - final def /[B](that: PathCodec[B])(implicit combiner: Combiner[A, B]): PathCodec[combiner.Out] = - self ++ that + final def /[B](that: PathCodec[B])(implicit combiner: Combiner[A, B]): PathCodec[combiner.Out] = { + if (self == PathCodec.empty) that.asInstanceOf[PathCodec[combiner.Out]] + else if (that == PathCodec.empty) self.asInstanceOf[PathCodec[combiner.Out]] + else self ++ that + } final def /[Env, Err](routes: Routes[Env, Err])(implicit ev: PathCodec[A] <:< PathCodec[Unit], diff --git a/zio-http/shared/src/main/scala/zio/http/codec/SegmentCodec.scala b/zio-http/shared/src/main/scala/zio/http/codec/SegmentCodec.scala index a8077c6b64..e60ea41f8c 100644 --- a/zio-http/shared/src/main/scala/zio/http/codec/SegmentCodec.scala +++ b/zio-http/shared/src/main/scala/zio/http/codec/SegmentCodec.scala @@ -114,6 +114,7 @@ sealed trait SegmentCodec[A] { self => } if (self ne SegmentCodec.Empty) b.append('/') loop(self.asInstanceOf[SegmentCodec[_]]) + if (b.isEmpty) b.appendAll("/") b.result() }