Scala: reflection and case classes - scala

The following code succeeds, but is there a better way of doing the same thing? Perhaps something specific to case classes? In the following code, for each field of type String in my simple case class, the code goes through my list of instances of that case class and finds the length of the longest string of that field.
case class CrmContractorRow(
id: Long,
bankCharges: String,
overTime: String,
name$id: Long,
mgmtFee: String,
contractDetails$id: Long,
email: String,
copyOfVisa: String)
object Go {
def main(args: Array[String]) {
val a = CrmContractorRow(1,"1","1",4444,"1",1,"1","1")
val b = CrmContractorRow(22,"22","22",22,"55555",22,"nine long","22")
val c = CrmContractorRow(333,"333","333",333,"333",333,"333","333")
val rows = List(a,b,c)
c.getClass.getDeclaredFields.filter(p => p.getType == classOf[String]).foreach{f =>
f.setAccessible(true)
println(f.getName + ": " + rows.map(row => f.get(row).asInstanceOf[String]).maxBy(_.length))
}
}
}
Result:
bankCharges: 3
overTime: 3
mgmtFee: 5
email: 9
copyOfVisa: 3

If you want to do this kind of thing with Shapeless, I'd strongly suggest defining a custom type class that handles the complicated part and allows you to keep that stuff separate from the rest of your logic.
In this case it sounds like the tricky part of what you're specifically trying to do is getting the mapping from field names to string lengths for all of the String members of a case class. Here's a type class that does that:
import shapeless._, shapeless.labelled.FieldType
trait StringFieldLengths[A] { def apply(a: A): Map[String, Int] }
object StringFieldLengths extends LowPriorityStringFieldLengths {
implicit val hnilInstance: StringFieldLengths[HNil] =
new StringFieldLengths[HNil] {
def apply(a: HNil): Map[String, Int] = Map.empty
}
implicit def caseClassInstance[A, R <: HList](implicit
gen: LabelledGeneric.Aux[A, R],
sfl: StringFieldLengths[R]
): StringFieldLengths[A] = new StringFieldLengths[A] {
def apply(a: A): Map[String, Int] = sfl(gen.to(a))
}
implicit def hconsStringInstance[K <: Symbol, T <: HList](implicit
sfl: StringFieldLengths[T],
key: Witness.Aux[K]
): StringFieldLengths[FieldType[K, String] :: T] =
new StringFieldLengths[FieldType[K, String] :: T] {
def apply(a: FieldType[K, String] :: T): Map[String, Int] =
sfl(a.tail).updated(key.value.name, a.head.length)
}
}
sealed class LowPriorityStringFieldLengths {
implicit def hconsInstance[K, V, T <: HList](implicit
sfl: StringFieldLengths[T]
): StringFieldLengths[FieldType[K, V] :: T] =
new StringFieldLengths[FieldType[K, V] :: T] {
def apply(a: FieldType[K, V] :: T): Map[String, Int] = sfl(a.tail)
}
}
This looks complex, but once you start working with Shapeless a bit you learn to write this kind of thing in your sleep.
Now you can write the logic of your operation in a relatively straightforward way:
def maxStringLengths[A: StringFieldLengths](as: List[A]): Map[String, Int] =
as.map(implicitly[StringFieldLengths[A]].apply).foldLeft(
Map.empty[String, Int]
) {
case (x, y) => x.foldLeft(y) {
case (acc, (k, v)) =>
acc.updated(k, acc.get(k).fold(v)(accV => math.max(accV, v)))
}
}
And then (given rows as defined in the question):
scala> maxStringLengths(rows).foreach(println)
(bankCharges,3)
(overTime,3)
(mgmtFee,5)
(email,9)
(copyOfVisa,3)
This will work for absolutely any case class.
If this is a one-off thing, you might as well use runtime reflection, or you could use the Poly1 approach in Giovanni Caporaletti's answer—it's less generic and it mixes up the different parts of the solution in a way I don't prefer, but it should work just fine. If this is something you're doing a lot of, though, I'd suggest the approach I've given here.

If you want to use shapeless to get the string fields of a case class and avoid reflection you can do something like this:
import shapeless._
import labelled._
trait lowerPriorityfilterStrings extends Poly2 {
implicit def default[A] = at[Vector[(String, String)], A] { case (acc, _) => acc }
}
object filterStrings extends lowerPriorityfilterStrings {
implicit def caseString[K <: Symbol](implicit w: Witness.Aux[K]) = at[Vector[(String, String)], FieldType[K, String]] {
case (acc, x) => acc :+ (w.value.name -> x)
}
}
val gen = LabelledGeneric[CrmContractorRow]
val a = CrmContractorRow(1,"1","1",4444,"1",1,"1","1")
val b = CrmContractorRow(22,"22","22",22,"55555",22,"nine long","22")
val c = CrmContractorRow(333,"333","333",333,"333",333,"333","333")
val rows = List(a,b,c)
val result = rows
// get for each element a Vector of (fieldName -> stringField) pairs for the string fields
.map(r => gen.to(r).foldLeft(Vector[(String, String)]())(filterStrings))
// get the maximum for each "column"
.reduceLeft((best, row) => best.zip(row).map {
case (kv1#(_, v1), (_, v2)) if v1.length > v2.length => kv1
case (_, kv2) => kv2
})
result foreach { case (k, v) => println(s"$k: $v") }

You probably want to use Scala reflection:
import scala.reflect.runtime.universe._
val rm = runtimeMirror(getClass.getClassLoader)
val instanceMirrors = rows map rm.reflect
typeOf[CrmContractorRow].members collect {
  case m: MethodSymbol if m.isCaseAccessor && m.returnType =:= typeOf[String] =>
    val maxValue = instanceMirrors map (_.reflectField(m).get.asInstanceOf[String]) maxBy (_.length)
    println(s"${m.name}: $maxValue")
}
So that you can avoid issues with cases like:
case class CrmContractorRow(id: Long, bankCharges: String, overTime: String, name$id: Long, mgmtFee: String, contractDetails$id: Long, email: String, copyOfVisa: String) {
val unwantedVal = "jdjd"
}
Cheers

I have refactored your code to something more reuseable:
import scala.reflect.ClassTag
case class CrmContractorRow(
id: Long,
bankCharges: String,
overTime: String,
name$id: Long,
mgmtFee: String,
contractDetails$id: Long,
email: String,
copyOfVisa: String)
object Go{
def main(args: Array[String]) {
val a = CrmContractorRow(1,"1","1",4444,"1",1,"1","1")
val b = CrmContractorRow(22,"22","22",22,"55555",22,"nine long","22")
val c = CrmContractorRow(333,"333","333",333,"333",333,"333","333")
val rows = List(a,b,c)
val initEmptyColumns = List.fill(a.productArity)(List())
def aggregateColumns[Tin:ClassTag,Tagg](rows: Iterable[Product], aggregate: Iterable[Tin] => Tagg) = {
val columnsWithMatchingType = (0 until rows.head.productArity).filter {
index => rows.head.productElement(index) match {case t: Tin => true; case _ => false}
}
def columnIterable(col: Int) = rows.map(_.productElement(col)).asInstanceOf[Iterable[Tin]]
columnsWithMatchingType.map(index => (index,aggregate(columnIterable(index))))
}
def extractCaseClassFieldNames[T: scala.reflect.ClassTag] = {
scala.reflect.classTag[T].runtimeClass.getDeclaredFields.filter(!_.isSynthetic).map(_.getName)
}
val agg = aggregateColumns[String,String] (rows,_.maxBy(_.length))
val fieldNames = extractCaseClassFieldNames[CrmContractorRow]
agg.map{case (index,value) => fieldNames(index) + ": "+ value}.foreach(println)
}
}
Using shapeless would get rid of the .asInstanceOf, but the essence would be the same. The main problem with the given code was that it was not re-usable since the aggregation logic was mixed with the reflection logic to get the field names.

Related

Scala with cats exercise Data validation on Kleisli, why my code fails fast instead of accumulating errors?

I'm reading the scala-with-cats book and follow it's exercise. When I come to the case study: data validation, I encounter some problems.
Here is my entire code (just the same with the book):
package org.scala.ch10.final_recap
import cats.Semigroup
import cats.data.Validated
import cats.data.Validated._
import cats.data.Kleisli
import cats.data.NonEmptyList
import cats.instances.either._
import cats.syntax.apply._
import cats.syntax.semigroup._
import cats.syntax.validated._
sealed trait Predicate[E, A] {
import Predicate._
def and(that: Predicate[E, A]): Predicate[E, A] =
And(this, that)
def or(that: Predicate[E, A]): Predicate[E, A] =
Or(this, that)
/**
* This part is for Kleislis
* #return
*/
def run(implicit s: Semigroup[E]): A => Either[E, A] =
(a: A) => this(a).toEither
def apply(a: A)(implicit s: Semigroup[E]): Validated[E, A] =
this match {
case Pure(func) =>
func(a)
case And(left, right) => (left(a), right(a)).mapN((_, _) => a)
case Or(left, right) =>
left(a) match {
case Valid(_) => Valid(a)
case Invalid(e1) =>
right(a) match {
case Valid(_) => Invalid(e1)
case Invalid(e2) => Invalid(e1 |+| e2)
}
}
}
}
object Predicate {
final case class And[E, A](left: Predicate[E, A], right: Predicate[E, A]) extends Predicate[E, A]
final case class Or[E, A](left: Predicate[E, A], right: Predicate[E, A]) extends Predicate[E, A]
final case class Pure[E, A](func: A => Validated[E, A]) extends Predicate[E, A]
def apply[E, A](f: A => Validated[E, A]): Predicate[E, A] = Pure(f)
def lift[E, A](err: E, fn: A => Boolean): Predicate[E, A] = Pure(a => if(fn(a)) a.valid else err.invalid)
}
object FinalRecapPredicate {
type Errors = NonEmptyList[String]
def error(s: String): NonEmptyList[String] = NonEmptyList(s, Nil)
type Result[A] = Either[Errors, A]
type Check[A, B] = Kleisli[Result, A, B]
def check[A, B](func: A => Result[B]): Check[A, B] = Kleisli(func)
def checkPred[A](pred: Predicate[Errors, A]): Check[A, A] =
Kleisli[Result, A, A](pred.run)
def longerThan(n: Int): Predicate[Errors, String] =
Predicate.lift(
error(s"Must be longer than $n characters"),
str => str.length > n
)
val alphanumeric: Predicate[Errors, String] =
Predicate.lift(
error(s"Must be all alphanumeric characters"),
str => str.forall(_.isLetterOrDigit)
)
def contains(char: Char): Predicate[Errors, String] =
Predicate.lift(
error(s"Must contain the character $char"),
str => str.contains(char)
)
def containsOnce(char: Char): Predicate[Errors, String] =
Predicate.lift(
error(s"Must contain the character $char only once"),
str => str.count(_ == char) == 1
)
val checkUsername: Check[String, String] = checkPred(longerThan(3) and alphanumeric)
val splitEmail: Check[String, (String, String)] = check(_.split('#') match {
case Array(name, domain) =>
Right((name, domain))
case _ =>
Left(error("Must contain a single # character"))
})
val checkLeft: Check[String, String] = checkPred(longerThan(0))
val checkRight: Check[String, String] = checkPred(longerThan(3) and contains('.'))
val joinEmail: Check[(String, String), String] =
check {
case (l, r) => (checkLeft(l), checkRight(r)).mapN(_ + "#" + _)
}
val checkEmail: Check[String, String] = splitEmail andThen joinEmail
final case class User(username: String, email: String)
def createUser(username: String, email: String): Either[Errors, User] =
(checkUsername.run(username),
checkEmail.run(email)).mapN(User)
def main(args: Array[String]): Unit = {
println(createUser("", "noel#underscore.io#io"))
}
}
It supposes the code should end up with the error message Left(NonEmptyList(Must be longer than 3 characters), Must contain a single # character) But what I actually is Left(NonEmptyList(Must be longer than 3 characters))
Obviously, it does not work as expected. It fails fast instead of accumulating errors... How to fix that plz? (I've spent hours now and can't get a workaround)
This is the "problematic" part:
def createUser(username: String, email: String): Either[Errors, User] =
(checkUsername.run(username),
checkEmail.run(email)).mapN(User)
You are combining a tuple of Results, where
type Result[A] = Either[Errors, A]
This means you are really doing a mapN on a pair of Eithers, an operation provided by the Semigroupal type class. This operation will not accumulate results.
There are several reasons for this, but one that I find particularly important is the preserving of behaviour if we find ourselves using a Semigroupal / Applicative which also happens to be a Monad. Why is that a problem? Because Monads are sequencing operations, making each step depend on the previous one, and having "fail early" semantics. When using some Monad, one might expect those semantics to be preserved when using constructs from the underlying Applicative (every Monad is also an Applicative). In that case, if the implementation of Applicative used "accumulation" semantics instead of "fail early" semantics, we would ruin some important laws like referential transparency.
You can use a parallel version of mapN, called parMapN, whose contract guarantees that the implementation will be evaluating all results in parallel. This means that it definitely cannot be expected to have the "fail early" semantics, and accumulating results is fine in that case.
Note that Validated accumulates results as well, usually in a NonEmptyList or a NonEmptyChain. This is probably why you expected to see your accumulated results; the only problem is, you were not using Validated values in the "problematic" part of your code, but raw Eithers instead.
Here's some simple code that demonstrates the above concepts:
import cats.data._
import cats.implicits._
val l1: Either[String, Int] = Left("foo")
val l2: Either[String, Int] = Left("bar")
(l1, l2).mapN(_ + _)
// Left(foo)
(l1, l2).parMapN(_ + _)
// Left(foobar)
val v1: ValidatedNel[String, Int] = l1.toValidatedNel
val v2: ValidatedNel[String, Int] = l2.toValidatedNel
(v1, v2).mapN(_ + _)
// Invalid(NonEmptyList(foo, bar))

Ordering of implicit method and mapping with shapeless

The ordering of implicit seems to matter when using shapeless.
Look at the example code below where it will not work.
import shapeless._
case class Userz(i: Int, j: String, k: Option[Boolean])
object r {
def func(): Userz = {
val a = Userz(100, "UserA", Some(false))
val b = Userz(400, "UserB", None)
val genA = Generic[Userz].to(a)
val genB = Generic[Userz].to(b)
val genC = genA zip genB
val genD = genC.map(Mergerz)
val User = Generic[Userz].from(genD)
return User
}
}
object Mergerz extends Poly1 {
implicit def caseInt = at[(Int, Int)] {a => a._1 + a._2}
implicit def caseString = at[(String, String)] {a => a._1 + a._2}
implicit def caseBoolean = at[(Option[Boolean], Option[Boolean])] {a => Some(a._1.getOrElse(a._2.getOrElse(false)))}
}
r.func()
And the code below which will work
import shapeless._
case class Userz(i: Int, j: String, k: Option[Boolean])
object Mergerz extends Poly1 {
implicit def caseInt = at[(Int, Int)] {a => a._1 + a._2}
implicit def caseString = at[(String, String)] {a => a._1 + a._2}
implicit def caseBoolean = at[(Option[Boolean], Option[Boolean])] {a => Some(a._1.getOrElse(a._2.getOrElse(false)))}
}
object r {
def func(): Userz = {
val a = Userz(100, "UserA", Some(false))
val b = Userz(400, "UserB", None)
val genA = Generic[Userz].to(a)
val genB = Generic[Userz].to(b)
val genC = genA zip genB
val genD = genC.map(Mergerz)
val User = Generic[Userz].from(genD)
return User
}
}
r.func()
My question is, why does the order matters? I already tried importing the implicits which doesn't work.
Because type inference within a file basically works top-to-bottom. When it's typechecking func, it hasn't gotten to caseInt etc and doesn't know they are suitable. Annotating their types should work as well, and is generally recommended for implicits, but it may be problematic when using a library like Shapeless: see Shapeless not finding implicits in test, but can in REPL for an example.

Constructing simple Scala case classes from Strings, strictly without boiler-plate

I seek succinct code to initialize simple Scala case classes from Strings (e.g. a csv line):
case class Person(name: String, age: Double)
case class Book(title: String, author: String, year: Int)
case class Country(name: String, population: Int, area: Double)
val amy = Creator.create[Person]("Amy,54.2")
val fred = Creator.create[Person]("Fred,23")
val hamlet = Creator.create[Book]("Hamlet,Shakespeare,1600")
val finland = Creator.create[Country]("Finland,4500000,338424")
What's the simplest Creator object to do this? I would learn a lot about Scala from seeing a good solution to this.
(Note that companion objects Person, Book and Country should not to be forced to exist. That would be boiler-plate!)
I'm going to give a solution that's about as simple as you can get given some reasonable constraints about type safety (no runtime exceptions, no runtime reflection, etc.), using Shapeless for generic derivation:
import scala.util.Try
import shapeless._
trait Creator[A] { def apply(s: String): Option[A] }
object Creator {
def create[A](s: String)(implicit c: Creator[A]): Option[A] = c(s)
def instance[A](parse: String => Option[A]): Creator[A] = new Creator[A] {
def apply(s: String): Option[A] = parse(s)
}
implicit val stringCreate: Creator[String] = instance(Some(_))
implicit val intCreate: Creator[Int] = instance(s => Try(s.toInt).toOption)
implicit val doubleCreate: Creator[Double] =
instance(s => Try(s.toDouble).toOption)
implicit val hnilCreator: Creator[HNil] =
instance(s => if (s.isEmpty) Some(HNil) else None)
private[this] val NextCell = "^([^,]+)(?:,(.+))?$".r
implicit def hconsCreate[H: Creator, T <: HList: Creator]: Creator[H :: T] =
instance {
case NextCell(cell, rest) => for {
h <- create[H](cell)
t <- create[T](Option(rest).getOrElse(""))
} yield h :: t
case _ => None
}
implicit def caseClassCreate[C, R <: HList](implicit
gen: Generic.Aux[C, R],
rc: Creator[R]
): Creator[C] = instance(s => rc(s).map(gen.from))
}
This work exactly as specified (although note that the values are wrapped in Option to represent the fact that the parsing operation can fail):
scala> case class Person(name: String, age: Double)
defined class Person
scala> case class Book(title: String, author: String, year: Int)
defined class Book
scala> case class Country(name: String, population: Int, area: Double)
defined class Country
scala> val amy = Creator.create[Person]("Amy,54.2")
amy: Option[Person] = Some(Person(Amy,54.2))
scala> val fred = Creator.create[Person]("Fred,23")
fred: Option[Person] = Some(Person(Fred,23.0))
scala> val hamlet = Creator.create[Book]("Hamlet,Shakespeare,1600")
hamlet: Option[Book] = Some(Book(Hamlet,Shakespeare,1600))
scala> val finland = Creator.create[Country]("Finland,4500000,338424")
finland: Option[Country] = Some(Country(Finland,4500000,338424.0))
Creator here is a type class that provides evidence that we can parse a string into a given type. We have to provide explicit instances for basic types like String, Int, etc., but we can use Shapeless to generically derive instances for case classes (assuming that we have Creator instances for all of their member types).
object Creator {
def create[T: ClassTag](params: String): T = {
val ctor = implicitly[ClassTag[T]].runtimeClass.getConstructors.head
val types = ctor.getParameterTypes
val paramsArray = params.split(",").map(_.trim)
val paramsWithTypes = paramsArray zip types
val parameters = paramsWithTypes.map {
case (param, clas) =>
clas.getName match {
case "int" => param.toInt.asInstanceOf[Object] // needed only for AnyVal types
case "double" => param.toDouble.asInstanceOf[Object] // needed only for AnyVal types
case _ =>
val paramConstructor = clas.getConstructor(param.getClass)
paramConstructor.newInstance(param).asInstanceOf[Object]
}
}
val r = ctor.newInstance(parameters: _*)
r.asInstanceOf[T]
}
}

Replacing options in Scala with default values if there is nothing there

Suppose I have an item of the type (Option[Long], Option[Long], Option[Long], Option[Long], Option[Long]), and I want to convert it to an item of type (Long, Long, Long, Long, Long). I want each coordinate to contain the value of the option (if the option contains a "Some" value), or be zero otherwise.
Usually if I have an item of type Option[Long], I'd do something like
item match {
case Some(n) => n
case None => 0
}
But I can't do that with a 5 coordinate item unless I want to list out all 32 possibilities. What can I do instead?
Simple solution:
item match {
case (a, b, c, d, e) => (a.getOrElse(0), b.getOrElse(0), c.getOrElse(0), d.getOrElse(0), e.getOrElse(0))
}
Obviously this isn't very generic. For that you'll probably want to look at Shapeless but I'll leave that answer to the resident experts. ;)
Using Shapeless you could do:
import shapeless._
import syntax.std.tuple._
import poly._
object defaultValue extends Poly1 {
implicit def defaultOptionLong = at[Option[Long]](_.getOrElse(0L))
}
val tuple : (Option[Long], Option[Long], Option[Long], Option[Long], Option[Long]) =
(Some(1L), None, Some(3L), Some(4L), None)
tuple.map(defaultValue)
// (Long, Long, Long, Long, Long) = (1,0,3,4,0)
You need to explicitly specify type Option[Int] if you don't use Option.apply (see this question).
(Option(1L), Option(2L)).map(defaultValue)
// (Long, Long) = (1,2)
(Some(3L), Some(4L)).map(defaulValue) // does not compile
val t : (Option[Long], Option[Long]) = (Some(3L), Some(4L))
t.map(defaultValue)
// (Long, Long) = (3,4)
(Option(5), None).map(defaultValue) // does not compile
val t2 (Option[Long], Option[Long]) = (Option(5), None)
t2.map(defaultValue)
// (Long, Long) = (5,0)
We could also provide default values for other types:
object defaultValue extends Poly1 {
implicit def caseLong = at[Option[Long]](_.getOrElse(0L))
implicit def caseInt = at[Option[Int]](_.getOrElse(0))
implicit def caseString = at[Option[String]](_.getOrElse("scala"))
}
val tuple2 : (Option[Int], Option[Long], Option[String]) = (None, None, None)
tuple2.map(defaultValue)
// (Int, Long, String) = (0,0,scala)
Edit: The problem with the need of explicit declaration of Some(5L) as Option[Long] can be solved using generics in the poly function :
objec defaultValue extends Poly1 {
implicit def caseLong[L <: Option[Long]] = at[L](_.getOrElse(0L))
implicit def caseInt[I <: Option[Int]] = at[I](_.getOrElse(0))
implicit def caseString[S <: Option[String]] = at[S](_.getOrElse("scala"))
}
(Some("A"), Some(1), None: Option[Int], None: Option[String]).map(defaultValue)
// (String, Int, Int, String) = (A,1,0,scala)
You can simply do:
val res = for {
a <- item._1.orElse(0L)
b <- item._2.orElse(0L)
c <- item._3.orElse(0L)
d <- item._4.orElse(0L)
e <- item._5.orElse(0L)
} yield (a, b, c, d, e)
Not the nicest but easy to implement and understand.
Another possible solution:
item.productIterator.collect{
case Some(a: Int) => a
case _ => 0
}.toList match {
case List(a,b,c,d,e) => (a,b,c,d,e)
case _ => (0,0,0,0,0) //or throw exception depending on your logic
}

Convert a List[String] to a case class using Shapeless

I was wondering if anyone could provide some insight on a problem I'm having. I've made a gist with some code and explanation of my problem: https://gist.github.com/tbrown1979/9993f07c8f4fa2786c83
Basically I'm trying to make something that will allow me to convert List[String] to a case class. I've made a Reader that will allow me to do so, but I've run into the issue where a Reader defined for a case class can't contain a reader for a separate case class.
Looking at the 'non-working example' below - I encounter an issue where, when reading, I don't know how many items to pull out of the list. With Bar, which holds a Test, I would need to pull 2 elements out (because Test has two parameters). Is there a way for me to know the amount of fields a case class has just from its type? Is there a better way to do this?
Here is an example of how to use the Reader. I've included a non-working example as well.
////Working Example////
case class Foo(a: Int, s: String)
object Foo {
implicit val FooReader : Reader[Foo] =
Reader[Int :: String :: HNil].map(Generic[Foo].from _)
}
val read: ValidationNel[String, Foo] = Reader.read[Foo](List("12","text"))
println(read)//Success(Foo(12, "text"))
///////////////////////////
////Non-working Example////
case class Test(a: Int, b: String)
object Test {
implicit val TestReader: Reader[Test] =
Reader[Int :: String :: HNil].map(Generic[Test].from _)
}
case class Bar(c: Test)
object Bar {
implicit val BarReader: Reader[Bar] =
Reader[Test :: HNil].map(Generic[Bar].from _)
}
val barRead = Reader.read[Bar](List("21", "someString"))
println(barRead) //Failure(NonEmptyList("Invalid String: List()", "Exepected empty, but contained value"))
//////////////////////////
Something like this works for me (modification of this)
object ShapelessStringToTypeConverters {
import cats._, implicits._, data.ValidatedNel
import mouse._, string._, option._
import shapeless._, labelled._
private type Result[A] = ValidatedNel[ParseFailure, A]
case class ParseFailure(error: String)
trait Convert[V] {
def parse(input: String): Result[V]
}
object Convert {
def to[V](input: String)(implicit C: Convert[V]): Result[V] =
C.parse(input)
def instance[V](body: String => Result[V]): Convert[V] = new Convert[V] {
def parse(input: String): Result[V] = body(input)
}
implicit def booleans: Convert[Boolean] =
Convert.instance(
s =>
s.parseBooleanValidated
.leftMap(e => ParseFailure(s"Not a Boolean ${e.getMessage}"))
.toValidatedNel)
implicit def ints: Convert[Int] =
Convert.instance(
s =>
s.parseIntValidated
.leftMap(e => ParseFailure(s"Not an Int ${e.getMessage}"))
.toValidatedNel)
implicit def longs: Convert[Long] =
Convert.instance(
s =>
s.parseLongValidated
.leftMap(e => ParseFailure(s"Not an Long ${e.getMessage}"))
.toValidatedNel)
implicit def doubles: Convert[Double] =
Convert.instance(
s =>
s.parseDoubleValidated
.leftMap(e => ParseFailure(s"Not an Double ${e.getMessage}"))
.toValidatedNel)
implicit def strings: Convert[String] = Convert.instance(s => s.validNel)
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
sealed trait SchemaMap[A] {
def readFrom(input: Map[String, String]): ValidatedNel[ParseFailure, A]
}
object SchemaMap {
def of[A](implicit s: SchemaMap[A]): SchemaMap[A] = s
private def instance[A](body: Map[String, String] => Result[A]): SchemaMap[A] = new SchemaMap[A] {
def readFrom(input: Map[String, String]): Result[A] =
body(input)
}
implicit val noOp: SchemaMap[HNil] =
SchemaMap.instance(_ => HNil.validNel)
implicit def parsing[K <: Symbol, V: Convert, T <: HList](implicit key: Witness.Aux[K], next: SchemaMap[T]): SchemaMap[FieldType[K, V] :: T] =
SchemaMap.instance { input =>
val fieldName = key.value.name
val parsedField = input
.get(fieldName)
.cata(entry => Convert.to[V](entry), ParseFailure(s"$fieldName is missing").invalidNel)
.map(f => field[K](f))
(parsedField, next.readFrom(input)).mapN(_ :: _)
}
implicit def classes[A, R <: HList](implicit repr: LabelledGeneric.Aux[A, R], schema: SchemaMap[R]): SchemaMap[A] =
SchemaMap.instance { input =>
schema.readFrom(input).map(x => repr.from(x))
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
sealed trait SchemaList[A] {
def readFrom(input: List[String]): ValidatedNel[ParseFailure, A]
}
object SchemaList {
def of[A](implicit s: SchemaList[A]): SchemaList[A] = s
private def instance[A](body: List[String] => Result[A]): SchemaList[A] = new SchemaList[A] {
def readFrom(input: List[String]): Result[A] = body(input)
}
implicit val noOp: SchemaList[HNil] =
SchemaList.instance(_ => HNil.validNel)
implicit def parsing[K <: Symbol, V: Convert, T <: HList](implicit key: Witness.Aux[K], next: SchemaList[T]): SchemaList[FieldType[K, V] :: T] =
SchemaList.instance { input =>
val fieldName = key.value.name
val parsedField = input
.headOption
.cata(entry => Convert.to[V](entry), ParseFailure(s"$fieldName is missing").invalidNel)
.map(f => field[K](f))
(parsedField, next.readFrom(input.tail)).mapN(_ :: _)
}
implicit def classes[A, R <: HList](implicit repr: LabelledGeneric.Aux[A, R], schema: SchemaList[R]): SchemaList[A] =
SchemaList.instance { input =>
schema.readFrom(input).map(x => repr.from(x))
}
}
}
/*
case class Foo(a: String, b: Int, c: Boolean)
def m: Map[String, String] = Map("a" -> "hello", "c" -> "true", "b" -> "100")
def e: Map[String, String] = Map("c" -> "true", "b" -> "a100")
val result = SchemaMap.of[Foo].readFrom(m)
val lst = List("145164983", "0.01862523", "16.11681596", "21:38:57", "bid")
case class Trade0(tid: Long, price: Double, amount: Double, time: String, tpe: String)
val result2 = SchemaList.of[Trade0].readFrom(lst)
*/