I am writing a generic function using shapeless that takes an instance of a case class
and trims all values of fields that are string. The case class can have optional fields, nested objects, lists etc.
I have a case class Person
.
case class Person(name: Option[String], address: List[String], friends: List[Person])
The function that I currently have:
import shapeless._, ops.hlist._
object trimmer extends Poly1 {
implicit val stringOptCase = at[Option[String]](_.map(_.trim))
implicit val stringListCase = at[List[String]](_.map(_.trim))
implicit def skipCase[A] = at[A](identity)
}
def trimStringValues[A, R <: HList](a: A)(implicit
gen: Generic.Aux[A, R],
mapper: Mapper.Aux[trimmer.type, R, R]
) = gen.from(mapper(gen.to(a)))
When I use the above function, it only works for the root level name
field of class Person
. It doesn't work for list or object field.
val person = Person(name = Some(" john "), address = List(" ny"," vegas "), friends = List(Person(Some(" alicia"), List(" peter"), Nil)))
trimStringValues(person) // Person(Some(john),List(ny, vegas),List(Person(Some( alicia),List( peter),List())))
How can I solve this ?
First, it does seem to be working on address
as well as name
in your commented output, which is to be expected. It's not working on friends
because List[Person]
matches the skipCase
case—it's neither an Option[String]
or a List[String]
.
The easiest way to fix this is to use Shapeless's everywhere
combinator. Given your code above, you can write the following:
scala> shapeless.everywhere(trimmer)(person)
res1: Person = Person(Some(john),List(ny, vegas),List(Person(Some(alicia),List(peter),List())))
In fact you can accomplish the same thing with an even simpler trimmer
implementation:
object trimStrings extends Poly1 {
implicit val stringCase: Case.Aux[String, String] = at[String](_.trim)
}
Or equivalently but even more concisely:
import shapeless.poly.->
object trimStrings extends (String -> String)(_.trim)
And then:
scala> shapeless.everywhere(trimStrings)(person)
res5: Person = Person(Some(john),List(ny, vegas),List(Person(Some(alicia),List(peter),List())))
If you wanted more control over exactly which strings are trimmed, you could return to your original implementation and either add an explicit List[Person]
case, or a more generic case that would match types like this and apply trimmer
recursively. Since you say you want to trim all strings, though, everywhere
sounds like it's what you want.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With