Last active
July 19, 2019 15:05
-
-
Save scottweaver/259e6262c31927d3d90f9e39d32a5a76 to your computer and use it in GitHub Desktop.
This is an example of how to extract field names generically using Shapeless's LabelledGeneric.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package vyze.funding.dwr | |
import simulacrum.typeclass | |
import shapeless._ | |
import shapeless.ops.hlist.ToTraversable | |
import shapeless.ops.record._ | |
@typeclass | |
trait HasColumnNames[T] { | |
def columnNames(t: T): List[String] | |
} | |
object HasColumnNames { | |
/** | |
* It took me a bit figure out the proper implicits needed and their associated type arguments. So, let's break it down. | |
* | |
* First the type arguments for the input `def`. | |
* | |
* - `T` The type we want to extract field names from. In most cases this will be a case class. | |
* - `R` This the generic representations, aka `Repr`, of type `T`. If we continue with the case class example, this will be an `HList` | |
* of all the fields extruded from `T`. | |
* - `K` This is an `HList` whose elements will always be a `scala.Symbol`. Again using the case class example, each Symbol in K | |
* represents an actual field name in the case class `T`. | |
* | |
* Now for the Shapeless type classes. | |
* | |
* - `LabelledGeneric.Aux[T, R]` Carries the generic representation of type `T` along with the name of each field in type `T`, assuming | |
* `T` is a case class. To expand on the description of `R` above, each element of the `HList` `R` is more than just the corresponding | |
* field in `T`, it is actually what Shapeless refers to as a record type which is encoded as a `shapeless.labelled.FieldType`. | |
* | |
* A `FieldType` is simply this: | |
* ``` | |
* type FieldType[K, +V] = V with KeyTag[K, V] | |
* ``` | |
* | |
* For case classes, the `V` is simply the value of field whilst the `K` is a Symbol representing the name of the field as defined | |
* on the case class itself. | |
* | |
* - `Keys.Aux[R, K]` Provides evidence that our `LabelledGeneric.Repr`, `R`, provides an `HList` of keys, `K`. Remember, `K` is | |
* simply carrying all of our case class's field names as `Symbols.` | |
* - `ToTraversable.Aux[K, List, Symbol]` This allows us to call `toList` on our `HList`, `K`, and convert it to a `List[Symbol]`. | |
* | |
*/ | |
implicit def GenericHasColumnNames[T, R <: HList, K <: HList]( | |
implicit ev0: LabelledGeneric.Aux[T, R], | |
ev1: Keys.Aux[R, K], | |
ev3: ToTraversable.Aux[K, List, Symbol] | |
): HasColumnNames[T] = { t: T => | |
def camel2Underscore(text: String) = text.drop(1).foldLeft(text.headOption.map(_.toLower + "") getOrElse "") { | |
case (acc, c) if c.isUpper => acc + "_" + c.toLower | |
case (acc, c) => acc + c | |
} | |
val gen = LabelledGeneric[T] | |
val fieldNames = Keys[gen.Repr].apply() | |
fieldNames.toList.map(fn => camel2Underscore(fn.name)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment