All Downloads are FREE. Search and download functionalities are using the official Maven repository.

zio.http.Body.scala Maven / Gradle / Ivy

/*
 * 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

import java.io.FileInputStream
import java.nio.charset._
import java.nio.file._

import zio._

import zio.stream.ZStream

import zio.schema.Schema
import zio.schema.codec.BinaryCodec

import zio.http.internal.BodyEncoding
import zio.http.multipart.mixed.MultipartMixed

/**
 * Represents the body of a request or response. The body can be a fixed chunk
 * of bytes, a stream of bytes, or form data, or any type that can be encoded
 * into such representations (such as textual data using some character
 * encoding, the contents of files, JSON, etc.).
 */
trait Body { self =>

  /**
   * A right-biased way of combining two bodies. If either body is empty, the
   * other will be returned. Otherwise, the right body will be returned.
   */
  def ++(that: Body): Body = if (that.isEmpty) self else that

  /**
   * Decodes the content of the body as a value based on a zio-schema
   * [[zio.schema.codec.BinaryCodec]].
* * Example for json: * {{{ * import zio.schema.json.codec._ * case class Person(name: String, age: Int) * implicit val schema: Schema[Person] = DeriveSchema.gen[Person] * val person = Person("John", 42) * val body = Body.from(person) * val decodedPerson = body.to[Person] * }}} */ def to[A](implicit codec: BinaryCodec[A], trace: Trace): Task[A] = asChunk.flatMap(bytes => ZIO.fromEither(codec.decode(bytes))) /** * Returns an effect that decodes the content of the body as array of bytes. * Note that attempting to decode a large stream of bytes into an array could * result in an out of memory error. */ def asArray(implicit trace: Trace): Task[Array[Byte]] /** * Returns an effect that decodes the content of the body as a chunk of bytes. * Note that attempting to decode a large stream of bytes into a chunk could * result in an out of memory error. */ def asChunk(implicit trace: Trace): Task[Chunk[Byte]] /** * Returns an effect that decodes the content of the body as a multipart form. * Note that attempting to decode a large stream of bytes into a form could * result in an out of memory error. */ def asMultipartForm(implicit trace: Trace): Task[Form] = { boundary match { case Some(boundary) => StreamingForm(asStream, boundary).collectAll case _ => for { bytes <- asChunk form <- Form.fromMultipartBytes(bytes, Charsets.Http, boundary) } yield form } } /** * Returns an effect that decodes the streaming body as a multipart form. * * The result is a stream of FormField objects, where each FormField may be a * StreamingBinary or a Text object. The StreamingBinary object contains a * stream of bytes, which has to be consumed asynchronously by the user to get * the next FormField from the stream. */ def asMultipartFormStream(implicit trace: Trace): Task[StreamingForm] = boundary match { case Some(boundary) => ZIO.succeed( StreamingForm(asStream, boundary), ) case None => ZIO.fail( new IllegalStateException("Cannot decode body as streaming multipart/form-data without a known boundary"), ) } /** * Returns an effect that decodes the streaming body as a multipart/mixed. * * The result is a stream of Part objects, where each Part has headers and * contents (binary stream), Part objects can be easily converted to a Body * objects which provide vast API for extracting their contents. */ def asMultipartMixed(implicit trace: Trace): Task[MultipartMixed] = ZIO.fromOption { MultipartMixed .fromBody(self) } .orElseFail(new IllegalStateException("Cannot decode body as multipart/mixed without a known boundary")) def asServerSentEvents[T: Schema](implicit trace: Trace): ZStream[Any, Throwable, ServerSentEvent[T]] = { val codec = ServerSentEvent.defaultBinaryCodec[T] asStream >>> codec.streamDecoder } /** * Returns a stream that contains the bytes of the body. This method is safe * to use with large bodies, because the elements of the returned stream are * lazily produced from the body. */ def asStream(implicit trace: Trace): ZStream[Any, Throwable, Byte] /** * Decodes the content of the body as a string with the default charset. Note * that attempting to decode a large stream of bytes into a string could * result in an out of memory error. */ final def asString(implicit trace: Trace): Task[String] = asArray.map(new String(_, Charsets.Http)) /** * Decodes the content of the body as a string with the provided charset. Note * that attempting to decode a large stream of bytes into a string could * result in an out of memory error. */ final def asString(charset: Charset)(implicit trace: Trace): Task[String] = asArray.map(new String(_, charset)) /** * Returns an effect that decodes the content of the body as form data. */ def asURLEncodedForm(implicit trace: Trace): Task[Form] = asString.flatMap(string => ZIO.fromEither(Form.fromURLEncoded(string, Charsets.Http))) /** * Returns whether or not the bytes of the body have been fully read. */ def isComplete: Boolean /** * Returns whether or not the content length is known */ def knownContentLength: Option[Long] /** * Returns whether or not the body is known to be empty. Note that some bodies * may not be known to be empty until an attempt is made to consume them. */ def isEmpty: Boolean def contentType: Option[Body.ContentType] def contentType(newContentType: Body.ContentType): Body /** * Materializes the body of the request into memory */ def materialize(implicit trace: Trace): UIO[Body] = asArray.foldCause(Body.ErrorBody(_), Body.ArrayBody(_, self.contentType)) /** * Returns the media type for this Body */ final def mediaType: Option[MediaType] = contentType.map(_.mediaType) /** * Updates the media type attached to this body, returning a new Body with the * updated media type */ final def contentType(newMediaType: MediaType): Body = this contentType { this.contentType .map(_.copy(mediaType = newMediaType)) .getOrElse(Body.ContentType(newMediaType)) } final def contentType(newMediaType: MediaType, newBoundary: Boundary): Body = contentType( Body.ContentType(newMediaType, Some(newBoundary)), ) private[zio] final def boundary: Option[Boundary] = contentType.flatMap(_.boundary) } object Body { /** * A body that contains no data. */ val empty: Body = EmptyBody final case class ContentType( mediaType: MediaType, boundary: Option[Boundary] = None, charset: Option[Charset] = None, ) { def asHeader: Header.ContentType = Header.ContentType(mediaType, boundary, charset) } object ContentType { def fromHeader(h: Header.ContentType): ContentType = ContentType(h.mediaType, h.boundary, h.charset) } /** * Constructs a [[zio.http.Body]] from a value based on a zio-schema * [[zio.schema.codec.BinaryCodec]].
Example for json: * {{{ * import zio.schema.codec.JsonCodec._ * case class Person(name: String, age: Int) * implicit val schema: Schema[Person] = DeriveSchema.gen[Person] * val person = Person("John", 42) * val body = Body.from(person) * }}} */ def from[A](a: A)(implicit codec: BinaryCodec[A], trace: Trace): Body = fromChunk(codec.encode(a)) /** * Constructs a [[zio.http.Body]] from the contents of a file. */ def fromCharSequence( charSequence: CharSequence, charset: Charset = Charsets.Http, ): Body = BodyEncoding.default.fromCharSequence(charSequence, charset) /** * Constructs a [[zio.http.Body]] from a chunk of bytes. */ def fromChunk(data: Chunk[Byte]): Body = ChunkBody(data) /** * Constructs a [[zio.http.Body]] from a chunk of bytes and sets the media * type. */ def fromChunk(data: Chunk[Byte], mediaType: MediaType): Body = fromChunk(data, Body.ContentType(mediaType)) def fromChunk(data: Chunk[Byte], contentType: Body.ContentType): Body = ChunkBody(data, Some(contentType)) /** * Constructs a [[zio.http.Body]] from an array of bytes. * * WARNING: The array must not be mutated after creating the body. */ def fromArray(data: Array[Byte]): Body = ArrayBody(data) /** * Constructs a [[zio.http.Body]] from the contents of a file. */ def fromFile(file: java.io.File, chunkSize: Int = 1024 * 4)(implicit trace: Trace): ZIO[Any, Nothing, Body] = { ZIO.attemptBlocking(file.length()).orDie.map { fileSize => FileBody(file, chunkSize, fileSize) } } /** * Constructs a [[zio.http.Body]] from from form data, using multipart * encoding and the specified character set, which defaults to UTF-8. */ def fromMultipartForm( form: Form, specificBoundary: Boundary, )(implicit trace: Trace): Body = { val bytes = form.multipartBytes(specificBoundary) StreamBody( bytes, knownContentLength = None, Some( Body.ContentType(MediaType.multipart.`form-data`, Some(specificBoundary)), ), ) } /** * Constructs a [[zio.http.Body]] from from form data, using multipart * encoding and the specified character set, which defaults to UTF-8. Utilizes * a random boundary based on a UUID. */ def fromMultipartFormUUID( form: Form, )(implicit trace: Trace): UIO[Body] = form.multipartBytesUUID.map { case (boundary, bytes) => StreamBody( bytes, knownContentLength = None, Some(Body.ContentType(MediaType.multipart.`form-data`, Some(boundary))), ) } /** * Constructs a [[zio.http.Body]] from a stream of bytes with a known length. */ def fromStream(stream: ZStream[Any, Throwable, Byte], contentLength: Long): Body = StreamBody(stream, knownContentLength = Some(contentLength)) /** * Constructs a [[zio.http.Body]] from a stream of bytes with a known length * that depends on `R`. */ def fromStreamEnv[R](stream: ZStream[R, Throwable, Byte], contentLength: Long)(implicit trace: Trace): RIO[R, Body] = ZIO.environmentWith[R][Body](r => fromStream(stream.provideEnvironment(r), contentLength)) /** * Constructs a [[zio.http.Body]] from stream of values based on a zio-schema * [[zio.schema.codec.BinaryCodec]].
* * Example for json: * {{{ * import zio.schema.codec.JsonCodec._ * case class Person(name: String, age: Int) * implicit val schema: Schema[Person] = DeriveSchema.gen[Person] * val persons = ZStream(Person("John", 42)) * val body = Body.fromStream(persons) * }}} */ def fromStream[A](stream: ZStream[Any, Throwable, A])(implicit codec: BinaryCodec[A], trace: Trace): Body = StreamBody(stream >>> codec.streamEncoder, knownContentLength = None) /** * Constructs a [[zio.http.Body]] from stream of values based on a zio-schema * [[zio.schema.codec.BinaryCodec]] and depends on `R`
*/ def fromStreamEnv[A, R]( stream: ZStream[R, Throwable, A], )(implicit codec: BinaryCodec[A], trace: Trace): RIO[R, Body] = ZIO.environmentWith[R][Body](r => fromStream(stream.provideEnvironment(r))) /** * Constructs a [[zio.http.Body]] from a stream of bytes of unknown length, * using chunked transfer encoding. */ def fromStreamChunked(stream: ZStream[Any, Throwable, Byte]): Body = StreamBody(stream, knownContentLength = None) /** * Constructs a [[zio.http.Body]] from a stream of bytes of unknown length * that depends on `R`, using chunked transfer encoding. */ def fromStreamChunkedEnv[R](stream: ZStream[R, Throwable, Byte])(implicit trace: Trace): RIO[R, Body] = ZIO.environmentWith[R][Body](r => fromStreamChunked(stream.provideEnvironment(r))) /** * Constructs a [[zio.http.Body]] from a stream of text with known length, * using the specified character set, which defaults to the HTTP character * set. */ def fromCharSequenceStream( stream: ZStream[Any, Throwable, CharSequence], contentLength: Long, charset: Charset = Charsets.Http, )(implicit trace: Trace, ): Body = fromStream(stream.map(seq => Chunk.fromArray(seq.toString.getBytes(charset))).flattenChunks, contentLength) /** * Constructs a [[zio.http.Body]] from a stream of text with known length that * depends on `R`, using the specified character set, which defaults to the * HTTP character set. */ def fromCharSequenceStreamEnv[R]( stream: ZStream[R, Throwable, CharSequence], contentLength: Long, charset: Charset = Charsets.Http, )(implicit trace: Trace, ): RIO[R, Body] = fromStreamEnv(stream.map(seq => Chunk.fromArray(seq.toString.getBytes(charset))).flattenChunks, contentLength) /** * Constructs a [[zio.http.Body]] from a stream of text with unknown length * using chunked transfer encoding, using the specified character set, which * defaults to the HTTP character set. */ def fromCharSequenceStreamChunked( stream: ZStream[Any, Throwable, CharSequence], charset: Charset = Charsets.Http, )(implicit trace: Trace, ): Body = fromStreamChunked(stream.map(seq => Chunk.fromArray(seq.toString.getBytes(charset))).flattenChunks) /** * Constructs a [[zio.http.Body]] from a stream of text with unknown length * using chunked transfer encoding that depends on `R`, using the specified * character set, which defaults to the HTTP character set. */ def fromCharSequenceStreamChunkedEnv[R]( stream: ZStream[R, Throwable, CharSequence], charset: Charset = Charsets.Http, )(implicit trace: Trace, ): RIO[R, Body] = fromStreamChunkedEnv(stream.map(seq => Chunk.fromArray(seq.toString.getBytes(charset))).flattenChunks) /** * Helper to create Body from String */ def fromString(text: String, charset: Charset = Charsets.Http): Body = fromCharSequence(text, charset) /** * Constructs a [[zio.http.Body]] from form data using URL encoding and the * default character set. */ def fromURLEncodedForm(form: Form, charset: Charset = StandardCharsets.UTF_8): Body = { fromString(form.urlEncoded(charset), charset).contentType(MediaType.application.`x-www-form-urlencoded`) } def fromSocketApp(app: WebSocketApp[Any]): WebsocketBody = WebsocketBody(app) private[zio] abstract class UnsafeBytes extends Body { self => private[zio] def unsafeAsArray(implicit unsafe: Unsafe): Array[Byte] final override def materialize(implicit trace: Trace): UIO[Body] = Exit.succeed(self) } /** * Helper to create empty Body */ private[zio] case object EmptyBody extends UnsafeBytes { override def asArray(implicit trace: Trace): Task[Array[Byte]] = zioEmptyArray override def asChunk(implicit trace: Trace): Task[Chunk[Byte]] = zioEmptyChunk override def asStream(implicit trace: Trace): ZStream[Any, Throwable, Byte] = ZStream.empty override def isComplete: Boolean = true override def isEmpty: Boolean = true override def toString: String = "Body.empty" override private[zio] def unsafeAsArray(implicit unsafe: Unsafe): Array[Byte] = Array.empty[Byte] override def contentType(newContentType: Body.ContentType): Body = this override def contentType: Option[Body.ContentType] = None override def knownContentLength: Option[Long] = Some(0L) } private[zio] final case class ErrorBody(cause: Cause[Throwable]) extends Body { override def asArray(implicit trace: Trace): Task[Array[Byte]] = Exit.failCause(cause) override def asChunk(implicit trace: Trace): Task[Chunk[Byte]] = Exit.failCause(cause) override def asStream(implicit trace: Trace): ZStream[Any, Throwable, Byte] = ZStream.failCause(cause) override def isComplete: Boolean = true override def isEmpty: Boolean = true override def toString: String = "Body.failed" override def contentType(newContentType: Body.ContentType): Body = this override def contentType: Option[Body.ContentType] = None override def knownContentLength: Option[Long] = Some(0L) } private[zio] final case class ChunkBody( data: Chunk[Byte], override val contentType: Option[Body.ContentType] = None, ) extends UnsafeBytes { self => override def asArray(implicit trace: Trace): Task[Array[Byte]] = ZIO.succeed(data.toArray) override def isComplete: Boolean = true override def isEmpty: Boolean = data.isEmpty override def asChunk(implicit trace: Trace): Task[Chunk[Byte]] = Exit.succeed(data) override def asStream(implicit trace: Trace): ZStream[Any, Throwable, Byte] = ZStream.unwrap(asChunk.map(ZStream.fromChunk(_))) override def toString: String = s"Body.fromChunk($data)" override private[zio] def unsafeAsArray(implicit unsafe: Unsafe): Array[Byte] = data.toArray override def contentType(newContentType: Body.ContentType): Body = copy(contentType = Some(newContentType)) override def knownContentLength: Option[Long] = Some(data.length.toLong) } private[zio] final case class ArrayBody( data: Array[Byte], override val contentType: Option[Body.ContentType] = None, ) extends UnsafeBytes { self => override def asArray(implicit trace: Trace): Task[Array[Byte]] = Exit.succeed(data) override def isComplete: Boolean = true override def isEmpty: Boolean = data.isEmpty override def asChunk(implicit trace: Trace): Task[Chunk[Byte]] = Exit.succeed(Chunk.fromArray(data)) override def asStream(implicit trace: Trace): ZStream[Any, Throwable, Byte] = ZStream.unwrap(asChunk.map(ZStream.fromChunk(_))) override def toString: String = s"Body.fromArray($data)" override private[zio] def unsafeAsArray(implicit unsafe: Unsafe): Array[Byte] = data override def contentType(newContentType: Body.ContentType): Body = copy(contentType = Some(newContentType)) override def knownContentLength: Option[Long] = Some(data.length.toLong) } private[zio] final case class FileBody( file: java.io.File, chunkSize: Int = 1024 * 4, fileSize: Long, override val contentType: Option[Body.ContentType] = None, ) extends Body { override def asArray(implicit trace: Trace): Task[Array[Byte]] = ZIO.attemptBlocking { Files.readAllBytes(file.toPath) } override def isComplete: Boolean = false override def isEmpty: Boolean = false override def asChunk(implicit trace: Trace): Task[Chunk[Byte]] = asArray.map(Chunk.fromArray) override def asStream(implicit trace: Trace): ZStream[Any, Throwable, Byte] = ZStream.unwrap { for { file <- ZIO.attempt(file) fs <- ZIO.attemptBlocking(new FileInputStream(file)) size <- ZIO.attemptBlocking(Math.min(chunkSize.toLong, file.length()).toInt) } yield ZStream .repeatZIOOption[Any, Throwable, Chunk[Byte]] { for { buffer <- ZIO.succeed(new Array[Byte](size)) len <- ZIO.attemptBlocking(fs.read(buffer)).mapError(Some(_)) bytes <- if (len > 0) ZIO.succeed(Chunk.fromArray(buffer.slice(0, len))) else ZIO.fail(None) } yield bytes } .ensuring(ZIO.attemptBlocking(fs.close()).ignoreLogged) }.flattenChunks override def contentType(newContentType: Body.ContentType): Body = copy(contentType = Some(newContentType)) override def knownContentLength: Option[Long] = Some(fileSize) } private[zio] final case class StreamBody( stream: ZStream[Any, Throwable, Byte], knownContentLength: Option[Long], override val contentType: Option[Body.ContentType] = None, ) extends Body { override def asArray(implicit trace: Trace): Task[Array[Byte]] = asChunk.map(_.toArray) override def isComplete: Boolean = false override def isEmpty: Boolean = false override def asChunk(implicit trace: Trace): Task[Chunk[Byte]] = stream.runCollect override def asStream(implicit trace: Trace): ZStream[Any, Throwable, Byte] = stream override def contentType(newContentType: Body.ContentType): Body = copy(contentType = Some(newContentType)) } private[zio] final case class WebsocketBody(socketApp: WebSocketApp[Any]) extends Body { def asArray(implicit trace: Trace): Task[Array[Byte]] = zioEmptyArray def asChunk(implicit trace: Trace): Task[Chunk[Byte]] = zioEmptyChunk def asStream(implicit trace: Trace): ZStream[Any, Throwable, Byte] = ZStream.empty def isComplete: Boolean = true def isEmpty: Boolean = true def contentType: Option[Body.ContentType] = None def contentType(newContentType: Body.ContentType): zio.http.Body = this override def knownContentLength: Option[Long] = Some(0L) } private val zioEmptyArray = Exit.succeed(Array.emptyByteArray) private val zioEmptyChunk = Exit.succeed(Chunk.empty[Byte]) }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy