Fahad Siddiqui
Fahad Siddiqui

Reputation: 1849

case class to shapeless Generic conversion

Problem:

I have a case class Foo which can be anything.

I need a function to create a cypher query from this.

The signature of this new function should be

def createQueryString[T](t: T): String = ???

Example:

I have Foo let's say having two members

case class Foo(x: Int, y: String)

I need it to be converted to cypher

CREATE (f:Foo { x: "1", y: "Hello" }) RETURN f

If I pass Foo(1, "Hello") into the function createQueryString mentioned above

createQueryString[Foo](Foo(1, "Hello"))

What have I tried so far?

I have tried using shapeless's Generic and Aux to achieve this

import shapeless._

case class Foo(x: Int, y: String)

def foo[T, HL <: HList](instance: T)(
  implicit gen: Generic.Aux[T, HL]
): HL = {
  gen.to(instance)
}

val myFoo = foo(Foo(1, "Hello"))
s"""CREATE (f:Foo { x: "${myFoo(0)}", y: "${myFoo(1)}" }) RETURN f"""

Is there any way I can use this foo(Foo(1, "Hello")) to implement inside the createQueryString mentioned above? I would want to basically pass types into this function down the road. Somewhat like

def createQueryString[T](t: T): String = {
  val gen = foo(t) // to get the generic
  s"""CREATE (t: T { x: "${gen(0)}", y: "${gen(1)}" }) RETURN t"""
}

Something like this. But by doing this I get the following error

Error:(77, 22) could not find implicit value for parameter gen: shapeless.Generic.Aux[T,HL]
val gen = foo(t) // to get the generic

Questions:

  1. What am I missing in my last implementation using Aux?
  2. How can I improve this make it generic enough to incorporate as arguments to the cypher query? There can be more than x and y in the case class.

Upvotes: 3

Views: 373

Answers (2)

Adeel Ahmad
Adeel Ahmad

Reputation: 1000

This may not be the answer you are looking for but for your given example, using Reflection:

def createQueryString[T](t: T): String = {

  def formatFields() = {
  t.getClass().getDeclaredFields().filterNot(_.getName.startsWith("(")).map(f => {
    f.setAccessible(true)
    f.getName + ": \"" + f.get(t).toString + "\""
  }).mkString(", ")
}

val className = t.getClass.getSimpleName
val paramName = nameOf(t)
s"""CREATE ($paramName:$className { $formatFields }) RETURN $paramName"""

}

Output:

CREATE (t:Foo { x: "1", y: "Hello" }) RETURN t

"(" fileters out the constructer and only gives vars/vals. This code can be tailored to manage nested classes through recursion.

Depndencies:

For reflection support:

"org.scala-lang" % "scala-reflect" % scalaVersion.value

For conveniently getting param name:

"com.github.dwickern" %% "scala-nameof" % "4.0.0" % "provided"

On a side note though, this could be one of rare good usecases for using reflection since nothing is being mutated or accessed by any hardcoded value.

Upvotes: 0

Dmytro Mitin
Dmytro Mitin

Reputation: 51648

You forgot implicit parameters (or context bounds).

Generic can produce not only an HList but also a Coproduct. That's why if you loose bound <: HList compiler doesn't know how to apply Generic's Repr to 0, 1.

Try

import shapeless.ops.hlist.At
import shapeless.nat._

def foo[T](instance: T)(
  implicit gen: Generic[T]
): gen.Repr = {
  gen.to(instance)
}

def createQueryString[T, L <: HList](t: T)(implicit 
  g: Generic.Aux[T, L],
  at0: At[L, _0], 
  at1: At[L, _1]
): String = {
  val gen = foo(t) // to get the generic
  s"""CREATE (t: T { x: "${gen(0)}", y: "${gen(1)}" }) RETURN t"""
}

If you need labels x, y then you need LabelledGeneric rather than Generic.

If you have arbitrary number of parameters then you can transform an HList to desired form and then fold it to a string.

{ x: "1", y: "Hello" } looks like a JSON. Look at Circe.

Upvotes: 4

Related Questions