From a9ab6fe6cf3e4d38966594bd5d4663ed208decb7 Mon Sep 17 00:00:00 2001 From: Jules Ivanic Date: Tue, 19 Nov 2024 13:58:28 +1100 Subject: [PATCH 1/2] Reproducer for #3144 --- .../zio/http/endpoint/OptionalBodySpec.scala | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 zio-http/jvm/src/test/scala/zio/http/endpoint/OptionalBodySpec.scala diff --git a/zio-http/jvm/src/test/scala/zio/http/endpoint/OptionalBodySpec.scala b/zio-http/jvm/src/test/scala/zio/http/endpoint/OptionalBodySpec.scala new file mode 100644 index 000000000..9255406cc --- /dev/null +++ b/zio-http/jvm/src/test/scala/zio/http/endpoint/OptionalBodySpec.scala @@ -0,0 +1,69 @@ +/* + * Copyright 2021 - 2023 Sporta Technologies PVT LTD & the ZIO HTTP contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package zio.http.endpoint + +import zio._ +import zio.http._ +import zio.http.codec.Doc +import zio.http.endpoint.AuthType.None +import zio.json.ast.Json +import zio.test._ + +object OptionalBodySpec extends ZIOHttpSpec { + + import zio.schema.codec.json._ + + private val endpoint: Endpoint[Unit, Option[Json], ZNothing, Json, None] = + Endpoint(RoutePattern.POST / "optional" / "body") + .in[Option[Json]](mediaType = MediaType.application.json, doc = Doc.p("Maybe data")) + .out[Json](mediaType = MediaType.application.json, doc = Doc.p("Result")) + + private val api: Routes[Any, Nothing] = + endpoint.implementPurely { + case Some(value) => value + case scala.None => Json.Obj("no" -> Json.Str("body")) + }.toRoutes + + private def makeRequest(body: Option[Json]): Request = + Request + .post( + url = URL.root / "optional" / "body", + body = body.fold(ifEmpty = Body.empty)(b => Body.fromString(b.toString())) + ) + .addHeader(Header.Accept(MediaType.application.json)) + + override def spec: Spec[TestEnvironment with Scope, Throwable] = + suite("OptionalBodySpec")( + test("accepts empty body") { + val body = Option.empty[Json] + + for { + response <- api.runZIO(makeRequest(body)) + body <- response.body.asString + } yield assertTrue(body == """{"no":"body"}""") + }, + test("accepts non-empty body") { + val body = Some(Json.Obj("key" -> Json.Str("value"))) + + for { + response <- api.runZIO(makeRequest(body)) + body <- response.body.asString + } yield assertTrue(body == """{"key":"value"}""") + }, + ) + +} From fa5c9f74f3ee0a0bb0404cca0a90e59d28d31ab5 Mon Sep 17 00:00:00 2001 From: Jules Ivanic Date: Tue, 19 Nov 2024 15:42:55 +1100 Subject: [PATCH 2/2] Fix for #3144 --- .../scala/zio/http/endpoint/Endpoint.scala | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala b/zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala index 7d3f09426..1f27f51ae 100644 --- a/zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala +++ b/zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala @@ -18,13 +18,9 @@ package zio.http.endpoint import scala.annotation.nowarn import scala.reflect.ClassTag - import zio._ - import zio.stream.ZStream - import zio.schema.Schema - import zio.http.Header.Accept.MediaTypeWithQFactor import zio.http._ import zio.http.codec._ @@ -367,66 +363,69 @@ final case class Endpoint[PathInput, Input, Err, Output, Auth <: AuthType]( * Returns a new endpoint derived from this one, whose request content must * satisfy the specified schema. */ - def in[Input2: HttpContentCodec](implicit + def in[Input2: EnvironmentTag: HttpContentCodec](implicit combiner: Combiner[Input, Input2], ): Endpoint[PathInput, combiner.Out, Err, Output, Auth] = - copy(input = input ++ HttpCodec.content[Input2]) + inCodec(HttpCodec.content[Input2]) /** * Returns a new endpoint derived from this one, whose request content must * satisfy the specified schema and is documented. */ - def in[Input2: HttpContentCodec](doc: Doc)(implicit + def in[Input2: EnvironmentTag: HttpContentCodec](doc: Doc)(implicit combiner: Combiner[Input, Input2], ): Endpoint[PathInput, combiner.Out, Err, Output, Auth] = - copy(input = input ++ HttpCodec.content[Input2] ?? doc) + inCodec(HttpCodec.content[Input2] ?? doc) /** * Returns a new endpoint derived from this one, whose request content must * satisfy the specified schema and is documented. */ - def in[Input2: HttpContentCodec](name: String)(implicit + def in[Input2: EnvironmentTag: HttpContentCodec](name: String)(implicit combiner: Combiner[Input, Input2], ): Endpoint[PathInput, combiner.Out, Err, Output, Auth] = - copy(input = input ++ HttpCodec.content[Input2](name)) + inCodec(HttpCodec.content[Input2](name)) /** * Returns a new endpoint derived from this one, whose request content must * satisfy the specified schema and is documented. */ - def in[Input2: HttpContentCodec](name: String, doc: Doc)(implicit + def in[Input2: EnvironmentTag: HttpContentCodec](name: String, doc: Doc)(implicit combiner: Combiner[Input, Input2], ): Endpoint[PathInput, combiner.Out, Err, Output, Auth] = - copy(input = input ++ (HttpCodec.content[Input2](name) ?? doc)) + inCodec(HttpCodec.content[Input2](name) ?? doc) - def in[Input2: HttpContentCodec](mediaType: MediaType)(implicit + def in[Input2: EnvironmentTag: HttpContentCodec](mediaType: MediaType)(implicit combiner: Combiner[Input, Input2], ): Endpoint[PathInput, combiner.Out, Err, Output, Auth] = - copy(input = input ++ HttpCodec.content[Input2](mediaType)) + inCodec(HttpCodec.content[Input2](mediaType)) - def in[Input2: HttpContentCodec](mediaType: MediaType, doc: Doc)(implicit + def in[Input2: EnvironmentTag: HttpContentCodec](mediaType: MediaType, doc: Doc)(implicit combiner: Combiner[Input, Input2], ): Endpoint[PathInput, combiner.Out, Err, Output, Auth] = - copy(input = input ++ (HttpCodec.content(mediaType) ?? doc)) + inCodec(HttpCodec.content(mediaType) ?? doc) - def in[Input2: HttpContentCodec](mediaType: MediaType, name: String)(implicit + def in[Input2: EnvironmentTag: HttpContentCodec](mediaType: MediaType, name: String)(implicit combiner: Combiner[Input, Input2], ): Endpoint[PathInput, combiner.Out, Err, Output, Auth] = - copy(input = input ++ HttpCodec.content(name, mediaType)) + inCodec(HttpCodec.content(name, mediaType)) - def in[Input2: HttpContentCodec](mediaType: MediaType, name: String, doc: Doc)(implicit + def in[Input2: EnvironmentTag: HttpContentCodec](mediaType: MediaType, name: String, doc: Doc)(implicit combiner: Combiner[Input, Input2], ): Endpoint[PathInput, combiner.Out, Err, Output, Auth] = - copy(input = input ++ (HttpCodec.content(name, mediaType) ?? doc)) + inCodec(HttpCodec.content(name, mediaType) ?? doc) /** * Returns a new endpoint derived from this one, whose request must satisfy * the specified codec. */ - def inCodec[Input2](codec: HttpCodec[HttpCodecType.RequestType, Input2])(implicit + def inCodec[Input2: EnvironmentTag](codec: HttpCodec[HttpCodecType.RequestType, Input2])(implicit combiner: Combiner[Input, Input2], - ): Endpoint[PathInput, combiner.Out, Err, Output, Auth] = - copy(input = input ++ codec) + ): Endpoint[PathInput, combiner.Out, Err, Output, Auth] = { + val isOptional = EnvironmentTag[Input2] <:< EnvironmentTag[Option[?]] + println(s"==================== Is optional: $isOptional ==================== ") + copy(input = input ++ (if (isOptional) codec.optional.asInstanceOf[zio.http.codec.HttpCodec[zio.http.codec.HttpCodecType.RequestType,Input2]] else codec)) + } /** * Returns a new endpoint derived from this one, whose input type is a stream