Is there a way to implement a hierarchical enumeration in scala? - scala

I am working in a project in which I need to tag the type of the received messages. Messages can come from different sources, but all these sources generate messages with the same conceptual type (so, the same meaning) but written in different ways.
For example from source1 I can receive
Source1:
{
"message_type": "typeA",
"value": 3
...
}
or
{
"message_type": "typeB",
"value": 3
...
}
But also from source2 I can receive
Source2:
{
"message_type": "A",
"value": 5
...
}
or
{
"message_type": "B",
"value": 2
...
}
I want to maximize the code reuse so I tried this solution.
The first scala file I created is a trait:
trait MessageType extends Enumeration {
val TYPE_A: Value
val TYPE_B: Value
}
then I implemented it in two object files:
object Source1MessageType extends MessageType{
override val TYPE_A: Value("typeA")
override val TYPE_B: Value("typeB")
object Source2MessageType extends MessageType{
override val TYPE_A: Value("A")
override val TYPE_B: Value("B")
So now what I want is to check the type of the message without knowing the source type, like this:
def foo(type: MessageType.Value) {
type match{
case MessageType.TYPE_A => ...do A action...
case MessageType.TYPE_B => ...do B action...
}
}
But if I write this code, the IDE (IntelliJ) highlights the parameter in red but it gives me no info about the error. It seems like I can use only Source1MessageType or Source2MessageType as parameter type.
I think the error is because Scala doesn't see the trait as an enumeration so I cannot access to the values of the enumeration.
Do you have any solution for that?

yes you can do hierarchical enumerations. So generally I would recommend against using Enumeration. Here's an article with why it's bad
https://medium.com/#yuriigorbylov/scala-enumerations-hell-5bdba2c1216
The most idiomatic way to do this is by leveraging sealed traits like this:
sealed trait MessageType{
def value:String
}
sealed trait MessageType1 extends MessageType
final case object TypeA extends MessageType1{
override def value:String = "typeA"
}
final case object TypeB extends MessageType1{
override def value:String = "typeB"
}
sealed trait MessageType2 extends MessageType
final case object A extends MessageType2{
override def value:String = "A"
}
final case object B extends MessageType2{
override def value:String = "B"
}
Note that all these definitions need to be in the same file. Now this works because sealed and final tells the compiler that inheritance can only happen in this file.
This means that given an instance of MessageType2 the compiler knows that it can only be either the object A or B it cannot be anything else (because of the sealed/final)
This gives you enums with exhaustiveness checks in patternmatching etc.

If "A" and "typeA" are names for the same message type then you need to deal with this when you read in the data. This means that you don't need any nesting in your enumeration.
trait MessageType
case object MessageTypeA extends MessageType
case object MessageTypeB extends MessageType
object MessageType {
def apply(s: String): MessageType =
s match {
case "A" | "typeA" => MessageTypeA
case "B" | "typeB" => MessageTypeB
case _ => throw BadMessageType
}
}
case class Message(msgType: MessageType, value: Int)
You can create the message type with MessageType(<string>) and it will return MessageTypeA or MessageTypeB as appropriate. You can use a normal match to determine which message type you have.
If you need to preserve the original string that generated the MessageType then you can store that as abstract value in the MessageType trait and fill it in when the appropriate instance is created:
trait MessageType {
def origin: String
}
case class MessageTypeA(origin: String) extends MessageType
case class MessageTypeB(origin: String) extends MessageType
object MessageType {
def apply(s: String): MessageType =
s match {
case "A" | "typeA" => MessageTypeA(s)
case "B" | "typeB" => MessageTypeB(s)
case _ => throw BadMessageType
}
}

Related

Scala sealed abstract class with enum object matching against string

I am a beginner in scala, I have a case class for the response I am getting as json payload
case class CallbackPayload(
automation_id: String,
business_key: String,
state: String #received , validating
)
I have a sealed abstract class
object ExecutionStatus {
sealed abstract class ExecutionState(status: String) {
override def toString: String = status
}
case object RECEIVED extends ExecutionState("received")
case object VALIDATING extends ExecutionState("validating")
}
Now based on the response payload state I want to do a match against the ExecutionStatus objects
Something like below
def callback(payload: CallbackPayload): Either[Throwable, Json] = {
payload.state match {
case VALIDATING => Right(Json.obj("status" -> Json.fromString("validating")))
.....
}
}
Now because of payload.state type string I am unable to . How to do that in scala.
It looks like you may want to match on the status string, like so (using a simplified model of your code):
object ExecutionStatus {
sealed abstract class ExecutionState(val status: String) {
override def toString: String = status
}
case object RECEIVED extends ExecutionState("received")
case object VALIDATING extends ExecutionState("validating")
}
def parse(state: String): Either[Throwable, Map[String, String]] = {
state match {
case ExecutionStatus.RECEIVED.status =>
Right(Map("status" -> "received"))
case ExecutionStatus.VALIDATING.status =>
Right(Map("status" -> "validating"))
case _ =>
Left(new IllegalArgumentException(s"unrecognized state $state"))
}
}
assert(parse("received") == Right(Map("status" -> "received")))
assert(parse("validating") == Right(Map("status" -> "validating")))
assert(parse("foo").isLeft)
You can play around with this code here on Scastie.
Notice that in order to do so I had to make status a val so that it's public (without it, status is only a constructor parameter).

how to get enums value using case classes in scala

I am trying to implement enum using case class in scala.
with reference to question type parameter in traits to define data types in scala
sealed trait ElementType[+A] {
def length: Int
}
sealed trait Tag extends ElementType[Int] {
override def length: Int = 0
}
case object Tag100 extends Tag
sealed trait Value[+A] extends ElementType[A] {
override def length: Int = 0
}
final case class Value100(a: String) extends Value[String] {
override def length: Int = a.length
}
case class MessageId(tag: Tag, value: Value[String]){
}
case class MessageRepType(tag: Tag[Int], value:MessageType ){
}
sealed trait Message[T] {def typeCode: T;}
object MessageTypes {
sealed trait MessageType[Int] extends Message[Int]
case object TYPE1 extends MessageType[Int]{val typeCode = 0;}
case object TYPE2 extends MessageType[Int]{val typeCode = 1;}
}
case class ABCMessage (
id:MessageId,
messageType:MessageRepType)
I want to print a message with something like below id and type in new line
type value
100 abc
200 0
Explanation: 100 abc represents MessageId
200 0 represnets MessageRepType and O represent TYPE1
Assuming your "message" is a ABCMessage(MessageId(Tag100, Value100("abc"),MessageRepType(Tag200, TYPE1)) (also assuming Tag200 is similar to Tag100 and case class MessageRepType(tag: Tag[Int], value:MessageType) is case class MessageRepType(tag: Tag, value:MessageType) and sealed trait MessageType extends Message[Int]):
val sep = System.lineSeparator
def convertTag(tag: Tag): Int = tag match {
case Tag100 => 100
case Tag200 => 200
}
def convertMessageId(message: MessageId): String = {
val typ = convertTag(message.tag)
val value = message.value match {
case Value100(v) => v
}
s"$typ\t$value"
}
val id = convertMessageId(message.id)
def convertType(message: MessgeRepType): String = {
val typ = convertTag(message.tag)
val value = message.value match {// I would put typeCode to MessageType, but it is not there, so pattern matching
case TYPE1 => TYPE1.typeCode
case TYPE2 => TYPE2.typeCode
}
s"$typ\t$value"
}
val typ = convertType(message)
val message: ABCMessage = ABCMessage(MessageId(Tag100, Value100("abc"),MessageRepType(Tag200, TYPE1))
val messageAsString = Seq("type\tvalue", id, typ).mkString(sep)
println(messageAsString)
A different approach as still not sure what you want to achieve. The other answer with pattern matching is what I would prefer, more idiomatic.
You can use something like a simplified the visitor pattern (removed the irrelevant parts):
trait Show {
def show: String
}
sealed trait Tag extends Show {
override def show: String = this.getClass.getSimpleName.init
}
//object Tag {
case object Tag100 extends Tag
case object Tag200 extends Tag
//}
This way you can show the tags like this:
Tag100.show // Tag100
The case classes can also implement show:
sealed trait Value[+A] extends Show {
def v: String
override def show: String = v
}
case class Value100(v: String) extends Value[String]
You can use this in other methods too to implement the printing you want:
def convertMessageId(message: MessageId): String = {
val typ = message.tag.show
val value = message.value.show
s"$typ\t$value"
}
val id = convertMessageId(message.id)
def convertType(message: MessgeRepType): String = {
val typ = message.tag.show
val value = message.value.show
s"$typ\t$value"
}
val message: ABCMessage = ABCMessage(MessageId(Tag100, Value100("abc"),MessageRepType(Tag200, TYPE1))
val messageAsString = Seq("type\tvalue", id, typ).mkString(sep)
println(messageAsString)
If type safety and easy, generic usage is also important, take a look at Shapeless' offerings and this blog post.
If type safety is not so important, maybe for your use case it is enough to know that case classes implement Product, so you can visit them one-by-one and use its content.
Alternative solution might be to convert your datastructure to for example JSON (there are many Scala libs for that) and use that for further processing. Still generic, but not type-safe.
(This question might also be relevant.)

Function with return type defined by a type member of its argument

Updated my question per feedback from commenters (Miles and m-z):
I'm finding duplicate "values" by either Name or Age.
sealed trait DuplicateResult
case class DuplicatesByName(name: String, people: Set[String]) extends DuplicateResult
case class DuplicatesByAge(age: Int, people: Set[String]) extends DuplicateResult
And the return type must be different depending on Name or Age:
sealed trait QueryByDuplicate {
type DuplicateResultType
}
case class Name(name: String) extends QueryByDuplicate {
override type DuplicateResultType = DuplicatesByName
}
case class Age(age: Int) extends QueryByDuplicate {
override type DuplicateResultType = DuplicatesByAge
}
Then, I define a function that compiles and runs:
def findDupes(x: QueryByDuplicate): DuplicateResult = x match {
case Name(n) => DuplicatesByName(n, Set("1", "2"))
case Age(a) => DuplicatesByAge(a, Set("42"))
}
scala> findDupes(Name("kevin"))
res0: DuplicateResult = DuplicatesByName(kevin,Set(1, 2))
scala> findDupes(Age(77))
res1: DuplicateResult = DuplicatesByAge(77,Set(42))
However, the type DuplicateResultType seems weak, since I could put any type there.
Please criticize and improve my implementation.
Option 1: The result type information is lost in your version because the caller cannot influence the fixed result type of findDupes. You can use generics so that the caller regains this control:
sealed trait QueryByDuplicate[T <: DuplicateResult]
case class Name(name: String) extends QueryByDuplicate[DuplicatesByName]
case class Age(age: Int) extends QueryByDuplicate[DuplicatesByAge]
def findDupes[T <: DuplicateResult](x: QueryByDuplicate[T]): T = x match {
case Name(n) => DuplicatesByName(n, Set("1", "2"))
case Age(a) => DuplicatesByAge(a, Set("42"))
}
val dupes: DuplicatesByName = findDupes(Name("kevin"))
Option 2: Since you asked for criticism too, I don't think the way you designed things is a good practice. Having several classes defining your queries, and a list of implementations for each class at a different place is duplicity hard to maintain. You could just use good old polymorphism:
sealed trait QueryByDuplicate[T <: DuplicateResult] {
def findDupes: T
}
class Name(name: String) extends QueryByDuplicate[DuplicatesByName] {
override def findDupes: DuplicatesByName = DuplicatesByName(name, Set("1", "2"))
}
class Age(age: Int) extends QueryByDuplicate[DuplicatesByAge] {
override def findDupes: DuplicatesByAge = DuplicatesByAge(age, Set("42"))
}
val dupes: DuplicatesByName = new Name("kevin").findDupes
This is how you would probably do it in Java. Sometimes it's better to stick to good old ways even when we have new toys.
Option 3: Talking about instanceOf, this also works:
sealed trait QueryByDuplicate {
type DuplicateResultType
}
case class Name(name: String) extends QueryByDuplicate {
override type DuplicateResultType = DuplicatesByName
}
case class Age(age: Int) extends QueryByDuplicate {
override type DuplicateResultType = DuplicatesByAge
}
def findDupes(x: QueryByDuplicate): x.DuplicateResultType = x match {
case Name(n) => DuplicatesByName(n, Set("1", "2")).asInstanceOf[x.DuplicateResultType]
case Age(a) => DuplicatesByAge(a, Set("42")).asInstanceOf[x.DuplicateResultType]
}
val dupes: DuplicatesByName = findDupes(Name("kevin"))
findDupes() will have a return type of Object in the bytecode, but Scala is smart enough to infer the correct type, apparently.

custom spray-json marshaller for a case class with Value type

case class HydraQueueMessage(tableType: HydraTableName.Value, payload: String)
object MedusaElementFormat extends DefaultJsonProtocol {
implicit object HydraElementFormat extends RootJsonFormat[HydraQueueMessage] {
def write(message: HydraQueueMessage) = JsObject(
MedusaEnum.HydraTableName.toString -> JsString(message.tableType.toString),
"payload" -> JsString(message.payload)
)
def read(value: JsValue) = {
value.asJsObject.getFields("tableType", "payload") match {
case Seq(JsString(tableType), JsString(payload)) =>
new HydraQueueMessage(tableType = tableType, payload = payload)
}
}
}
}
There's a type miss-match in this example, is there a cleaner way to achieve this? And still having the tableType as a Value and not String?
My marshaller throws a type miss-match with the Value type, I can't marshall JsValue either. So how would I proceed to marshall the HydraQueueMessage case class without using a String type for tableType?
You are trying to process too much at once. I would divide your problem in 2:
Process HydraTableName.Value
Process HydraQueueMessage
That will make things much easier.
To process your enum:
implicit object StepFormatJsonFormat extends RootJsonFormat[HydraTableName.Value] {
def write(obj: HydraTableName.Value): JsValue = JsString(obj.toString)
def read(jsonValue: JsValue): HydraTableName.Value = jsonValue match {
case JsString(stringObj) => HydraTableName.withName(stringObj)
case otherValue => throw new DeserializationException(s"HydraTableName.Value not found in ${otherValue.toString}")
}
}
Then the typical case class formatting:
implicit val HydraQueueMessageFormat = jsonFormat2(HydraQueueMessage.apply)
Probably the way to process the enum should be adapted.
If you include the HydraTableName, I can update the code.

spray-json serialization in spray-routing with custom JsonFormats

Using Spray with spray-json for a system, version:
"io.spray" %% "spray-json" % "1.2.6"
I cannot figure how to get custom JsonFormat definitions to work for serialization that is being handled by spray-routing.
I've had two separate circumstances that have failed.
1. Nested Case Classes
Basic case class JSON serialization has worked fine
case class Something(a: String, b: String)
implicit val something2Json = jsonFormat3(Something)
However if I have a nested case class in the case class to be serialized, I can resolve compile issues by providing another JsonFormat implicit, yet at run-time it refuses to serialize
case class Subrecord(value: String)
case class Record(a: String, b: String, subrecord: Subrecord)
object MyJsonProtocol extends DefaultJsonProtocol {
implicit object SubrecordJsonFormat extends JsonFormat[Subrecord] {
def write(sub: Subrecord) = JsString(sub.value)
def read(value: JsValue) = value match {
case JsString(s) => Subrecord(s)
case _ => throw new DeserializationException("Cannot parse Subrecord")
}
}
implicit val record2Json = jsonFormat3(Record)
}
This will throw a MappingException at runtime, explaining there is no usable value for subrecord
2. Trait with various 0-N case extensions
Here I have a trait that serves as a capturing type for a group of case classes. Some of the extending classes have vals while others have no vals and are objects. When serialization occurs, it seems like my implicit defined JsonFormat is completely ignored and I'm just give an empty JsObject, particularly when the actual underlying type was one of the case object's with no vals.
sealed trait Errors
sealed trait ErrorsWithReason extends Errors {
def reason: String
}
case class ValidationError(reason: String) extends ErrorsWithReason
case object EntityNotFound extends Errors
case class DatabaseError(reason: String) extends ErrorsWithReason
object MyJsonProtocol extends DefaultJsonProtocol {
implicit object ErrorsJsonFormat extends JsonFormat[Errors] {
def write(err: Errors) = failure match {
case e: ErrorsWithReason => JsString(e.reason)
case x => JsString(x.toString())
}
def read(value: JsValue) = {
value match {
//Really only intended to serialize to JSON for API responses
case _ => throw new DeserializationException("Can't reliably deserialize Error")
}
}
}
}
So given the above, if the actual type being serialized is EntityNotFound, then the serialization becomes a RootJsonFormat turning into {}. If it's an ErrorsWithReason then it becomes a RootJsonFormat turning into { "reason": "somevalue" }. I may be confused with how the JsonFormat definition is supposed to work, but it doesn't seem to be using my write method at all and instead has suddenly figured out how to serialize on its own.
EDIT
Specific serialization cases are using read/deserialization like:
entity(as[JObject]) { json =>
val extraction: A = json.extract[A]
}
And write/serialization with the complete directive.
I now am realizing thanks to the first answer posted here that my JsonDefaultProtocol and JsonFormat implementations are for spray-json classes, meanwhile the entity directive extraction in the deserialization is using json4s JObject as opposed to spray-json JsObject.
Another approach for clean JSON output
import spray.json._
import spray.json.DefaultJsonProtocol._
// #1. Subrecords
case class Subrecord(value: String)
case class Record(a: String, b: String, subrecord: Subrecord)
implicit object RecordFormat extends JsonFormat[Record] {
def write(obj: Record): JsValue = {
JsObject(
("a", JsString(obj.a)),
("b", JsString(obj.b)),
("reason", JsString(obj.subrecord.value))
)
}
def read(json: JsValue): Record = json match {
case JsObject(fields)
if fields.isDefinedAt("a") & fields.isDefinedAt("b") & fields.isDefinedAt("reason") =>
Record(fields("a").convertTo[String],
fields("b").convertTo[String],
Subrecord(fields("reason").convertTo[String])
)
case _ => deserializationError("Not a Record")
}
}
val record = Record("first", "other", Subrecord("some error message"))
val recordToJson = record.toJson
val recordFromJson = recordToJson.convertTo[Record]
println(recordToJson)
assert(recordFromJson == record)
If you need both reads and writes you can do it this way:
import spray.json._
import spray.json.DefaultJsonProtocol._
// #1. Subrecords
case class Subrecord(value: String)
case class Record(a: String, b: String, subrecord: Subrecord)
implicit val subrecordFormat = jsonFormat1(Subrecord)
implicit val recordFormat = jsonFormat3(Record)
val record = Record("a", "b", Subrecord("c"))
val recordToJson = record.toJson
val recordFromJson = recordToJson.convertTo[Record]
assert(recordFromJson == record)
// #2. Sealed traits
sealed trait Errors
sealed trait ErrorsWithReason extends Errors {
def reason: String
}
case class ValidationError(reason: String) extends ErrorsWithReason
case object EntityNotFound extends Errors
case class DatabaseError(reason: String) extends ErrorsWithReason
implicit object ErrorsJsonFormat extends JsonFormat[Errors] {
def write(err: Errors) = err match {
case ValidationError(reason) =>
JsObject(
("error", JsString("ValidationError")),
("reason", JsString(reason))
)
case DatabaseError(reason) =>
JsObject(
("error", JsString("DatabaseError")),
("reason", JsString(reason))
)
case EntityNotFound => JsString("EntityNotFound")
}
def read(value: JsValue) = value match {
case JsString("EntityNotFound") => EntityNotFound
case JsObject(fields) if fields("error") == JsString("ValidationError") =>
ValidationError(fields("reason").convertTo[String])
case JsObject(fields) if fields("error") == JsString("DatabaseError") =>
DatabaseError(fields("reason").convertTo[String])
}
}
val validationError: Errors = ValidationError("error")
val databaseError: Errors = DatabaseError("error")
val entityNotFound: Errors = EntityNotFound
assert(validationError.toJson.convertTo[Errors] == validationError)
assert(databaseError.toJson.convertTo[Errors] == databaseError)
assert(entityNotFound.toJson.convertTo[Errors] == entityNotFound)