-
-
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.