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

com.avsystem.commons.misc.SealedUtils.scala Maven / Gradle / Ivy

The newest version!
package com.avsystem.commons
package misc

import com.avsystem.commons.annotation.explicitGenerics
import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec}

object SealedUtils {
  /**
    * A macro which reifies a list of all case objects of a sealed trait or class `T`.
    * WARNING: the order of case objects in the resulting list is guaranteed to be consistent with
    * declaration order ONLY for enums extending [[OrderedEnum]]. Otherwise, the order may be arbitrary.
    */
  @explicitGenerics
  def caseObjectsFor[T]: List[T] = macro macros.misc.SealedMacros.caseObjectsFor[T]

  /**
    * Infers a list of instances of given typeclass `TC` for all non-abstract subtypes of a sealed hierarchy root `T`.
    */
  @explicitGenerics
  def instancesFor[TC[_], T]: List[TC[_ <: T]] = macro macros.misc.SealedMacros.instancesFor[TC[_], T]
}

/**
  * Base trait for companion objects of sealed traits that serve as enums, i.e. their only values are case objects.
  * For example:
  *
  * {{{
  *   sealed trait SomeEnum
  *   object SomeEnum extends SealedEnumCompanion[SomeEnum] {
  *     case object FirstValue extends SomeEnum
  *     case object SecondValue extends SomeEnum
  *     case object ThirdValue extends SomeEnum
  *
  *     // it's important to explicitly specify the type so that `caseObjects` macro works properly
  *     val values: List[SomeEnum] = caseObjects
  *   }
  * }}}
  */
trait SealedEnumCompanion[T] {
  /**
    * Thanks to this implicit, [[SealedEnumCompanion]] and its subtraits can be used as typeclasses.
    */
  implicit def evidence: this.type = this

  /**
    * Holds a list of all case objects of a sealed trait or class `T`. This must be implemented separately
    * for every sealed enum, but can be implemented simply by using the [[caseObjects]] macro.
    * It's important to *always* state the type of `values` explicitly, as a workaround for SI-7046. For example:
    *
    * {{{
    *   val values: List[MyEnum] = caseObjects
    * }}}
    *
    * Also, be aware that [[caseObjects]] macro guarantees well-defined order of elements only for
    * [[com.avsystem.commons.misc.OrderedEnum OrderedEnum]].
    */
  val values: ISeq[T]

  /**
    * A macro which reifies a list of all case objects of the sealed trait or class `T`.
    * WARNING: the order of case objects in the resulting list is well defined only for enums that extend [[OrderedEnum]].
    * In such case, the order is consistent with declaration order in source file. However, if the enum is not an
    * [[OrderedEnum]], the order may be arbitrary.
    */
  protected def caseObjects: List[T] = macro macros.misc.SealedMacros.caseObjectsFor[T]
}

abstract class AbstractSealedEnumCompanion[T] extends SealedEnumCompanion[T]

/**
  * Base trait for enums implemented as sealed hierarchy with case objects where every enum value has distinct
  * textual representation (name).
  *
  * Typically, if a trait or class extends `NamedEnum`, its companion object extends [[NamedEnumCompanion]].
  * Enum values can then be looked up by name using [[NamedEnumCompanion.byName]].
  */
trait NamedEnum extends Serializable {
  /**
    * Used as a key for a map returned from `byName`. It is recommended to override this method uniquely
    * by each case object in the sealed hierarchy.
    */
  def name: String
  override def toString: String = name
}

/**
  * Subtrait of [[NamedEnum]] which requires its values to be `Product`s and uses `Product.productPrefix` as
  * the name of each enum constant. In practice this means that all the objects extending [[AutoNamedEnum]] should
  * be `case object`s so that object names are automatically used as enum constant names.
  * That's because case classes and objects automatically implement `Product` and use their source
  * name as `Product.productPrefix`.
  */
trait AutoNamedEnum extends NamedEnum with Product {
  def name: String = productPrefix
}

/**
  * Like [[AutoNamedEnum]] but derived names are uncapitalized (first letter lowercased).
  */
trait LowerCaseAutoNamedEnum extends AutoNamedEnum {
  override def name: String = super.name.uncapitalize
}

/**
  * Base trait for companion objects of sealed traits that serve as named enums. `NamedEnumCompanion` is an
  * extension of [[SealedEnumCompanion]] which additionally requires that every enum value has distinct string
  * representation. Values can then be looked up by that representation using [[NamedEnumCompanion.byName]]
  *
  * Example:
  *
  * {{{
  *   sealed abstract class Color(val name: String) extends NamedEnum
  *   object Color extends NamedEnumCompanion[Color] {
  *     case object Red extends Color("red")
  *     case object Blue extends Color("blue")
  *     case object Green extends Color("green")
  *
  *     // it's important to explicitly specify the type so that `caseObjects` macro works properly
  *     val values: List[Color] = caseObjects
  *   }
  * }}}
  *
  * `NamedEnumCompanion` also automatically provides implicit typeclass instances for
  * [[com.avsystem.commons.serialization.GenKeyCodec GenKeyCodec]] and [[com.avsystem.commons.serialization.GenCodec GenCodec]].
  */
trait NamedEnumCompanion[T <: NamedEnum] extends SealedEnumCompanion[T] {
  /**
    * Returns a map from all case objects names to their instances.
    * Since `byName` uses [[caseObjects]] macro it does NOT guarantee an order of elements. It is also essential
    * to provide unique names for each case object in the sealed hierarchy to retrieve valid hierarchy.
    */
  lazy val byName: Map[String, T] = values.toMapBy(_.name)

  private def decode(str: String): T =
    byName.getOrElse(str, throw new NoSuchElementException(
      s"Invalid value: $str, expected one of: ${values.iterator.map(_.name).mkString(",")}"))

  implicit lazy val keyCodec: GenKeyCodec[T] = GenKeyCodec.create(decode, _.name)
  implicit lazy val codec: GenCodec[T] = GenCodec.nullableSimple[T](
    input => decode(input.readString()),
    (output, value) => output.writeString(value.name)
  )
}

/**
  * Trait to be extended by enums whose values are ordered by declaration order. Ordering is derived from
  * [[SourceInfo]] object, which is typically accepted as an implicit, e.g.
  *
  * {{{
  *   sealed abstract class MyOrderedEnum(implicit val sourceInfo: SourceInfo) extends OrderedEnum
  *   object MyOrderedEnum {
  *     case object First extends MyOrderedEnum
  *     case object Second extends MyOrderedEnum
  *     case object Third extends MyOrderedEnum
  *
  *     val values: List[MyOrderedEnum] = caseObjects
  *   }
  * }}}
  *
  * In the example above, `values` is guaranteed to return `First`, `Second` and `Third` objects in exactly that order.
  */
trait OrderedEnum {
  def sourceInfo: SourceInfo
}
object OrderedEnum {
  private object reusableOrdering extends Ordering[OrderedEnum] {
    def compare(x: OrderedEnum, y: OrderedEnum) = Integer.compare(x.sourceInfo.offset, y.sourceInfo.offset)
  }
  implicit def ordering[T <: OrderedEnum]: Ordering[T] =
    reusableOrdering.asInstanceOf[Ordering[T]]
}

abstract class AbstractNamedEnumCompanion[T <: NamedEnum]
  extends AbstractSealedEnumCompanion[T] with NamedEnumCompanion[T]




© 2015 - 2025 Weber Informatics LLC | Privacy Policy