0

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.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Shivam Sahil
  • 4,055
  • 3
  • 31
  • 62
  • 3
    Using a `case class` is the wrong tool for this job. A `case class` is a value type that's effectively a product of some other types. It compares for value equality, has a nice `toString`, and hashes by value. Your proposed type is *not* a product of some other types. It's a mapping from keys to values. And we call a mapping from keys to values a *hashmap*. – Silvio Mayolo Nov 13 '22 at 04:03
  • Its not like just a hashmap, there are some attributes where its key to values (values being case class) however its a mixture of multiple things like values could be case class,strings, int and booleans as well. – Shivam Sahil Nov 13 '22 at 04:21
  • Check out my edit to understand the context better, hashmap would suit to me only if it was about key value pairs however case class is needed because there's a variety of stuff other than just key value pair that I need – Shivam Sahil Nov 13 '22 at 04:23
  • @ShivamSahil What is `[key:String]: Student`? This is not Scala syntax. How is `Example` defined in Scala exactly? – Dmytro Mitin Nov 13 '22 at 04:25
  • @DmytroMitin yes correct its not scala syntax but I added that to convey my message how exactly I want it to be defined. – Shivam Sahil Nov 13 '22 at 04:29
  • @ShivamSahil Again, `[key:String]: HashMap(string,FiltersCaseClass)` is not valid Scala. Write valid definition of `Example` in Scala. – Dmytro Mitin Nov 13 '22 at 04:31
  • 1
    @DmytroMitin I want to know the valid syntax of the same only. If I know the valid syntax then my problem would be solved. My question is how possibly can I construct the case class `Example` which has some boolean, some String and then flexible key value type attribute that helps to add as many keys as required with the value as another case class (`FilterCaseClass` here). And if its not possible using Case class what's the best possible alternative I can have? – Shivam Sahil Nov 13 '22 at 04:34

1 Answers1

2

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

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Hi Dmytro, I am sorry I think I gave you a half context, see my edit, its not only about key value pairs but also about other variables that exist within the same case class like booleans and strings as well – Shivam Sahil Nov 13 '22 at 04:25
  • 1
    Thanks a lot! I see... okay the output you shared instead of `{"m":{"student1":{"name":"name","age":12},"student2":{"name":"name2","age":13}},"_logOp":[true],"queryName":"abc"}` I want it to be: `{"student1":{"name":"name","age":12},"student2":{"name":"name2","age":13},"_logOp":[true],"queryName":"abc"}` Is that possible? @DmytroMitin – Shivam Sahil Nov 13 '22 at 04:41
  • 1
    It shouldn't be nested within `m` rather it should be at the outmost case class Example – Shivam Sahil Nov 13 '22 at 04:42
  • @ShivamSahil I added a code with actual case-class definition – Dmytro Mitin Nov 13 '22 at 17:17