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

io.bullet.borer.compat.akkaHttp.scala Maven / Gradle / Ivy

/*
 * Copyright (c) 2019-2024 Mathias Doenitz
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

package io.bullet.borer.compat

import scala.concurrent.Future
import scala.reflect.ClassTag
import _root_.akka.http.scaladsl.marshalling.{Marshaller, ToEntityMarshaller}
import _root_.akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, FromMessageUnmarshaller, Unmarshaller}
import _root_.akka.http.scaladsl.common.EntityStreamingSupport
import _root_.akka.http.scaladsl.marshalling._
import _root_.akka.http.scaladsl.model._
import _root_.akka.http.scaladsl.util.FastFuture
import _root_.akka.http.scaladsl.util.FastFuture._
import _root_.akka.stream.scaladsl.{Flow, Keep, Source}
import _root_.akka.util.ByteString
import _root_.akka.NotUsed
import io.bullet.borer._

trait AkkaHttpCompat {

  // brevity aliases
  type CborDecodingSetup = DecodingSetup.Api[Cbor.DecodingConfig]
  type JsonDecodingSetup = DecodingSetup.Api[Json.DecodingConfig]

  /**
   * Override with a parameterized call to the `borerUnmarshaller` method to apply a custom configuration.
   */
  implicit def borerFromEntityUnmarshaller[T: Decoder]: FromEntityUnmarshaller[T] = borerUnmarshaller()

  implicit final def borerFromMessageUnmarshaller[T: Decoder]: FromMessageUnmarshaller[T] =
    Unmarshaller.messageUnmarshallerFromEntityUnmarshaller(borerFromEntityUnmarshaller)

  /**
   * Provides a [[FromEntityUnmarshaller]] for [[T]] given an implicit borer Decoder for [[T]].
   * Supports both CBOR and JSON with the given MediaTypes.
   */
  final def borerUnmarshaller[T: Decoder](
      cborMediaType: MediaType = MediaTypes.`application/cbor`,
      jsonMediaType: MediaType = MediaTypes.`application/json`,
      configureCbor: CborDecodingSetup => CborDecodingSetup = identity,
      configureJson: JsonDecodingSetup => JsonDecodingSetup = identity
  ): FromEntityUnmarshaller[T] =
    Unmarshaller.withMaterializer { implicit ec => implicit mat => httpEntity =>
      akkaHttp.byteArrayUnmarshaller(httpEntity).fast.flatMap { bytes =>
        if (bytes.length > 0) {
          httpEntity.contentType.mediaType match {
            case `cborMediaType` => FastFuture(configureCbor(Cbor.decode(bytes)).to[T].valueTry)
            case `jsonMediaType` => FastFuture(configureJson(Json.decode(bytes)).to[T].valueTry)
            case _ => FastFuture.failed(Unmarshaller.UnsupportedContentTypeException(cborMediaType, jsonMediaType))
          }
        } else FastFuture.failed(Unmarshaller.NoContentException)
      }
    }

  /**
   * Provides a [[FromEntityUnmarshaller]] for [[T]] given an implicit borer Decoder for [[T]].
   * Supports only CBOR with the given MediaType.
   */
  final def borerCborUnmarshaller[T: Decoder](
      cborMediaType: MediaType = MediaTypes.`application/cbor`,
      configureCbor: CborDecodingSetup => CborDecodingSetup = identity): FromEntityUnmarshaller[T] =
    Unmarshaller.withMaterializer { implicit ec => implicit mat => httpEntity =>
      akkaHttp.byteArrayUnmarshaller(httpEntity).fast.flatMap { bytes =>
        if (bytes.length > 0) {
          httpEntity.contentType.mediaType match {
            case `cborMediaType` => FastFuture(configureCbor(Cbor.decode(bytes)).to[T].valueTry)
            case _               => FastFuture.failed(Unmarshaller.UnsupportedContentTypeException(cborMediaType))
          }
        } else FastFuture.failed(Unmarshaller.NoContentException)
      }
    }

  /**
   * Provides a [[FromEntityUnmarshaller]] for [[T]] given an implicit borer Decoder for [[T]].
   * Supports only JSON with the given MediaType.
   */
  final def borerJsonUnmarshaller[T: Decoder](
      jsonMediaType: MediaType = MediaTypes.`application/json`,
      configureJson: JsonDecodingSetup => JsonDecodingSetup = identity
  ): FromEntityUnmarshaller[T] =
    Unmarshaller.withMaterializer { implicit ec => implicit mat => httpEntity =>
      akkaHttp.byteArrayUnmarshaller(httpEntity).fast.flatMap { bytes =>
        if (bytes.length > 0) {
          httpEntity.contentType.mediaType match {
            case `jsonMediaType` => FastFuture(configureJson(Json.decode(bytes)).to[T].valueTry)
            case _               => FastFuture.failed(Unmarshaller.UnsupportedContentTypeException(jsonMediaType))
          }
        } else FastFuture.failed(Unmarshaller.NoContentException)
      }
    }

  // brevity aliases
  type CborEncodingSetup = EncodingSetup.Api[Cbor.EncodingConfig]
  type JsonEncodingSetup = EncodingSetup.Api[Json.EncodingConfig]

  /**
   * Override with a parameterized call to the `borerMarshaller` method to apply a custom configuration.
   */
  implicit def borerToEntityMarshaller[T: Encoder]: ToEntityMarshaller[T] = borerMarshaller()

  implicit final def borerToResponseMarshaller[T: Encoder]: ToResponseMarshaller[T] =
    Marshaller.liftMarshaller(borerToEntityMarshaller)

  /**
   * Provides a [[ToEntityMarshaller]] for [[T]] given an implicit borer Encoder for [[T]].
   * Supports both CBOR and JSON with the given MediaTypes.
   * Content negotiation will determine, whether CBOR or JSON is produced.
   * If the client accepts both formats with equal q-value the marshaller will produce
   * whatever the `prefer` parameter specifies.
   */
  final def borerMarshaller[T: Encoder](
      cborContentType: ContentType = MediaTypes.`application/cbor`,
      jsonContentType: ContentType = MediaTypes.`application/json`,
      configureCbor: CborEncodingSetup => CborEncodingSetup = identity,
      configureJson: JsonEncodingSetup => JsonEncodingSetup = identity,
      prefer: Target = Json
  ): ToEntityMarshaller[T] = {
    val cborMarshaller = borerCborMarshaller(cborContentType, configureCbor)
    val jsonMarshaller = borerJsonMarshaller(jsonContentType, configureJson)
    if (prefer == Cbor)
      Marshaller.oneOf(cborMarshaller, jsonMarshaller)
    else
      Marshaller.oneOf(jsonMarshaller, cborMarshaller)
  }

  /**
   * Provides a [[ToEntityMarshaller]] for [[T]] given an implicit borer Encoder for [[T]].
   * Supports only CBOR with the given MediaType.
   */
  final def borerCborMarshaller[T: Encoder](
      cborContentType: ContentType = MediaTypes.`application/cbor`,
      configureCbor: CborEncodingSetup => CborEncodingSetup = identity): ToEntityMarshaller[T] =
    Marshaller.byteArrayMarshaller(cborContentType).compose[T] { value =>
      configureCbor(Cbor.encode(value)).toByteArray
    }

  /**
   * Provides a [[ToEntityMarshaller]] for [[T]] given an implicit borer Encoder for [[T]].
   * Supports only JSON with the given MediaType.
   */
  final def borerJsonMarshaller[T: Encoder](
      jsonContentType: ContentType = MediaTypes.`application/json`,
      configureJson: JsonEncodingSetup => JsonEncodingSetup = identity
  ): ToEntityMarshaller[T] =
    Marshaller.byteArrayMarshaller(jsonContentType).compose[T] { value =>
      configureJson(Json.encode(value)).toByteArray
    }

  /**
   * Override with a parameterized call to the `borerStreamUnmarshaller` method to apply a custom configuration.
   */
  implicit def borerJsonStreamFromEntityUnmarshaller[T: Decoder]: FromEntityUnmarshaller[Source[T, NotUsed]] =
    borerStreamUnmarshaller(EntityStreamingSupport.json())

  implicit final def borerJsonStreamFromMessageUnmarshaller[T: Decoder]: FromMessageUnmarshaller[Source[T, NotUsed]] =
    Unmarshaller.messageUnmarshallerFromEntityUnmarshaller(borerJsonStreamFromEntityUnmarshaller)

  /**
   * Provides a [[FromEntityUnmarshaller]] which produces streams of [[T]] given an implicit borer Decoder
   * for [[T]]. Supports JSON or CSV, depending on the given [[EntityStreamingSupport]].
   *
   * @see https://doc.akka.io/api/akka-http/10.1.9/akka/http/scaladsl/common/EntityStreamingSupport.html
   */
  final def borerStreamUnmarshaller[T: Decoder](
      ess: EntityStreamingSupport): FromEntityUnmarshaller[Source[T, NotUsed]] =
    Unmarshaller.withMaterializer { implicit ec => implicit mat =>
      val unmarshalSync = (byteString: ByteString) => Json.decode(byteString.toArray[Byte]).to[T].value
      val unmarshallingFlow =
        if (ess.parallelism > 1) {
          val unmarshalAsync = (byteString: ByteString) => Future(unmarshalSync(byteString))
          if (ess.unordered) Flow[ByteString].mapAsyncUnordered(ess.parallelism)(unmarshalAsync)
          else Flow[ByteString].mapAsync(ess.parallelism)(unmarshalAsync)
        } else Flow[ByteString].map(unmarshalSync)

      httpEntity => {
        if (ess.supported matches httpEntity.contentType) {
          val frames = httpEntity.dataBytes.via(ess.framingDecoder)
          FastFuture.successful(frames.viaMat(unmarshallingFlow)(Keep.right))
        } else FastFuture.failed(Unmarshaller.UnsupportedContentTypeException(ess.supported))
      }
    }

  /**
   * Override with a parameterized call to the `borerStreamMarshaller` method to apply a custom configuration.
   */
  implicit def borerJsonStreamToEntityMarshaller[T: ToEntityMarshaller: ClassTag]
      : ToEntityMarshaller[Source[T, NotUsed]] =
    borerStreamMarshaller[T](EntityStreamingSupport.json())

  implicit final def borerJsonStreamToResponseMarshaller[T: ToEntityMarshaller: ClassTag]
      : ToResponseMarshaller[Source[T, NotUsed]] =
    Marshaller.liftMarshaller(borerJsonStreamToEntityMarshaller)

  /**
   * Provides a [[ToEntityMarshaller]] for streams of [[T]] given an implicit borer Encoder for [[T]].
   * Supports JSON or CSV, depending on the given [[EntityStreamingSupport]].
   */
  final def borerStreamMarshaller[T](ess: EntityStreamingSupport)(
      implicit marshaller: ToEntityMarshaller[T],
      classTag: ClassTag[T]): ToEntityMarshaller[Source[T, NotUsed]] = {

    type Marshallings = List[Marshalling[MessageEntity]]
    val contentType = ess.contentType
    val selectMarshalling: Marshallings => Option[() => MessageEntity] =
      contentType match {
        case _: ContentType.Binary | _: ContentType.WithFixedCharset | _: ContentType.WithMissingCharset =>
          (_: Marshallings).collectFirst { case Marshalling.WithFixedContentType(`contentType`, marshal) => marshal }
        case ContentType.WithCharset(mediaType, charset) =>
          (_: Marshallings).collectFirst {
            case Marshalling.WithFixedContentType(`contentType`, marshal) => marshal
            case Marshalling.WithOpenCharset(`mediaType`, marshal)        => () => marshal(charset)
          }
      }

    Marshaller[Source[T, NotUsed], MessageEntity] { implicit ec => source =>
      FastFuture successful {
        Marshalling.WithFixedContentType(
          contentType,
          () => {
            val byteStream =
              source
                .mapAsync(1)(value => marshaller(value)(ec))
                .map { marshallings =>
                  selectMarshalling(marshallings)
                    .orElse(marshallings collectFirst { case Marshalling.Opaque(x) => x })
                    .getOrElse(
                      throw new NoStrictlyCompatibleElementMarshallingAvailableException[T](contentType, marshallings))
                }
                .flatMapConcat(_.apply().dataBytes) // marshal!
                .via(ess.framingRenderer)

            HttpEntity(contentType, byteStream)
          }) :: Nil
      }
    }
  }
}

/**
 * Automatic to and from CBOR and/or JSON marshalling/unmarshalling using in-scope borer Encoders / Decoders.
 */
object akkaHttp extends AkkaHttpCompat {

  private[compat] val byteArrayUnmarshaller: FromEntityUnmarshaller[Array[Byte]] =
    Unmarshaller.byteArrayUnmarshaller
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy