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 5b7368de2b..2c00573abf 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 @@ -1398,8 +1398,7 @@ object OpenAPIGenSpec extends ZIOSpecDefault { | "width", | "height", | "metadata" - | ], - | "description" : "Test doc\n\n" + | ] | } | } | } diff --git a/zio-http/shared/src/main/scala/zio/http/codec/HttpCodec.scala b/zio-http/shared/src/main/scala/zio/http/codec/HttpCodec.scala index df546bd0da..8a2fe790a8 100644 --- a/zio-http/shared/src/main/scala/zio/http/codec/HttpCodec.scala +++ b/zio-http/shared/src/main/scala/zio/http/codec/HttpCodec.scala @@ -566,6 +566,19 @@ object HttpCodec extends ContentCodecs with HeaderCodecs with MethodCodecs with def tag: AtomTag = AtomTag.Content def index(index: Int): Content[A] = copy(index = index) + + /** + * Returns a new codec, where the value produced by this one is optional. + */ + override def optional: HttpCodec[HttpCodecType.Content, Option[A]] = + Annotated( + Content( + codec.optional, + name, + index, + ), + Metadata.Optional(), + ) } private[http] final case class ContentStream[A]( codec: HttpContentCodec[A], diff --git a/zio-http/shared/src/main/scala/zio/http/codec/HttpContentCodec.scala b/zio-http/shared/src/main/scala/zio/http/codec/HttpContentCodec.scala index c0223e4fe6..3f8a3f29a2 100644 --- a/zio-http/shared/src/main/scala/zio/http/codec/HttpContentCodec.scala +++ b/zio-http/shared/src/main/scala/zio/http/codec/HttpContentCodec.scala @@ -148,6 +148,36 @@ sealed trait HttpContentCodec[A] { self => choices.headOption.map(_._2).getOrElse { throw new IllegalArgumentException(s"No codec defined") } + + def optional: HttpContentCodec[Option[A]] = + self match { + case HttpContentCodec.Choices(choices) => + HttpContentCodec.Choices( + choices.map { case (mediaType, BinaryCodecWithSchema(fromConfig, schema)) => + mediaType -> BinaryCodecWithSchema(fromConfig.andThen(optBinaryCodec), schema.optional) + }, + ) + case HttpContentCodec.Filtered(codec, mediaType) => + HttpContentCodec.Filtered(codec.optional, mediaType) + } + + private def optBinaryCodec(bc: BinaryCodec[A]): BinaryCodec[Option[A]] = new BinaryCodec[Option[A]] { + override def encode(value: Option[A]): Chunk[Byte] = value match { + case Some(a) => bc.encode(a) + case None => Chunk.empty + } + + override def decode(bytes: Chunk[Byte]): Either[DecodeError, Option[A]] = + if (bytes.isEmpty) Right(None) + else bc.decode(bytes).map(Some(_)) + + override def streamDecoder: ZPipeline[Any, DecodeError, Byte, Option[A]] = + ZPipeline.chunks[Byte].map(bc.decode).map(_.toOption) + + override def streamEncoder: ZPipeline[Any, Nothing, Option[A], Byte] = + ZPipeline.identity[Option[A]].map(_.fold(Chunk.empty[Byte])(bc.encode)).flattenChunks + } + } object HttpContentCodec { diff --git a/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala b/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala index 4163ace700..cdc38d22d2 100644 --- a/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala +++ b/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala @@ -117,8 +117,8 @@ private[openapi] object BoolOrSchema { private[openapi] sealed trait TypeOrTypes { self => def add(value: String): TypeOrTypes = self match { - case TypeOrTypes.Type(string) => TypeOrTypes.Types(Chunk(string, value)) - case TypeOrTypes.Types(chunk) => TypeOrTypes.Types(chunk :+ value) + case TypeOrTypes.Type(string) => TypeOrTypes.Types(Chunk(string, value).distinct) + case TypeOrTypes.Types(chunk) => TypeOrTypes.Types((chunk :+ value).distinct) } }