All Downloads are FREE. Search and download functionalities are using the official Maven repository.

slick.additions.codegen.GenerationRules.scala Maven / Gradle / Ivy

The newest version!
package slick.additions.codegen

import java.nio.file.Path
import java.sql.Types

import scala.concurrent.ExecutionContext
import scala.meta._

import slick.additions.codegen.ScalaMetaDsl._
import slick.dbio.DBIO
import slick.jdbc.JdbcProfile
import slick.jdbc.meta._


/** Information about a table obtained from the Slick JDBC metadata APIs
  */
case class TableMetadata(
  table: MTable,
  columns: Seq[MColumn],
  primaryKeys: Seq[MPrimaryKey],
  foreignKeys: Seq[MForeignKey])

/** How a database column is to be represented in code
  *
  * @param column
  *   the column this is for
  * @param tableFieldTerm
  *   the identifier used in the Slick table definition
  * @param modelFieldTerm
  *   the identifier used in the model class
  * @param scalaType
  *   the type that will represent data in the column in code
  * @param scalaDefault
  *   the default value to provide in the model class
  */
case class ColumnConfig(
  column: MColumn,
  tableFieldTerm: Term.Name,
  modelFieldTerm: Term.Name,
  scalaType: Type,
  scalaDefault: Option[Term])

/** How a database table is to be represented in code
  *
  * @param tableMetadata
  *   the metadata for the table this is for
  * @param tableClassName
  *   the name of the Slick table definition
  * @param modelClassName
  *   the name of the model class
  * @param columns
  *   configurations for this table's columns
  */
case class TableConfig(
  tableMetadata: TableMetadata,
  tableClassName: String,
  modelClassName: String,
  columns: List[ColumnConfig])

/** Generates [[TableConfig]]s (and their [[ColumnConfig]]s by reading database metadata. Extend this trait directly or
  * indirectly, and override methods freely to customize.
  *
  * The default implementation does not generate code that requires `slick-additions`, uses camelCase for corresponding
  * snake_case names in the database, and names model classes by appending `Row` to the camel-cased table name.
  */
trait GenerationRules {
  def packageName: String
  def container: String
  def extraImports                         = List.empty[String]
  def includeTable(table: MTable): Boolean = true

  def filePath(base: Path) = (packageName.split(".") :+ (container + ".scala")).foldLeft(base)(_ resolve _)

  def columnNameToIdentifier(name: String)      = snakeToCamel(name)
  def tableNameToIdentifier(name: MQName)       = snakeToCamel(name.name).capitalize
  def modelClassName(tableName: MQName): String = tableNameToIdentifier(tableName) + "Row"

  /** Determine the base Scala type for a column. If the column is nullable, the type returned from this method will be
    * wrapped in `Option[...]`.
    *
    * Extend by overriding with `orElse`.
    *
    * @example
    *   {{{ override def baseColumnType(current: TableMetadata, all: Seq[TableMetadata]) = super.baseColumnType(current,
    *   all).orElse { case ... } }}}
    * @see
    *   [[columnConfig]]
    */
  def baseColumnType(currentTableMetadata: TableMetadata, all: Seq[TableMetadata]): PartialFunction[MColumn, Type] = {
    case ColType(Types.NUMERIC, "numeric", _)                                => typ"BigDecimal"
    case ColType(Types.DOUBLE, "double precision" | "float8", _)             => typ"Double"
    case ColType(Types.BIGINT, "bigserial" | "bigint" | "int8", _)           => typ"Long"
    case ColType(Types.BIT | Types.BOOLEAN, "bool" | "boolean", _)           => typ"Boolean"
    case ColType(Types.INTEGER, _, _)                                        => typ"Int"
    case ColType(Types.VARCHAR, "character varying" | "text" | "varchar", _) => typ"String"
    case ColType(Types.DATE, "date", _)                                      =>
      term"java".termSelect(term"time").typeSelect(typ"LocalDate")
    case ColType(_, "lo", _)                                                 =>
      term"java".termSelect(term"sql").typeSelect(typ"Blob")
  }

  /** Determine the base Scala default value for a column. If the columns is nullable, the expression returned from this
    * method will be wrapped in `Some(...)`.
    *
    * Extend by overriding with `orElse`.
    *
    * @example
    *   {{{ override def baseColumnDefault(current: TableMetadata, all: Seq[TableMetadata]) =
    *   super.baseColumnDefault(current, all).orElse { case ... } }}}
    * @see
    *   [[columnConfig]]
    */
  def baseColumnDefault(currentTableMetadata: TableMetadata, all: Seq[TableMetadata])
    : PartialFunction[MColumn, Term] = {
    case ColType(Types.BIT, "boolean" | "bool", Some(AsBoolean(b)))              => Lit.Boolean(b)
    case ColType(Types.INTEGER, _, Some(AsInt(i)))                               => Lit.Int(i)
    case ColType(Types.DOUBLE, "double precision" | "float8", Some(AsDouble(d))) => Lit.Double(d)
    case ColType(Types.NUMERIC, "numeric", Some(s))                              =>
      term"BigDecimal".termApply(Lit.String(s))
    case ColType(Types.DATE, "date", Some("now()" | "LOCALTIMESTAMP"))           =>
      term"java".termSelect("time").termSelect("LocalDate").termSelect("now")
        .termApply()
    case ColType(
          Types.VARCHAR,
          "character varying" | "text" | "varchar",
          Some(s)
        ) =>
      Lit.String(s.stripPrefix("'").stripSuffix("'"))
  }

  def columnConfig(column: MColumn, currentTableMetadata: TableMetadata, all: Seq[TableMetadata]): ColumnConfig = {
    val ident    = Term.Name(snakeToCamel(column.name))
    val typ0     = baseColumnType(currentTableMetadata, all).applyOrElse(column, (_: MColumn) => typ"Nothing")
    val default0 = baseColumnDefault(currentTableMetadata, all).lift(column)

    val (typ, default) =
      if (!column.nullable.contains(true))
        typ0                        -> default0
      else
        typ"Option".typeApply(typ0) ->
          Some(default0.map(t => term"Some".termApply(t)).getOrElse(term"None"))

    ColumnConfig(column, ident, ident, typ, default)
  }

  def columnConfigs(currentTableMetadata: TableMetadata, all: Seq[TableMetadata]) =
    currentTableMetadata.columns.toList.map(columnConfig(_, currentTableMetadata, all))

  def tableConfig(currentTableMetadata: TableMetadata, all: Seq[TableMetadata]) =
    TableConfig(
      tableMetadata = currentTableMetadata,
      tableClassName = tableNameToIdentifier(currentTableMetadata.table.name),
      modelClassName = modelClassName(currentTableMetadata.table.name),
      columns = columnConfigs(currentTableMetadata, all)
    )

  def tableConfigs(slickProfileClass: Class[_ <: JdbcProfile])(implicit ec: ExecutionContext)
    : DBIO[List[TableConfig]] = {
    val slickProfileInstance = slickProfileClass.getField("MODULE$").get(null).asInstanceOf[JdbcProfile]
    for {
      tables        <- slickProfileInstance.defaultTables
      includedTables = tables.toList.filter(includeTable)
      infos         <- DBIO.sequence(
                         includedTables.map { t =>
                           for {
                             cols <- t.getColumns
                             pks  <- t.getPrimaryKeys
                             fks  <- t.getImportedKeys
                           } yield TableMetadata(
                             table = t,
                             columns = cols,
                             primaryKeys = pks,
                             foreignKeys = fks
                           )
                         }
                       )
    } yield infos.map(tableConfig(_, infos))
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy