Skip to content

Macro Annotations

Angel Sanadinov edited this page Jun 27, 2017 · 3 revisions

Enumerations

It is often useful to use enums for container fields and they require custom serialization. That often is done by converting the enum to/from strings and this basic functionality can be achieved by using the DatabaseEnum macro annotation:

import core3.meta.enums._

//... container definition ...

object Organization {
  @DatabaseEnum
  sealed trait OrganizationType
  object OrganizationType {
    case object Client extends OrganizationType
    case object InternalTeam extends OrganizationType
  }
}

On its own, this annotation only generates a fromString method; however, when generating container definitions via the appropriate container annotations, extra code is generated for the enum to ensure proper (de)serialization:

//Slick: WithSlickContainerDefinition

implicit val OrganizationTypeColumnType =
  MappedColumnType.base[OrganizationType, String](
    { enum => enum.toString },
    { string => OrganizationType.fromString(string) }
  )
//Json: WithJsonContainerDefinition

implicit val OrganizationTypeReads: Reads[OrganizationType] = Reads {
  json => json.validate[String].map(OrganizationType.fromString)
}

implicit val OrganizationTypeWrites: Writes[OrganizationType] = Writes {
  enum => JsString(enum.toString)
}

Examples

  @DatabaseEnum
  sealed trait SomeType1
  object SomeType1 {
    case object EnumOne extends SomeType1
    case object EnumTwo extends SomeType1
  }
  
  sealed trait SomeType2
  @DatabaseEnum
  object SomeType2 {
    case object EnumOne extends SomeType2
    case object EnumTwo extends SomeType2
  }

  @DatabaseEnum
  sealed class SomeType3
  object SomeType3 {
    case object EnumOne extends SomeType3
    case object EnumTwo extends SomeType3
  }

Containers

Container conversion boilerplate can be easily generated:

WithBasicContainerDefinition

Generates

  • override def getDatabaseName: String = "<some name>" - the name is generated based on the container class name and it attempts to split any words (camel case is assumed); for example: SomeClass becomes some_class
  • override def matchCustomQuery(queryName: String, queryParams: Map[String, String], container: core3.database.containers.Container): Boolean = ??? - throws a NotImplementedError

Can Override

  • matchCustomQuery - normally, this method will be overridden to provide actual query functionality (see the examples below)

WithJsonContainerDefinition

Generates

  • enum reads/writes - See enumerations above
  • implicit val SomeContainerWrites: play.api.libs.json.Writes[SomeContainer] - for converting a container to a Json object
  • implicit val SomeContainerReads: play.api.libs.json.Reads[SomeContainer] - for converting a Json object to a container
  • override def toJsonData(container: Container) - helper method
  • override def fromJsonData(data: .JsValue) - helper method

Can Override

Nothing

WithSlickContainerDefinition

Generates

  • Slick Imports - all standard imports (Slick profile, shapeless, slickless)
  • Slick Table Definition - the standard table definition for Slick; column names are based on the container field names (camel case is assumed); for example: someIntField will become SOME_INT_FIELD
  • Slick Queries - default query definition (get object by ID)
  • Slick Actions - all standard actions plus a customQueryAction that throws a NotImplementedError

Can Override

  • customQueryAction - normally, this method will be overridden to provide actual query functionality (see the examples below)
  • additional queries - any additional queries need to be define explicitly (see the examples below)

Multiple annotations can be applied to the same container.

Examples

import core3.database
import core3.database.{ContainerType, ObjectID, RevisionID, RevisionSequenceNumber}
import core3.database.containers._
import core3.utils._
import core3.meta.containers._
import core3.meta.enums._

@WithBasicContainerDefinition
@WithJsonContainerDefinition
@WithSlickContainerDefinition
case class Organization(
    var name: String,
    var description: String,
    organizationType: Organization.OrganizationType,
    created: Timestamp,
    var updated: Timestamp,
    var updatedBy: String,
    id: ObjectID,
    var revision: RevisionID,
    var revisionNumber: RevisionSequenceNumber
  ) extends MutableContainer {
  override val objectType: ContainerType = "Organization"
}

object Organization {
  @DatabaseEnum
  sealed trait OrganizationType
  object OrganizationType {
    case object Internal extends OrganizationType
    case object External extends OrganizationType
  }

  trait BasicDefinition extends BasicContainerDefinition {
    //basic custom query
    override def matchCustomQuery(...): Boolean = {
      queryName match {
        case "getByName" =>
          queryParams("name") == container.asInstanceOf[Organization].name
        
        case _ => throw new IllegalArgumentException(
          s"Organization::matchCustomQuery > Query [$queryName] is not supported."
        )
      }
    }
  }

  trait SlickDefinition extends SlickContainerDefinition {

    //custom slick query
    private val compiledGetByName =
      Compiled((name: Rep[String]) => query.filter(_.name === name))

    //custom slick query action
    override def customQueryAction(...): ... = {
      queryName match {
        case "getByName" => compiledGetByName(queryParams("name")).result
        case _ => throw new IllegalArgumentException(
          s"Organization::matchCustomQuery > Query [$queryName] is not supported."
        )
      }
    }
  }
}

Initializing Definitions

Before being supplied to a DAL or a Workflow Engine the definition needs to be initialized:

// initializes the organization definitions (see above) with MariaDB support
val organizationDefinitions =
  new core.Group.BasicDefinition 
    with core.Group.JsonDefinition 
    with core.Group.SlickDefinition {
  override protected def withProfile: JdbcProfile = slick.jdbc.MySQLProfile
}

Where to implement/override queries

Normally, all queries will be defined in the container traits/definitions, however, any definition method can be overridden when specifying the initialization:

// initializes the organization definitions (see above) with MariaDB support
val organizationDefinitions =
  new core.Group.BasicDefinition 
    with core.Group.JsonDefinition 
    with core.Group.SlickDefinition {
  override protected def withProfile: JdbcProfile = slick.jdbc.MySQLProfile
  
  //overrides the method specified in `BasicDefinition`
  override def matchCustomQuery(...): Boolean = ???
}

Clone this wiki locally