0

I want to build a Scala DSL to convert from a existing structure of Java POJOs to a structure equivalent to a Map.

However the incoming objects structure is very likely to contain a lot of null references, which will result in no value in the output map.

The performance is very important in this context so I need to avoid both reflection and throw/catch NPE.

I have considered already this topic which does not meet with my requirements.

I think the answer may lie in the usage of macros to generate some special type but I have no experience in the usage of scala macros.

More formally :

POJO classes provided by project : (there will be like 50 POJO, nested, so I want a solution which does not require to hand-write and maintain a class or trait for each of them)

  case class Level1(
    @BeanProperty var a: String,
    @BeanProperty var b: Int)

  case class Level2(
    @BeanProperty var p: Level1,
    @BeanProperty var b: Int)

expected behaviour :

  println(convert(null)) // == Map()
  println(convert(Level2(null, 3))) // == Map("l2.b" -> 3)
  println(convert(Level2(Level1("a", 2), 3))) // == Map(l2.p.a -> a, l2.p.b -> 2, l2.b -> 3)

correct implementation but I want an easier DSL for writing the mappings

  implicit def toOptionBuilder[T](f: => T) = new {
    def ? : Option[T] = Option(f)
  }

 def convert(l2: Level2): Map[String, _] = l2? match {
    case None => Map()
    case Some(o2) => convert(o2.p, "l2.p.") + ("l2.b" -> o2.b)
  }

  def convert(l1: Level1, prefix: String = ""): Map[String, _] = l1? match {
    case None => Map()
    case Some(o1) => Map(
      prefix + "a" -> o1.a,
      prefix + "b" -> o1.b)
  }

Here is how I want to write with a DSL :

 def convertDsl(l2:Level2)={
      Map(
          "l2.b" -> l2?.b,
          "l2.p.a" -> l2?.l1?.a,
          "l2.p.b" -> l2?.l1?.b
          )
    }

Note that it is perfectly fine for me to specify that the property is optional with '?'. What I want is to generate statically using a macro a method l2.?l1 or l2?.l1 which returns Option[Level1] (so type checking is done correctly in my DSL).

Community
  • 1
  • 1
Michel Daviot
  • 206
  • 1
  • 10
  • I was hinted on a mailing-list to use macros quasiquotes but it seems a bit tricky to implement in scala 2.10 at the moment. http://docs.scala-lang.org/overviews/macros/quasiquotes.html ; https://gist.github.com/anonymous/7ab617d054f28d68901b ; http://docs.scala-lang.org/overviews/macros/paradise.html#macro_paradise_for_210x – Michel Daviot Jul 11 '13 at 08:56

1 Answers1

0

I couldn't refine it down to precisely the syntax you gave above, but generally, something like this might work:

sealed trait FieldSpec

sealed trait ValueFieldSpec[+T] extends FieldSpec
{
  def value: Option[T]
}

case class IntFieldSpec(value: Option[Int]) extends ValueFieldSpec[Int]
case class StringFieldSpec(value: Option[String]) extends ValueFieldSpec[String]

case class Level1FieldSpec(input: Option[Level1]) extends FieldSpec
{
  def a: ValueFieldSpec[_] = StringFieldSpec(input.map(_.a))
  def b: ValueFieldSpec[_] = IntFieldSpec(input.map(_.b))
}

case class Level2FieldSpec(input: Option[Level2]) extends FieldSpec
{
  def b: ValueFieldSpec[_] = IntFieldSpec(input.map(_.b))
  def l1 = Level1FieldSpec(input.map(_.p))
}

case class SpecArrowAssoc(str: String)
{
  def ->(value: ValueFieldSpec[_]) = (str, value)
}

implicit def str2SpecArrowAssoc(str: String) = SpecArrowAssoc(str)

implicit def Level2ToFieldSpec(input: Option[Level2]) = Level2FieldSpec(input)

def map(fields: (String, ValueFieldSpec[_])*): Map[String, _] =
  Map[String, Any]((for {
    field <- fields
    value <- field._2.value
  } yield (field._1, value)):_*)

def convertDsl(implicit l2: Level2): Map[String, _] =
{
  map(
    "l2.b" -> l2.?.b,
    "l2.p.a" -> l2.?.l1.a,
    "l2.p.b" -> l2.?.l1.b
  )
}

Then we get:

scala> val myL2 = Level2(Level1("a", 2), 3)
myL2: Level2 = Level2(Level1(a,2),3)

scala> convertDsl(myL2)
res0: scala.collection.immutable.Map[String,Any] = Map(l2.b -> 3, l2.p.a -> a, l2.p.b -> 2)

Note that the DSL uses '.?' rather than just '?' as the only way I could see around Scala's trouble with semicolon inference and postfix operators (see, eg., @0__ 's answer to scala syntactic suger question).

Also, the strings you can provide are arbitrary (no checking or parsing of them is done), and this simplistic 'FieldSpec' hierarchy will assume that all your POJOs use 'a' for String fields and 'b' for Int fields etc.

With a bit of time and effort I'm sure this could be improved on.

Community
  • 1
  • 1
Shadowlands
  • 14,994
  • 4
  • 45
  • 43
  • Thanks for the answer, but this is not the direction I want to go : it would require a lot of hand-writing and maintenance for all the FooSpec classes (I will have something like 50 POJOs to convert). – Michel Daviot Jul 11 '13 at 08:53
  • Quite possibly @MichelDaviot, and I haven't played around with macros at all myself, but I wonder if it might turn out not to be much less code to achieve the same results via macros than via the above. You could also think about splitting out the separate methods within the Level1 and Level2 (etc) FieldSpec classes into their own traits, so you could just compose the handler for a particular POJO from the traits handling the specific 'value' fields it has. Then introducing a new POJO to the mix shouldn't be too much of a burden. – Shadowlands Jul 12 '13 at 05:57