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

fs2.kafka.HeaderDeserializer.scala Maven / Gradle / Ivy

/*
 * Copyright 2018-2024 OVO Energy Limited
 *
 * SPDX-License-Identifier: Apache-2.0
 */

package fs2.kafka

import java.nio.charset.{Charset, StandardCharsets}
import java.util.UUID

import scala.annotation.tailrec

import cats.{Eval, Monad}
import cats.syntax.either.*

/**
  * [[HeaderDeserializer]] is a functional deserializer for Kafka record header values. It's similar
  * to [[Deserializer]], except it only has access to the header bytes, and it does not interoperate
  * with the Kafka `Deserializer` interface.
  */
sealed abstract class HeaderDeserializer[A] {

  /**
    * Deserializes the header value bytes into a value of type `A`.
    */
  def deserialize(bytes: Array[Byte]): A

  /**
    * Creates a new [[HeaderDeserializer]] which applies the specified function `f` to the result of
    * this [[HeaderDeserializer]].
    */
  final def map[B](f: A => B): HeaderDeserializer[B] =
    HeaderDeserializer.instance { bytes =>
      f(deserialize(bytes))
    }

  /**
    * Creates a new [[HeaderDeserializer]] using the result of this [[HeaderDeserializer]] and the
    * specified function.
    */
  final def flatMap[B](f: A => HeaderDeserializer[B]): HeaderDeserializer[B] =
    HeaderDeserializer.instance { bytes =>
      f(deserialize(bytes)).deserialize(bytes)
    }

  /**
    * Creates a new [[HeaderDeserializer]] which deserializes both using this [[HeaderDeserializer]]
    * and that [[HeaderDeserializer]], and returns both results in a tuple.
    */
  final def product[B](that: HeaderDeserializer[B]): HeaderDeserializer[(A, B)] =
    HeaderDeserializer.instance { bytes =>
      val a = deserialize(bytes)
      val b = that.deserialize(bytes)
      (a, b)
    }

  /**
    * Creates a new [[HeaderDeserializer]] which does deserialization lazily by wrapping this
    * [[HeaderDeserializer]] in `Eval.always`.
    */
  final def delay: HeaderDeserializer[Eval[A]] =
    HeaderDeserializer.instance { bytes =>
      Eval.always(deserialize(bytes))
    }

  /**
    * Creates a new [[HeaderDeserializer]] which catches any non-fatal exceptions during
    * deserialization with this [[HeaderDeserializer]].
    */
  final def attempt: HeaderDeserializer.Attempt[A] =
    HeaderDeserializer.instance { bytes =>
      Either.catchNonFatal(deserialize(bytes))
    }

  /**
    * Creates a new [[HeaderDeserializer]] which returns `None` when the bytes are `null`, and
    * otherwise returns the result of this [[HeaderDeserializer]] wrapped in `Some`.
    */
  final def option: HeaderDeserializer[Option[A]] =
    HeaderDeserializer.instance { bytes =>
      if (bytes != null)
        Some(deserialize(bytes))
      else
        None
    }

}

object HeaderDeserializer {

  /**
    * Alias for `HeaderDeserializer[Either[Throwable, A]]`.
    */
  type Attempt[A] = HeaderDeserializer[Either[Throwable, A]]

  def apply[A](implicit
    deserializer: HeaderDeserializer[A]
  ): HeaderDeserializer[A] =
    deserializer

  def attempt[A](implicit
    deserializer: HeaderDeserializer.Attempt[A]
  ): HeaderDeserializer.Attempt[A] =
    deserializer

  /**
    * Creates a new [[HeaderDeserializer]] which deserializes all bytes to the specified value of
    * type `A`.
    */
  def const[A](a: A): HeaderDeserializer[A] =
    HeaderDeserializer.instance(_ => a)

  /**
    * Creates a new [[HeaderDeserializer]] which delegates deserialization to the specified Kafka
    * `Deserializer`. Please note that the `close` and `configure` functions won't be called for the
    * delegate. Also, the topic is an empty `String` and no headers are provided.
    */
  def delegate[A](deserializer: KafkaDeserializer[A]): HeaderDeserializer[A] =
    HeaderDeserializer.instance { bytes =>
      deserializer.deserialize("", bytes)
    }

  /**
    * Creates a new [[HeaderDeserializer]] from the specified function.
    */
  def instance[A](f: Array[Byte] => A): HeaderDeserializer[A] =
    new HeaderDeserializer[A] {

      override def deserialize(bytes: Array[Byte]): A =
        f(bytes)

      override def toString: String =
        "HeaderDeserializer$" + System.identityHashCode(this)

    }

  /**
    * Creates a new [[HeaderDeserializer]] which deserializes `String` values using the specified
    * `Charset`. Note that the default `String` deserializer uses `UTF-8`.
    */
  def string(charset: Charset): HeaderDeserializer[String] =
    HeaderDeserializer.instance(bytes => new String(bytes, charset))

  /**
    * Creates a new [[HeaderDeserializer]] which deserializes `String` values using the specified
    * `Charset` as `UUID`s. Note that the default `UUID` deserializer uses `UTF-8`.
    */
  def uuid(charset: Charset): HeaderDeserializer.Attempt[UUID] =
    HeaderDeserializer.string(charset).map(UUID.fromString).attempt

  /**
    * The identity [[HeaderDeserializer]], which does not perform any kind of deserialization,
    * simply using the input bytes as the output.
    */
  implicit val identity: HeaderDeserializer[Array[Byte]] =
    HeaderDeserializer.instance(bytes => bytes)

  /**
    * The option [[HeaderDeserializer]] returns `None` when the bytes are `null`, and otherwise
    * deserializes using the deserializer for the type `A`, wrapping the result in `Some`.
    */
  implicit def option[A](implicit
    deserializer: HeaderDeserializer[A]
  ): HeaderDeserializer[Option[A]] =
    deserializer.option

  implicit val monad: Monad[HeaderDeserializer] =
    new Monad[HeaderDeserializer] {

      override def pure[A](a: A): HeaderDeserializer[A] =
        HeaderDeserializer.const(a)

      override def map[A, B](
        deserializer: HeaderDeserializer[A]
      )(f: A => B): HeaderDeserializer[B] =
        deserializer.map(f)

      override def flatMap[A, B](
        deserializer: HeaderDeserializer[A]
      )(f: A => HeaderDeserializer[B]): HeaderDeserializer[B] =
        deserializer.flatMap(f)

      override def product[A, B](
        first: HeaderDeserializer[A],
        second: HeaderDeserializer[B]
      ): HeaderDeserializer[(A, B)] =
        first.product(second)

      override def tailRecM[A, B](a: A)(
        f: A => HeaderDeserializer[Either[A, B]]
      ): HeaderDeserializer[B] =
        HeaderDeserializer.instance { bytes =>
          @tailrec def go(deserializer: HeaderDeserializer[Either[A, B]]): B =
            deserializer.deserialize(bytes) match {
              case Right(_b) => _b
              case Left(_a)  => go(f(_a))
            }

          go(f(a))
        }

    }

  implicit val double: HeaderDeserializer.Attempt[Double] =
    HeaderDeserializer
      .delegate {
        (new org.apache.kafka.common.serialization.DoubleDeserializer)
          .asInstanceOf[org.apache.kafka.common.serialization.Deserializer[Double]]
      }
      .attempt

  implicit val float: HeaderDeserializer.Attempt[Float] =
    HeaderDeserializer
      .delegate {
        (new org.apache.kafka.common.serialization.FloatDeserializer)
          .asInstanceOf[org.apache.kafka.common.serialization.Deserializer[Float]]
      }
      .attempt

  implicit val int: HeaderDeserializer.Attempt[Int] =
    HeaderDeserializer
      .delegate {
        (new org.apache.kafka.common.serialization.IntegerDeserializer)
          .asInstanceOf[org.apache.kafka.common.serialization.Deserializer[Int]]
      }
      .attempt

  implicit val long: HeaderDeserializer.Attempt[Long] =
    HeaderDeserializer
      .delegate {
        (new org.apache.kafka.common.serialization.LongDeserializer)
          .asInstanceOf[org.apache.kafka.common.serialization.Deserializer[Long]]
      }
      .attempt

  implicit val short: HeaderDeserializer.Attempt[Short] =
    HeaderDeserializer
      .delegate {
        (new org.apache.kafka.common.serialization.ShortDeserializer)
          .asInstanceOf[org.apache.kafka.common.serialization.Deserializer[Short]]
      }
      .attempt

  implicit val string: HeaderDeserializer[String] =
    HeaderDeserializer.string(StandardCharsets.UTF_8)

  implicit val unit: HeaderDeserializer[Unit] =
    HeaderDeserializer.const(())

  implicit val uuid: HeaderDeserializer.Attempt[UUID] =
    HeaderDeserializer.string.map(UUID.fromString).attempt

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy