Scala Macro for generating generic maps for my DAO

so this is my macro

package com.fullfacing.ticketing.macros

import com.mongodb.DBObject

import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context

/**
 * Project: com.fullfacing.ticketing.macros
 * Created on 2015/05/26.
 * ryno aka lemonxah -
 * https://github.com/lemonxah
 * http://stackoverflow.com/users/2919672/lemon-xah
 */
trait Mappable[A,B] {
  def toDBType(a: A): B
  def fromDBType(b: B): A
}

object MongoMappable {
  implicit def materializeMappable[A]: Mappable[A, DBObject] = macro materializeMappableImpl[A]

  def materializeMappableImpl[A: c.WeakTypeTag](c: Context): c.Expr[Mappable[A, DBObject]] = {
    import c.universe._
    val tpe = weakTypeOf[A]
    val companion = tpe.typeSymbol.companion

    val fields = tpe.decls.collectFirst {
      case m: MethodSymbol if m.isPrimaryConstructor ⇒ m
    }.get.paramLists.head

    val (toDBObject, fromDBObject) = fields.map { field ⇒
      val name = field.name.toTermName
      val decoded = name.decodedName.toString
      val returnType = tpe.decl(name).typeSignature

      (q"$decoded → t.$name", q"dbo.as[$returnType]($decoded)")
    }.unzip

    c.Expr[Mappable[A,DBObject]] { q"""
      new Mappable[$tpe] {
        def toMap(t: $tpe): DBObject = MongoDBObject(..$toDBObject)
        def fromMap(dbo: DBObject): $tpe = $companion(..$fromDBObject)
      }
    """ }
  }
}

      

and this is how i use it

package com.fullfacing.ticketing.common.dao

import com.fullfacing.ticketing.common.model._
import com.fullfacing.ticketing.macros.{MongoMappable, Mappable}

import com.fullfacing.ticketing.macros.MongoMappable._
import com.mongodb.DBObject
import com.mongodb.casbah.commons.MongoDBObject
import com.mongodb.casbah.Imports._
import com.mongodb.casbah.query.Imports

/**
 * Project: com.fullfacing.liono.common.data.dao
 * Created on 2015/05/11.
 * ryno aka lemonxah -
 * https://github.com/lemonxah
 * http://stackoverflow.com/users/2919672/lemon-xah
 */
class MongoDAO[A <: Model](coll: MongoCollection) extends DAO[A, DBObject] {
  import MongoDAO._
  def interpreter(q: Query): Option[DBObject] = {
    def loop(q: Query, acc: MongoDBObject): Imports.DBObject = q match {
      case Or(left, right) => acc ++ $or(loop(left, MongoDBObject()), loop(right, MongoDBObject()))
      case And(left, right) => acc ++ $and(loop(left, MongoDBObject()), loop(right, MongoDBObject()))
      case Equals(f, v: Int) => f.name $eq v
      case Equals(f, v: String) => f.name $eq v
      case GreaterOrEqual(f, v: Int) => f.name $gte v
      case GreaterThan(f, v: Int) => f.name $gt v
      case LessOrEqual(f, v: Int) => f.name $lte v
      case LessThan(f, v: Int) => f.name $lt v
      case NotEquals(f, v: Int) => f.name $ne v
      case NotEquals(f, v: String) => f.name $ne v
      case _ => throw new UnsupportedOperationException()
    }
    try { Some(loop(q,MongoDBObject())) } catch {
      case e: UnsupportedOperationException => None
    }
  }

  override def list: Vector[A] = {
    coll.find().toVector.map(mapf)
  }

  override def filter(query: Query): Vector[A] = {
    interpreter(query) match {
      case Some(q) => coll.find(q).toVector.map(mapf)
      case None => Vector()
    }
  }

  override def headOption(query: Query): Option[A] = {
    interpreter(query) match {
      case Some(q) => coll.find(q).toVector.map(mapf).headOption
      case None => None
    }
  }

  def insert(a: A) {
    coll.insert(MongoDAO.mapt[A](a))
  }

  def update(a: A): Unit = {
//    val q = MongoDBObject("id" -> a.id.id)
//    val u = $set(toMap(a).toList: _*)
//    coll.update(q,u)
  }

  override def delete(a: A): Unit = ???

  override def delete(query: Query): Unit = {
    interpreter(query) match {
      case Some(q) => coll.findAndRemove(q)
      case None =>
    }
  }
}

object MongoDAO {
  def apply[A <: Model](coll: MongoCollection): MongoDAO[A] = new MongoDAO[A](coll)
  def mapt[A: Mappable](a: A) = implicitly[Mappable[A, DBObject]].toDBType(a)
  def mapf[A: Mappable](b: DBObject) = implicitly[Mappable[A, DBObject]].fromDBType(b)
}

      

well this is how i want to use it

this is my DAO sign and AST request for interpreter

package com.fullfacing.ticketing.common.dao

import com.fullfacing.ticketing.common.model.Model

/**
 * Project: com.fullfacing.liono.common.data.dao
 * Created on 2015/05/19.
 * ryno aka lemonxah -
 * https://github.com/lemonxah
 * http://stackoverflow.com/users/2919672/lemon-xah
 */
trait DAO[A <: Model, B] {
  def interpreter(q: Query): Option[B]
  def +=(a: A) = insert(a)
  def insert(a: A)
  def list: Vector[A]
  def filter(query: Query): Vector[A]
  def headOption(query: Query): Option[A]
  def update(a: A)
  def delete(a: A)
  def delete(query: Query)
}
case class Field[A](name: String) {
  def ===(value: A): Query = Equals(this, value)
  def !==(value: A): Query = NotEquals(this, value)
  def <(value: A): Query = LessThan(this, value)
  def >(value: A): Query = GreaterThan(this, value)
  def >=(value: A): Query = GreaterOrEqual(this, value)
  def <=(value: A): Query = LessOrEqual(this, value)
}
sealed trait Query { self =>
  def &&(t: Query): Query = and(t)
  def and(t: Query): Query = And(self, t)
  def ||(t: Query): Query = or(t)
  def or(t: Query): Query = Or(self, t)
}
sealed trait Operator extends Query { def left: Query; def right: Query}
case class Or(left: Query, right: Query) extends Operator
case class And(left: Query, right: Query) extends Operator
sealed trait Operand[+A] extends Query { def field: Field[_]; def value: A }
case class GreaterOrEqual[A](field: Field[A], value: A) extends Operand[A]
case class GreaterThan[A](field: Field[A], value: A) extends Operand[A]
case class LessOrEqual[A](field: Field[A], value: A) extends Operand[A]
case class LessThan[A](field: Field[A], value: A) extends Operand[A]
case class Equals[A](field: Field[A], value: A) extends Operand[A]
case class NotEquals[A](field: Field[A], value: A) extends Operand[A]

      

The idea behind all this is that I can create new DAOs like MongoDAO and I don't have to change a lot of code, just add the db implementation and all the top level code will stay the same, maybe change the import or use a different db context, and it should just work.

but doing so i get this problem

Error:(81, 50) not enough arguments for method implicitly: (implicit e: com.fullfacing.ticketing.macros.Mappable[A,com.mongodb.DBObject])com.fullfacing.ticketing.macros.Mappable[A,com.mongodb.DBObject].
Unspecified value parameter e.
  def mapf[A: Mappable](b: DBObject) = implicitly[Mappable[A, DBObject]].fromDBType(b)
                                             ^

      

I'm not sure if what I want to do is possible. But I want to refrain from making mappers like this.

  object DeviceMap extends DAOMap[Device, DBObject] {
    def mapt(device: Device): DBObject = {
      MongoDBObject(
        "uuid" -> device.uuid,
        "created" -> device.created,
        "id" -> device.id.id
      )
    }

    def mapf(o: DBObject): Device = {
      Device(
        uuid = o.as[String]("uuid"),
        created = o.as[Long]("created"),
        id = Id(o.as[UUID]("id"))
      )
    }
  }

      

because it would be very tedious to do this for all data objects that will be implemented in the final version of this tool.

+3


source to share





All Articles