I'm trying to list null fields recursively with Shapeless. But it doesn't seem to show all the fields:
https://scastie.scala-lang.org/PtLdSRC2Qfipu054Hzerrw
import shapeless._
import shapeless.labelled.FieldType
trait NullFields[T] {
def apply(value: T, parent: String): Seq[String]
}
object NullFields extends NullFieldsLowPriority {
implicit lazy val hNil = new NullFields[HNil] {
override def apply(value: HNil, parent: String) = Seq.empty
}
implicit def hConsOption[K <: Symbol, V, TailFields <: HList](implicit
witness: Witness.Aux[K],
tailNullFields: NullFields[TailFields]
) = new NullFields[FieldType[K, Option[V]] :: TailFields] {
override def apply(
params: FieldType[K, Option[V]] :: TailFields,
parent: String
) =
(if (params.head.isEmpty) Seq(parent + witness.value.name)
else Seq.empty) ++
tailNullFields(params.tail, parent)
}
implicit def hConsCaseClass[
K <: Symbol,
CC <: Product,
Fields <: HList,
TailFields <: HList
](implicit
generic: LabelledGeneric.Aux[CC, Fields],
nullFields: NullFields[Fields],
witness: Witness.Aux[K],
tailNullFields: NullFields[TailFields]
) = new NullFields[FieldType[K, CC] :: TailFields] {
override def apply(params: FieldType[K, CC] :: TailFields, parent: String) =
nullFields(generic.to(params.head), s"$parent${witness.value.name}.") ++
tailNullFields(params.tail, parent)
}
implicit def caseClass[CC, Fields <: HList](implicit
generic: LabelledGeneric.Aux[CC, Fields],
nullFields: NullFields[Fields]
) = new NullFields[CC] {
override def apply(value: CC, parent: String) =
nullFields(generic.to(value), parent)
}
}
trait NullFieldsLowPriority {
implicit def hConsDefault[K <: Symbol, V, TailFields <: HList](implicit
tailNullFields: NullFields[TailFields]
) = new NullFields[FieldType[K, V] :: TailFields] {
override def apply(params: FieldType[K, V] :: TailFields, parent: String) =
tailNullFields(params.tail, parent)
}
}
case class B(
balba: Int = 0,
profile_experience_desc: Option[String] = None,
vmaf: Va
)
case class Va(
tw_segment: Option[Double],
i20_segment: Option[Double],
ttq_segment: Option[Int]
)
println(implicitly[NullFields[B]].apply(B(vmaf = Va(None, None, None)), ""))
I am not sure this is exactly what you want, but I hope you can tweak it to make it work for your need. I have considered the following as nulls:
All Nones
Any other types which are null
Recursive nulls or Nones found in case classes and/or Options
You can add further implicits to handle other cases:
import shapeless._
import shapeless.labelled.FieldType
trait NullFields[T] {
def apply(value: T, parent: String): Seq[String]
}
object NullFields extends LowPrioriyImplicits {
def apply[A](implicit nullFields: NullFields[A]): NullFields[A] = nullFields
implicit class NullFieldsOps[A](a: A) {
def nulls(implicit ev: NullFields[A]) = NullFields[A].apply(a, "")
}
implicit def option[V](implicit nullFields: NullFields[V]): NullFields[Option[V]] =
(option: Option[V], parent: String) =>
if (option.isEmpty) Seq(parent)
else nullFields(option.head, parent)
implicit def caseClass[CC, Fields <: HList](implicit
generic: LabelledGeneric.Aux[CC, Fields],
nullFields: NullFields[Fields]
): NullFields[CC] = (value: CC, parent: String) => nullFields(generic.to(value), parent)
implicit def hCons[K <: Symbol, Field, TailFields <: HList](implicit
witness: Witness.Aux[K],
nullFields: Lazy[NullFields[Field]],
tailNullFields: Lazy[NullFields[TailFields]]
): NullFields[FieldType[K, Field] :: TailFields] =
(params: FieldType[K, Field] :: TailFields, parent: String) =>
nullFields.value(params.head, (if (parent.isEmpty) "" else s"$parent.") + witness.value.name) ++
tailNullFields.value(params.tail, parent)
implicit val hNil: NullFields[HNil] = (value: HNil, parent: String) => Seq.empty
}
trait LowPrioriyImplicits {
implicit def other[A]: NullFields[A] =
(value: A, parent: String) => if (value == null) Seq(parent) else Seq.empty
}
Scastie link for the same
Related
Edit:
Last revision was deemed unhelpful as it did not contain necessary information that help narrow down my issue. hence the need to also include the AST.
Below is a library in its entirety that allows parsing and writing of play-json's json based on user defined schema; Similar to what Scala's slick offers for database columns to some extent:
import scala.language.higherKinds
import play.api.libs.functional.syntax._
import play.api.libs.json._
import scala.language.{higherKinds, implicitConversions}
type PathNodes = List[PathNode]
sealed trait Field[A] {
def pathNodes: PathNodes
def jsPath: JsPath = JsPath(pathNodes)
def relativePath: JsPath = JsPath(List(pathNodes.last))
def format: Format[A]
def nestedFormatter(path: JsPath): OFormat[A]
def nestedFormat: OFormat[A] = nestedFormatter(relativePath)
}
case class PlainField[A: Format](prefix: PathNodes) extends Field[A] {
override def pathNodes: PathNodes = prefix
def format: Format[A] = implicitly[Format[A]]
override def nestedFormatter(path: JsPath): OFormat[A] = path.format(format)
}
abstract class JsonSchema[T](val _prefix: PathNodes) extends Field[T] with SchemaExtensionMethods {
override def pathNodes: PathNodes = _prefix
def format: OFormat[T]
protected def plain[A: Format](name: String): PlainField[A] = PlainField[A](_prefix :+ KeyPathNode(name))
protected def nested[N](name: String, factory: PathNodes => N): N = factory(_prefix :+ KeyPathNode(name))
protected def nested[B, G <: JsonSchema[B]](name: String)(implicit sm: HasJsonSchema[B, G]): G = sm.apply(_prefix :+ KeyPathNode(name))
override def nestedFormatter(path: JsPath): OFormat[T] = path.format(format)
}
case class Optional[F, A](field: F)(implicit ev: F <:< Field[A]) extends Field[Option[A]] {
override def pathNodes: PathNodes = field.pathNodes
override def format: Format[Option[A]] = {
implicit val writes: Writes[Option[A]] = JsPath.writeNullable(field.format)
implicit val reads: Reads[Option[A]] = JsPath.readNullable(field.format)
implicitly[Format[Option[A]]]
}
def map[G, B](f: F => G)(implicit ev: G <:< Field[B]): Optional[G, B] = new Optional[G, B](f(field))
def flatMap[G <: Field[B], B](f: F => Optional[G, B]): Optional[G, B] = f(field)
override def nestedFormatter(path: JsPath): OFormat[Option[A]] = path.formatNullable(field.format)
}
case class Collection[F, A](field: F)(implicit ev: F <:< Field[A], repath: Repath[F]) extends Field[Seq[A]] {
override def pathNodes: PathNodes = field.pathNodes
override def format: Format[Seq[A]] = {
implicit val writes: Writes[Seq[A]] = Writes.seq(field.format)
implicit val reads: Reads[Seq[A]] = Reads.seq(field.format)
implicitly[Format[Seq[A]]]
}
def apply(idx: Int): F = implicitly[Repath[F]].apply(field, IdxPathNode(idx))
override def nestedFormatter(path: JsPath): OFormat[Seq[A]] = path.format(format)
}
class FormatExtensionMethods[T](val arg: T) {
def <>[A, B, Fun](apply: Fun, unapply: B => Option[A])(implicit jss: JsonShape[A, B, T, Fun]): OFormat[B] = jss.format(arg, apply, unapply andThen (_.get))
}
class FieldExtensionMethods[F](val field: F) {
def optional[A](implicit ev: F <:< Field[A]): Optional[F, A] = new Optional[F, A](field)
def sequence[A](implicit ev: F <:< Field[A], repath: Repath[F]): Collection[F, A] = new Collection[F, A](field)
}
trait SchemaExtensionMethods {
implicit def formatExtensionMethods[M](t: M): FormatExtensionMethods[M] = new FormatExtensionMethods[M](t)
implicit def fieldExtensionMethods[M, A](t: M): FieldExtensionMethods[M] = new FieldExtensionMethods[M](t)
}
trait Repath[F] {
def apply(f: F, node: PathNode): F
}
object Repath {
implicit def plain[T]: Repath[PlainField[T]] = new Repath[PlainField[T]] {
override def apply(t: PlainField[T], node: PathNode): PlainField[T] =
PlainField[T](t.pathNodes :+ node)(t.format)
}
implicit def schema[S <: JsonSchema[_]](implicit sm: HasJsonSchema[_, S]): Repath[S] = new Repath[S] {
override def apply(t: S, node: PathNode): S =
sm.apply(t.pathNodes :+ node)
}
implicit def option[F <: Field[T] : Repath, T]: Repath[Optional[F, T]] = new Repath[Optional[F, T]] {
override def apply(t: Optional[F, T], node: PathNode): Optional[F, T] =
new Optional[F, T](implicitly[Repath[F]].apply(t.field, node))
}
implicit def sequence[F <: Field[T] : Repath, T]: Repath[Collection[F, T]] = new Repath[Collection[F, T]] {
override def apply(t: Collection[F, T], node: PathNode): Collection[F, T] =
new Collection[F, T](implicitly[Repath[F]].apply(t.field, node))
}
}
trait JsonShape[A, B, -T, Func] {
def format(t: T, apply: Func, unapply: B => A): OFormat[B]
}
object JsonShape {
type F[T] = Field[T]
implicit def cc1[A, B]: JsonShape[A, B, F[A], (A) => B] = (t: F[A], apply: (A) => B, unapply: B => A) => {
val name = t.pathNodes.last.asInstanceOf[KeyPathNode].key
OFormat[B](
Reads[B](jsv => (jsv \ name).validate[A](t.format).map(apply)),
OWrites[B](b => JsObject(Map(name -> Json.toJson(unapply(b))(t.format))))
)
}
implicit def cc2[T1, T2, B]: JsonShape[(T1, T2), B, (F[T1], F[T2]), (T1, T2) => B] = (t: (F[T1], F[T2]), apply: (T1, T2) => B, unapply: B => (T1, T2)) => {
(
t._1.nestedFormat and
t._2.nestedFormat
) (apply, unapply)
}
implicit def cc3[T1, T2, T3, B]: JsonShape[(T1, T2, T3), B, (F[T1], F[T2], F[T3]), (T1, T2, T3) => B] = (t: (F[T1], F[T2], F[T3]), apply: (T1, T2, T3) => B, unapply: B => (T1, T2, T3)) => {
(
t._1.nestedFormat and
t._2.nestedFormat and
t._3.nestedFormat
) (apply, unapply)
}
//this goes up to 22
}
abstract class HasJsonSchema[T, +S <: JsonSchema[T]](val apply: PathNodes => S) extends OFormat[T] {
val root: S = apply(Nil)
def format: OFormat[T] = root.format
def writes(o: T): JsObject = root.format.writes(o)
def reads(json: JsValue): JsResult[T] = root.format.reads(json)
}
Now let's write a small piece of client code that reproduce the issue:
case class MessageSchema(prefix: PathNodes) extends JsonSchema[Message](prefix) {
def underlying = plain[String]("underlying")
//def underlying = plain[String]("underlying").optional if I wanted the field to be Option[String]
//def underlying = plain[String]("underlying").sequence if I wanted the field to be Seq[String]
override def format = underlying <> (Message.apply _, Message.unapply)
}
case class Message(underlying: String)
object Message {
implicit object sm extends HasJsonSchema[Message, MessageSchema](MessageSchema.apply)
}
case class LanguageTaggedSchema[T, S <: JsonSchema[T]](prefix: PathNodes)(implicit evT: HasJsonSchema[T, S]) extends JsonSchema[LanguageTagged[T]](prefix) {
def lang = plain[String]("lang")
def data: S = nested("data")(evT)
def format = (lang, data) <> (LanguageTagged.apply[T] _, LanguageTagged.unapply[T])
}
case class LanguageTagged[T](lang: String, data: T)
object LanguageTagged {
implicit def schemaMapper[T, S <: JsonSchema[T]](implicit ev: HasJsonSchema[T, S]): HasJsonSchema[LanguageTagged[T], LanguageTaggedSchema[T, S]] =
new HasJsonSchema[LanguageTagged[T], LanguageTaggedSchema[T, S]](LanguageTaggedSchema.apply[T, S]) {}
}
def toJson[T, S <: JsonSchema[T]](a: T)(implicit ev: HasJsonSchema[T, S]): JsValue = Json.toJson(a)(ev.format)
toJson(Message("hi")) //Ok!
toJson(LanguageTagged("en", Message("hi"))) //Ok!
//or simply write
Json.toJson(LanguageTagged("en", Message("hi")))
//and if i wanted to traverse a json path i would do:
val schema = implicitly[HasJsonSchema[LanguageTagged[Message],LanguageTaggedSchema[Message,MessageSchema]]].root
schema.data.underlying.jsPath
//prints: res2: play.api.libs.json.JsPath = /data/underlying
//Now to where the problem starts:
def getSchema[T, S <: JsonSchema[T]](a: T)(implicit ev: HasJsonSchema[T, S]): S = ev.root
getSchema(Message("hi")) //Ok!
getSchema(LanguageTagged("en", Message("hi"))) //Not Ok but why?
//Error:(211, 11) could not find implicit value for
//parameter ev: A$A6.this.HasJsonSchema[A$A6.this.LanguageTagged[A$A6.this.Message],S]
//getSchema(LanguageTagged("en", Message("hi")));//
//^
I have a huge suspicion that the compiler runs into issues because of the bounded type of S inHasJsonSchema[T, S <: JsonSchema[T]] when infering the implicit type S. and so far only in that specific situation as shown on the last line of all the code. as a dubugging attempt I created a similar situation and realized that if the type S was not bounded I wouldn't have this issue. Any sort of solution that refactors the code such that it doesn't depend on bounded types or one that simply solves the implicit resolution is appreciated
What You're trying to achieve cannot be done with subtyping. You should use type-classes instead, a more in-depth explanation:
http://danielwestheide.com/blog/2013/02/06/the-neophytes-guide-to-scala-part-12-type-classes.html
I'm trying this and it works:
`
case class Foo(name: String)
class Morphic(map: Map[String, Any]) {
def add(k: String, v: Any) = {
new Morphic((map + (k -> v)))
}
def to[T](): T = {
def toClass[A]: ToCase[A] = new ToCase[A] // This is class to convert from Map to case class
val res = toClass[Foo].from(map).get // <-- problem is here - cannot use T
res.asInstanceOf[T]
}
}
object testApp extends App {
var m = new Morphic(Map[String, Any]())
var m1 = m.add("name", "john")
println(m1.to[Foo])
}
I should use T instead of Foo in val res = toClass[T].from(map).get but it doesn't compile saying implicit is missing
toClass[T].from is a function creating a given type of case class from Map
How do I make that implicit (and possibly others on which .from relies) available?
I tried def to[T, H <: HList]()(implicit gen: LabelledGeneric.Aux[A, H]) = ... but then I need to specify both types when calling the .to and I can't figure out what to specify for H
Thanks
You can transform a Map into HList, and then the HList into T:
import shapeless.{::, HList, HNil, LabelledGeneric, Witness}
import shapeless.labelled._
case class Foo(name: String)
trait MapToHList[L <: HList] {
def apply(map: Map[String, Any]): Option[L]
}
object MapToHList {
implicit object hNilMapToHList extends MapToHList[HNil] {
override def apply(map: Map[String, Any]): Option[HNil] = Some(HNil)
}
implicit def hConsMapToHList[K <: Symbol, V, T <: HList](implicit
mapToHList: MapToHList[T],
witness: Witness.Aux[K]
): MapToHList[FieldType[K, V] :: T] =
new MapToHList[FieldType[K, V] :: T] {
override def apply(map: Map[String, Any]): Option[FieldType[K, V] :: T] = {
val str = witness.value.toString.tail
for {
v <- map.get(str)
t <- mapToHList(map)
} yield field[K](v.asInstanceOf[V]) :: t
}
}
}
trait ToCase[A] {
def from(map: Map[String, Any]): Option[A]
}
object ToCase {
implicit def mkToCase[A, L <: HList](implicit
gen: LabelledGeneric.Aux[A, L],
mapToHList: MapToHList[L]
): ToCase[A] =
new ToCase[A] {
override def from(map: Map[String, Any]): Option[A] = mapToHList(map).map(gen.from)
}
}
class Morphic(map: Map[String, Any]) {
def add(k: String, v: Any) = {
new Morphic((map + (k -> v)))
}
def to[T](implicit toCase: ToCase[T]): T = toCase.from(map).get
}
object testApp extends App {
var m = new Morphic(Map[String, Any]())
var m1 = m.add("name", "john")
println(m1.to[Foo]) // Foo(john)
}
I tried
def to[T, H <: HList]()(implicit gen: LabelledGeneric.Aux[A, H]) ...
but then I need to specify both types when calling the .to
and I can't figure out what to specify for H
You can call it as m1.to[Foo, FieldType[Witness.`'name`.T, String] :: HNil]() or m1.to[Foo, Record.`'name -> String`.T]().
do you think what i did make sense? Is there a better way do encode a case class into Item? e.g. i'm not happy with ignoring in some cases an input param!
import shapeless.labelled.FieldType
import shapeless.{::, DepFn2, HList, HNil, LabelledGeneric, Witness}
import scala.collection.mutable
// mock of sdk item
class Item(val map: mutable.Map[String, Any] = mutable.Map[String, Any]()) {
def getString(attrName: String): String = map.get(attrName).get.asInstanceOf[String]
def getInt(attrName: String): Int = map.get(attrName).get.asInstanceOf[Int]
def getBoolean(attrName: String): Boolean = map.get(attrName).get.asInstanceOf[Boolean]
// def getMap(attrName: String): Map[String, String] = Map("attrName" -> "attrValue")
def setString(attrName: String, value: String): Unit = map.put(attrName, value)
def setInt(attrName: String, value: Int): Unit = map.put(attrName, value)
def setBoolean(attrName: String, value: Boolean): Unit = map.put(attrName, value)
override def toString() = map.toString()
}
trait ItemEncoder[A] extends DepFn2[String, A] {
type Out = Item
}
object ItemEncoder {
def apply[A](implicit encoder: ItemEncoder[A]): ItemEncoder[A] = encoder
def instance[A](f: (String, A) => Item): ItemEncoder[A] =
new ItemEncoder[A] {
override def apply(attrName: String, value: A): Out = f(attrName, value)
}
}
implicit val stringEncoder: ItemEncoder[String] =
ItemEncoder.instance { (attrName, value) =>
val item = new Item()
item.setString(attrName, value)
item
}
implicit val intEncoder: ItemEncoder[Int] =
ItemEncoder.instance { (attrName, value) =>
val item = new Item()
item.setInt(attrName, value)
item
}
implicit val booleanEncoder: ItemEncoder[Boolean] =
ItemEncoder.instance { (attrName, value) =>
val item = new Item()
item.setBoolean(attrName, value)
item
}
implicit val hnilEncoder: ItemEncoder[HNil] =
ItemEncoder.instance((attrName, value) => new Item())
def merge(i1: Item, i2: Item): Item = new Item(i1.map ++ i2.map)
implicit def hlistEncoder[K <: Symbol, L, H, T <: HList](
implicit
witness: Witness.Aux[K],
hEncoder: ItemEncoder[H],
tEncoder: ItemEncoder[T]
): ItemEncoder[FieldType[K, H] :: T] = {
ItemEncoder.instance { (_, value) =>
val attrName = witness.value.name
merge(hEncoder.apply(attrName, value.head), tEncoder.apply(attrName, value.tail))
}
}
implicit def genericEncoder[A, R](
implicit
generic: LabelledGeneric.Aux[A, R],
itemEncoder: ItemEncoder[R]
): ItemEncoder[A] =
ItemEncoder.instance { (attrName, value) =>
itemEncoder.apply(attrName, generic.to(value))
}
case class Person(name: String, age: Int, married: Boolean, manager: Boolean)
case class IceCream(name: String, subName: String, price: Int)
val genericPerson = LabelledGeneric[Person].to(Person("bob", 37, true, true))
def encode[A](toEncode: A)(implicit itemEncoder: ItemEncoder[A]) =
itemEncoder("", toEncode)
Maybe it will be better to use ToMap or something like this, and the convert it to Item
After getting deeper into the topic i managed to implement ItemEncoder that converts a case class with arbitrary nesting into Item like this:
import com.amazonaws.services.dynamodbv2.document.Item
import shapeless.labelled.FieldType
import shapeless.{::, HList, HNil, LabelledGeneric, Witness, _}
trait ItemEncoder[A] {
def encode(value: A): Item
}
object ItemEncoder {
def apply[A](implicit encoder: ItemEncoder[A]): ItemEncoder[A] = encoder
def instance[A](f: A => Item): ItemEncoder[A] =
new ItemEncoder[A] {
override def encode(value: A): Item = f(value)
}
implicit def stringEncoder[K <: Symbol, V <: String](
implicit witness: Witness.Aux[K]
): ItemEncoder[FieldType[K, V]] =
instance { value =>
val item = new Item
item.withString(witness.value.name, value)
item
}
implicit def intEncoder[K <: Symbol, V <: Int](
implicit witness: Witness.Aux[K]
): ItemEncoder[FieldType[K, V]] =
instance { value =>
val item = new Item
item.withInt(witness.value.name, value)
item
}
implicit def booleanEncoder[K <: Symbol, V <: Boolean](
implicit witness: Witness.Aux[K]
): ItemEncoder[FieldType[K, V]] =
instance { value =>
val item = new Item
item.withBoolean(witness.value.name, value)
item
}
// K is key, A is value, R is HList representation of A
implicit def nestedClassEncoder[K <: Symbol, A, R](
implicit
witness: Witness.Aux[K],
generic: LabelledGeneric.Aux[A, R],
encoder: ItemEncoder[R]
): ItemEncoder[FieldType[K, A]] =
instance { value =>
val i = encoder.encode(generic.to(value))
val item = new Item
val m = new java.util.HashMap[String, Any]()
item.withMap(witness.value.name, i.asMap())
item
}
import cats.Monoid
implicit val itemMonoid: Monoid[Item] = new Monoid[Item] {
override def empty: Item = new Item()
override def combine(x: Item, y: Item): Item = {
val m = x.asMap
m.putAll(y.asMap())
Item.fromMap(m)
}
}
implicit val hnilEncoder: ItemEncoder[HNil] =
instance(_ => new Item())
implicit def hlistEncoder[H, T <: HList](
implicit
hEncoder: Lazy[ItemEncoder[H]],
tEncoder: ItemEncoder[T],
monoid: Monoid[Item]
): ItemEncoder[H :: T] =
instance { value =>
// println("hlist enc")
val itemX = hEncoder.value.encode(value.head)
val itemY = tEncoder.encode(value.tail)
monoid.combine(itemX, itemY)
}
implicit def genericEncoder[A, R](
implicit
generic: LabelledGeneric.Aux[A, R],
itemEncoder: Lazy[ItemEncoder[R]]
): ItemEncoder[A] =
instance { value =>
// println("gen enc")
itemEncoder.value.encode(generic.to(value))
}
def encode[A](toEncode: A)(implicit itemEncoder: ItemEncoder[A]) =
itemEncoder.encode(toEncode)
}
Current implementation is a bit simplified. So it contains ItemEncoder implementation only for such a primitive types as String, Int and Boolean. But other primitive types can be easily added by using the present ones as example.
You can find complete implementation with QuickCheck tests on Git
I'm trying to modify caseclassmerge example from shapeless library to only merge non-null fields.
object mergeSyntax {
implicit class MergeSyntax[T](t: T) {
def merge[U](u: U)(implicit merge: CaseClassMerge[T, U]): T = merge(t, u)
}
}
trait CaseClassMerge[T, U] {
def apply(t: T, u: U): T
}
object CaseClassMerge {
import ops.record.Merger
def apply[T, U](implicit merge: CaseClassMerge[T, U]): CaseClassMerge[T, U] = merge
implicit def mkCCMerge[T, U, RT <: HList, RU <: HList]
(implicit
tgen: LabelledGeneric.Aux[T, RT],
ugen: LabelledGeneric.Aux[U, RU],
merger: Merger.Aux[RT, RU, RT]
): CaseClassMerge[T, U] =
new CaseClassMerge[T, U] {
def apply(t: T, u: U): T =
tgen.from(merger(tgen.to(t), ugen.to(u)))
}
}
How to modify the merging logic in a way that only non-null fields in the second argument will be merged into the first argument?
You could add you own implicit merger implementation to the current scope, overriding the standard merger:
object NotNullHListMerger {
import shapeless.labelled._
import shapeless.ops.record.Merger
import shapeless.ops.record.Remover
implicit def notNullHListMerger[K, V, T <: HList, M <: HList, MT <: HList]
(implicit
rm: Remover.Aux[M, K, (V, MT)],
mt: Merger[T, MT]
): Merger.Aux[FieldType[K, V] :: T, M, FieldType[K, V] :: mt.Out] =
new Merger[FieldType[K, V] :: T, M] {
type Out = FieldType[K, V] :: mt.Out
def apply(l: FieldType[K, V] :: T, m: M): Out = {
val (mv, mr) = rm(m)
val up = field[K](mv)
// Replace only if value is not null
val h = Option(up).getOrElse(l.head)
h :: mt(l.tail, mr)
}
}
}
import mergeSyntax._
import NotNullHListMerger._
case class Foo(i: Int, s: String, b: Boolean)
case class Bar(b: Boolean, s: String)
val foo = Foo(23, "foo", true)
val bar = Bar(false, null)
val merged = foo merge bar
assert(merged == Foo(23, "foo", false))
In this question, I noticed the need to separate logic into two steps with two class constructors and yet to fully understand the rationale behind it. But here I am, having a more complex problem.
In this case I need to achieve the following
case class RawReq(ip: String, header: Map[String, String] = Map())
case class Result(status: String, body: Option[String] = None)
type Directive[T] = T ⇒ Result
class ActionConstructor[T] {
def apply[ExtractedRepr <: HList, ExtraInputRepr <: HList]
(extractor: RawReq ⇒ ExtractedRepr, dir: Directive[T])
(implicit supplement: Supplement.Aux[T, ExtractedRepr, ExtraInputRepr])
: ExtraInputRepr => RawReq => Result
= ???
}
// The Supplement is something to be implemented or replaced ( it should
// generate a ExtraInputRepr type that basically provide the missing
// fields that ExtractedRepr doesn't provide for creating a T, the
// example below explains it better.
// Example usage below
case class RequestMessage(name: String, ipAllowed: Boolean, userId: String)
val dir : Directive[RequestMessage] = (rm: RequestMessage) ⇒
if(rm.ipAllowed)
Result("200", Some("hello! " + rm.name ))
else Result("401")
// the extractor can get one piece of information from the RawReq
val extractor = (r: RawReq) => ('ipAllowed ->> (r.ip.startWith("10.4")> 3)) :: HNil
val ac = new ActionConstructor[RequestMessage]
val action = ac(extractor, dir)
// The other two fields of RequestMessage are provided throw method call
val result = action(('name ->> "Mike") :: ('userId ->> "aId") :: HNil)(RawReq(ip = "10.4.2.5"))
result == Result("200", Some("hello! Mike"))
My several attempts to implement this all failed. Here is the last one
class PartialHandlerConstructor[T, Repr <: HList, ExtractedRepr <: HList]
(extractor: RawReq ⇒ ExtractedRepr)
(implicit lgen: LabelledGeneric.Aux[T, Repr]) {
def apply[TempFull <: HList, InputRepr <: HList]
(dir: Directive[T])
(implicit removeAll: RemoveAll.Aux[Repr, ExtractedRepr, InputRepr],
prepend: Prepend.Aux[InputRepr, ExtractedRepr, TempFull],
align: Align[TempFull, Repr]): InputRepr ⇒ RawReq ⇒ Result =
(inputRepr: InputRepr) ⇒ (raw: RawReq) ⇒ {
dir(lgen.from(align(inputRepr ++ extractor(raw))))
}
}
class HandlerConstructor[T]() {
def apply[ExtractedRepr <: HList, Repr <: HList]
(extractor: RawReq ⇒ ExtractedRepr)
(implicit lgen: LabelledGeneric.Aux[T, Repr]) = {
new PartialHandlerConstructor[T, Repr, ExtractedRepr](extractor)
}
}
val hc = new HandlerConstructor[RequestMessage]
val handler1 = hc((r: RawReq) ⇒ ('ipAllowed ->> (r.ip.length > 3)) :: HNil)
val dir: Directive[RequestMessage] = (rm: RequestMessage) ⇒ Result(rm.ipAllowed.toString, rm.name)
val action = handler1(dir) // <=== compiler fail
val result = action(('name ->> "big") :: ('userId ->> "aId") :: HNil)(RawReq("anewiP"))
The compiler error message I am getting is at the line p(dir)
Error:(85, 26) could not find implicit value for parameter removeAll:
shapeless.ops.hlist.RemoveAll.Aux[this.Out,shapeless.::[Boolean with
shapeless.labelled.KeyTag[Symbol with
shapeless.tag.Tagged[String("ipAllowed")],Boolean],shapeless.HNil],InputRepr]
val action = handler1(dir)
^
I think the fundamental reason that I failed is that I don't understand why and how to organize this type construction logic into different classes. And I don't quite understand why the compiler can find the implicit in some cases but not in others.
Figured out a solution. I have to create my own TypeClass RestOf
package com.iheart.playSwagger
import org.specs2.mutable.Specification
import shapeless._
import ops.hlist._
import syntax.singleton._
object Test {
case class RawReq(ip: String, header: Map[String, String] = Map())
case class Result(status: String, body: String)
type Directive[T] = T ⇒ Result
case class RequestMessage(name: String, ipAllowed: Boolean, userId: String)
trait RestOf[L <: HList, SL <: HList] {
type Out <: HList
}
object RestOf {
type Aux[L <: HList, SL <: HList, Out0 <: HList] = RestOf[L, SL] {
type Out = Out0
}
implicit def hlistRestOfNil[L <: HList]: Aux[L, HNil, L] = new RestOf[L, HNil] { type Out = L }
implicit def hlistRestOf[L <: HList, E, RemE <: HList, Rem <: HList, SLT <: HList]
(implicit rt: Remove.Aux[L, E, (E, RemE)], st: Aux[RemE, SLT, Rem]): Aux[L, E :: SLT, Rem] =
new RestOf[L, E :: SLT] { type Out = Rem }
}
class PartialHandlerConstructor[T, Repr <: HList, ExtractedRepr <: HList, InputRepr <: HList]
(extractor: RawReq ⇒ ExtractedRepr)
(implicit lgen: LabelledGeneric.Aux[T, Repr]) {
def apply[TempFull <: HList](dir: Directive[T])
(implicit prepend: Prepend.Aux[InputRepr, ExtractedRepr, TempFull],
align: Align[TempFull, Repr]): InputRepr ⇒ RawReq ⇒ Result =
(inputRepr: InputRepr) ⇒ (raw: RawReq) ⇒ {
dir(lgen.from(align(inputRepr ++ extractor(raw))))
}
}
class HandlerConstructor[T]() {
def apply[Repr <: HList,ExtractedRepr <: HList, InputRepr <: HList]
(extractor: RawReq ⇒ ExtractedRepr)
(implicit lgen: LabelledGeneric.Aux[T, Repr],
restOf: RestOf.Aux[Repr, ExtractedRepr, InputRepr]) = {
new PartialHandlerConstructor[T, Repr, ExtractedRepr, InputRepr](extractor)
}
}
}
class HandlerSpec extends Specification {
import Test._
"handler" should {
"generate action functions" in {
val hc = new HandlerConstructor[RequestMessage]().apply
val handler1 = hc((r: RawReq) ⇒ ('ipAllowed ->> (r.ip.length > 3)) :: HNil)
val dir: Directive[RequestMessage] =
(rm: RequestMessage) ⇒ Result(rm.ipAllowed.toString, rm.name)
val action = handler1(dir)
val result = action(('name ->> "big") :: ('userId ->> "aId") :: HNil)(RawReq("anewiP"))
result === Result("true", "big")
}
}
}