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

org.coursera.common.collection.Enum.scala Maven / Gradle / Ivy

/*
 * Copyright 2016 Coursera Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.coursera.common.collection

import org.coursera.common.reflect.CompanionReflector

import scala.collection.immutable.SortedSet

/**
 * Provides a convenient way to define "ADT" based Scala enumerations (enumerations defined with
 * case objects extending a sealed trait) while still retaining the ability to:
 *
 * - Look up all the symbols of the enumeration
 * - Look up a particular enumeration symbol given its string name
 *
 * Using case objects for enumerations has some important advantages over using
 * [[scala.Enumeration]]:
 *
 * - Exhaustive pattern matching
 * - The types used to declare enumeration symbols are 1st class scala types
 *
 * Example usage:
 *
 * ```
 * sealed trait Direction extends EnumSymbol
 *
 * object Direction extends Enum[Direction] {
 *   case object NORTH extends Direction
 *   case object SOUTH extends Direction
 *   case object EAST extends Direction
 *   case object WEST extends Direction
 * }
 *
 * scala> Direction.symbols
 * res1: Set[Direction] = Set(NORTH, SOUTH, EAST, WEST)
 *
 * scala> Direction.withName("SOUTH")
 * res2: Direction = Direction.SOUTH
 * ```
 *
 */
trait Enum[SymbolType <: EnumSymbol] { self =>

  /**
   * All the symbols of the enumeration.
   */
  lazy val symbols: Set[SymbolType] = findSymbols

  protected[collection] def findSymbols: Set[SymbolType] = {
    val enumObject = self.getClass
    val symbolType = CompanionReflector.findScalaClassOfCompanionClass[SymbolType](enumObject)

    // For reasons I don't understand,
    // depending on depth of subclass nesting, the enum symbol classes may either appear
    // under the enum's type or the enum symbol's type when locating them via reflection.
    // E.g. under "org.example.Color" for top level declarations and
    // "org.example.Container$Color$" for nested declarations.
    val subclasses = if (enumObject.getEnclosingClass != null) {
      enumObject.getDeclaredClasses
    } else {
      symbolType.getDeclaredClasses
    }
    subclasses.collect {
      case c if symbolType.isAssignableFrom(c) =>
        CompanionReflector.findCompanionInstanceOfCompanionClass(c.asInstanceOf[Class[SymbolType]])
    }.collect {
      // Due to type erasure we can't check that the symbols are actually of
      // type SymbolType at runtime.
      case t: EnumSymbol => t.asInstanceOf[SymbolType]
    }.to[Set]
  }

  private[this] lazy val byName = symbols.map { s => s.name -> s }.toMap

  /**
   * Gets a Enum symbol by its name.
   *
   * @param name provide the enum symbol's name.
   * @return the Enum symbol matching the given name.
   * @throws NoSuchElementException if no enum symbol is found for the given name
   */
  def withName(name: String): SymbolType = {
    byName.getOrElse(name, throw new NoSuchElementException(s"No value found for '$name'"))
  }
}

/**
 * An [[Enum]] symbol.
 *
 * See [[Enum]] for usage details.
 *
 * For case objects, the symbol name will be the case object's "simple name" (unqualified Scala
 * type name). For case objects requiring a different symbol name than their Scala name,
 * see [[AliasedEnumSymbol]].
 */
trait EnumSymbol {
  def name: String = toString
}

/**
 * An [[Enum]] symbol with a symbol name that can be explicitly set and which may be different
 * than its corresponding Scala case object's name.
 *
 * Example usage:
 *
 * ```
 * abstract class RenamedCompany(currentName: String) extends AliasedEnumSymbol(currentName)
 * object RenamedCompany extends Enum[Aliased] {
 *  case object BackRub extends RenamedCompany("Google")
 *  case object TypeSafe extends RenamedCompany("Lightbend")
 * }
 *
 * scala> RenamedCompany.BackRub.name
 * res1: String = "Google"
 *
 * scala RenamedCompany.withName("Lightbend")
 * res2: RenamedCompany = RenamedCompany.TypeSafe
 * ```
 *
 * @param name provides the symbol name.
 */
abstract class AliasedEnumSymbol(name: String) extends EnumSymbol {
  override def toString: String = name
}

/**
 * An [[IndexedEnum]] symbol indexed by an Int 'id'.
 *
 * See [[IndexedEnum]] for usage details.
 *
 * @param id provides the symbol index.
 */
// TODO(jbetz): Ideally we would use a self type here, e.g.:
// abstract class IndexedEnumSymbol[T <: IndexedEnumSymbol[T]] ... with Ordered[T]
// but we've run into https://issues.scala-lang.org/browse/SI-8541 so in the interest
// of stability, we've loosened up our type precision, at least temporarily.
abstract class IndexedEnumSymbol(val id: Int)
  extends EnumSymbol with Ordered[IndexedEnumSymbol] {

  def compare(that: IndexedEnumSymbol): Int = {
    this.id.compareTo(that.id)
  }
}

/**
 * An extension of [[Enum]] supporting [[IndexedEnumSymbol]] symbols.
 *
 * This is indented to provide functionality very close to that of [[scala.Enumeration]] to help
 * ease conversions of types using [[scala.Enumeration]]'s ids.
 *
 * Example usage:
 *
 * ```
 * sealed abstract class Indexed(id: Int) extends IndexedEnumSymbol(id)
 * object Indexed extends IndexedEnum[Indexed] {
 *   case object Zero extends Indexed(0)
 *   case object One extends Indexed(1)
 * }
 * ```
 */
trait IndexedEnum[SymbolType <: IndexedEnumSymbol] extends Enum[SymbolType] {
  lazy val ids: SortedSet[Int] = byId.keys.to[SortedSet]

  /**
   * All the symbols of the enumeration, sorted by id.
   */
  override lazy val symbols: Set[SymbolType] = {
    // TODO(jbetz): Remove ordering here if/when we reestablish self type for IndexedEnumSymbol
    // above.
    // TODO(jbetz): Can we safely make the declared return type SortedSet[SymbolType] here?
    // We get "implicit divergent expansion" if we don't make it SortedSet and developers
    // do simple things like `symbols.map(s => s.id -> s)`.
    implicit val ordering = Ordering.by { s: SymbolType => s.id }
    findSymbols.to[SortedSet]
  }

  private[this] lazy val byId: Map[Int, SymbolType] = {
    symbols.toSet[SymbolType].map { s =>
      s.id -> s
    }.toMap
  }

  def apply(id: Int) = {
    byId(id)
  }
}

/**
 * Both a [[IndexedEnumSymbol]] and a [[AliasedEnumSymbol]]. Should be used in conjunction with
 * [[IndexedEnum]].
 *
 * Example usage:
 *
 * ```
 * sealed abstract class AliasedIndexed(id: Int, name: String)
 *   extends AliasedIndexedEnumSymbol(id, name)
 *
 * object AliasedIndexed extends IndexedEnum[AliasedIndexed] {
 *   case object Zero extends AliasedIndexed(0, "zero")
 *   case object One extends AliasedIndexed(1, "one")
 * }
 * ```
 *
 * @param id provides the symbol index.
 * @param name provides the symbol name
 */
abstract class AliasedIndexedEnumSymbol(id: Int, name: String)
  extends IndexedEnumSymbol(id) {
  override def toString: String = name
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy