Skip to content

Commit

Permalink
Improve syntax and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
geirolz committed Sep 4, 2024
1 parent d46ea15 commit bf8f869
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import cats.data.Kleisli
import cats.implicits._
import cats.{Applicative, ApplicativeError, ApplicativeThrow}
import dev.profunktor.fs2rabbit.model.codec.AmqpFieldDecoder
import dev.profunktor.fs2rabbit.model.{AmqpFieldValue, AmqpProperties, ExchangeName, RoutingKey}
import dev.profunktor.fs2rabbit.model.{AmqpFieldValue, AmqpProperties, ExchangeName, HeaderKey, Headers, RoutingKey}

object EnvelopeDecoder extends EnvelopeDecoderInstances {

Expand All @@ -41,47 +41,50 @@ object EnvelopeDecoder extends EnvelopeDecoderInstances {
Kleisli(e => e.redelivered.pure[F])

// header
def header[F[_]: ApplicativeThrow](name: String): EnvelopeDecoder[F, AmqpFieldValue] =
Kleisli(_.properties.headers.getRaw[F](name))
def headers[F[_]: ApplicativeThrow]: EnvelopeDecoder[F, Headers] =
Kleisli(_.properties.headers.pure[F])

def headerAs[F[_]: ApplicativeThrow, T: AmqpFieldDecoder](name: String): EnvelopeDecoder[F, T] =
Kleisli(_.properties.headers.getAs[F, T](name))
def header[F[_]: ApplicativeThrow](key: String): EnvelopeDecoder[F, AmqpFieldValue] =
Kleisli(_.properties.headers.get[F](key))

def optHeader[F[_]: Applicative](name: String): EnvelopeDecoder[F, Option[AmqpFieldValue]] =
Kleisli(_.properties.headers.getOptRaw(name).pure[F])
def headerAs[F[_]: ApplicativeThrow, T: AmqpFieldDecoder](key: HeaderKey): EnvelopeDecoder[F, T] =
Kleisli(_.properties.headers.getAs[F, T](key))

def optHeaderAs[F[_]: ApplicativeThrow, T: AmqpFieldDecoder](name: String): EnvelopeDecoder[F, Option[T]] =
Kleisli(_.properties.headers.getOptAsF[F, T](name))
def optHeader[F[_]: Applicative](key: HeaderKey): EnvelopeDecoder[F, Option[AmqpFieldValue]] =
Kleisli(_.properties.headers.getOpt(key).pure[F])

@deprecated("Use headerAs[F, String] instead", "2.0.0")
def optHeaderAs[F[_]: ApplicativeThrow, T: AmqpFieldDecoder](key: HeaderKey): EnvelopeDecoder[F, Option[T]] =
Kleisli(_.properties.headers.getOptAsF[F, T](key))

@deprecated("Use headerAs[F, String] instead", "5.3.0")
def stringHeader[F[_]: ApplicativeThrow](name: String): EnvelopeDecoder[F, String] =
headerAs[F, String](name)

@deprecated("Use headerAs[F, Int] instead", "2.0.0")
@deprecated("Use headerAs[F, Int] instead", "5.3.0")
def intHeader[F[_]: ApplicativeThrow](name: String): EnvelopeDecoder[F, Int] =
headerAs[F, Int](name)

@deprecated("Use headerAs[F, Long] instead", "2.0.0")
@deprecated("Use headerAs[F, Long] instead", "5.3.0")
def longHeader[F[_]: ApplicativeThrow](name: String): EnvelopeDecoder[F, Long] =
headerAs[F, Long](name)

@deprecated("Use optHeaderAs[F, String] instead", "2.0.0")
@deprecated("Use optHeaderAs[F, String] instead", "5.3.0")
def optStringHeader[F[_]: ApplicativeThrow](name: String): EnvelopeDecoder[F, Option[String]] =
optHeaderAs[F, String](name)

@deprecated("Use optHeaderAs[F, Int] instead", "2.0.0")
@deprecated("Use optHeaderAs[F, Int] instead", "5.3.0")
def optIntHeader[F[_]: ApplicativeThrow](name: String): EnvelopeDecoder[F, Option[Int]] =
optHeaderAs[F, Int](name)

@deprecated("Use optHeaderAs[F, Long] instead", "2.0.0")
@deprecated("Use optHeaderAs[F, Long] instead", "5.3.0")
def optLongHeader[F[_]: ApplicativeThrow](name: String): EnvelopeDecoder[F, Option[Long]] =
optHeaderAs[F, Long](name)

@deprecated("Use headerAs[F, collection.Seq[Any]] instead", "2.0.0")
@deprecated("Use headerAs[F, collection.Seq[Any]] instead", "5.3.0")
def arrayHeader[F[_]: ApplicativeThrow](name: String): EnvelopeDecoder[F, collection.Seq[Any]] =
headerAs[F, collection.Seq[Any]](name)(ApplicativeThrow[F], AmqpFieldDecoder.collectionSeqDecoder[Any])

@deprecated("Use optHeaderAs[F, collection.Seq[T]] instead", "2.0.0")
@deprecated("Use optHeaderAs[F, collection.Seq[Any]] instead", "5.3.0")
def optArrayHeader[F[_]: ApplicativeThrow](name: String): EnvelopeDecoder[F, Option[collection.Seq[Any]]] =
optHeaderAs[F, collection.Seq[Any]](name)(ApplicativeThrow[F], AmqpFieldDecoder.collectionSeqDecoder[Any])
}
Expand Down
60 changes: 24 additions & 36 deletions core/src/main/scala/dev/profunktor/fs2rabbit/model/Headers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -77,33 +77,32 @@ import scala.util.Try
*/
case class Headers(toMap: Map[HeaderKey, AmqpFieldValue]) {

// append
def :+[T: AmqpFieldEncoder](kv: (HeaderKey, T)): Headers =
append(kv._1, kv._2)
def exists(p: ((HeaderKey, AmqpFieldValue)) => Boolean): Boolean =
toMap.exists(p)

def append[T: AmqpFieldEncoder](key: HeaderKey, value: T): Headers =
appendAll(Map((key, AmqpFieldEncoder[T].encode(value))))
def contains(key: HeaderKey): Boolean =
toMap.contains(key)

def ++(that: Headers): Headers =
appendAll(that.toMap)

def appendAll(that: Map[HeaderKey, AmqpFieldValue]): Headers =
Headers(toMap ++ that)
// add
def +(kv: (HeaderKey, AmqpFieldValue)): Headers =
updated(kv._1, kv._2)

// prepend
def +:[T: AmqpFieldEncoder](pair: (HeaderKey, T)): Headers =
prepend(pair._1, pair._2)
def updated[T: AmqpFieldEncoder](key: HeaderKey, value: T): Headers =
Headers(toMap.updated(key, AmqpFieldEncoder[T].encode(value)))

def prepend[T: AmqpFieldEncoder](key: HeaderKey, value: T): Headers =
prependAll(Map((key, AmqpFieldEncoder[T].encode(value))))

def prependAll(that: Map[HeaderKey, AmqpFieldValue]): Headers =
Headers(that ++ toMap)
def ++(that: Headers): Headers =
Headers(toMap ++ that.toMap)

// remove
def remove(key: HeaderKey, keys: HeaderKey*): Headers =
Headers(toMap -- (key +: keys))

def -(key: HeaderKey): Headers =
remove(key)

def --(that: Headers): Headers =
Headers(toMap -- that.toMap.keys)

// as
/** Decodes the value of the mandatory header to the specified type `T`.
* - If the header is missing, it will raise a `MissingHeader` error.
Expand All @@ -112,7 +111,7 @@ case class Headers(toMap: Map[HeaderKey, AmqpFieldValue]) {
* the name of the header
*/
def getAs[F[_], T: AmqpFieldDecoder](name: HeaderKey)(implicit F: ApplicativeThrow[F]): F[T] =
F.fromEither(getRaw[Either[Throwable, *]](name).flatMap(_.as[T]))
F.fromEither(get[Either[Throwable, *]](name).flatMap(_.as[T]))

/** Decodes the value of the optional header to the specified type `T`.
* - If the header is missing, it will return `None`.
Expand All @@ -133,39 +132,28 @@ case class Headers(toMap: Map[HeaderKey, AmqpFieldValue]) {
* the name of the header
*/
def getOptAsF[F[_], T: AmqpFieldDecoder](name: HeaderKey)(implicit F: ApplicativeThrow[F]): F[Option[T]] =
F.fromEither(getOptRaw(name).map(_.as[T]).sequence)
F.fromEither(getOpt(name).map(_.as[T]).sequence)

// raw
/** Returns the raw value of the mandatory header.
* - If the header is missing, it will raise a `MissingHeader` error.
* @param name
* the name of the header
*/
def getRaw[F[_]](name: HeaderKey)(implicit F: ApplicativeThrow[F]): F[AmqpFieldValue] =
getOptRaw(name) match {
def get[F[_]](name: HeaderKey)(implicit F: ApplicativeThrow[F]): F[AmqpFieldValue] =
getOpt(name) match {
case Some(value) => F.pure(value)
case None => F.raiseError(new MissingHeader(name))
case None => F.raiseError(MissingHeader(name))
}

/** Returns the raw optional value of the header.
* - If the header is missing, it will return `None`.
* @param name
* the name of the header
*/
def getOptRaw(name: HeaderKey): Option[AmqpFieldValue] =
def getOpt(name: HeaderKey): Option[AmqpFieldValue] =
toMap.get(name)

// backwards compatibility - to remove in the future versions
// TODO Choose the version
@deprecated("Use getAs[F, AmqpFieldValue] instead", "2.0.0")
def get(name: HeaderKey): Option[AmqpFieldValue] =
getOptRaw(name)

// TODO Choose the version
@deprecated("Use getAs[F, AmqpFieldValue] instead", "2.0.0")
def apply[F[_]: ApplicativeThrow](name: HeaderKey): AmqpFieldValue =
getOptRaw(name).getOrElse(throw new MissingHeader(name))

override def toString: String =
Headers.show.show(this)

Expand All @@ -181,7 +169,7 @@ object Headers {
def apply(values: (HeaderKey, AmqpFieldValue)*): Headers = Headers(values.toMap)
def apply(values: Map[HeaderKey, AmqpFieldValue]): Headers = new Headers(values)

class MissingHeader(name: String) extends RuntimeException(s"Missing header: $name")
case class MissingHeader(name: String) extends RuntimeException(s"Missing header: $name")

private[fs2rabbit] def unsafeFromMap(values: Map[String, AnyRef]): Headers =
Headers(values.map { case (k, v) => k -> AmqpFieldValue.unsafeFrom(v) })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,13 @@ sealed trait AmqpFieldDecoderInstances {
}

implicit val dateDecoder: AmqpFieldDecoder[Date] =
instantDecoder.map(Date.from)
instantDecoder.emap { instant =>
Either
.catchNonFatal(
Date.from(instant)
)
.leftMap(err => DecodingError("Error decoding Date", Some(err)))
}

implicit val booleanDecoder: AmqpFieldDecoder[Boolean] =
AmqpFieldDecoder.instance {
Expand Down Expand Up @@ -177,7 +183,7 @@ sealed trait AmqpFieldDecoderInstances {
case other => DecodingError.expectedButGot(s"ArrayVal", other.toString).asLeft
}

implicit def seqDecoder[T: AmqpFieldDecoder]: AmqpFieldDecoder[Seq[T]] =
implicit def seqDecoder[T: AmqpFieldDecoder]: AmqpFieldDecoder[immutable.Seq[T]] =
collectionSeqDecoder[T].map(_.toSeq)

implicit def listDecoder[T: AmqpFieldDecoder]: AmqpFieldDecoder[List[T]] =
Expand Down
Loading

0 comments on commit bf8f869

Please sign in to comment.