Scala 2.13: Case class with extendable variable attributes?

215 views Asked by At

I want to create a case class which can incorporate a record of string and another case class entity.

For example:

case class Student(
name: String
age: Int
)
case class Example(
[key:String]: Student
)

Now I want to use Example to add multiple attributes where attribute could have N number of elements however the type of all those attributes would remain Student. Here's an example:

Example(student1 = Student("name",12),student2=Student("name2",13))

Reason why I am using Case class is that I need to transform this into a JSON using UPickle library and so I wanted to know on the feasibility of achieving the same.

Please note that Example class not just contains [key:String]: Student attribute types but also somethings like:

case class Example(
[key:String]: Student,
_logOp: Option[Boolean] = false,
queryName: String,
...
)

The transformed result for case class:

case class Example(
_logOp: String,
variation: String,
[key:String]: FiltersCaseClass 
/* This line I have added to simplify and make my problem more understandable. Basically the case class would contain some properties like `_logOp` `variation` and then a lot of keys with their values as another case class `FilterCaseClass`
*/
)

should look something like this:

{"_logOp":"AND","variation": "en","ids": {"_logOp": "OR","_expressions": [{"value": "242424"},{"value": "242422"}]}}

where FilterCaseClass is:

case class FilterCaseClass(
_logOp: String,
_expressions: Seq[SingleValueFilter]
)

where SingleValueFilter is another case class containing values

Edit 1:

As per one of the answers by Dymtro:

case class Example(
  m: Map[String, Student],
  _logOp: Option[Boolean] = Some(false),
  queryName: String
)
object Example {
  implicit val rw: ReadWriter[Example] = macroRW
}

write(Example(
  Map("student1" -> Student("name",12), "student2" -> Student("name2",13)),
  Some(true),
  "abc"
))
//{"m":{"student1":{"name":"name","age":12},"student2":{"name":"name2","age":13}},"_logOp":[true],"queryName":"abc"}

The only difference I want here is:

{"student1":{"name":"name","age":12},"student2":{"name":"name2","age":13},"_logOp":[true],"queryName":"abc"}

The difference is that I want case class to be flexible to add key value pairs of Student class.

1

There are 1 answers

4
Dmytro Mitin On BEST ANSWER

You don't need a case class Example, in µPickle you can create a json mixing manual construction and case-class construction

import upickle.default.{macroRW, ReadWriter, write, transform} // "com.lihaoyi" %% "ujson" % "0.9.6"

case class Student(
  name: String,
  age: Int
)

object Student {
  implicit val rw: ReadWriter[Student] = macroRW
}

ujson.Obj(
  "student1" -> write(Student("name",12)), 
  "student2" -> write(Student("name2",13))
)
//{"student1":"{\"name\":\"name\",\"age\":12}","student2":"{\"name\":\"name2\",\"age\":13}"}

ujson.Obj(
  "student1" -> transform(Student("name",12)).to[ujson.Value],
  "student2" -> transform(Student("name2",13)).to[ujson.Value]
)
//{"student1":{"name":"name","age":12},"student2":{"name":"name2","age":13}}

If [key:String]: Student means Map[String, Student] then µPickle seems to support this out-of-the-box

case class Example(
  m: Map[String, Student],
  _logOp: Option[Boolean] = Some(false),
  queryName: String
)
object Example {
  implicit val rw: ReadWriter[Example] = macroRW
}

write(Example(
  Map("student1" -> Student("name",12), "student2" -> Student("name2",13)),
  Some(true),
  "abc"
))
//{"m":{"student1":{"name":"name","age":12},"student2":{"name":"name2","age":13}},"_logOp":[true],"queryName":"abc"}

It shouldn't be nested within m

You can achieve this with a custom codec (pickler)

import upickle.default.{ReadWriter, macroRW, readwriter, transform, write, read}
import scala.collection.mutable

case class Example(
  m: Map[String, Student],
  _logOp: Option[Boolean] = Some(false),
  queryName: String
)
object Example {
  implicit val rw: ReadWriter[Example] = {
    val standardExampleRW = macroRW[Example]
    readwriter[ujson.Value].bimap[Example](
      example => transform[Example](example)(standardExampleRW).to[ujson.Value] match {
        case ujson.Obj(standardMap) =>
          val newMap = mutable.LinkedHashMap.empty[String, ujson.Value]
          standardMap.remove("m")
          newMap.addAll(example.m.map { case (str, stud) => str -> transform[Student](stud).to[ujson.Value]})
            .addAll(standardMap)
          ujson.Obj(newMap)
      },
      // if you don't need a reversed transform i.e. from a json to an Example then you can omit this part
      // _ => ??? 
      {
        case ujson.Obj(newMap) =>
          val logOpJson = newMap.remove("_logOp")
          val logOp = logOpJson.map(transform[ujson.Value](_).to[Option[Boolean]])
          val queryNameJson = newMap.remove("queryName")
          val queryName = queryNameJson.map(transform[ujson.Value](_).to[String]).getOrElse("")
          val m = newMap.map { case (str, json) => str -> transform[ujson.Value](json).to[Student] }.toMap
          logOp.map(Example(m, _, queryName)).getOrElse(Example(m, queryName = queryName))
      }
    )
  }
}

write(Example(
  Map("student1" -> Student("name",12), "student2" -> Student("name2",13)),
  Some(true),
  "abc"
))
//{"student1":{"name":"name","age":12},"student2":{"name":"name2","age":13},"_logOp":[true],"queryName":"abc"}

read[Example](
  """{"student1":{"name":"name","age":12},"student2":{"name":"name2","age":13},"_logOp":[true],"queryName":"abc"}"""
)
//Example(Map(student1 -> Student(name,12), student2 -> Student(name2,13)),Some(true),abc)

So, basically you can generate case classes in Scala but it's not necessary for serialization into json format.


Just for the completeness, since your original question was how to define a case class, here is a code with actual case-class definition. But this code is slow (since it uses runtime reflection and runtime compilation) and is not conventional Scala code (on contrary to the above custom picklers)

case class Example(
  m: Map[String, Student],
  _logOp: Option[Boolean] = Some(false),
  queryName: String
)

import scala.reflect.runtime.{currentMirror => rm} // libraryDependencies += scalaOrganization.value % "scala-reflect" % "2.13.10"
import scala.reflect.runtime.universe.{Quasiquote, TermName, typeOf, termNames}
import scala.tools.reflect.{ToolBox, FrontEnd} // libraryDependencies += scalaOrganization.value % "scala-compiler" % "2.13.10"
val tb = rm.mkToolBox(
//  frontEnd = new FrontEnd {
//    override def display(info: Info): Unit = println(info)
//  },
//  options = "-d out"
)

implicit val rw: ReadWriter[Example] =
  readwriter[ujson.Value].bimap[Example](
    example => {
      val studentFields = example.m.keys.map(str =>
        q"val ${TermName(str)}: ${typeOf[Student]}"
      )
      val students = example.m.values.toSeq
      val fields = studentFields ++ Seq(
        q"val _logOp: Option[Boolean] = Some(false)",
        q"val queryName: String"
      )
      val classSymbol = tb.define(q"case class Example1(..$fields)").asClass
      val constructorSymbol =
        classSymbol.typeSignature.decl(termNames.CONSTRUCTOR).asMethod
      val classInstance = tb.mirror.reflectClass(classSymbol)
        .reflectConstructor(constructorSymbol)
        .apply(students ++ Seq(example._logOp, example.queryName): _*)
      tb.eval(q"""
        import upickle.default._
        implicit val rw: ReadWriter[$classSymbol] = macroRW[$classSymbol]
        transform[$classSymbol](_: $classSymbol).to[ujson.Value]
      """).asInstanceOf[Any => ujson.Value].apply(classInstance)
    },
    json => ???
  )

val x = write(Example(
  Map("student1" -> Student("name",12), "student2" -> Student("name2",13)),
  Some(true),
  "abc"
))
//{"student1":{"name":"name","age":12},"student2":{"name":"name2","age":13},"_logOp":[true],"queryName":"abc"}

q"..." is a string interpolator for quasiquotes (creating abstract syntax trees).