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

scodec.codecs.DiscriminatorCodec.scala Maven / Gradle / Ivy

/*
 * Copyright (c) 2013, Scodec
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors
 *    may be used to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package scodec
package codecs

import scala.reflect.ClassTag

import scodec.bits.BitVector
import DiscriminatorCodec.{Case, Prism}

/** Codec that supports the binary structure `tag ++ value` where the `tag` identifies the encoding/decoding of
  * the value.
  *
  * To build an instance of this codec, call [[discriminated]] and specify the tag type via the `by` method. Then
  * call one more more of the case combinators on this class.
  *
  * The most general case combinators are `caseO` and `caseP`.
  * In addition to a tag, the `caseO` combinator is defined by providing a mapping from
  * `A` to `Option[R]`, a mapping from `R` to `A`, and a `Codec[R]`. The case is used for encoding if the
  * mapping from `A` to `Option[R]` returns a `Some` and it is used for decoding upon matching the tag value.
  * The `caseP` combinators work the same but take a `PartialFunction[A, R]` instead of an `A => Option[R]`.
  *
  * If `R` is a subtype of `A`, then the mapping from `R` to `A` can be omitted. Hence, the
  * `subcaseO` and `subcaseP` constrain `R` to being a subtype of `A` and do not take a `R => A` function.
  *
  * Finally, the least generic case combinators are the `typecase` combinators which add further constraints
  * to the `subcase*` combinators. Specifically, the typecase operators omit the `A => Option[R]` or
  * `PartialFunction[A, R]` in favor of doing subtype checks. For example, the following codec is a `Codec[AnyVal]`
  * that encodes a 0 if passed a `Boolean` and a 1 if passed an `Int`: {{{
  *   discriminated[AnyVal].by(uint8).typecase(0, bool).typecase(1, int32)
  * }}}
  *
  * Often, the values are size-delimited -- that is, there is a `size` field after the `tag` field and before`
  * the `value` field. To support this, use the `framing` method to provide a transformation to each
  * value codec. For example, `framing(new CodecTransformation { def apply[X](c: Codec[X]) = variableSizeBytes(uint8, c) })`.
  *
  * @see [[discriminated]]
  *
  * @param by codec that encodec/decodes the tag value
  * @param cases cases, ordered from highest priority to lowest priority, that handle subsets of `A`
  *
  * @define methodCaseCombinator Returns a new discriminator codec with a new case added for the specified tag.
  * @define typeR representative type that this case handles
  * @define paramTag tag value for this case
  * @define paramEncodeTag tag value to use during encoding for this case
  * @define paramDecodeTag function that determines if this case should be used for decoding given a decoded tag
  * @define paramToRep function used during encoding that converts an `A` to an `Option[R]`
  * @define paramToRepPartial partial function from `A` to `R` used during encoding
  * @define paramFromRep function used during decoding that converts an `R` to an `A`
  * @define paramCr codec that encodes/decodes `R`s
  */
final class DiscriminatorCodec[A, B] private[codecs] (
    by: Codec[B],
    cases: Vector[Case[A, B, Any]],
    framing: [x] => Codec[x] => Codec[x]
) extends Codec[A]
    with KnownDiscriminatorType[B]:

  /** $methodCaseCombinator
    *
    * @tparam R $typeR
    * @param tag $paramTag
    * @param toRep $paramToRep
    * @param fromRep $paramFromRep
    * @param cr $paramCr
    */
  def caseO[R](
      tag: B
  )(toRep: A => Option[R])(fromRep: R => A)(cr: Codec[R]): DiscriminatorCodec[A, B] =
    appendCase(Case(tag, Prism(toRep, fromRep, cr)))

  /** $methodCaseCombinator
    *
    * @tparam R $typeR
    * @param tag $paramTag
    * @param toRep $paramToRepPartial
    * @param fromRep $paramFromRep
    * @param cr $paramCr
    */
  def caseP[R](
      tag: B
  )(toRep: PartialFunction[A, R])(fromRep: R => A)(cr: Codec[R]): DiscriminatorCodec[A, B] =
    appendCase(Case(tag, Prism(toRep.lift, fromRep, cr)))

  /** $methodCaseCombinator
    *
    * @tparam R $typeR
    * @param tag $paramTag
    * @param toRep $paramToRep
    * @param cr $paramCr
    */
  def subcaseO[R <: A](tag: B)(toRep: A => Option[R])(cr: Codec[R]): DiscriminatorCodec[A, B] =
    caseO(tag)(toRep)((r: R) => r)(cr)

  /** $methodCaseCombinator
    *
    * @tparam R $typeR
    * @param tag $paramTag
    * @param toRep $paramToRepPartial
    * @param cr $paramCr
    */
  def subcaseP[R <: A](
      tag: B
  )(toRep: PartialFunction[A, R])(cr: Codec[R]): DiscriminatorCodec[A, B] =
    caseP(tag)(toRep)((r: R) => r)(cr)

  /** $methodCaseCombinator
    *
    * Note: when encoding a value of `A`, this combinator compares the runtime class of that value to the
    * runtime class of the supplied `ClassTag[R]`. As such, the *erased* type of `A` is used and hence,
    * this operation is not safe to use with parameterized representation types.
    *
    * @tparam R $typeR
    * @param tag $paramTag
    * @param cr $paramCr
    */
  def typecase[R <: A: ClassTag](tag: B, cr: Codec[R]): DiscriminatorCodec[A, B] =
    subcaseO(tag)(a => if matchesClass[R](a) then Some(a.asInstanceOf[R]) else None)(cr)

  private def matchesClass[R](a: A)(using ctr: ClassTag[R]) =
    val clazz = ctr match
      case ClassTag.Byte    => classOf[java.lang.Byte]
      case ClassTag.Char    => classOf[java.lang.Character]
      case ClassTag.Short   => classOf[java.lang.Short]
      case ClassTag.Int     => classOf[java.lang.Integer]
      case ClassTag.Long    => classOf[java.lang.Long]
      case ClassTag.Float   => classOf[java.lang.Float]
      case ClassTag.Double  => classOf[java.lang.Double]
      case ClassTag.Boolean => classOf[java.lang.Boolean]
      case ct               => ct.runtimeClass
    clazz.isAssignableFrom(a.getClass)

  def singleton[R <: A](tag: B, value: R): DiscriminatorCodec[A, B] =
    subcaseP(tag) { case v if value == v => v }(codecs.provide(value))

  private def appendCase[R](c: Case[A, B, R]): DiscriminatorCodec[A, B] =
    new DiscriminatorCodec[A, B](by, cases :+ c.asInstanceOf[Case[A, B, Any]], framing)

  private val matcher: B => Attempt[Case[A, B, Any]] =
    def errOrCase(b: B, opt: Option[Case[A, B, Any]]) =
      Attempt.fromOption(opt, new UnknownDiscriminator(b))
    // we reverse the cases so earlier cases 'win' in event of overlap
    val tbl = cases.reverse.map(kase => kase.tag -> kase).toMap
    b => errOrCase(b, tbl.get(b))

  /** Replaces the current framing logic with the specified codec transformation.
    *
    * Every representative codec is wrapped with the framing logic when encoding/decoding.
    *
    * @param framing new framing logic
    */
  def framing(framing: [x] => Codec[x] => Codec[x]): DiscriminatorCodec[A, B] =
    new DiscriminatorCodec[A, B](by, cases, framing)

  def sizeBound =
    by.sizeBound + SizeBound.choice(cases.map(c => framing(c.prism.repCodec).sizeBound))

  def encode(a: A) =
    val itr = cases.iterator
      .flatMap { k =>
        k.prism
          .preview(a)
          .map { r =>
            by.encode(k.tag)
              .flatMap { bits =>
                framing(k.prism.repCodec).encode(r).map(bits ++ _)
              }
          }
          .map(List(_))
          .getOrElse(List())
      }
    if itr.hasNext then itr.next
    else Attempt.failure(new Err.MatchingDiscriminatorNotFound(a))

  def decode(bits: BitVector) =
    (for
      b <- by
      k <- Decoder.liftAttempt(matcher(b))
      r <- framing(k.prism.repCodec)
    yield k.prism.review(r)).decode(bits)

  override def toString = s"discriminated($by)"

/** Companion for [[Discriminator]]. */
private[codecs] object DiscriminatorCodec:

  /** Provides an injection between `A` and `R` and a `Codec[R]`. */
  private[codecs] case class Prism[A, R](
      preview: A => Option[R],
      review: R => A,
      repCodec: Codec[R]
  )

  /** Maps a discrimination tag to a prism that supports encoding/decoding a value of type `A`.
    */
  private[codecs] case class Case[A, B, R](tag: B, prism: Prism[A, R])




© 2015 - 2025 Weber Informatics LLC | Privacy Policy