-
-
Save chaotic3quilibrium/57add1cd762eb90f6b24 to your computer and use it in GitHub Desktop.
README.txt - DIY Scala Enumeration | |
Copyright (C) 2014-2016 Jim O'Flaherty | |
Overview: | |
Provide in Scala the closest equivalent to Java Enum | |
- includes decorating each declared Enum member with extended information | |
- guarantees pattern matching exhaustiveness checking | |
- this is not available with scala.Enumeration | |
ScalaOlio library (GPLv3) which contains more up-to-date versions of both `org.scalaolio.util.Enumeration` and `org.scalaolio.util.EnumerationDecorated`: | |
http://scalaolio.org/ | |
https://github.com/chaotic3quilibrium/scala-olio | |
StackOverflow answer covering entire Scala enumeration options domain: | |
http://stackoverflow.com/a/25923651/501113 | |
Gist Files: | |
Enumeration.scala - Baseline to extend for any implementation | |
ChessPiecesSimplest.scala - Shows the absolute minimum required to generate an implementation of Enumeration | |
WorkSheetChessPiecesSimplestTest.scala - Simple test cases for validating ChessPiecesSimplest | |
ChessPiecesEnhanced.scala - Shows a more elaborate method to extend Enumeration to associate data to a Member | |
WorkSheetChessPiecesEnhancedTest.scala - Simple test cases for validating ChessPiecesEnhanced | |
EnumerationDecorated.scala - Extends Enumeration to enable safely adding a non-Serialized singleton-ness Decoration to each declared member | |
ChessPiecesEnhancedDecorated.scala - Shows how ChessPiecesEnhanced.scala can be simplified by using this Enumeration extension | |
WorkSheetChessPiecesEnhancedDecoratedTest.scala - Simple test cases for validating ChessPiecesEnhancedDecorated | |
Most terse usage (see example files for more expansive details): | |
object ChessPiecesSimplest extends Enumeration { | |
case object KING extends Member | |
case object QUEEN extends Member | |
case object BISHOP extends Member | |
case object KNIGHT extends Member | |
case object ROOK extends Member | |
case object PAWN extends Member | |
protected val orderedSet: List[Member] = List(KING, QUEEN, BISHOP, KNIGHT, ROOK, PAWN) | |
sealed trait Member extends MemberBase | |
override def typeTagMember: TypeTag[_] = typeTag[Member] | |
} | |
-------------------- | |
Details: | |
Below is a shorter version of a more thorough answer I posted on StackOverflow: | |
http://stackoverflow.com/a/25923651/501113 | |
Ever since moving to Scala, I have longed to have the Scala-ized equivalent of Java's Enum. This meant having the following benefits: | |
A) Not requiring a Scala/Java mixed project just to use Java's Enum | |
B) Minimize the amount of overhead/boilerplate required to declare an Enumeration | |
C) Minimize the amount of overhead/boilerplate required to associate extended attributes to an Enumeration | |
D) Leverage the more sophisticated Scala compiler's ability to perform pattern matching exhaustiveness checking | |
E) If there was any requirement to "duplicate" the enumeration members, runtime catch any discrepancies | |
F) Deal with the many non-obvious JVM class/object initialization issues which have undermined other elegant solutions | |
After looking at scala.Enumeration and discovering it did not work for D, I searched for solutions that might optimally work. I came across the following StackOverflow solution: | |
http://stackoverflow.com/a/8620085/501113 | |
The comments on this solution end up chasing down some weird JVM class/object initialization issues which are detailed here: | |
http://stackoverflow.com/questions/14947179/using-a-custom-enum-in-the-scala-worksheet-i-am-receiving-an-error-java-lang-ex | |
This solution pathway didn't work out (see final comment on original post). It just wasn't reasonable to require a client access "the correct case object" to guarantee proper Enumeration initialization. And without that, the same case object might received a different ordinal on any future run. | |
Pause two years. I need to have a reasonable implementation of the above benefits. I searched hoping something had turned up. I became quite excited when I came across Viktor Klang's (attempted) solution to the problem: | |
https://gist.github.com/viktorklang/1057513 | |
Well, after spending considerable time trying to get Viktor's solution to work (in 2.11.2) with pattern matching exhaustiveness checking, I finally punted and decided to tackle this problem again. And close to 50 hours and a bazillion tangents later, this solution emerged. And it now offers all of the above benefits. |
//ChessPiecesEnhanced.scala | |
// - Example usage of org.public_domain.scala.util.Enumeration | |
//Copyright (C) 2014-2016 Jim O'Flaherty | |
// | |
// | |
//This is free and unencumbered software released into the public domain. | |
// | |
//Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, | |
//either in source code form or as a compiled binary, for any purpose, commercial or | |
//non-commercial, and by any means. | |
// | |
//In jurisdictions that recognize copyright laws, the author or authors of this software | |
//dedicate any and all copyright interest in the software to the public domain. We make this | |
//dedication for the benefit of the public at large and to the detriment of our heirs and | |
//successors. We intend this dedication to be an overt act of relinquishment in perpetuity | |
//of all present and future rights to this software under copyright law. | |
// | |
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | |
//INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR | |
//PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES | |
//OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT | |
//OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
// | |
//For more information, please refer to <http://unlicense.org> | |
// | |
//If you would like to obtain a custom/different/commercial license for this, please send an | |
//email with your request to <[email protected]>. | |
package org.public_domain.chess | |
import scala.reflect.runtime.universe.{TypeTag,typeTag} | |
import org.public_domain.scala.utils.Enumeration | |
object ChessPiecesEnhanced extends Enumeration { | |
//A. Enumerated values | |
case object KING extends Member | |
case object QUEEN extends Member | |
case object BISHOP extends Member | |
case object KNIGHT extends Member | |
case object ROOK extends Member | |
case object PAWN extends Member | |
//B. from Enumeration [item B], required type extension | |
sealed trait Member extends MemberBase { | |
//due to the unpredictable nature of object initialization, avoid | |
//using val anywhere within this trait (use only def and lazy val) | |
//if val must be used, it must not depend up on any aspect of the initialization of | |
// this Member instance, the enclosing base Enumeration object or it's derivation (within which | |
// this definition sits, which in this example is ChessPiecesEnhanced) | |
//B.1 Adding extended information via a def to avoid serialization overhead | |
def attributes: Attributes = attributesByMember(this) | |
//change to lazy val only if serialization isn't important and you *know* Serialization will | |
// never be important (which is a very difficult assertion to make for the entire future use | |
// of this) | |
} | |
//C. from Enumeration [item L] | |
override def typeTagMember: TypeTag[_] = typeTag[Member] | |
//D. Safe place to extend any/all needed data for a specific Member; i.e use all the vals you like | |
sealed case class Attributes private[ChessPiecesEnhanced] (member: Member, char: Char, pointValue: Int) { | |
val description: String = member.name.toLowerCase.capitalize | |
} | |
//E. Defining the associated data | |
val attributesList: List[Attributes] = | |
List( | |
Attributes(KING, 'K', 0) | |
, Attributes(QUEEN, 'Q', 9) | |
, Attributes(BISHOP, 'B', 3) | |
, Attributes(KNIGHT, 'N', 3) | |
, Attributes(ROOK, 'R', 5) | |
, Attributes(PAWN, 'P', 1) | |
) | |
//F. Generate the association between the specific Attributes instance and its member | |
// This is used by the Member trait [item B] above | |
val attributesByMember: Map[Member, Attributes] = | |
attributesList.map(attributes => (attributes.member, attributes)).toMap | |
//G. From Enumeration [item M], required ordering of case objects | |
// it's pushed way down here to enable an interation of attributeList to avoid having to | |
// specify the members a third time | |
protected val orderedSet: List[Member] = attributesList.map(_.member) | |
//because object/class initialization ordering is not predictable | |
//if client happens to access ChessPiecesSimplest.ROOK first, the initialization order is... | |
//object ChessPiecesSimplest, complete ROOK and then initialize KING, QUEEN, BISHOP, KNIGHT PAWN | |
//because ChessPieces.getClass.getDeclaredClasses order of returned classes is not predictable | |
//H. from Enumeration [item N], optional ordinal assignment | |
override def ordinalStart = 5 | |
override def ordinalStep = 5 | |
} | |
// | |
//I. Simple test for exhaustive pattern matching | |
// - just comment out any of the case entries | |
object ChessPiecesEnhancedTestExhaustivePatternMatch { | |
def stringMe(member: ChessPiecesEnhanced.Member): String = | |
member match { | |
case ChessPiecesEnhanced.KING => "Of the World" | |
case ChessPiecesEnhanced.QUEEN => "Of the World" | |
case ChessPiecesEnhanced.BISHOP => "Of the Church" | |
case ChessPiecesEnhanced.KNIGHT => "Of the Church" | |
case ChessPiecesEnhanced.ROOK => "Of the Castle" | |
case ChessPiecesEnhanced.PAWN => "Of the Field" | |
} | |
} |
//ChessPiecesEnhancedDecorated.scala | |
// - Example usage of org.public_domain.scala.util.Enumeration | |
//Copyright (C) 2014-2016 Jim O'Flaherty | |
// | |
// | |
//This is free and unencumbered software released into the public domain. | |
// | |
//Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, | |
//either in source code form or as a compiled binary, for any purpose, commercial or | |
//non-commercial, and by any means. | |
// | |
//In jurisdictions that recognize copyright laws, the author or authors of this software | |
//dedicate any and all copyright interest in the software to the public domain. We make this | |
//dedication for the benefit of the public at large and to the detriment of our heirs and | |
//successors. We intend this dedication to be an overt act of relinquishment in perpetuity | |
//of all present and future rights to this software under copyright law. | |
// | |
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | |
//INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR | |
//PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES | |
//OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT | |
//OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
// | |
//For more information, please refer to <http://unlicense.org> | |
// | |
//If you would like to obtain a custom/different/commercial license for this, please send an | |
//email with your request to <[email protected]>. | |
package org.public_domain.chess | |
import scala.reflect.runtime.universe.{TypeTag,typeTag} | |
import org.public_domain.scala.utils.EnumerationDecorated | |
object ChessPiecesEnhancedDecorated extends EnumerationDecorated { | |
//A. Enumerated values | |
case object KING extends Member | |
case object QUEEN extends Member | |
case object BISHOP extends Member | |
case object KNIGHT extends Member | |
case object ROOK extends Member | |
case object PAWN extends Member | |
//B. Defining the associated data | |
val decorationOrderedSet: List[Decoration] = | |
List( | |
Decoration(KING, 'K', 0) | |
, Decoration(QUEEN, 'Q', 9) | |
, Decoration(BISHOP, 'B', 3) | |
, Decoration(KNIGHT, 'N', 3) | |
, Decoration(ROOK, 'R', 5) | |
, Decoration(PAWN, 'P', 1) | |
) | |
//C. Safe place to extend any/all needed data for a specific Member; i.e use all the vals you like | |
final case class Decoration private[ChessPiecesEnhancedDecorated] (member: Member, char: Char, pointValue: Int) extends DecorationBase { | |
val description: String = member.name.toLowerCase.capitalize | |
} | |
//D. from Enumeration [item L] | |
override def typeTagMember: TypeTag[_] = typeTag[Member] | |
//E. from EnumerationDecoration [item C], required type extension | |
sealed trait Member extends MemberDecorated | |
} | |
// | |
//F. Simple test for exhaustive pattern matching | |
// - just comment out any of the case entries | |
object ChessPiecesEnhancedDecoratedTestExhaustivePatternMatch { | |
def stringMe(member: ChessPiecesEnhancedDecorated.Member): String = | |
member match { | |
case ChessPiecesEnhancedDecorated.KING => "Of the World" | |
case ChessPiecesEnhancedDecorated.QUEEN => "Of the World" | |
case ChessPiecesEnhancedDecorated.BISHOP => "Of the Church" | |
case ChessPiecesEnhancedDecorated.KNIGHT => "Of the Church" | |
case ChessPiecesEnhancedDecorated.ROOK => "Of the Castle" | |
case ChessPiecesEnhancedDecorated.PAWN => "Of the Field" | |
} | |
} |
//ChessPiecesSimplest.scala | |
// - Example usage of org.public_domain.scala.util.Enumeration | |
//Copyright (C) 2014-2016 Jim O'Flaherty | |
// | |
// | |
//This is free and unencumbered software released into the public domain. | |
// | |
//Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, | |
//either in source code form or as a compiled binary, for any purpose, commercial or | |
//non-commercial, and by any means. | |
// | |
//In jurisdictions that recognize copyright laws, the author or authors of this software | |
//dedicate any and all copyright interest in the software to the public domain. We make this | |
//dedication for the benefit of the public at large and to the detriment of our heirs and | |
//successors. We intend this dedication to be an overt act of relinquishment in perpetuity | |
//of all present and future rights to this software under copyright law. | |
// | |
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | |
//INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR | |
//PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES | |
//OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT | |
//OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
// | |
//For more information, please refer to <http://unlicense.org> | |
// | |
//If you would like to obtain a custom/different/commercial license for this, please send an | |
//email with your request to <[email protected]>. | |
package org.public_domain.chess | |
import scala.reflect.runtime.universe.{TypeTag,typeTag} | |
import org.public_domain.scala.utils.Enumeration | |
object ChessPiecesSimplest extends Enumeration { | |
//A. Enumerated values | |
case object KING extends Member | |
case object QUEEN extends Member | |
case object BISHOP extends Member | |
case object KNIGHT extends Member | |
case object ROOK extends Member | |
case object PAWN extends Member | |
//case object PAWN2 extends Member | |
//B. from Enumeration [item M], required ordering of case objects | |
protected val orderedSet: List[Member] = List(KING, QUEEN, BISHOP, KNIGHT, ROOK, PAWN) | |
//required because object/class initialization ordering is not predictable | |
//if client happens to access ChessPiecesSimplest.ROOK first, the resulting object | |
// initialization order is: | |
//ROOK (to add), ChessPiecesSimplest, ROOK (after add), KING, QUEEN, BISHOP, KNIGHT PAWN | |
//C. from Enumeration [item B], required type extension | |
sealed trait Member extends MemberBase | |
//D. from Enumeration [item L] | |
override def typeTagMember: TypeTag[_] = typeTag[Member] | |
} | |
// | |
//E. Simple test for exhaustive pattern matching | |
// - just comment out any of the case entries | |
object ChessPiecesSimplestTestExhaustivePatternMatch { | |
def stringMe(chessPiece: ChessPiecesSimplest.Member): String = | |
chessPiece match { | |
case ChessPiecesSimplest.KING => "Of the World" | |
case ChessPiecesSimplest.QUEEN => "Of the World" | |
case ChessPiecesSimplest.BISHOP => "Of the Church" | |
case ChessPiecesSimplest.KNIGHT => "Of the Church" | |
case ChessPiecesSimplest.ROOK => "Of the Castle" | |
case ChessPiecesSimplest.PAWN => "Of the Field" | |
} | |
} |
//Enumeration.scala | |
// - DIY Scala Enumeration (closest possible Java Enum equivalent with guaranteed pattern | |
// matching exhaustiveness checking) | |
//Copyright (C) 2014-2016 Jim O'Flaherty | |
// | |
// | |
//This Scala class is free software: you can redistribute it and/or modify it under the terms | |
//of the GNU General Public License as published by the Free Software Foundation, either | |
//version 3 of the License, or any later version. | |
// | |
//This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; | |
//without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | |
//See the GNU General Public License for more details. | |
// | |
//To see details of the GPLv3 License, please see <http://www.gnu.org/copyleft/gpl.html>. | |
//To see details of the GNU General Public License, please see <http://www.gnu.org/licenses/>. | |
// | |
//If you would like to obtain a custom/different/commercial license for this, please send an | |
//email with your request to <[email protected]>. | |
package org.public_domain.scala.utils | |
import scala.reflect.runtime.universe.{Symbol,TypeTag} | |
import scala.util.{Failure, Success, Try} | |
//import org.public_domain.scala.utils.Try_.{CompletedNoException,completedNoExceptionSingleton} | |
trait Enumeration { | |
//A. Placeholder Success type+singleton for Try | |
// - manually inlined from org.public_domain.scala.utils.Try_ | |
sealed case class CompletedNoException() | |
final val completedNoExceptionSingleton = new CompletedNoException() | |
//B. Type Member which must be found and _sealed_ in the descendant object | |
// - see protected trait MemberBase [item O] | |
type Member <: MemberBase | |
//C. Collects all of the sealed class descendants of Member | |
// - depends upon protected (abstract) def typeTagMember [item L] being implemented in client descendant | |
// - making client implement is ugly | |
// - working to replace with experimental change to use getClass entry point for Member | |
private lazy val memberSealedTraitDescendantNames: Try[Set[String]] = { | |
def fetchMemberSealedTraitDescendantNames: Try[Set[String]] = { | |
def fetchSealedTraitDescendantsViaTraitTypeTag: Option[Set[Symbol]] = { | |
def fetchSealedTraitDescendantsViaTraitSymbol(symbol: Symbol): Option[Set[Symbol]] = { | |
val internal = symbol.asInstanceOf[scala.reflect.internal.Symbols#Symbol] | |
if (internal.isSealed) | |
Some(internal.sealedDescendants.map(_.asInstanceOf[Symbol]) - symbol) | |
else None | |
} | |
fetchSealedTraitDescendantsViaTraitSymbol(typeTagMember.tpe.typeSymbol) | |
} | |
fetchSealedTraitDescendantsViaTraitTypeTag match { | |
case Some(symbols) => Success(symbols.map(_.toString.stripPrefix("object "))) | |
case None => Failure(new IllegalStateException("reflection failed to obtain descendants of sealed trait Member")) | |
} | |
} | |
fetchMemberSealedTraitDescendantNames | |
} | |
//D. Adds a member to temporary internal storage | |
// - for errors, see val status in protected trait MemberBase [item O.D] | |
// - Does NOT generate the ordinal (due to JVM class initialization order - see val orderedSet below for all reasons) | |
@volatile | |
private var addFailure: Option[Throwable] = None | |
@volatile | |
private var isAddClosed: Boolean = false | |
@volatile | |
private var membersTempInternal: List[Member] = Nil | |
@volatile | |
private var memberNamesLowerCaseAsSetInternal: Set[String] = Set() | |
final private def add(member: Member): Try[CompletedNoException] = { | |
def postAddFailure(throwable: Throwable): Try[CompletedNoException] = { | |
val failure = Failure(throwable) | |
addFailure = Some(throwable) | |
failure | |
} | |
this.synchronized { | |
addFailure match { | |
case Some(throwable) => Failure(throwable) | |
case None => | |
if (!isAddClosed) | |
memberSealedTraitDescendantNames.flatMap(memberSealedTraitDescendantNamesFm => { | |
val memberSealedTraitDescendantNamesFmLowerCase = memberSealedTraitDescendantNamesFm.map(_.toLowerCase) | |
if (getClass.getName.toLowerCase == member.nameSpace.toLowerCase) | |
if (!memberNamesLowerCaseAsSetInternal.contains(member.name.toLowerCase)) { | |
membersTempInternal = member :: membersTempInternal | |
memberNamesLowerCaseAsSetInternal = memberNamesLowerCaseAsSetInternal + member.name.toLowerCase | |
isAddClosed = memberNamesLowerCaseAsSetInternal == memberSealedTraitDescendantNamesFmLowerCase | |
if (isAddClosed) | |
if (orderedSet.nonEmpty) { | |
val orderedSetAsRealSet = orderedSet.toSet | |
if (orderedSet.size == orderedSetAsRealSet.size) { | |
val orderedSetNamesLowerCase = orderedSetAsRealSet.map(_.name.toLowerCase).toSet | |
if (orderedSetNamesLowerCase == memberSealedTraitDescendantNamesFmLowerCase) | |
//D.9 Info: Everything checks out and we're done; | |
// - all case object initializations equal the reflection discovered sealed trait | |
// descendant names equals the client provided orderedSet | |
Success(completedNoExceptionSingleton) | |
else { | |
//D.8 Paranoimia: Catch possible weird initialization effects | |
// - hard to imagine how this could occur | |
val inOnlyOrderedSet = | |
orderedSetNamesLowerCase.--(memberSealedTraitDescendantNamesFmLowerCase) | |
val inOnlyMemberSealedTraitDescendantNamesFm = | |
memberSealedTraitDescendantNamesFmLowerCase.--(orderedSetNamesLowerCase) | |
postAddFailure( | |
new IllegalStateException( | |
s"orderedSetNamesLowerCase [${orderedSetNamesLowerCase.mkString(",")}] is not equal to " | |
+ s"memberSealedTraitDescendantNamesFmLowerCase" | |
+ s" [${memberSealedTraitDescendantNamesFmLowerCase.mkString(",")}]" | |
+ (if (inOnlyOrderedSet.nonEmpty) | |
s" - orderedSetNamesLowerCase contains values [${inOnlyOrderedSet.mkString(",")}]" | |
+ s" not in memberSealedTraitDescendantNamesFmLowerCase" | |
else "" | |
) | |
+ (if (inOnlyMemberSealedTraitDescendantNamesFm.nonEmpty) | |
s" - memberSealedTraitDescendantNamesFmLowerCase contains values" | |
+ s" [${inOnlyMemberSealedTraitDescendantNamesFm.mkString(",")}] not in" | |
+ s" orderedSetNamesLowerCase" | |
else "" | |
) | |
) | |
) | |
} | |
} | |
else | |
//D.7 Guard: Client attempted to provide a non-unique List (there is no insertion order Set | |
// implementation in the Scala collections library) | |
postAddFailure( | |
new IllegalStateException( | |
s"orderedSet.size [${orderedSet.size}] must be equal to orderedSetAsRealSet.size" | |
+ s" [${orderedSetAsRealSet.size}] (isAddClosed is true)" | |
) | |
) | |
} | |
else | |
//D.6 Guard: Client attempted to provide an empty set | |
postAddFailure(new IllegalStateException("orderedSet must not be empty (isAddClosed is true)")) | |
else | |
if (memberNamesLowerCaseAsSetInternal.size != memberSealedTraitDescendantNamesFmLowerCase.size) | |
//D.5 Info: Initializations remain | |
Success(completedNoExceptionSingleton) | |
else { | |
//D.4 Paranoimia: Catch possible weird initialization effects | |
// - hard to imagine this could occur if the compiler is ensuring the sealed trait | |
// contract | |
val inOnlyMemberNamesLowerCaseAsSetInternal = | |
memberNamesLowerCaseAsSetInternal.--(memberSealedTraitDescendantNamesFmLowerCase) | |
val inOnlyMemberSealedTraitDescendantNamesFm = | |
memberSealedTraitDescendantNamesFmLowerCase.--(memberNamesLowerCaseAsSetInternal) | |
postAddFailure( | |
new IllegalStateException( | |
s"while being the same size [${memberNamesLowerCaseAsSetInternal.size}], both" | |
+ s" memberNamesLowerCaseAsSetInternal" | |
+ s" [${memberNamesLowerCaseAsSetInternal.mkString(",")}] and" | |
+ s" memberSealedTraitDescendantNamesFmLowerCase" | |
+ s" [${memberSealedTraitDescendantNamesFmLowerCase.mkString(",")}] are not equal (not sure how" | |
+ s" this is even possible)" | |
+ (if (inOnlyMemberNamesLowerCaseAsSetInternal.nonEmpty) | |
s" - memberNamesLowerCaseAsSetInternal contains values" | |
+ s" [${inOnlyMemberNamesLowerCaseAsSetInternal.mkString(",")}] not in" | |
+ s" memberSealedTraitDescendantNamesFmLowerCase" | |
else "" | |
) | |
+ (if (inOnlyMemberSealedTraitDescendantNamesFm.nonEmpty) | |
s" - memberSealedTraitDescendantNamesFmLowerCase contains values" | |
+ s" [${inOnlyMemberSealedTraitDescendantNamesFm.mkString(",")}] not in" | |
+ s" memberNamesLowerCaseAsSetInternal" | |
else "" | |
) | |
) | |
) | |
} | |
} | |
else | |
//D.3 Guard: Disallow case name sensitivity | |
// - KING and King are considered equivalent and whichever initializes second will be rejected | |
postAddFailure( | |
new IllegalArgumentException( | |
s"attempting to add member with name [${member.name}] which was previously " | |
+ s"(case insensitively) added [${member.name.toLowerCase}]" | |
) | |
) | |
else | |
//D.2 Watchful: Require all instances must come from the parent name space | |
postAddFailure( | |
new IllegalArgumentException( | |
s"member [${member.name}] (with nameSpace [${member.nameSpace}]) must be declared" | |
+ s"inside of the derived Enumeration's nameSpace [${getClass.getName}]" | |
) | |
) | |
} | |
) | |
else | |
//D.1 Paranoimia: Catch possible weird multi-threading conflicts | |
// - hard to imagine this could occur given how strongly defined JVM class | |
// initialization is defined | |
postAddFailure( | |
new IllegalStateException(s"attempting to add member [${member.name}] after isAddClosed was set true") | |
) | |
} | |
} | |
} | |
//E. Generates the ordinals for the members (only if all classes/objects initialized correctly) | |
// - uses lazy val to postpone defining/assigning ordinal until after all case object | |
// initializations complete i.e. just prior to completing client's first call | |
// - returns Try to facilitate external capturing/logging/reporting Failure (in def status below) | |
private lazy val membersInternal: Try[List[Member]] = { | |
this.synchronized { | |
addFailure match { | |
case Some(throwable) => Failure(throwable) | |
case None => | |
if (isAddClosed) | |
Success(orderedSet) | |
else | |
if (orderedSet.nonEmpty) { | |
val orderedSetAsRealSet = orderedSet.toSet | |
if (orderedSet.size == orderedSetAsRealSet.size) { | |
val orderedSetNamesLowerCase = orderedSetAsRealSet.map(_.name.toLowerCase).toSet | |
memberSealedTraitDescendantNames.flatMap(memberSealedTraitDescendantNamesFm => { | |
val memberSealedTraitDescendantNamesFmLowerCase = memberSealedTraitDescendantNamesFm.map(_.toLowerCase) | |
val inOnlyOrderedSet = orderedSetNamesLowerCase.--(memberSealedTraitDescendantNamesFmLowerCase) | |
val inOnlyMemberSealedTraitDescendantNamesFm = | |
memberSealedTraitDescendantNamesFmLowerCase.--(orderedSetNamesLowerCase) | |
Failure( | |
new IllegalStateException( | |
s"orderedSetNamesLowerCase [${orderedSetNamesLowerCase.mkString(",")}] is not equal to " | |
+ s"memberSealedTraitDescendantNamesFmLowerCase" | |
+ s"[${memberSealedTraitDescendantNamesFmLowerCase.mkString(",")}]" | |
+ (if (inOnlyOrderedSet.nonEmpty) | |
s" - orderedSetNamesLowerCase contains values [${inOnlyOrderedSet.mkString(",")}] not in" | |
+ s" memberSealedTraitDescendantNamesFmLowerCase" | |
else "" | |
) | |
+ (if (inOnlyMemberSealedTraitDescendantNamesFm.nonEmpty) | |
s" - memberSealedTraitDescendantNamesFmLowerCase contains values" | |
+ s" [${inOnlyMemberSealedTraitDescendantNamesFm.mkString(",")}] not in orderedSetNamesLowerCase" | |
else "" | |
) | |
) | |
) | |
}) | |
} | |
else | |
Failure( | |
new IllegalStateException( | |
s"orderedSet.size [${orderedSet.size}] must be equal to orderedSetAsRealSet.size" | |
+ s" [${orderedSetAsRealSet.size}] (isAddClosed is true)" | |
) | |
) | |
} | |
else | |
Failure(new IllegalStateException("orderedSet must not be empty")) | |
} | |
} | |
} | |
//F. Provide range covering all ordinals | |
final lazy val ordinalRange: Range = | |
ordinalRangeLift match { | |
case Success(range) => range | |
case Failure(exception) => throw exception | |
} | |
final lazy val ordinalRangeLift: Try[Range] = | |
if (ordinalStep != 0) | |
membersLift.flatMap(membersFm => | |
Success(ordinalStart.until(ordinalStart + (membersFm.size * ordinalStep), ordinalStep)) | |
) | |
else | |
Failure(new IllegalStateException(s"ordinalStep must not be equal to 0")) | |
//G. Enable lookup for ordinal by Member (used by Member.ordinal) | |
final lazy val ordinalByMember: Map[Member, Int] = | |
ordinalByMemberLift match { | |
case Success(map) => map | |
case Failure(exception) => throw exception | |
} | |
final lazy val ordinalByMemberLift: Try[Map[Member, Int]] = | |
membersInternal.flatMap( | |
membersInternalFm => { | |
ordinalRangeLift.flatMap( | |
ordinalRangeFm => | |
Success(membersInternalFm.zip(ordinalRangeFm).toMap) | |
) | |
} | |
) | |
//H. Enable lookup for Member by ordinal | |
final lazy val memberByOrdinal: Map[Int, Member] = | |
memberByOrdinalLift match { | |
case Success(map) => map | |
case Failure(exception) => throw exception | |
} | |
final lazy val memberByOrdinalLift: Try[Map[Int, Member]] = | |
ordinalByMemberLift.flatMap( | |
ordinalByMemberFm => { | |
val ordinalAndMemberPairs = | |
ordinalByMemberFm.map( | |
memberAndOrdinalPair => | |
(memberAndOrdinalPair._2, memberAndOrdinalPair._1) | |
) | |
Success(ordinalAndMemberPairs.toMap) | |
} | |
) | |
//I. Enable lookup for Member by name | |
private lazy val memberNames: String = | |
membersLift match { | |
case Success(membersLiftGet) => membersLiftGet.map(_.name).mkString(",") | |
case Failure(exception) => exception.getMessage | |
} | |
final def memberByName(name: String): Member = | |
memberByNameLift(name) match { | |
case Success(member) => member | |
case Failure(exception) => throw exception | |
} | |
final def memberByNameLift(name: String): Try[Member] = | |
memberByNameUpperCaseLift.flatMap( | |
map => | |
map.get(name.toUpperCase) match { | |
case Some(member) => | |
Success(member) | |
case None => | |
Failure(new IllegalArgumentException(s"name [$name.toUpperCase] not found in member names [$memberNames]")) | |
} | |
) | |
final lazy val memberByNameUpperCase: Map[String, Member] = | |
memberByNameUpperCaseLift match { | |
case Success(map) => map | |
case Failure(exception) => throw exception | |
} | |
final lazy val memberByNameUpperCaseLift: Try[Map[String, Member]] = | |
membersInternal.flatMap( | |
membersInternalFm => { | |
val nameUpperCaseAndMemberPairs = | |
membersInternalFm.map( | |
member => | |
(member.name.toUpperCase, member) | |
) | |
Success(nameUpperCaseAndMemberPairs.toMap) | |
} | |
) | |
//J. orderedSet of Members in their ordinal order | |
// - assuming stable class object initializations and validation, it is a reference to | |
// exactly the same data provided by the client in orderedSet) | |
final def members: List[Member] = | |
membersLift match { | |
case Success(list) => list | |
case Failure(exception) => throw exception | |
} | |
final def membersLift: Try[List[Member]] = membersInternal | |
//K. Provide Try status of initialization/validation | |
final lazy val status: Try[CompletedNoException] = | |
membersLift.flatMap(_ => Success(completedNoExceptionSingleton)) | |
//L. Client provided reflection hook | |
// - ugly implementation requirement until I can figure out the route using Member.getClass | |
protected def typeTagMember: TypeTag[_] | |
//M. Required client provided ordering | |
// - orderedSet.size must be equal to orderedSet.toSet.size | |
// - client must declare as val to force immediate evaluation (or declare as lazy val and | |
// have another declared val depend upon on it similar to EnumerationDecorated's | |
// implementation) | |
// - required because object/class initialization ordering is not predictable | |
// - if client happens to access any explicit case object member (other than the first), | |
// the case object initialization order is undefined | |
// - required because the order of the returned classes from getClass.getDeclaredClasses | |
// is undefined | |
protected def orderedSet: List[Member] | |
//N. Optional client override for starting ordinal and step | |
protected def ordinalStart = 0 | |
protected def ordinalStep = 1 //requirement: cannot equal 0 | |
//O. Required client implementation to provide basis for unique Enumeration members | |
protected trait MemberBase { | |
self: Member => | |
//O.A Overridden for serialization's readResolve to ensure singleton-ness | |
// by fetching existing instance via case insensitive name | |
private def readResolve(): Object = | |
memberByName(name) | |
//O.B Automatic naming based on getClass | |
// - all case objects must be in the same containing name space | |
// - name is case insensitive, IOW adding two different members where they only differ in | |
// letter casing is restricted; i.e. attempting to declare KING and King will cause | |
// an initialization exception | |
final def nameSpace: String = | |
getClass.getName.stripSuffix(getClass.getSimpleName) | |
final val name: String = | |
getClass.getSimpleName.stripSuffix("$") | |
//O.C The assigned ordinal based on Enumeration.order above | |
final lazy val ordinal: Int = | |
ordinalLift match { | |
case Success(int) => int | |
case Failure(exception) => throw exception | |
} | |
final lazy val ordinalLift: Try[Int] = | |
Enumeration.this.status match { | |
case Success(_) => | |
ordinalByMemberLift match { | |
case Success(map) => Success(map(this)) | |
case Failure(exception) => Failure(exception) | |
} | |
case Failure(exception) => | |
Failure( | |
new IllegalStateException(s"access denied - Enumeration.status is a Failure - ${exception.getMessage}") | |
) | |
} | |
//O.D Provide Try status of specific Member initialization/validation | |
final val status: Try[CompletedNoException] = add(this) | |
} | |
} |
//EnumerationDecorated.scala | |
// - DIY Scala Enumeration (closest possible Java Enum equivalent with guaranteed pattern | |
// matching exhaustiveness checking) with extended attributes within a decoration | |
//Copyright (C) 2014-2016 Jim O'Flaherty | |
// | |
// | |
//This Scala class is free software: you can redistribute it and/or modify it under the terms | |
//of the GNU General Public License as published by the Free Software Foundation, either | |
//version 3 of the License, or any later version. | |
// | |
//This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; | |
//without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | |
//See the GNU General Public License for more details. | |
// | |
//To see details of the GPLv3 License, please see <http://www.gnu.org/copyleft/gpl.html>. | |
//To see details of the GNU General Public License, please see <http://www.gnu.org/licenses/>. | |
// | |
//If you would like to obtain a custom/different/commercial license for this, please send an | |
//email with your request to <[email protected]>. | |
package org.public_domain.scala.utils | |
trait EnumerationDecorated extends Enumeration { | |
//A. Type Member which must be found in the descendant object | |
// - see protected trait DecorationBase [item B] | |
type Decoration <: DecorationBase | |
//B. From Enumeration [item M], required ordering of case objects | |
// it's pushed way down here to enable an iteration of decorationOrderedSet to avoid | |
// having to specify the members a third time | |
protected lazy val orderedSet: List[Member] = decorationOrderedSet.map(_.member) | |
//C. from Enumeration [item B], required type extension which must be overridden | |
// and _sealed_ by client | |
trait MemberDecorated extends MemberBase { | |
self: Member => | |
def decoration: Decoration = decorationByMember(this) | |
} | |
//D. Defining the Decoration data | |
// - intended to be the only other time the Members must be mapped out right | |
// along with their specific decorative additions | |
val decorationOrderedSet: List[Decoration] | |
//E. Enable Decoration lookup by Member | |
lazy val decorationByMember: Map[Member, Decoration] = | |
decorationOrderedSet.map(decoration => (decoration.member, decoration)).toMap | |
//F. Required client implementation to provide basis for Decorations | |
trait DecorationBase { | |
self: Decoration => | |
def member: Member | |
} | |
} |
package org.public_domain.chess | |
object WorkSheetChessPiecesEnhancedDecoratedTest { | |
println("Welcome to the Scala worksheet") | |
// | |
import scala.util.{Failure, Success} | |
// | |
import org.public_domain.chess.{ChessPiecesEnhancedDecorated=>ChessPieces} | |
// | |
def stringMe(chessPiece: ChessPieces.Member): String = | |
chessPiece match { | |
case ChessPieces.KING => "Of the World" | |
case ChessPieces.QUEEN => "Of the World" | |
case ChessPieces.BISHOP => "Of the Church" | |
case ChessPieces.KNIGHT => "Of the Church" | |
case ChessPieces.ROOK => "Of the Castle" | |
case ChessPieces.PAWN => "Of the Field" | |
} | |
//to test various initialization ordering issues, rearrange these six lines into any order | |
val rook = ChessPieces.ROOK | |
val king = ChessPieces.KING | |
val pawn = ChessPieces.PAWN | |
val queen = ChessPieces.QUEEN | |
val bishop = ChessPieces.BISHOP | |
val knight = ChessPieces.KNIGHT | |
// | |
val statusChessPieces = | |
ChessPieces.status | |
val isStatusChessPiecesSuccess = | |
statusChessPieces.isSuccess | |
val statusChessPiecesMemberFailures = | |
ChessPieces.members.map(_.status).filter(_.isFailure) | |
val isStatusChessPiecesMembersSuccess = | |
statusChessPiecesMemberFailures.isEmpty | |
val isOrderedCorrectly = | |
ChessPieces.members == List(ChessPieces.KING, ChessPieces.QUEEN, ChessPieces.BISHOP, ChessPieces.KNIGHT, ChessPieces.ROOK, ChessPieces.PAWN) | |
val ordinalRange = | |
ChessPieces.ordinalRange | |
val ordinalNamePairs = | |
ChessPieces.members.map(member => (member.ordinal, member.name)) | |
// | |
val bishopByName = ChessPieces.memberByName("bIsHop") | |
// | |
val decorationBishop = ChessPieces.decorationByMember(ChessPieces.BISHOP) | |
val decorations = ChessPieces.decorationOrderedSet | |
} |
package org.public_domain.chess | |
object WorkSheetChessPiecesEnhancedTest { | |
println("Welcome to the Scala worksheet") | |
// | |
import scala.util.{Failure, Success} | |
// | |
import org.public_domain.chess.{ChessPiecesEnhanced=>ChessPieces} | |
// | |
def stringMe(chessPiece: ChessPieces.Member): String = | |
chessPiece match { | |
case ChessPieces.KING => "Of the World" | |
case ChessPieces.QUEEN => "Of the World" | |
case ChessPieces.BISHOP => "Of the Church" | |
case ChessPieces.KNIGHT => "Of the Church" | |
case ChessPieces.ROOK => "Of the Castle" | |
case ChessPieces.PAWN => "Of the Field" | |
} | |
//to test various initialization ordering issues, rearrange these six lines into any order | |
val rook = ChessPieces.ROOK | |
val king = ChessPieces.KING | |
val pawn = ChessPieces.PAWN | |
val queen = ChessPieces.QUEEN | |
val bishop = ChessPieces.BISHOP | |
val knight = ChessPieces.KNIGHT | |
// | |
val statusChessPieces = | |
ChessPieces.status | |
val isStatusChessPiecesSuccess = | |
statusChessPieces.isSuccess | |
val statusChessPiecesMemberFailures = | |
ChessPieces.members.map(_.status).filter(_.isFailure) | |
val isStatusChessPiecesMembersSuccess = | |
statusChessPiecesMemberFailures.isEmpty | |
val isOrderedCorrectly = | |
ChessPieces.members == List(ChessPieces.KING, ChessPieces.QUEEN, ChessPieces.BISHOP, ChessPieces.KNIGHT, ChessPieces.ROOK, ChessPieces.PAWN) | |
val ordinalRange = | |
ChessPieces.ordinalRange | |
val ordinalNamePairs = | |
ChessPieces.members.map(member => (member.ordinal, member.name)) | |
// | |
val bishopByName = ChessPieces.memberByName("bIsHop") | |
// | |
val decorationBishop = ChessPieces.attributesByMember(ChessPieces.BISHOP) | |
val decorations = ChessPieces.attributesList | |
} |
package org.public_domain.chess | |
object WorkSheetChessPiecesSimplestTest { | |
println("Welcome to the Scala worksheet") | |
// | |
import scala.util.{Failure, Success} | |
// | |
import org.public_domain.chess.{ChessPiecesSimplest=>ChessPieces} | |
// | |
def stringMe(chessPiece: ChessPieces.Member): String = | |
chessPiece match { | |
case ChessPieces.KING => "Of the World" | |
case ChessPieces.QUEEN => "Of the World" | |
case ChessPieces.BISHOP => "Of the Church" | |
case ChessPieces.KNIGHT => "Of the Church" | |
case ChessPieces.ROOK => "Of the Castle" | |
case ChessPieces.PAWN => "Of the Field" | |
} | |
// | |
//to test various initialization ordering issues, rearrange these six lines into any order | |
val rook = ChessPieces.ROOK | |
val king = ChessPieces.KING | |
val pawn = ChessPieces.PAWN | |
val queen = ChessPieces.QUEEN | |
val bishop = ChessPieces.BISHOP | |
val knight = ChessPieces.KNIGHT | |
// | |
val statusChessPieces = | |
ChessPieces.status | |
val isStatusChessPiecesSuccess = | |
statusChessPieces.isSuccess | |
val statusChessPiecesMemberFailures = | |
ChessPieces.members.map(_.status).filter(_.isFailure) | |
val isStatusChessPiecesMembersSuccess = | |
statusChessPiecesMemberFailures.isEmpty | |
val isOrderedCorrectly = | |
ChessPieces.members == List(ChessPieces.KING, ChessPieces.QUEEN, ChessPieces.BISHOP, ChessPieces.KNIGHT, ChessPieces.ROOK, ChessPieces.PAWN) | |
val ordinalRange = | |
ChessPieces.ordinalRange | |
val ordinalNamePairs = | |
ChessPieces.members.map(member => (member.ordinal, member.name)) | |
// | |
val bishopByName = ChessPieces.memberByName("bIsHop") | |
} |
Hi, thank you for such an exhaustive and clear implementation for Enumeration in Scala. I am new to scala and for my project I am looking with similar requiremetns as you have outlined in Stackoverflow for Enumeration. On top of this I need the Enumeration object to be used inside my other member classes which I want to be able to pass through a REST API.
Meaning, I've an _EmployeeType as the enumeration with values {"contractor", "permanent"} and I've an employee class with {"id", "name", "type: _EmployeeType} as the member class.
Hence to be able to expose the Employee REST API, I should be able to stream the Employee object through jsonFormat3(Employee). But now I am not able to convert the _EmployeeType into a jsonFormat protocol. Can you please help?
PS: I am using spray-json library for the JSON to object and vice versa conversion.
Thanks and Regards,
Phani
Hi! I've just now (2015/Oct/02) saw your message. It's much better to hit me at my personal email jim dot oflaherty dot jr at gmail dot com as I see that every day.
I am not currently using that library. So I am not familiar enough with what you might be hitting. What kind of error are you receiving?
This is working like a charm - only had a little while to interact with it but the situations it "covers" seems to be very extensive. Looking forward to putting it through it's paces! It's author is very responsive.