I am trying to parse a csv row here and each field can be a different type. To handle the error accumulation I am using Either[String, B] where the String is an error message and B is the value. The issue here is that B can be different types, Option[Int], String, Array[String], resulting in my Map being type (String, Either[String,java.io.Serializable]) effectively making the Map unreusable. Is there a way (I'm definitely sure there is) to more elegantly accumulate errors while also reusing those values to populate properties on an object?
override def parseCsv(implicit fields: Map[String, String]): Either[String, User] = {
val parsedValues = Map(Headers.Id -> getFieldAsString(Headers.Id),
Headers.FirstName -> getFieldAsString(Headers.FirstName),
Headers.LastName -> getFieldAsString(Headers.LastName),
Headers.Email -> getFieldAsString(Headers.Email),
Headers.TimeZone -> getFieldAsString(Headers.TimeZone),
Headers.Region -> getOption(Headers.Region),
Headers.Phone -> getOption(Headers.Phone),
Headers.ProfileImage -> getFieldAsString(Headers.ProfileImage),
Headers.Roles -> getFieldAsArray(Headers.Roles))
val errors = parsedValues.collect { case (key, Left(errors)) => errors }
if (!errors.isEmpty) Left(errors.mkString(", "))
else {
val user = new User
user.id = getFieldAsString(Headers.Id).right.get
user.firstName = getFieldAsString(Headers.FirstName).right.get
user.lastName = getFieldAsString(Headers.LastName).right.get
user.email = getFieldAsString(Headers.Email).right.get
user.timeZone = getFieldAsString(Headers.TimeZone).right.get
user.phoneNumber = (for {
region <- getOption(Headers.Region).right.get
phone <- getOption(Headers.Phone).right.get
_ = validatePhoneNumber(phone, region)
} yield {
new PhoneNumber(region, phone)
}).orNull
user.profileImageUrl = getFieldAsString(Headers.ProfileImage).right.get
user.roles = getFieldAsArray(Headers.Roles).right.get
Right(user)
}
}
Create case classes for all types of Bs. These case classes must extend some common trait. While populating the user object just pattern match and retrieve values.
sealed trait Result {
val paramName: String
}
case class OptionInt(override val paramName: String, value: Option[Int]) extends Result
case class ArrayString(override val paramName: String, value: Array[String]) extends Result
case class StringValue(override val paramName: String, value: String) extends Result
now the final type would be like Either[String, Result]
after parsing the whole file create a List[Result]
If you are expecting age as Option[Int] and firstName as String then do this
list.foreach { result =>
result match {
case Option("age", value) => userId.age = value.getOrElse(defaultAge)
case StringValue("firstName", value) => userId.firstName = value
case StringValue("email", value) => userId.email = value
case _ => //do nothing
}
}
Related
Is there a nice way I can convert a Scala case class instance, e.g.
case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")
into a mapping of some kind, e.g.
getCCParams(x) returns "param1" -> "hello", "param2" -> "world"
Which works for any case class, not just predefined ones. I've found you can pull the case class name out by writing a method that interrogates the underlying Product class, e.g.
def getCCName(caseobj: Product) = caseobj.productPrefix
getCCName(x) returns "MyClass"
So I'm looking for a similar solution but for the case class fields. I'd imagine a solution might have to use Java reflection, but I'd hate to write something that might break in a future release of Scala if the underlying implementation of case classes changes.
Currently I'm working on a Scala server and defining the protocol and all its messages and exceptions using case classes, as they are such a beautiful, concise construct for this. But I then need to translate them into a Java map to send over the messaging layer for any client implementation to use. My current implementation just defines a translation for each case class separately, but it would be nice to find a generalised solution.
This should work:
def getCCParams(cc: AnyRef) =
cc.getClass.getDeclaredFields.foldLeft(Map.empty[String, Any]) { (a, f) =>
f.setAccessible(true)
a + (f.getName -> f.get(cc))
}
Because case classes extend Product one can simply use .productIterator to get field values:
def getCCParams(cc: Product) = cc.getClass.getDeclaredFields.map( _.getName ) // all field names
.zip( cc.productIterator.to ).toMap // zipped with all values
Or alternatively:
def getCCParams(cc: Product) = {
val values = cc.productIterator
cc.getClass.getDeclaredFields.map( _.getName -> values.next ).toMap
}
One advantage of Product is that you don't need to call setAccessible on the field to read its value. Another is that productIterator doesn't use reflection.
Note that this example works with simple case classes that don't extend other classes and don't declare fields outside the constructor.
Starting Scala 2.13, case classes (as implementations of Product) are provided with a productElementNames method which returns an iterator over their field's names.
By zipping field names with field values obtained with productIterator we can generically obtain the associated Map:
// case class MyClass(param1: String, param2: String)
// val x = MyClass("hello", "world")
(x.productElementNames zip x.productIterator).toMap
// Map[String,Any] = Map("param1" -> "hello", "param2" -> "world")
If anybody looks for a recursive version, here is the modification of #Andrejs's solution:
def getCCParams(cc: Product): Map[String, Any] = {
val values = cc.productIterator
cc.getClass.getDeclaredFields.map {
_.getName -> (values.next() match {
case p: Product if p.productArity > 0 => getCCParams(p)
case x => x
})
}.toMap
}
It also expands the nested case-classes into maps at any level of nesting.
Here's a simple variation if you don't care about making it a generic function:
case class Person(name:String, age:Int)
def personToMap(person: Person): Map[String, Any] = {
val fieldNames = person.getClass.getDeclaredFields.map(_.getName)
val vals = Person.unapply(person).get.productIterator.toSeq
fieldNames.zip(vals).toMap
}
scala> println(personToMap(Person("Tom", 50)))
res02: scala.collection.immutable.Map[String,Any] = Map(name -> Tom, age -> 50)
If you happen to be using Json4s, you could do the following:
import org.json4s.{Extraction, _}
case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")
Extraction.decompose(x)(DefaultFormats).values.asInstanceOf[Map[String,String]]
Solution with ProductCompletion from interpreter package:
import tools.nsc.interpreter.ProductCompletion
def getCCParams(cc: Product) = {
val pc = new ProductCompletion(cc)
pc.caseNames.zip(pc.caseFields).toMap
}
You could use shapeless.
Let
case class X(a: Boolean, b: String,c:Int)
case class Y(a: String, b: String)
Define a LabelledGeneric representation
import shapeless._
import shapeless.ops.product._
import shapeless.syntax.std.product._
object X {
implicit val lgenX = LabelledGeneric[X]
}
object Y {
implicit val lgenY = LabelledGeneric[Y]
}
Define two typeclasses to provide the toMap methods
object ToMapImplicits {
implicit class ToMapOps[A <: Product](val a: A)
extends AnyVal {
def mkMapAny(implicit toMap: ToMap.Aux[A, Symbol, Any]): Map[String, Any] =
a.toMap[Symbol, Any]
.map { case (k: Symbol, v) => k.name -> v }
}
implicit class ToMapOps2[A <: Product](val a: A)
extends AnyVal {
def mkMapString(implicit toMap: ToMap.Aux[A, Symbol, Any]): Map[String, String] =
a.toMap[Symbol, Any]
.map { case (k: Symbol, v) => k.name -> v.toString }
}
}
Then you can use it like this.
object Run extends App {
import ToMapImplicits._
val x: X = X(true, "bike",26)
val y: Y = Y("first", "second")
val anyMapX: Map[String, Any] = x.mkMapAny
val anyMapY: Map[String, Any] = y.mkMapAny
println("anyMapX = " + anyMapX)
println("anyMapY = " + anyMapY)
val stringMapX: Map[String, String] = x.mkMapString
val stringMapY: Map[String, String] = y.mkMapString
println("anyMapX = " + anyMapX)
println("anyMapY = " + anyMapY)
}
which prints
anyMapX = Map(c -> 26, b -> bike, a -> true)
anyMapY = Map(b -> second, a -> first)
stringMapX = Map(c -> 26, b -> bike, a -> true)
stringMapY = Map(b -> second, a -> first)
For nested case classes, (thus nested maps)
check another answer
I don't know about nice... but this seems to work, at least for this very very basic example. It probably needs some work but might be enough to get you started? Basically it filters out all "known" methods from a case class (or any other class :/ )
object CaseMappingTest {
case class MyCase(a: String, b: Int)
def caseClassToMap(obj: AnyRef) = {
val c = obj.getClass
val predefined = List("$tag", "productArity", "productPrefix", "hashCode",
"toString")
val casemethods = c.getMethods.toList.filter{
n =>
(n.getParameterTypes.size == 0) &&
(n.getDeclaringClass == c) &&
(! predefined.exists(_ == n.getName))
}
val values = casemethods.map(_.invoke(obj, null))
casemethods.map(_.getName).zip(values).foldLeft(Map[String, Any]())(_+_)
}
def main(args: Array[String]) {
println(caseClassToMap(MyCase("foo", 1)))
// prints: Map(a -> foo, b -> 1)
}
}
commons.mapper.Mappers.Mappers.beanToMap(caseClassBean)
Details: https://github.com/hank-whu/common4s
With the use of Java reflection, but no change of access level. Converts Product and case class to Map[String, String]:
def productToMap[T <: Product](obj: T, prefix: String): Map[String, String] = {
val clazz = obj.getClass
val fields = clazz.getDeclaredFields.map(_.getName).toSet
val methods = clazz.getDeclaredMethods.filter(method => fields.contains(method.getName))
methods.foldLeft(Map[String, String]()) { case (acc, method) =>
val value = method.invoke(obj).toString
val key = if (prefix.isEmpty) method.getName else s"${prefix}_${method.getName}"
acc + (key -> value)
}
}
Modern variation with Scala 3 might also be a bit simplified as with the following example that is similar to the answer posted by Walter Chang above.
def getCCParams(cc: AnyRef): Map[String, Any] =
cc.getClass.getDeclaredFields
.tapEach(_.setAccessible(true))
.foldLeft(Map.empty)((a, f) => a + (f.getName -> f.get(cc)))
I have the following 3 case classes:
case class Profile(name: String,
age: Int,
bankInfoData: BankInfoData,
userUpdatedFields: Option[UserUpdatedFields])
case class BankInfoData(accountNumber: Int,
bankAddress: String,
bankNumber: Int,
contactPerson: String,
phoneNumber: Int,
accountType: AccountType)
case class UserUpdatedFields(contactPerson: String,
phoneNumber: Int,
accountType: AccountType)
this is just enums, but i added anyway:
sealed trait AccountType extends EnumEntry
object AccountType extends Enum[AccountType] {
val values: IndexedSeq[AccountType] = findValues
case object Personal extends AccountType
case object Business extends AccountType
}
my task is - i need to write a funcc Profile and compare UserUpdatedFields(all of the fields) with SOME of the fields in BankInfoData...this func is to find which fields where updated.
so I wrote this func:
def findDiff(profile: Profile): Seq[String] = {
var listOfFieldsThatChanged: List[String] = List.empty
if (profile.bankInfoData.contactPerson != profile.userUpdatedFields.get.contactPerson){
listOfFieldsThatChanged = listOfFieldsThatChanged :+ "contactPerson"
}
if (profile.bankInfoData.phoneNumber != profile.userUpdatedFields.get.phoneNumber) {
listOfFieldsThatChanged = listOfFieldsThatChanged :+ "phoneNumber"
}
if (profile.bankInfoData.accountType != profile.userUpdatedFields.get.accountType) {
listOfFieldsThatChanged = listOfFieldsThatChanged :+ "accountType"
}
listOfFieldsThatChanged
}
val profile =
Profile(
"nir",
34,
BankInfoData(1, "somewhere", 2, "john", 123, AccountType.Personal),
Some(UserUpdatedFields("lee", 321, AccountType.Personal))
)
findDiff(profile)
it works, but wanted something cleaner..any suggestions?
Each case class extends Product interface so we could use it to convert case classes into sets of (field, value) elements. Then we can use set operations to find the difference. For example,
def findDiff(profile: Profile): Seq[String] = {
val userUpdatedFields = profile.userUpdatedFields.get
val bankInfoData = profile.bankInfoData
val updatedFieldsMap = userUpdatedFields.productElementNames.zip(userUpdatedFields.productIterator).toMap
val bankInfoDataMap = bankInfoData.productElementNames.zip(bankInfoData.productIterator).toMap
val bankInfoDataSubsetMap = bankInfoDataMap.view.filterKeys(userUpdatedFieldsMap.keys.toList.contains)
(bankInfoDataSubsetMap.toSet diff updatedFieldsMap.toSet).toList.map { case (field, value) => field }
}
Now findDiff(profile) should output List(phoneNumber, contactPerson). Note we are using productElementNames from Scala 2.13 to get the filed names which we then zip with corresponding values
userUpdatedFields.productElementNames.zip(userUpdatedFields.productIterator)
Also we rely on filterKeys and diff.
A simple improvement would be to introduce a trait
trait Fields {
val contactPerson: String
val phoneNumber: Int
val accountType: AccountType
def findDiff(that: Fields): Seq[String] = Seq(
Some(contactPerson).filter(_ != that.contactPerson).map(_ => "contactPerson"),
Some(phoneNumber).filter(_ != that.phoneNumber).map(_ => "phoneNumber"),
Some(accountType).filter(_ != that.accountType).map(_ => "accountType")
).flatten
}
case class BankInfoData(accountNumber: Int,
bankAddress: String,
bankNumber: Int,
contactPerson: String,
phoneNumber: Int,
accountType: String) extends Fields
case class UserUpdatedFields(contactPerson: String,
phoneNumber: Int,
accountType: AccountType) extends Fields
so it was possible to call
BankInfoData(...). findDiff(UserUpdatedFields(...))
If you want to further-improve and avoid naming all the fields multiple times, for example shapeless could be used to do it compile time. Not exactly the same but something like this to get started. Or use reflection to do it runtime like this answer.
That would be a very easy task to achieve if it would be an easy way to convert case class to map. Unfortunately, case classes don't offer that functionality out-of-box yet in Scala 2.12 (as Mario have mentioned it will be easy to achieve in Scala 2.13).
There's a library called shapeless, that offers some generic programming utilities. For example, we could write an extension function toMap using Record and ToMap from shapeless:
object Mappable {
implicit class RichCaseClass[X](val x: X) extends AnyVal {
import shapeless._
import ops.record._
def toMap[L <: HList](
implicit gen: LabelledGeneric.Aux[X, L],
toMap: ToMap[L]
): Map[String, Any] =
toMap(gen.to(x)).map{
case (k: Symbol, v) => k.name -> v
}
}
}
Then we could use it for findDiff:
def findDiff(profile: Profile): Seq[String] = {
import Mappable._
profile match {
case Profile(_, _, bankInfo, Some(userUpdatedFields)) =>
val bankInfoMap = bankInfo.toMap
userUpdatedFields.toMap.toList.flatMap{
case (k, v) if bankInfoMap.get(k).exists(_ != v) => Some(k)
case _ => None
}
case _ => Seq()
}
}
I'd like to put some data into a HashMap and retrieve these as typed values using a function. The function takes the expected type and also a default value in case the value is not stored in the HashMap. Type erasure of the JVM makes this a tricky thing.
Q: How can I retrieve a typed value?
Code and results below.
abstract class Parameters(val name: String) {
val parameters = new HashMap[String, Any]()
def put(key: String, value: Any) = parameters(key) = value
def get(key: String) = parameters.getOrElse(key, None)
def remove(key: String) = parameters.remove(key)
def g0[T: TypeTag](key: String, defaultValue: T) = {
get(key) match {
case x: T => x
case None => defaultValue
case _ => defaultValue
}
}
def g1[T: ClassTag](key: String, defaultValue: T) = {
val compareClass = implicitly[ClassTag[T]].runtimeClass
get(key) match {
case None => defaultValue
case x if compareClass.isInstance(x) => x.asInstanceOf[T]
}
}
}
class P extends Parameters("AParmList") {
put("1", 1)
put("3", "three")
put("4", 4.0)
put("width", 600)
println(g0[Int]("width", -1))
println(g0[Int]("fail", -2))
println(g1[Int]("width", -3))
println(g1[Int]("fail", -4))
}
object TypeMatching {
def main(args: Array[String]) {
new P
}
}
The output is (comments in parenthesis):
600 (as expected)
None (expected -2)
and a match error (java.lang.Integer stored, Int required)
Edit: I've fixed the problem - I was incorrectly calling .map(f => f.typeSignature.asInstanceOf[TypeRef].args.head) on recursiveOpt, which meant that field.name was giving me the wrong field name in my copy method. I've removed the map and everything is working now.
I am writing a macro that will create a map of update methods for a case class, e.g.
case class Inner(innerStr: String)
case class Outer(outerStr: String, inner: Inner, innerOpt: Option[Inner])
should produce an update map for Outer that is something like
val innerMap = Map("innerStr" -> {(json: JsValue) => Try{(inner: Inner) => inner.copy(innerStr = json)}})
val outerMap = Map("outerStr" -> {(json: JsValue) => Try{(outer: Outer) => outer.copy(outerStr = json)}},
"inner.innerStr" -> {(json: JsValue) => Try{(outer: Outer) => innerMap.get("innerStr").get(json).flatMap(update => outer.copy(inner = update(outer.inner)))},
"innerOpt.innerStr" -> {(json: JsValue) => Try{(outer: Outer) => innerMap.get("innerStr").get(json).flatMap(update => outer.copy(inner = outer.inner.map(inner => update(inner))))})
which would then be called like
val oldOuter = Outer("str", Inner("str"), Some(Inner("str")))
val updatedOuter = outerMap.get("inner.innerStr").get(JsString("newStr")).get(oldOuter)
The idea is that given a json kv pair, I can retrieve the appropriate update method from the map using the key and then apply the update using the value, using implicit conversions to convert from the json value to the appropriate type.
My macro is working for the case of a flat case class, e.g. Inner(innerStr: String), and for a nested case class, e.g. Outer(outerStr: String, inner: Inner). However, the case of the nested option case class, Outer(outerStr: String, innerOpt: Option[Inner]), is crashing the compiler. I'm not sure if I'm doing something disastrously wrong, or if there's a bug in the compiler, or third option. This was done using the Scala 2.11.0-M7 REPL
Below is my code - I'm constructing a Map that accepts String input instead of JsValue input so that I don't need to import the play framework into my REPL. The blacklist filters out fields that should not be in the update map (e.g. one of the case classes we're applying this to has fields like "crypted_password" and "salt" that should never be updated via json sent in through a REST route). baseMethods constructs the key -> method tuples for the flat case, recursiveMethods constructs the key-method tuples for the nested case, and recursiveOptMethods constructs the key-value tuples for the nested option case; at the bottom of the macro these are all merged into a flat sequence and a placed in a Map.
I've tested the code in the recursiveOptMethods quasiquotes to ensure that I'm constructing a properly typed sequence of tuples and haven't found an error (also, this code is extremely similar to the recursiveMethods quasiquotes, which are functioning correctly), and I've tested the code that constructs the base, recursive, and recursiveOpt sequences of symbols and they seem to be doing their job.
Any help as to why I'm crashing the compiler would be greatly appreciated.
import scala.language.experimental.macros
def copyTestImpl[T: c.WeakTypeTag](c: scala.reflect.macros.Context)(blacklist: c.Expr[String]*): c.Expr[Map[String, (String) => scala.util.Try[(T) => T]]] = {
import c.universe._
val blacklistList: Seq[String] = blacklist.map(e => c.eval(c.Expr[String](c.resetAllAttrs(e.tree))))
def isCaseClass(tpe: Type): Boolean = tpe.typeSymbol.isClass && tpe.typeSymbol.asClass.isCaseClass
def isCaseClassOpt(tpe: Type): Boolean = tpe.typeSymbol.name.decoded == "Option" && isCaseClass(tpe.asInstanceOf[TypeRef].args.head)
def rec(tpe: Type): c.Expr[Map[String, (String) => scala.util.Try[(T) => T]]] = {
val typeName = tpe.typeSymbol.name.decoded
val fields = tpe.declarations.collectFirst {
case m: MethodSymbol if m.isPrimaryConstructor => m
}.get.paramss.head.filterNot(field => blacklistList.contains(typeName + "." + field.name.decoded))
val recursive = fields.filter(f => isCaseClass(f.typeSignature))
val recursiveOpt = fields.filter(f => isCaseClassOpt(f.typeSignature))
val base = fields.filterNot(f => isCaseClass(f.typeSignature) || isCaseClassOpt(f.typeSignature))
val recursiveMethods = recursive.map {
field => {
val fieldName = field.name
val fieldNameDecoded = fieldName.decoded
val map = rec(field.typeSignature)
q"""{
val innerMap = $map
innerMap.toSeq.map(tuple => ($fieldNameDecoded + "." + tuple._1) -> {
(str: String) => {
val innerUpdate = tuple._2(str)
innerUpdate.map(innerUpdate => (outer: $tpe) => outer.copy($fieldName = innerUpdate(outer.$fieldName)))
}
})}"""
}}
val recursiveOptMethods = recursiveOpt.map {
field => {
val fieldName = field.name
val fieldNameDecoded = fieldName.decoded
val map = rec(field.typeSignature.asInstanceOf[TypeRef].args.head)
q"""{
val innerMap = $map
innerMap.toSeq.map(tuple => ($fieldNameDecoded + "." + tuple._1) -> {
(str: String) => {
val innerUpdate = tuple._2(str)
innerUpdate.map(innerUpdate => (outer: $tpe) => outer.copy($fieldName = (outer.$fieldName).map(inner => innerUpdate(inner))))
}
})}"""
}}
val baseMethods = base.map {
field => {
val fieldName = field.name
val fieldNameDecoded = fieldName.decoded
val fieldType = field.typeSignature
val fieldTypeName = fieldType.toString
q"""{
$fieldNameDecoded -> {
(str: String) => scala.util.Try {
val x: $fieldType = str
(t: $tpe) => t.copy($fieldName = x)
}.recoverWith {
case e: Exception => scala.util.Failure(new IllegalArgumentException("Failed to parse " + str + " as " + $typeName + "." + $fieldNameDecoded + ": " + $fieldTypeName))
}
}}"""
}}
c.Expr[Map[String, (String) => scala.util.Try[(T) => T]]] {
q"""{ Map((List(..$recursiveMethods).flatten ++ List(..$recursiveOptMethods).flatten ++ List(..$baseMethods)):_*) }"""
}
}
rec(weakTypeOf[T])
}
def copyTest[T](blacklist: String*) = macro copyTestImpl[T]
And the top and bottom of my error from the 2.11.0-M7 REPL when calling copyTest[Outer]() (where Outer has an Option[Inner] field)
scala> copyTest[Outer]()
scala.MatchError: AnyRef
with Product
with Serializable {
val innerStr: String
private[this] val innerStr: String
def <init>(innerStr: String): Inner
def copy(innerStr: String): Inner
def copy$default$1: String #scala.annotation.unchecked.uncheckedVariance
override def productPrefix: String
def productArity: Int
def productElement(x$1: Int): Any
override def productIterator: Iterator[Any]
def canEqual(x$1: Any): Boolean
override def hashCode(): Int
override def toString(): String
override def equals(x$1: Any): Boolean
} (of class scala.reflect.internal.Types$ClassInfoType)
at scala.reflect.internal.Variances$class.inType$1(Variances.scala:181)
at scala.reflect.internal.Variances$$anonfun$inArgs$1$1.apply(Variances.scala:176)
at scala.reflect.internal.Variances$$anonfun$inArgs$1$1.apply(Variances.scala:176)
at scala.reflect.internal.util.Collections$class.map2(Collections.scala:55)
at scala.reflect.internal.SymbolTable.map2(SymbolTable.scala:14)
at scala.reflect.internal.Variances$class.inArgs$1(Variances.scala:176)
at scala.reflect.internal.Variances$class.inType$1(Variances.scala:189)
at scala.reflect.internal.Variances$$anonfun$inArgs$1$1.apply(Variances.scala:176)
at scala.reflect.internal.Variances$$anonfun$inArgs$1$1.apply(Variances.scala:176)
at scala.reflect.internal.util.Collections$class.map2(Collections.scala:55)
at scala.reflect.internal.SymbolTable.map2(SymbolTable.scala:14)
at scala.tools.nsc.typechecker.Analyzer$typerFactory$$anon$3.run(Analyzer.scala:93)
at scala.tools.nsc.Global$Run.compileUnitsInternal(Global.scala:1603)
at scala.tools.nsc.Global$Run.compileUnits(Global.scala:1588)
at scala.tools.nsc.Global$Run.compileSources(Global.scala:1583)
at scala.tools.nsc.interpreter.IMain.compileSourcesKeepingRun(IMain.scala:387)
at scala.tools.nsc.interpreter.IMain$ReadEvalPrint.compileAndSaveRun(IMain.scala:816)
at scala.tools.nsc.interpreter.IMain$ReadEvalPrint.compile(IMain.scala:775)
at scala.tools.nsc.interpreter.IMain$Request.compile$lzycompute(IMain.scala:951)
at scala.tools.nsc.interpreter.IMain$Request.compile(IMain.scala:946)
at scala.tools.nsc.interpreter.IMain.compile(IMain.scala:530)
at scala.tools.nsc.interpreter.IMain.interpret(IMain.scala:518)
at scala.tools.nsc.interpreter.IMain.interpret(IMain.scala:516)
at scala.tools.nsc.interpreter.ILoop.reallyInterpret$1(ILoop.scala:748)
at scala.tools.nsc.interpreter.ILoop.interpretStartingWith(ILoop.scala:793)
at scala.tools.nsc.interpreter.ILoop.command(ILoop.scala:660)
at scala.tools.nsc.interpreter.ILoop.processLine(ILoop.scala:427)
at scala.tools.nsc.interpreter.ILoop.loop(ILoop.scala:444)
at scala.tools.nsc.interpreter.ILoop$$anonfun$process$1.apply$mcZ$sp(ILoop.scala:862)
at scala.tools.nsc.interpreter.ILoop$$anonfun$process$1.apply(ILoop.scala:848)
at scala.tools.nsc.interpreter.ILoop$$anonfun$process$1.apply(ILoop.scala:848)
at scala.reflect.internal.util.ScalaClassLoader$.savingContextLoader(ScalaClassLoader.scala:95)
at scala.tools.nsc.interpreter.ILoop.process(ILoop.scala:848)
at scala.tools.nsc.MainGenericRunner.runTarget$1(MainGenericRunner.scala:81)
at scala.tools.nsc.MainGenericRunner.process(MainGenericRunner.scala:94)
at scala.tools.nsc.MainGenericRunner$.main(MainGenericRunner.scala:103)
at scala.tools.nsc.MainGenericRunner.main(MainGenericRunner.scala)
That entry seems to have slain the compiler. Shall I replay
your session? I can re-run each line except the last one.
I found the problem - originally I had val recursiveOpt = fields.filter(f => isCaseClassOpt(f.typeSignature)).map(f => f.typeSignature.asInstanceOf[TypeRef].args.head), which meant that when I called field.name on the recursiveOpt fields I was getting the wrong name back.
Is there a nice way I can convert a Scala case class instance, e.g.
case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")
into a mapping of some kind, e.g.
getCCParams(x) returns "param1" -> "hello", "param2" -> "world"
Which works for any case class, not just predefined ones. I've found you can pull the case class name out by writing a method that interrogates the underlying Product class, e.g.
def getCCName(caseobj: Product) = caseobj.productPrefix
getCCName(x) returns "MyClass"
So I'm looking for a similar solution but for the case class fields. I'd imagine a solution might have to use Java reflection, but I'd hate to write something that might break in a future release of Scala if the underlying implementation of case classes changes.
Currently I'm working on a Scala server and defining the protocol and all its messages and exceptions using case classes, as they are such a beautiful, concise construct for this. But I then need to translate them into a Java map to send over the messaging layer for any client implementation to use. My current implementation just defines a translation for each case class separately, but it would be nice to find a generalised solution.
This should work:
def getCCParams(cc: AnyRef) =
cc.getClass.getDeclaredFields.foldLeft(Map.empty[String, Any]) { (a, f) =>
f.setAccessible(true)
a + (f.getName -> f.get(cc))
}
Because case classes extend Product one can simply use .productIterator to get field values:
def getCCParams(cc: Product) = cc.getClass.getDeclaredFields.map( _.getName ) // all field names
.zip( cc.productIterator.to ).toMap // zipped with all values
Or alternatively:
def getCCParams(cc: Product) = {
val values = cc.productIterator
cc.getClass.getDeclaredFields.map( _.getName -> values.next ).toMap
}
One advantage of Product is that you don't need to call setAccessible on the field to read its value. Another is that productIterator doesn't use reflection.
Note that this example works with simple case classes that don't extend other classes and don't declare fields outside the constructor.
Starting Scala 2.13, case classes (as implementations of Product) are provided with a productElementNames method which returns an iterator over their field's names.
By zipping field names with field values obtained with productIterator we can generically obtain the associated Map:
// case class MyClass(param1: String, param2: String)
// val x = MyClass("hello", "world")
(x.productElementNames zip x.productIterator).toMap
// Map[String,Any] = Map("param1" -> "hello", "param2" -> "world")
If anybody looks for a recursive version, here is the modification of #Andrejs's solution:
def getCCParams(cc: Product): Map[String, Any] = {
val values = cc.productIterator
cc.getClass.getDeclaredFields.map {
_.getName -> (values.next() match {
case p: Product if p.productArity > 0 => getCCParams(p)
case x => x
})
}.toMap
}
It also expands the nested case-classes into maps at any level of nesting.
Here's a simple variation if you don't care about making it a generic function:
case class Person(name:String, age:Int)
def personToMap(person: Person): Map[String, Any] = {
val fieldNames = person.getClass.getDeclaredFields.map(_.getName)
val vals = Person.unapply(person).get.productIterator.toSeq
fieldNames.zip(vals).toMap
}
scala> println(personToMap(Person("Tom", 50)))
res02: scala.collection.immutable.Map[String,Any] = Map(name -> Tom, age -> 50)
If you happen to be using Json4s, you could do the following:
import org.json4s.{Extraction, _}
case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")
Extraction.decompose(x)(DefaultFormats).values.asInstanceOf[Map[String,String]]
Solution with ProductCompletion from interpreter package:
import tools.nsc.interpreter.ProductCompletion
def getCCParams(cc: Product) = {
val pc = new ProductCompletion(cc)
pc.caseNames.zip(pc.caseFields).toMap
}
You could use shapeless.
Let
case class X(a: Boolean, b: String,c:Int)
case class Y(a: String, b: String)
Define a LabelledGeneric representation
import shapeless._
import shapeless.ops.product._
import shapeless.syntax.std.product._
object X {
implicit val lgenX = LabelledGeneric[X]
}
object Y {
implicit val lgenY = LabelledGeneric[Y]
}
Define two typeclasses to provide the toMap methods
object ToMapImplicits {
implicit class ToMapOps[A <: Product](val a: A)
extends AnyVal {
def mkMapAny(implicit toMap: ToMap.Aux[A, Symbol, Any]): Map[String, Any] =
a.toMap[Symbol, Any]
.map { case (k: Symbol, v) => k.name -> v }
}
implicit class ToMapOps2[A <: Product](val a: A)
extends AnyVal {
def mkMapString(implicit toMap: ToMap.Aux[A, Symbol, Any]): Map[String, String] =
a.toMap[Symbol, Any]
.map { case (k: Symbol, v) => k.name -> v.toString }
}
}
Then you can use it like this.
object Run extends App {
import ToMapImplicits._
val x: X = X(true, "bike",26)
val y: Y = Y("first", "second")
val anyMapX: Map[String, Any] = x.mkMapAny
val anyMapY: Map[String, Any] = y.mkMapAny
println("anyMapX = " + anyMapX)
println("anyMapY = " + anyMapY)
val stringMapX: Map[String, String] = x.mkMapString
val stringMapY: Map[String, String] = y.mkMapString
println("anyMapX = " + anyMapX)
println("anyMapY = " + anyMapY)
}
which prints
anyMapX = Map(c -> 26, b -> bike, a -> true)
anyMapY = Map(b -> second, a -> first)
stringMapX = Map(c -> 26, b -> bike, a -> true)
stringMapY = Map(b -> second, a -> first)
For nested case classes, (thus nested maps)
check another answer
I don't know about nice... but this seems to work, at least for this very very basic example. It probably needs some work but might be enough to get you started? Basically it filters out all "known" methods from a case class (or any other class :/ )
object CaseMappingTest {
case class MyCase(a: String, b: Int)
def caseClassToMap(obj: AnyRef) = {
val c = obj.getClass
val predefined = List("$tag", "productArity", "productPrefix", "hashCode",
"toString")
val casemethods = c.getMethods.toList.filter{
n =>
(n.getParameterTypes.size == 0) &&
(n.getDeclaringClass == c) &&
(! predefined.exists(_ == n.getName))
}
val values = casemethods.map(_.invoke(obj, null))
casemethods.map(_.getName).zip(values).foldLeft(Map[String, Any]())(_+_)
}
def main(args: Array[String]) {
println(caseClassToMap(MyCase("foo", 1)))
// prints: Map(a -> foo, b -> 1)
}
}
commons.mapper.Mappers.Mappers.beanToMap(caseClassBean)
Details: https://github.com/hank-whu/common4s
With the use of Java reflection, but no change of access level. Converts Product and case class to Map[String, String]:
def productToMap[T <: Product](obj: T, prefix: String): Map[String, String] = {
val clazz = obj.getClass
val fields = clazz.getDeclaredFields.map(_.getName).toSet
val methods = clazz.getDeclaredMethods.filter(method => fields.contains(method.getName))
methods.foldLeft(Map[String, String]()) { case (acc, method) =>
val value = method.invoke(obj).toString
val key = if (prefix.isEmpty) method.getName else s"${prefix}_${method.getName}"
acc + (key -> value)
}
}
Modern variation with Scala 3 might also be a bit simplified as with the following example that is similar to the answer posted by Walter Chang above.
def getCCParams(cc: AnyRef): Map[String, Any] =
cc.getClass.getDeclaredFields
.tapEach(_.setAccessible(true))
.foldLeft(Map.empty)((a, f) => a + (f.getName -> f.get(cc)))