├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── build.sbt
├── jvmopts.travis
├── modules
├── core
│ └── src
│ │ ├── main
│ │ └── scala
│ │ │ └── io
│ │ │ └── strongtyped
│ │ │ └── active
│ │ │ └── slick
│ │ │ ├── ActiveRecord.scala
│ │ │ ├── CrudActions.scala
│ │ │ ├── DBIOExtensions.scala
│ │ │ ├── EntityActions.scala
│ │ │ ├── EntityActionsLike.scala
│ │ │ ├── JdbcProfileProvider.scala
│ │ │ ├── Lens.scala
│ │ │ ├── OptimisticLocking.scala
│ │ │ ├── SchemaManagement.scala
│ │ │ ├── exceptions
│ │ │ ├── ActiveSlickException.scala
│ │ │ ├── NoRowsAffectedException.scala
│ │ │ ├── RowNotFoundException.scala
│ │ │ ├── StaleObjectStateException.scala
│ │ │ └── TooManyRowsAffectedException.scala
│ │ │ └── package.scala
│ │ └── test
│ │ └── scala
│ │ └── io
│ │ └── strongtyped
│ │ └── active
│ │ └── slick
│ │ ├── BeerTest.scala
│ │ ├── CrudTest.scala
│ │ ├── EntityActionsBeforeInsertUpdateTest.scala
│ │ ├── Schema.scala
│ │ ├── SupplierTest.scala
│ │ └── test
│ │ ├── DbSuite.scala
│ │ └── H2Suite.scala
├── samples
│ └── src
│ │ ├── main
│ │ ├── resources
│ │ │ └── codegen_schema.sql
│ │ └── scala
│ │ │ └── io
│ │ │ └── strongtyped
│ │ │ └── active
│ │ │ └── slick
│ │ │ └── docexamples
│ │ │ ├── ActiveSlickWithCodegen.scala
│ │ │ ├── MappingWithActiveSlick.scala
│ │ │ ├── MappingWithoutActiveSlick.scala
│ │ │ └── OptimisticLockingExample.scala
│ │ └── test
│ │ └── scala
│ │ └── io
│ │ └── strongtyped
│ │ └── active
│ │ └── slick
│ │ └── docexamples
│ │ └── ActiveSlickWithCodegenTest.scala
└── shapeless
│ └── src
│ └── main
│ └── scala
│ └── io
│ └── strongtyped
│ └── active
│ └── slick
│ └── shapeless
│ └── ShapelessSlick.scala
├── project
├── BuildSettings.scala
├── Dependencies.scala
├── build.properties
├── plugins.sbt
└── sbt-ui.sbt
├── scripts
└── test-shapeless.scala
├── sonatype.sbt
├── src
└── asciidoctor
│ ├── foundation-blue.css
│ ├── index.adoc
│ └── pygments-monokai.css
└── version.sbt
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /.idea_modules
3 | target
4 | /.classpath
5 | /.project
6 | /.settings
7 | /.cache
8 | .sbtserver*
9 | src/asciidoctor/index.html
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: scala
2 | sudo: false
3 | script: sbt -jvm-opts jvmopts.travis +test
4 | jdk:
5 | - oraclejdk8
6 | notifications:
7 | webhooks:
8 | urls:
9 | - https://webhooks.gitter.im/e/7e03eb850971919f740b
10 | on_success: change
11 | on_failure: always
12 | on_start: false
13 | after_success:
14 | - ! '[[ $TRAVIS_BRANCH == "develop" ]] && { sbt "+publish" ; };'
15 | env:
16 | global:
17 | - secure: VVyIsdj+WEXW6jwW7PxbGmlMmUYpy/qjH6vCvYgHK7Rrz/cX4GuT1FXE9xLnAZua2tWejkfBCSy+DPxAnDsVugAa0cvfHgA+i13/Zbm7LkH2PPvnIdjgl9LlhhxAHFr/OL8dKthPWw1U7wkFhCgFxN/OGytya0TItMOYiBG2cvk=
18 | - secure: QdGIhfPlMVPFR8e9REJwDLKbKwcleOTY5c+foJzLtyW3Y3yXt0jOzqdBUZqEMyTjvYDPcwabL6IMVp6vir3cIYLIxQx9eFofzAnwUfSOzJ7ASxdkPD5LZJXQHzHN+alskbyPF7y28kAuJEFF+FPlkH+3rSr7eBew3NAJqqUlpzI=
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This software is licensed under the Apache 2 license, quoted below.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with
4 | the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
5 |
6 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
7 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
8 | language governing permissions and limitations under the License.
9 |
10 | Copyright 2014 Strong[Typed] (@StrongTyped)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This project is not maintained anymore and therefore it's archived. Feel free to contact the owner (@octonato), if you would like to have unarchive it and have it transfer to you.
2 |
3 | # ActiveSlick
4 |
5 | [](https://travis-ci.org/strongtyped/active-slick)
6 |
7 | [](https://gitter.im/strongtyped/active-slick?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
8 |
9 |
10 | ActiveSlick is a library that offers CRUD operations for Slick 3.x projects. The main goal is to provide some basic operations to manage the lifecycle of persisted objects (new/persisted/deleted/stale) and enable the implementation of the Active Record Pattern on Slick mapped case classes.
11 |
12 | ### Documentation (WIP)
13 | http://www.strongtyped.io/active-slick/
14 |
15 | ### Main features
16 | - Basic CRUD and auxiliary methods - add/update/save, delete, findById, count and fetchAll (backed by Reactive Streams).
17 | - Generic Id type.
18 | - Optimistic locking my means of incremental version.
19 | - Before insert and update hooks.
20 | - **ActiveRecord** trait to enable the Active Record Pattern on mapped case classes via class extensions (pimp-my-library style)
21 |
22 | ### Project artifact
23 |
24 | The artifacts are published to Sonatype Repository. Simply add the following to your build.sbt.
25 |
26 | As of version 0.3.x we don't support Slick 2.0 anymore. The differences between Slick 2.x and Slick 3.x are so huge that it makes impossible to support two versions.
27 |
28 | ```scala
29 | libraryDependencies += "io.strongtyped" %% "active-slick" % "0.3.5"
30 | ```
31 |
32 | Source code for version 0.3.5 can be found at:
33 | https://github.com/strongtyped/active-slick/tree/v0.3.5
34 |
35 |
36 | The version supporting Slick 2.0 is still available on Sonatype Repo. However, this documentation only covers the current series (i.e.: 0.3.x).
37 |
38 | ```scala
39 | libraryDependencies += "io.strongtyped" %% "active-slick" % "0.2.2"
40 | ```
41 |
42 | Source code for version 0.2.2 can be found at:
43 | https://github.com/strongtyped/active-slick/tree/v0.2.2
44 |
45 | ### Contribution policy
46 |
47 | Contributions via GitHub pull requests are gladly accepted from their original author. Along with any pull requests, please state that the contribution is your original work and that you license the work to the project under the project's open source license. Whether or not you state this explicitly, by submitting any copyrighted material via pull request, email, or other means you agree to license the material under the project's open source license and warrant that you have the legal authority to do so.
48 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | import Dependencies._
2 | import BuildSettings._
3 |
4 | scalaVersion in ThisBuild := "2.11.11"
5 |
6 | ivyScala := ivyScala.value map {
7 | _.copy(overrideScalaVersion = true)
8 | }
9 |
10 | organization in ThisBuild := "io.strongtyped"
11 |
12 | crossScalaVersions in ThisBuild := Seq("2.11.11", "2.12.2")
13 |
14 | parallelExecution in Test := false
15 |
16 |
17 | lazy val root = Project(
18 | id = "active-slick-root",
19 | base = file("."),
20 | settings = projSettings ++ Seq(
21 | publishArtifact := false
22 | )
23 | ) aggregate(activeSlick, shapelessIntegration, samples)
24 |
25 |
26 | // Core ==========================================
27 | lazy val activeSlick = Project(
28 | id = "active-slick",
29 | base = file("modules/core"),
30 | settings = projSettings ++ mainDeps ++ testDeps
31 | )
32 | //================================================
33 |
34 |
35 | // Shapeless Integration ========================
36 | lazy val shapelessIntegration = {
37 |
38 | // settings to include shapeless dependencies
39 | val shapeless = Seq(
40 | libraryDependencies ++= shapelessDeps
41 | )
42 |
43 | Project(
44 | id = "active-slick-shapeless",
45 | base = file("modules/shapeless"),
46 | settings = projSettings ++ mainDeps ++ shapeless ++ testDeps
47 | ) dependsOn activeSlick
48 | }
49 | //================================================
50 |
51 |
52 | // Samples =======================================
53 | // contains examples used on the docs, not intended to be released
54 | lazy val samples = Project(
55 | id = "active-slick-samples",
56 | base = file("modules/samples"),
57 | settings = projSettings ++ Seq(
58 | publishArtifact := false,
59 | libraryDependencies ++= Seq(
60 | "com.typesafe.slick" %% "slick-codegen" % slickVersion,
61 | "com.h2database" % "h2" % "1.4.187",
62 | "org.slf4j" % "slf4j-nop" % "1.7.12", // To silence build warnings
63 | scalaTest
64 | ),
65 | slick <<= slickCodeGenTask,
66 | sourceGenerators in Compile <+= slickCodeGenTask
67 | ) ++ mainDeps
68 | ).dependsOn(activeSlick % "compile->compile;test->test")
69 | //======+=========================================
70 |
71 | lazy val slick = TaskKey[Seq[File]]("gen-tables")
72 |
73 | lazy val slickCodeGenTask = (sourceManaged, dependencyClasspath in Compile, runner in Compile, streams) map { (srcManaged, cp, r, s) =>
74 | val pkg = "io.strongtyped.active.slick.docexamples.codegen"
75 | val url = "jdbc:h2:mem:test;INIT=runscript from 'modules/samples/src/main/resources/codegen_schema.sql'" // connection info for a pre-populated throw-away, in-memory db for this demo, which is freshly initialized on every run
76 | val jdbcDriver = "org.h2.Driver"
77 | val slickDriver = "slick.jdbc.H2Profile"
78 | toError(r.run("slick.codegen.SourceCodeGenerator", cp.files, Array(slickDriver, jdbcDriver, url, srcManaged.getPath, pkg), s.log))
79 | val outputDir = srcManaged / pkg.replace(".", "/")
80 | Seq(outputDir / "Tables.scala")
81 | }
--------------------------------------------------------------------------------
/jvmopts.travis:
--------------------------------------------------------------------------------
1 | -Xmx1024M
2 | -Xss2M
3 | -XX:+DoEscapeAnalysis
4 | -XX:+UseParallelGC
5 | -XX:+UseCompressedOops
6 | -XX:ReservedCodeCacheSize=128M
7 |
--------------------------------------------------------------------------------
/modules/core/src/main/scala/io/strongtyped/active/slick/ActiveRecord.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick
2 |
3 | import slick.dbio.DBIO
4 | import scala.concurrent.ExecutionContext
5 |
6 |
7 | abstract class ActiveRecord[R <: CrudActions](val repository: R) {
8 |
9 | def model: repository.Model
10 |
11 | def save()(implicit exc: ExecutionContext): DBIO[repository.Model] =
12 | repository.save(model)
13 |
14 | def update()(implicit exc: ExecutionContext): DBIO[repository.Model] =
15 | repository.update(model)
16 |
17 | def delete()(implicit exc: ExecutionContext): DBIO[Int] = repository.delete(model)
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/modules/core/src/main/scala/io/strongtyped/active/slick/CrudActions.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick
2 |
3 | import slick.jdbc.JdbcProfile
4 |
5 | import scala.concurrent.ExecutionContext
6 |
7 | /**
8 | * Define basic CRUD actions.
9 | *
10 | * This trait make no assumption about the presence of an Entity and a corresponding ID.
11 | * Therefore it can also be used for persistence of Value Objects.
12 | */
13 | trait CrudActions {
14 |
15 | val jdbcProfile: JdbcProfile
16 |
17 | import jdbcProfile.api._
18 |
19 | type Model
20 | // tag::adoc[]
21 | /** Returns total table count */
22 | def count: DBIO[Int]
23 |
24 | /** Insert or Update a Model
25 | * Insert `Model` if not yet persisted, otherwise update it.
26 | * @return DBIO[Model] for a `Model` as persisted in the table.
27 | */
28 | def save(entity: Model)(implicit exc: ExecutionContext): DBIO[Model]
29 |
30 | /** Update a `Model`.
31 | * @return DBIO[Model] for a `Model` as persisted in the table.
32 | */
33 | def update(entity: Model)(implicit exc: ExecutionContext): DBIO[Model]
34 |
35 | /** Delete a `Model`.
36 | * @return DBIO[Int] with the number of affected rows
37 | */
38 | def delete(entity: Model)(implicit exc: ExecutionContext): DBIO[Int]
39 |
40 | /** Fetch all elements from a table.
41 | * @param fetchSize - the number of row to fetch, defaults to 100
42 | * @return StreamingDBIO[Seq[Model], Model]
43 | */
44 | def fetchAll(fetchSize: Int = 100)
45 | (implicit exc: ExecutionContext): StreamingDBIO[Seq[Model], Model]
46 |
47 | // end::adoc[]
48 | }
49 |
--------------------------------------------------------------------------------
/modules/core/src/main/scala/io/strongtyped/active/slick/DBIOExtensions.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick
2 |
3 | import io.strongtyped.active.slick.exceptions.{TooManyRowsAffectedException, NoRowsAffectedException}
4 | import slick.dbio.DBIO
5 |
6 | import scala.concurrent.ExecutionContext
7 |
8 | object DBIOExtensions {
9 |
10 | implicit class UpdateActionExtensionMethods(dbAction: DBIO[Int]) {
11 |
12 | def mustAffectOneSingleRow(implicit exc: ExecutionContext): DBIO[Int] = {
13 | dbAction.flatMap {
14 | case 1 => DBIO.successful(1) // expecting one result
15 | case 0 => DBIO.failed(NoRowsAffectedException)
16 | case n if n > 1 => DBIO.failed(new TooManyRowsAffectedException(affectedRowCount = n, expectedRowCount = 1))
17 | }
18 | }
19 |
20 | def mustAffectAtLeastOneRow(implicit exc: ExecutionContext): DBIO[Int] = {
21 |
22 | dbAction.flatMap {
23 | case n if n >= 1 => DBIO.successful(n) // expecting one or more results
24 | case 0 => DBIO.failed(NoRowsAffectedException)
25 | }
26 | }
27 | }
28 |
29 | implicit class SelectSingleExtensionMethods[R](dbAction: DBIO[Seq[R]]) {
30 |
31 | def mustSelectSingleRecord(implicit exc: ExecutionContext): DBIO[R] = {
32 | dbAction.flatMap {
33 | case s if s.size == 1 => DBIO.successful(s.head)
34 | case s if s.isEmpty => DBIO.failed(NoRowsAffectedException)
35 | case s => DBIO.failed(new TooManyRowsAffectedException(affectedRowCount = s.size, expectedRowCount = 1))
36 | }
37 | }
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/modules/core/src/main/scala/io/strongtyped/active/slick/EntityActions.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick
2 |
3 | import io.strongtyped.active.slick.DBIOExtensions._
4 | import io.strongtyped.active.slick.exceptions.{NoRowsAffectedException, RowNotFoundException}
5 | import slick.ast.BaseTypedType
6 | import slick.dbio.{FailureAction, SuccessAction}
7 | import slick.profile.RelationalProfile
8 |
9 | import scala.concurrent.ExecutionContext
10 | import scala.language.{existentials, higherKinds, implicitConversions}
11 | import scala.util.{Failure, Success}
12 |
13 | abstract class EntityActions extends EntityActionsLike {
14 | this: JdbcProfileProvider =>
15 |
16 | import jdbcProfile.api._
17 |
18 | def baseTypedType: BaseTypedType[Id]
19 |
20 | protected implicit lazy val btt: BaseTypedType[Id] = baseTypedType
21 |
22 | type EntityTable <: RelationalProfile#Table[Entity]
23 |
24 | def tableQuery: TableQuery[EntityTable]
25 |
26 | def $id(table: EntityTable): Rep[Id]
27 |
28 | def idLens: Lens[Entity, Option[Id]]
29 |
30 | override def count: DBIO[Int] = tableQuery.size.result
31 |
32 | override def findById(id: Id): DBIO[Entity] =
33 | filterById(id).result.head
34 |
35 | override def findOptionById(id: Id): DBIO[Option[Entity]] =
36 | filterById(id).result.headOption
37 |
38 | override def save(entity: Entity)(implicit exc: ExecutionContext): DBIO[Entity] = {
39 | idLens.get(entity) match {
40 | // if has an Id, try to update it
41 | case Some(id) => update(entity)
42 |
43 | // if has no Id, try to add it
44 | case None => insert(entity).map { id =>
45 | idLens.set(entity, Option(id))
46 | }
47 | }
48 | }
49 |
50 | /**
51 | * Before insert interceptor method. This method is called just before record insertion.
52 | * The default implementation returns a successful DBIO wrapping the passed entity.
53 | *
54 | * The returned `DBIOAction` is combined with the final insert `DBIOAction`
55 | * and 'marked' to run on the same transaction.
56 | *
57 | * Override this method if you need to add extract validation or modify the entity before insert.
58 | *
59 | * See examples bellow:
60 | * {{{
61 | * // simple validation example
62 | * override def beforeInsert(foo: Foo)(implicit exc: ExecutionContext): DBIO[Foo] = {
63 | * if (foo.name.trim.isEmpty) {
64 | * DBIO.failed(new RuntimeException("Name can't be empty!!!")
65 | * } else {
66 | * DBIO.successful(foo)
67 | * }
68 | * }
69 | * }}}
70 | *
71 | * {{{
72 | * // simple audit example
73 | * override def beforeInsert(foo: Foo)(implicit exc: ExecutionContext): DBIO[Foo] = {
74 | * // ensure that created and lastUpdate fields are updated just before insert
75 | * val audited = foo.copy(created = DateTime.now, lastUpdate = DateTime.now)
76 | * DBIO.successful(audited)
77 | * }
78 | * }}}
79 | */
80 | def beforeInsert(entity: Entity)(implicit exc: ExecutionContext): DBIO[Entity] = {
81 | // default implementation does nothing
82 | DBIO.successful(entity)
83 | }
84 |
85 |
86 | /**
87 | * Before update interceptor method. This method is called just before record update.
88 | * The default implementation returns a successful DBIO wrapping the passed entity.
89 | *
90 | * The returned `DBIOAction` is combined with the final update `DBIOAction`
91 | * and 'marked' to run on the same transaction.
92 | *
93 | * Override this method if you need to add extract validation or modify the entity before update.
94 | *
95 | * See examples bellow:
96 | *
97 | * {{{
98 | * // simple validation example
99 | * override def beforeUpdate(id: Int, foo: Foo)(implicit exc: ExecutionContext): DBIO[Foo] = {
100 | * findById(id).flatMap { oldFoo =>
101 | * if (oldFoo.name != foo.name) {
102 | * DBIO.failed(new RuntimeException("Can't modify name!!!")
103 | * } else {
104 | * DBIO.successful(foo)
105 | * }
106 | * }
107 | * }
108 | * }}}
109 | *
110 | * {{{
111 | * // simple audit example
112 | * override def beforeUpdate(id: Int, foo: Foo)(implicit exc: ExecutionContext): DBIO[Foo] = {
113 | * // ensure that lastUpdate fields are updated just before update
114 | * val audited = foo.copy(lastUpdate = DateTime.now)
115 | * DBIO.successful(audited)
116 | * }
117 | * }}}
118 | */
119 | def beforeUpdate(id: Id, entity: Entity)(implicit exc: ExecutionContext): DBIO[Entity] = {
120 | // default implementation does nothing
121 | DBIO.successful(entity)
122 | }
123 |
124 | override def insert(entity: Entity)(implicit exc: ExecutionContext): DBIO[Id] = {
125 | val action = beforeInsert(entity).flatMap { preparedModel =>
126 | tableQuery.returning(tableQuery.map($id)) += preparedModel
127 | }
128 | // beforeInsert and '+=' must run on same tx
129 | action.transactionally
130 | }
131 |
132 | override def fetchAll(fetchSize: Int = 100)(implicit exc: ExecutionContext): StreamingDBIO[Seq[Entity], Entity] = {
133 | tableQuery
134 | .result
135 | .transactionally
136 | .withStatementParameters(fetchSize = fetchSize)
137 | }
138 |
139 | override def update(entity: Entity)(implicit exc: ExecutionContext): DBIO[Entity] = {
140 | val action =
141 | for {
142 | id <- tryExtractId(entity)
143 | preparedModel <- beforeUpdate(id, entity)
144 | updatedModel <- update(id, preparedModel)
145 | } yield updatedModel
146 |
147 | // beforeUpdate and update must run on same tx
148 | action.transactionally
149 | }
150 |
151 |
152 | protected def update(id: Id, entity: Entity)(implicit exc: ExecutionContext): DBIO[Entity] = {
153 |
154 | val triedUpdate = filterById(id).update(entity).mustAffectOneSingleRow.asTry
155 |
156 | triedUpdate.flatMap {
157 | case Success(_) => DBIO.successful(entity)
158 | case Failure(NoRowsAffectedException) => DBIO.failed(new RowNotFoundException(entity))
159 | case Failure(ex) => DBIO.failed(ex)
160 | }
161 |
162 | }
163 |
164 | override def delete(entity: Entity)(implicit exc: ExecutionContext): DBIO[Int] = {
165 | tryExtractId(entity).flatMap { id =>
166 | deleteById(id)
167 | }
168 | }
169 |
170 | def deleteById(id: Id)(implicit exc: ExecutionContext): DBIO[Int] = {
171 | filterById(id).delete.mustAffectOneSingleRow
172 | }
173 |
174 |
175 | private def tryExtractId(entity: Entity): DBIO[Id] = {
176 | idLens.get(entity) match {
177 | case Some(id) => SuccessAction(id)
178 | case None => FailureAction(new RowNotFoundException(entity))
179 | }
180 | }
181 |
182 | def filterById(id: Id) = tableQuery.filter($id(_) === id)
183 |
184 |
185 | }
--------------------------------------------------------------------------------
/modules/core/src/main/scala/io/strongtyped/active/slick/EntityActionsLike.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick
2 |
3 | import slick.ast.BaseTypedType
4 | import slick.jdbc.JdbcProfile
5 |
6 | import scala.concurrent.ExecutionContext
7 |
8 | /**
9 | * Define basic life cycle actions for a Entity that involve.
10 | *
11 | */
12 | trait EntityActionsLike extends CrudActions {
13 |
14 | val jdbcProfile: JdbcProfile
15 |
16 | import jdbcProfile.api._
17 |
18 | /** The type of the Entity */
19 | type Entity
20 | /** The `Entity`'s Id type */
21 | type Id
22 | /** CrudActions.Model is the `Entity` in this context */
23 | type Model = Entity
24 |
25 | // tag::adoc[]
26 | /** Insert a new `Entity`
27 | * @return DBIO[Id] for the generated `Id`
28 | */
29 | def insert(entity: Entity)(implicit exc: ExecutionContext): DBIO[Id]
30 |
31 | /** Delete a `Entity` by `Id`
32 | * @return DBIO[Int] with the number of affected rows
33 | */
34 | def deleteById(id: Id)(implicit exc: ExecutionContext): DBIO[Int]
35 |
36 | /** Finds `Entity` referenced by `Id`.
37 | * May fail if no `Entity` is found for passed `Id`
38 | * @return DBIO[Entity] for the `Entity`
39 | */
40 | def findById(id: Id): DBIO[Entity]
41 |
42 | /** Finds `Entity` referenced by `Id` optionally.
43 | * @return DBIO[Option[Entity]] for the `Entity`
44 | */
45 | def findOptionById(id: Id): DBIO[Option[Entity]]
46 |
47 | // end::adoc[]
48 | }
49 |
--------------------------------------------------------------------------------
/modules/core/src/main/scala/io/strongtyped/active/slick/JdbcProfileProvider.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick
2 |
3 | import slick.jdbc.{DerbyProfile, SQLiteProfile, MySQLProfile, HsqldbProfile, PostgresProfile, H2Profile, JdbcProfile}
4 |
5 |
6 | trait JdbcProfileProvider {
7 | type JP <: JdbcProfile
8 | val jdbcProfile: JP
9 | }
10 |
11 | object JdbcProfileProvider {
12 |
13 | trait H2ProfileProvider extends JdbcProfileProvider {
14 | override type JP = H2Profile
15 | override val jdbcProfile: H2Profile = H2Profile
16 | }
17 |
18 | trait PostgresProfileProvider extends JdbcProfileProvider {
19 | override type JP = PostgresProfile
20 | override val jdbcProfile: PostgresProfile = PostgresProfile
21 | }
22 |
23 |
24 | trait DerbyProfileProvider extends JdbcProfileProvider {
25 | override type JP = DerbyProfile
26 | override val jdbcProfile: DerbyProfile = DerbyProfile
27 | }
28 |
29 | trait HsqlProfileProvider extends JdbcProfileProvider {
30 | override type JP = HsqldbProfile
31 | override val jdbcProfile: HsqldbProfile = HsqldbProfile
32 | }
33 |
34 | trait MySQLProfileProvider extends JdbcProfileProvider {
35 | override type JP = MySQLProfile
36 | override val jdbcProfile: MySQLProfile = MySQLProfile
37 | }
38 |
39 | trait SQLLiteProfileProvider extends JdbcProfileProvider {
40 | override type JP = SQLiteProfile
41 | override val jdbcProfile: SQLiteProfile = SQLiteProfile
42 | }
43 |
44 | }
--------------------------------------------------------------------------------
/modules/core/src/main/scala/io/strongtyped/active/slick/Lens.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick
2 |
3 | /** A dead simple lens to provide a getter and a setter for
4 | * lifecycle model/table fields like 'id' and 'version'.
5 | *
6 | * This implementation is by no means target as a general Lens library. It's sole purpose is
7 | * to provide an abstraction for setting and getting first level fields on an arbitrary model.
8 | *
9 | * @param get function mapping O -> V
10 | * where O is the Object we want to 'look' inside and V the value of the field we want to extract
11 | *
12 | * @param set function to assign a the a new value to field affect by this Lens.
13 | */
14 | case class Lens[O, V](get: O => V, set: (O, V) => O)
15 |
16 | object Lens {
17 |
18 | /**
19 | * Convenience method to allow creation of a Lens using curried syntax.
20 | * {{{
21 | * case class Coffee(id:Int, name:String)
22 | *
23 | * val idLens = lens { coffee: Coffee => coffee.id }
24 | * { (coffee, id) => coffee.copy(id = id) }
25 | * }}}
26 | */
27 | def lens[O, V](get: O => V)(set: (O, V) => O) = Lens(get, set)
28 | }
--------------------------------------------------------------------------------
/modules/core/src/main/scala/io/strongtyped/active/slick/OptimisticLocking.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick
2 |
3 | import io.strongtyped.active.slick.exceptions.{NoRowsAffectedException, StaleObjectStateException}
4 | import io.strongtyped.active.slick.DBIOExtensions._
5 | import scala.concurrent.ExecutionContext
6 | import scala.util.{Failure, Success}
7 |
8 | trait OptimisticLocking {
9 | self: EntityActions =>
10 |
11 |
12 | import self.jdbcProfile.api._
13 |
14 | def $version(table: EntityTable): Rep[Long]
15 |
16 | def versionLens: Lens[self.Entity, Long]
17 |
18 | override protected def update(id: self.Id, versionable: self.Entity)(implicit exc: ExecutionContext): DBIO[self.Entity] = {
19 |
20 | // extract current version
21 | val currentVersion = versionLens.get(versionable)
22 |
23 | // build a query selecting entity with current version
24 | val queryByIdAndVersion = self.filterById(id).filter($version(_) === currentVersion)
25 |
26 | // model with incremented version
27 | val modelWithNewVersion = versionLens.set(versionable, currentVersion + 1)
28 |
29 | val tryUpdate = queryByIdAndVersion.update(modelWithNewVersion).mustAffectOneSingleRow.asTry
30 |
31 | // in case of failure, we want a more meaningful exception ie: StaleObjectStateException
32 | tryUpdate.flatMap {
33 | case Success(_) => DBIO.successful(modelWithNewVersion)
34 | case Failure(NoRowsAffectedException) => DBIO.failed(new StaleObjectStateException(versionable))
35 | case Failure(e) => DBIO.failed(e)
36 | }
37 | }
38 |
39 | override def save(versionable: self.Entity)(implicit exc: ExecutionContext): DBIO[self.Entity] = {
40 | self.idLens.get(versionable) match {
41 | // if has an Id, try to update it
42 | case Some(id) => update(id, versionable)
43 |
44 | // if has no Id, try to add it
45 | case None =>
46 | // initialize versioning
47 | val modelWithVersion = versionLens.set(versionable, 1)
48 | self.insert(modelWithVersion).map { id => idLens.set(modelWithVersion, Option(id)) }
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/modules/core/src/main/scala/io/strongtyped/active/slick/SchemaManagement.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick
2 |
3 | trait SchemaManagement {
4 | self: EntityActions =>
5 |
6 | import jdbcProfile.api._
7 |
8 | def createSchema: DBIO[Unit] = tableQuery.schema.create
9 |
10 | def dropSchema: DBIO[Unit] = tableQuery.schema.drop
11 | }
12 |
--------------------------------------------------------------------------------
/modules/core/src/main/scala/io/strongtyped/active/slick/exceptions/ActiveSlickException.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick.exceptions
2 |
3 | class ActiveSlickException(msg: String) extends RuntimeException(msg)
4 |
--------------------------------------------------------------------------------
/modules/core/src/main/scala/io/strongtyped/active/slick/exceptions/NoRowsAffectedException.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick.exceptions
2 |
3 | object NoRowsAffectedException extends ActiveSlickException("No rows affected")
4 |
--------------------------------------------------------------------------------
/modules/core/src/main/scala/io/strongtyped/active/slick/exceptions/RowNotFoundException.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick.exceptions
2 |
3 | class RowNotFoundException[T](notFoundRecord: T) extends ActiveSlickException(s"Row not found: $notFoundRecord")
--------------------------------------------------------------------------------
/modules/core/src/main/scala/io/strongtyped/active/slick/exceptions/StaleObjectStateException.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick.exceptions
2 |
3 | case class StaleObjectStateException[T](staleObject: T)
4 | extends ActiveSlickException(s"Optimistic locking error - object in stale state: $staleObject")
--------------------------------------------------------------------------------
/modules/core/src/main/scala/io/strongtyped/active/slick/exceptions/TooManyRowsAffectedException.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick.exceptions
2 |
3 | class TooManyRowsAffectedException(affectedRowCount: Int, expectedRowCount: Int)
4 | extends ActiveSlickException(s"Expected $expectedRowCount row(s) affected, got $affectedRowCount instead")
5 |
--------------------------------------------------------------------------------
/modules/core/src/main/scala/io/strongtyped/active/slick/package.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active
2 |
3 | package object slick {
4 |
5 | type H2ProfileProvider = JdbcProfileProvider.H2ProfileProvider
6 | type PostgresProfileProvider = JdbcProfileProvider.PostgresProfileProvider
7 | type DerbyProfileProvider = JdbcProfileProvider.DerbyProfileProvider
8 | type HsqlProfileProvider = JdbcProfileProvider.HsqlProfileProvider
9 | type MySQLProfileProvider = JdbcProfileProvider.MySQLProfileProvider
10 | type SQLLiteProfileProvider = JdbcProfileProvider.SQLLiteProfileProvider
11 | }
12 |
--------------------------------------------------------------------------------
/modules/core/src/test/scala/io/strongtyped/active/slick/BeerTest.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick
2 |
3 | import io.strongtyped.active.slick.exceptions.RowNotFoundException
4 | import io.strongtyped.active.slick.test.H2Suite
5 | import org.scalatest._
6 | import scala.concurrent.ExecutionContext.Implicits.global
7 | import scala.language.postfixOps
8 |
9 | class BeerTest extends FlatSpec with H2Suite with Schema {
10 |
11 | behavior of "A Beer"
12 |
13 | it should "be persistable" in {
14 | val (supplier, beer) =
15 | rollback {
16 | for {
17 | supplier <- Supplier("Acme, Inc.").save()
18 | beer <- Beer("Abc", supplier.id.get, 3.2).save()
19 | beerSupplier <- beer.supplier()
20 | } yield {
21 | beerSupplier.value shouldBe supplier
22 | supplier.id shouldBe defined
23 | (supplier, beer)
24 | }
25 | }
26 | }
27 |
28 | it should "not be persisted with an id chosen by a user" in {
29 | val (supplier, triedBeer) =
30 | rollback {
31 | for {
32 | supplier <- Supplier("Acme, Inc.").save()
33 | beer <- Beer("Abc", supplier.id.get, 3.2, Some(10)).save().asTry
34 | } yield (supplier, beer)
35 | }
36 |
37 | supplier.id shouldBe defined
38 | triedBeer.failure.exception shouldBe a[RowNotFoundException[_]]
39 | }
40 |
41 |
42 | override def createSchemaAction: jdbcProfile.api.DBIO[Unit] = {
43 | jdbcProfile.api.DBIO.seq(Suppliers.createSchema, Beers.createSchema)
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/modules/core/src/test/scala/io/strongtyped/active/slick/CrudTest.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick
2 |
3 | import io.strongtyped.active.slick.test.H2Suite
4 | import org.scalatest.FlatSpec
5 | import slick.ast.BaseTypedType
6 | import scala.concurrent.ExecutionContext.Implicits.global
7 | import scala.language.postfixOps
8 | import io.strongtyped.active.slick.Lens._
9 |
10 | class CrudTest extends FlatSpec with H2Suite with JdbcProfileProvider {
11 |
12 | behavior of "An EntityDao (CRUD)"
13 |
14 | it should "support all CRUD operations" in {
15 | rollback {
16 | for {
17 | // collect initial count
18 | initialCount <- Foos.count
19 |
20 | // save new entry
21 | savedEntry <- Foo("Foo").save()
22 |
23 | // count again, must be initialCount + 1
24 | count <- Foos.count
25 |
26 | // update entry
27 | updatedEntry <- savedEntry.copy(name = "Bar").save()
28 |
29 | // find it back from DB
30 | found <- Foos.findById(savedEntry.id.get)
31 |
32 | // delete it
33 | _ <- found.delete()
34 |
35 | // count total one more time
36 | finalCount <- Foos.count
37 | } yield {
38 |
39 | // check that we can add new entry
40 | count shouldBe (initialCount + 1)
41 |
42 | // check entity properties
43 | savedEntry.id shouldBe 'defined
44 | savedEntry.name shouldBe "Foo"
45 |
46 | // found entry must be a 'Bar'
47 | found.name shouldBe "Bar"
48 |
49 | // after delete finalCount must equal initialCount
50 | finalCount shouldBe initialCount
51 |
52 | savedEntry
53 | }
54 | }
55 | }
56 |
57 | override def createSchemaAction = {
58 | Foos.createSchema
59 | }
60 |
61 |
62 | case class Foo(name: String, id: Option[Int] = None)
63 |
64 | class FooDao extends EntityActions with H2ProfileProvider {
65 |
66 | import jdbcProfile.api._
67 |
68 | val baseTypedType: BaseTypedType[Id] = implicitly[BaseTypedType[Id]]
69 |
70 | type Entity = Foo
71 | type Id = Int
72 |
73 | class FooTable(tag: Tag) extends Table[Foo](tag, "FOO_CRUD_TEST") {
74 |
75 | def name = column[String]("NAME")
76 |
77 | def id = column[Int]("ID", O.PrimaryKey, O.AutoInc)
78 |
79 | def * = (name, id.?) <>(Foo.tupled, Foo.unapply)
80 | }
81 |
82 | type EntityTable = FooTable
83 | val tableQuery = TableQuery[EntityTable]
84 |
85 | def $id(table: FooTable) = table.id
86 |
87 | val idLens = lens { foo: Foo => foo.id } { (entry, id) => entry.copy(id = id) }
88 |
89 | def createSchema = {
90 | import jdbcProfile.api._
91 | tableQuery.schema.create
92 | }
93 | }
94 |
95 | val Foos = new FooDao
96 |
97 |
98 | implicit class EntryExtensions(val model: Foo) extends ActiveRecord(Foos)
99 |
100 | }
--------------------------------------------------------------------------------
/modules/core/src/test/scala/io/strongtyped/active/slick/EntityActionsBeforeInsertUpdateTest.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick
2 |
3 | import io.strongtyped.active.slick.test.H2Suite
4 | import org.scalatest.FlatSpec
5 | import slick.ast.BaseTypedType
6 | import io.strongtyped.active.slick.Lens._
7 | import scala.concurrent.ExecutionContext
8 |
9 | class EntityActionsBeforeInsertUpdateTest
10 | extends FlatSpec with H2Suite with JdbcProfileProvider {
11 |
12 | behavior of "An EntityDao with validation "
13 |
14 | it should "return an error if beforeInsert is not successful" in {
15 |
16 | import scala.concurrent.ExecutionContext.Implicits.global
17 |
18 | val result =
19 | rollback {
20 | for {
21 | result <- Foo(" ").save().asTry
22 | } yield result
23 | }
24 |
25 | result.failure.exception shouldBe a[NameShouldNotBeEmptyException]
26 | }
27 |
28 | it should "return an error if beforeUpdate is not successful" in {
29 |
30 | import scala.concurrent.ExecutionContext.Implicits.global
31 |
32 | val result =
33 | rollback {
34 |
35 | val finalAction =
36 | for {
37 | savedEntry <- Foo("abc").save()
38 | // update name should fail according to beforeUpdate method definition
39 | updatedEntry <- savedEntry.copy(name = "Bar").save()
40 | } yield updatedEntry
41 |
42 | finalAction.asTry
43 | }
44 |
45 | result.failure.exception shouldBe a[NameCanNotBeModifiedException]
46 | }
47 |
48 |
49 | override def createSchemaAction = {
50 | Foos.createSchema
51 | }
52 |
53 |
54 | case class Foo(name: String, id: Option[Int] = None)
55 |
56 | class NameShouldNotBeEmptyException extends RuntimeException("Name should not be empty")
57 |
58 | class NameCanNotBeModifiedException extends RuntimeException("Name can not be modified")
59 |
60 | class FooDao extends EntityActions with H2ProfileProvider {
61 |
62 | import jdbcProfile.api._
63 |
64 | val baseTypedType: BaseTypedType[Id] = implicitly[BaseTypedType[Id]]
65 |
66 | type EntityTable = FooTable
67 | type Entity = Foo
68 | type Id = Int
69 |
70 | class FooTable(tag: Tag) extends Table[Foo](tag, "FOO_VALIDATION_TEST") {
71 |
72 | def name = column[String]("NAME")
73 |
74 | def id = column[Int]("ID", O.PrimaryKey, O.AutoInc)
75 |
76 | def * = (name, id.?) <>(Foo.tupled, Foo.unapply)
77 |
78 | }
79 |
80 | val tableQuery = TableQuery[FooTable]
81 |
82 | def $id(table: FooTable) = table.id
83 |
84 | val idLens = lens { foo: Foo => foo.id } { (entry, id) => entry.copy(id = id) }
85 |
86 | //@formatter:off
87 | // tag::adoc[]
88 | override def beforeInsert(model: Foo)
89 | (implicit exc: ExecutionContext): DBIO[Foo] = {
90 | if (model.name.trim.isEmpty) {
91 | DBIO.failed(new NameShouldNotBeEmptyException)
92 | } else {
93 | DBIO.successful(model)
94 | }
95 | }
96 |
97 | override def beforeUpdate(id: Int, model: Foo)
98 | (implicit exc: ExecutionContext): DBIO[Foo] = {
99 | findById(id).flatMap { oldModel =>
100 | if (oldModel.name != model.name) {
101 | DBIO.failed(new NameCanNotBeModifiedException)
102 | } else {
103 | DBIO.successful(model)
104 | }
105 | }
106 | }
107 | // end::adoc[]
108 | //@formatter:on
109 |
110 | def createSchema = {
111 | import jdbcProfile.api._
112 | tableQuery.schema.create
113 | }
114 | }
115 |
116 | val Foos = new FooDao
117 |
118 |
119 | implicit class EntryExtensions(val model: Foo) extends ActiveRecord(Foos)
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/modules/core/src/test/scala/io/strongtyped/active/slick/Schema.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick
2 |
3 | import io.strongtyped.active.slick.Lens._
4 |
5 | import slick.ast.BaseTypedType
6 |
7 | import scala.language.existentials
8 |
9 | trait Schema extends JdbcProfileProvider {
10 |
11 | case class Supplier(name: String, version: Long = 0, id: Option[Int] = None)
12 |
13 | case class Beer(name: String,
14 | supID: Int,
15 | price: Double,
16 | id: Option[Int] = None)
17 |
18 | class SupplierDao extends EntityActions
19 | with OptimisticLocking
20 | with SchemaManagement
21 | with H2ProfileProvider {
22 |
23 | import jdbcProfile.api._
24 |
25 | val baseTypedType: BaseTypedType[Id] = implicitly[BaseTypedType[Id]]
26 |
27 | type Entity = Supplier
28 | type Id = Int
29 | type EntityTable = SuppliersTable
30 |
31 | class SuppliersTable(tag: Tag) extends Table[Supplier](tag, "SUPPLIERS") {
32 |
33 | def version = column[Long]("VERSION")
34 |
35 | def name = column[String]("SUPPLIER_NAME")
36 |
37 | def id = column[Int]("ID", O.PrimaryKey, O.AutoInc)
38 |
39 | def * = (name, version, id.?) <> (Supplier.tupled, Supplier.unapply)
40 |
41 | }
42 |
43 | val tableQuery = TableQuery[EntityTable]
44 |
45 | def $id(table: EntityTable) = table.id
46 |
47 | val idLens = lens { supp: Supplier => supp.id } { (supp, id) => supp.copy(id = id) }
48 |
49 | def $version(table: EntityTable) = table.version
50 |
51 | val versionLens = lens { supp: Supplier => supp.version } { (supp, version) => supp.copy(version = version) }
52 |
53 | }
54 |
55 | val Suppliers = new SupplierDao
56 |
57 | implicit class SupplierRecord(val model: Supplier) extends ActiveRecord(Suppliers)
58 |
59 | class BeersDao extends EntityActions with SchemaManagement with H2ProfileProvider {
60 |
61 | import jdbcProfile.api._
62 |
63 | val baseTypedType: BaseTypedType[Id] = implicitly[BaseTypedType[Id]]
64 |
65 | type Entity = Beer
66 | type Id = Int
67 | type EntityTable = BeersTable
68 |
69 | // Beer Table, DAO and Record extension
70 | class BeersTable(tag: Tag) extends Table[Beer](tag, "BEERS") {
71 |
72 | def name = column[String]("BEER_NAME")
73 |
74 | def supID = column[Int]("SUP_ID")
75 |
76 | def price = column[Double]("PRICE")
77 |
78 | def id = column[Int]("ID", O.PrimaryKey, O.AutoInc)
79 |
80 | def * = (name, supID, price, id.?) <> (Beer.tupled, Beer.unapply)
81 |
82 | def supplier = foreignKey("SUP_FK", supID, Suppliers.tableQuery)(_.id)
83 | }
84 |
85 | val tableQuery = TableQuery[EntityTable]
86 |
87 | def $id(table: EntityTable) = table.id
88 |
89 | val idLens = lens { beer: Beer => beer.id } { (beer, id) => beer.copy(id = id) }
90 |
91 | }
92 |
93 | val Beers = new BeersDao
94 |
95 | implicit class BeerRecord(val model: Beer) extends ActiveRecord(Beers) {
96 |
97 | def supplier() = Suppliers.findOptionById(model.supID)
98 | }
99 |
100 | }
--------------------------------------------------------------------------------
/modules/core/src/test/scala/io/strongtyped/active/slick/SupplierTest.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick
2 |
3 | import java.sql.SQLException
4 | import scala.concurrent.ExecutionContext.Implicits.global
5 | import io.strongtyped.active.slick.exceptions.StaleObjectStateException
6 | import io.strongtyped.active.slick.test.H2Suite
7 | import org.scalatest._
8 | import slick.dbio.DBIO
9 |
10 | class SupplierTest extends FlatSpec with H2Suite with Schema {
11 |
12 | behavior of "A Supplier"
13 |
14 | it should "be persistable" in {
15 | val initialCount = query(Suppliers.count)
16 |
17 | val supplier = Supplier("Acme, Inc.")
18 | supplier.id should not be defined
19 |
20 | val savedSupplier =
21 | commit {
22 | supplier.save()
23 | }
24 | savedSupplier.id shouldBe defined
25 |
26 | val countAfterSave = query(Suppliers.count)
27 | countAfterSave shouldBe (initialCount + 1)
28 |
29 | commit(savedSupplier.delete())
30 |
31 | val countAfterDelete = query(Suppliers.count)
32 | countAfterDelete shouldBe initialCount
33 |
34 | }
35 |
36 | it should "be versionable" in {
37 |
38 | val supplier = Supplier("abc")
39 | // no version yet
40 | supplier.version shouldBe 0
41 |
42 | val persistedSupp = commit(supplier.save())
43 | persistedSupp.version should not be 0
44 |
45 | // modify two versions and try to persist them
46 | val suppWithNewVersion = commit(persistedSupp.copy(name = "abc1").save())
47 |
48 | intercept[StaleObjectStateException[Supplier]] {
49 | // supplier was persisted in the mean time, so version must be different by now
50 | commit(persistedSupp.copy(name = "abc2").save())
51 | }
52 |
53 | // supplier with new version can be persisted again
54 | commit(suppWithNewVersion.copy(name = "abc").save())
55 | }
56 |
57 | it should "return an error when deleting a supplier with beers linked to it" in {
58 |
59 | val deleteResult =
60 | rollback {
61 | for {
62 | supplier <- Supplier("Acme, Inc.").save()
63 | beer <- Beer("Abc", supplier.id.get, 3.2).save()
64 | deleteResult <- supplier.delete().asTry
65 | } yield deleteResult
66 | }
67 |
68 | deleteResult.failure.exception shouldBe a[SQLException]
69 | }
70 |
71 | override def createSchemaAction: jdbcProfile.api.DBIO[Unit] = {
72 | jdbcProfile.api.DBIO.seq(Suppliers.createSchema, Beers.createSchema)
73 | }
74 |
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/modules/core/src/test/scala/io/strongtyped/active/slick/test/DbSuite.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick.test
2 |
3 | import io.strongtyped.active.slick.JdbcProfileProvider
4 | import org.scalatest._
5 | import slick.basic.DatabasePublisher
6 |
7 | import scala.concurrent.duration.{FiniteDuration, _}
8 | import scala.concurrent.{Await, ExecutionContext}
9 | import scala.language.postfixOps
10 | import scala.util.{Failure, Success}
11 |
12 | trait DbSuite extends BeforeAndAfterAll with Matchers with OptionValues with TryValues {
13 |
14 | self: Suite with JdbcProfileProvider =>
15 |
16 | import jdbcProfile.api._
17 |
18 | def setupDb: jdbcProfile.backend.DatabaseDef
19 |
20 | private lazy val database: jdbcProfile.backend.DatabaseDef = setupDb
21 |
22 | override protected def afterAll(): Unit = {
23 | database.close()
24 | }
25 |
26 | def query[T](dbAction: DBIO[T])(implicit ex: ExecutionContext, timeout: FiniteDuration = 5 seconds): T =
27 | runAction(dbAction)
28 |
29 | def stream[T](dbAction: StreamingDBIO[T, T])(implicit ex: ExecutionContext, timeout: FiniteDuration = 5 seconds): DatabasePublisher[T] = {
30 | database.stream(dbAction.transactionally)
31 | }
32 |
33 | def commit[T](dbAction: DBIO[T])(implicit ex: ExecutionContext, timeout: FiniteDuration = 5 seconds): T =
34 | runAction(dbAction.transactionally)
35 |
36 | def rollback[T](dbAction: DBIO[T])(implicit ex: ExecutionContext, timeout: FiniteDuration = 5 seconds): T = {
37 |
38 | case class RollbackException(expected: T) extends RuntimeException("rollback exception")
39 |
40 | val markedForRollback = dbAction.flatMap { result =>
41 | DBIO
42 | .failed(RollbackException(result))
43 | .map(_ => result) // map back to T
44 | }.transactionally.asTry
45 |
46 | val finalAction =
47 | markedForRollback.map {
48 | case Success(result) => result
49 | case Failure(RollbackException(result)) => result
50 | case Failure(other) => throw other
51 | }
52 |
53 | runAction(finalAction)
54 | }
55 |
56 | private def runAction[T](dbAction: DBIO[T])(implicit ex: ExecutionContext, timeout: FiniteDuration): T = {
57 | val result = database.run(dbAction)
58 | Await.result(result, timeout)
59 | }
60 |
61 | }
--------------------------------------------------------------------------------
/modules/core/src/test/scala/io/strongtyped/active/slick/test/H2Suite.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick.test
2 |
3 | import io.strongtyped.active.slick.H2ProfileProvider
4 | import org.scalatest.Suite
5 |
6 | import scala.concurrent.Await
7 | import scala.concurrent.duration._
8 | import scala.language.postfixOps
9 |
10 | trait H2Suite extends DbSuite with H2ProfileProvider {
11 | self: Suite =>
12 |
13 | import jdbcProfile.api._
14 |
15 | def createSchemaAction: DBIO[Unit] = DBIO.successful(())
16 |
17 | def timeout = 5 seconds
18 |
19 | override def setupDb: jdbcProfile.backend.DatabaseDef = {
20 | // each test suite gets its own isolated DB
21 | val dbUrl = s"jdbc:h2:mem:${this.getClass.getSimpleName};DB_CLOSE_DELAY=-1"
22 | val db = Database.forURL(dbUrl, driver = "org.h2.Driver")
23 | val result = db.run(createSchemaAction.transactionally)
24 | Await.result(result, timeout)
25 | db
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/modules/samples/src/main/resources/codegen_schema.sql:
--------------------------------------------------------------------------------
1 | create table if not exists "Computers" (id bigint auto_increment not null, name varchar not null);
2 |
--------------------------------------------------------------------------------
/modules/samples/src/main/scala/io/strongtyped/active/slick/docexamples/ActiveSlickWithCodegen.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick.docexamples
2 |
3 | import io.strongtyped.active.slick.{ActiveRecord, JdbcProfileProvider, Lens, EntityActions}
4 | import io.strongtyped.active.slick.docexamples.codegen.Tables
5 | import slick.ast.BaseTypedType
6 |
7 | /**
8 | * Shows how to configure active-slick with a schema that was generated via slick-codegen. In this case,
9 | * A schema is generated on build as the object io.strongtyped.active.slick.docexamples.codegen.Tables.
10 | * See codegen_schema.sql for the schema that feeds into the codegen.
11 | */
12 | object ActiveSlickWithCodegen {
13 |
14 | abstract class ComputersRepo(tables: Tables) extends EntityActions with JdbcProfileProvider {
15 | //
16 | // Implement JdbcProfileProvider with JDBCProfile from generated Tables.scala
17 | //
18 | override type JP = Tables.profile.type
19 | // Sucks that this is necessary. Did we have to define this type in JdbcProfileProvider? Why not just use JdbcProfile?
20 | override val jdbcProfile: JP = Tables.profile
21 |
22 | //
23 | // Implement EntityActions
24 | //
25 |
26 | import jdbcProfile.api._
27 |
28 | type Entity = Tables.ComputersRow
29 | type Id = Long
30 | type EntityTable = Tables.Computers
31 |
32 | val baseTypedType = implicitly[BaseTypedType[Id]]
33 | val tableQuery = Tables.Computers
34 | val idLens: Lens[Tables.ComputersRow, Option[Long]] = {
35 | // For the getter, use 0L as a sentinel value because generated ID is usually non-optional
36 | Lens.lens { row: Tables.ComputersRow => if (row.id == 0L) None else Some(row.id) } { (row, maybeId) => maybeId map { id => row.copy(id = id) } getOrElse row }
37 | }
38 |
39 | override def $id(table: EntityTable): Rep[Long] = {
40 | table.id
41 | }
42 |
43 | implicit class EntryExtensions(val model: Tables.ComputersRow) extends ActiveRecord(ComputersRepo)
44 |
45 | }
46 |
47 | object ComputersRepo extends ComputersRepo(Tables)
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/modules/samples/src/main/scala/io/strongtyped/active/slick/docexamples/MappingWithActiveSlick.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick.docexamples
2 |
3 | //@formatter:off
4 | // tag::adoc[]
5 | import io.strongtyped.active.slick._
6 | import slick.ast.BaseTypedType
7 | import io.strongtyped.active.slick.Lens._
8 | import scala.language.postfixOps
9 | import scala.concurrent.ExecutionContext.Implicits.global
10 |
11 | object MappingWithActiveSlick {
12 |
13 | case class Coffee(name: String, id: Option[Int] = None)
14 |
15 |
16 |
17 | object CoffeeRepo extends EntityActions with H2ProfileProvider {
18 |
19 | import jdbcProfile.api._ // #<1>
20 | val baseTypedType = implicitly[BaseTypedType[Id]] // #<2>
21 |
22 | type Entity = Coffee // #<3>
23 | type Id = Int // #<4>
24 | type EntityTable = CoffeeTable // # <5>
25 |
26 | val tableQuery = TableQuery[CoffeeTable] // # <6>
27 |
28 | def $id(table: CoffeeTable): Rep[Id] = table.id // # <7>
29 |
30 | val idLens = lens { coffee: Coffee => coffee.id } // # <8>
31 | { (coffee, id) => coffee.copy(id = id) }
32 |
33 | class CoffeeTable(tag: Tag) extends Table[Coffee](tag, "COFFEE") { // #<9>
34 | def name = column[String]("NAME")
35 | def id = column[Id]("ID", O.PrimaryKey, O.AutoInc)
36 | def * = (name, id.?) <>(Coffee.tupled, Coffee.unapply)
37 | }
38 |
39 | def findByName(name:String): DBIO[Seq[Coffee]] = {
40 | tableQuery.filter(_.name === name).result
41 | }
42 |
43 | }
44 |
45 | implicit class EntryExtensions(val model: Coffee) extends ActiveRecord(CoffeeRepo)
46 |
47 | val saveAction = Coffee("Colombia").save()
48 | }
49 | // end::adoc[]
50 | //@formatter:on
51 |
--------------------------------------------------------------------------------
/modules/samples/src/main/scala/io/strongtyped/active/slick/docexamples/MappingWithoutActiveSlick.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick.docexamples
2 |
3 | import scala.language.postfixOps
4 |
5 | //@formatter:off
6 | object MappingWithoutActiveSlick {
7 |
8 | // tag::adoc[]
9 | import slick.jdbc.H2Profile.api._
10 |
11 | case class Coffee(name: String, id: Option[Int] = None) // #<1>
12 |
13 | class CoffeeTable(tag: Tag) extends Table[Coffee](tag, "COFFEE") {
14 | def name = column[String]("NAME")
15 | def id = column[Int]("ID", O.PrimaryKey, O.AutoInc) // #<2>
16 | def * = (name, id.?) <>(Coffee.tupled, Coffee.unapply)
17 | }
18 |
19 | val Coffees = TableQuery[CoffeeTable]
20 |
21 | val coffee = Coffee("Colombia")
22 | val insertAction = Coffees.returning(Coffees.map(_.id)) += coffee // #<3>
23 | // end::adoc[]
24 | }
25 | //@formatter:on
--------------------------------------------------------------------------------
/modules/samples/src/main/scala/io/strongtyped/active/slick/docexamples/OptimisticLockingExample.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick.docexamples
2 |
3 |
4 | import io.strongtyped.active.slick._
5 | import slick.ast.BaseTypedType
6 | import io.strongtyped.active.slick.Lens._
7 | import scala.language.postfixOps
8 | import scala.concurrent.ExecutionContext.Implicits.global
9 |
10 | object OptimisticLockingExample {
11 |
12 | //@formatter:off
13 | // tag::adoc[]
14 | case class Coffee(name: String, version: Long = 0, id: Option[Int] = None)
15 |
16 | object CoffeeRepo extends EntityActions with OptimisticLocking with H2ProfileProvider {
17 |
18 | import jdbcProfile.api._
19 |
20 | def $version(table: CoffeeTable): Rep[Long] = table.version // #<1>
21 | def versionLens = lens { coffee:Coffee => coffee.version } // #<2>
22 | { (coffee, vers) => coffee.copy(version = vers) }
23 |
24 |
25 | class CoffeeTable(tag: Tag) extends Table[Coffee](tag, "COFFEE") {
26 | def name = column[String]("NAME")
27 | def id = column[Id]("ID", O.PrimaryKey, O.AutoInc)
28 | def version = column[Long]("VERSION")
29 | def * = (name, version, id.?) <>(Coffee.tupled, Coffee.unapply)
30 | }
31 | // end::adoc[]
32 | //@formatter:on
33 |
34 | val baseTypedType = implicitly[BaseTypedType[Id]]
35 |
36 | type Entity = Coffee
37 | type Id = Int
38 | type EntityTable = CoffeeTable
39 |
40 | val tableQuery = TableQuery[CoffeeTable]
41 |
42 | def $id(table: CoffeeTable): Rep[Id] = table.id
43 |
44 | val idLens = lens { coffee: Coffee => coffee.id } { (coffee, id) => coffee.copy(id = id) }
45 |
46 |
47 | def findByName(name: String): DBIO[Seq[Coffee]] = {
48 | tableQuery.filter(_.name === name).result
49 | }
50 | }
51 |
52 | implicit class EntryExtensions(val model: Coffee) extends ActiveRecord(CoffeeRepo) {
53 |
54 | }
55 |
56 | val saveAction = Coffee("Colombia").save()
57 | }
58 |
--------------------------------------------------------------------------------
/modules/samples/src/test/scala/io/strongtyped/active/slick/docexamples/ActiveSlickWithCodegenTest.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick.docexamples
2 |
3 | import io.strongtyped.active.slick.docexamples.ActiveSlickWithCodegen.ComputersRepo
4 | import io.strongtyped.active.slick.docexamples.ActiveSlickWithCodegen.ComputersRepo.EntryExtensions
5 | import io.strongtyped.active.slick.test.H2Suite
6 | import org.scalatest._
7 | import ComputersRepo.EntryExtensions
8 | import io.strongtyped.active.slick.docexamples.codegen.Tables.ComputersRow
9 | import scala.concurrent.ExecutionContext.Implicits.global
10 | import scala.language.postfixOps
11 |
12 | class ActiveSlickWithCodegenTest extends FlatSpec with H2Suite with OptionValues {
13 |
14 | "ActiveSlickWithCodegen" should "provide crud and active record semantics for generated tables" in {
15 |
16 | // At the beginning the Computers DB should be completely empty
17 | query(ComputersRepo.findOptionById(1)) shouldBe None
18 | query(ComputersRepo.count) shouldBe 0
19 |
20 | // Add a Computer without default ID 0. Make sure it gets an ID and all data are stored
21 | val newComputer = ComputersRow(id = 0L, name = "Macbook Pro 15")
22 | val savedComputer = commit(newComputer.save())
23 | savedComputer.id should be(1L)
24 | savedComputer.name should be("Macbook Pro 15")
25 | query(ComputersRepo.count) shouldBe 1
26 |
27 | // Query the Computer we saved, update it, and delete it.
28 | query(ComputersRepo.findById(1L)) shouldBe savedComputer
29 |
30 | val updatedComputer = commit(savedComputer.copy(name = "MBP 15").save())
31 | updatedComputer.id should be(savedComputer.id)
32 | updatedComputer.name should be("MBP 15")
33 |
34 | commit(updatedComputer.delete()) shouldBe 1
35 | query(ComputersRepo.findOptionById(updatedComputer.id)) shouldBe None
36 |
37 | }
38 |
39 | override def setupDb: jdbcProfile.backend.DatabaseDef = {
40 | import jdbcProfile.api._
41 | val db = Database.forURL(
42 | url = s"jdbc:h2:mem:${this.getClass.getSimpleName};AUTOCOMMIT=TRUE;INIT=runscript from 'modules/samples/src/main/resources/codegen_schema.sql'",
43 | driver = "org.h2.Driver"
44 | )
45 | db.createSession().conn.setAutoCommit(true)
46 | db
47 | }
48 |
49 |
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/modules/shapeless/src/main/scala/io/strongtyped/active/slick/shapeless/ShapelessSlick.scala:
--------------------------------------------------------------------------------
1 | package io.strongtyped.active.slick.shapeless
2 |
3 | import io.strongtyped.active.slick.Lens
4 | import shapeless._
5 | import scala.language.implicitConversions
6 |
7 | object ShapelessSlick {
8 |
9 | /**
10 | * Converts a Shapeless lens to ActiveSlick's Lens
11 | */
12 | implicit def shapelessToSimple[O, V](lens: shapeless.Lens[O, V]): Lens[O, V] = {
13 | Lens(
14 | obj => lens.get(obj),
15 | (obj, value) => lens.set(obj)(value)
16 | )
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/project/BuildSettings.scala:
--------------------------------------------------------------------------------
1 | import sbt._
2 | import sbt.Keys._
3 | import com.typesafe.sbt.SbtScalariform
4 | import xerial.sbt.Sonatype.SonatypeKeys._
5 | import xerial.sbt.Sonatype.sonatypeSettings
6 |
7 | object BuildSettings {
8 |
9 | val projSettings = Seq(
10 | scalacOptions := Seq("-unchecked", "-deprecation", "-feature", "-Xlint")
11 | ) ++
12 | SbtScalariform.defaultScalariformSettings ++
13 | sonatypePublishSettings
14 |
15 | def sonatypePublishSettings = sonatypeSettings ++ Seq(
16 | pomExtra := https://github.com/strongtyped/active-slick
17 |
18 |
19 | Apache-style
20 | http://www.apache.org/licenses/LICENSE-2.0
21 | repo
22 |
23 |
24 |
25 | https://github.com/strongtyped/active-slick.git
26 | scm:git:git@github.com:strongtyped/active-slick.git
27 |
28 |
29 |
30 | @renatocaval
31 | Renato Cavalcanti
32 | http://www.strongtyped.io/
33 |
34 |
35 | )
36 | }
--------------------------------------------------------------------------------
/project/Dependencies.scala:
--------------------------------------------------------------------------------
1 | import sbt.Keys._
2 | import sbt._
3 |
4 | object Dependencies {
5 | val slickVersion = "3.2.0"
6 | val slick = "com.typesafe.slick" %% "slick" % slickVersion
7 |
8 | val shapelessDeps = Seq("com.chuusai" %% "shapeless" % "2.3.2")
9 | /*Def setting (
10 | CrossVersion partialVersion scalaVersion.value match {
11 | case Some((2, scalaMajor)) if scalaMajor >= 11 =>
12 | Seq("com.chuusai" %% "shapeless" % "2.3.1")
13 | case Some((2, 10)) =>
14 | Seq(
15 | "com.chuusai" % "shapeless" % "2.1.0" cross CrossVersion.full,
16 | compilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
17 | )
18 | }
19 | )*/
20 |
21 |
22 | val scalaTest = "org.scalatest" %% "scalatest" % "3.0.1" % "test"
23 | val h2database = "com.h2database" % "h2" % "1.4.181" % "test"
24 |
25 | val mainDeps = Seq(
26 | libraryDependencies ++= Seq(slick)
27 | )
28 |
29 | val testDeps = Seq(
30 | libraryDependencies ++= Seq(h2database, scalaTest)
31 | )
32 | }
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.8
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.3.0")
2 |
3 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "0.5.1")
4 |
5 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0")
6 |
--------------------------------------------------------------------------------
/project/sbt-ui.sbt:
--------------------------------------------------------------------------------
1 | // This plugin represents functionality that is to be added to sbt in the future
2 |
3 | addSbtPlugin("org.scala-sbt" % "sbt-core-next" % "0.1.1")
--------------------------------------------------------------------------------
/scripts/test-shapeless.scala:
--------------------------------------------------------------------------------
1 | import shapeless._
2 | import shapeless.Lens
3 |
4 | case class Foo(name:String, id:Int)
5 |
6 | def fooLens(lensId: Lens[Foo, Int])(foo:Foo) = println(lensId.get(foo))
7 |
8 | val foo = Foo("John", 1)
9 | val idLens = lens[Foo].id
10 |
11 | fooLens(lens[Foo] >> 'id)(foo)
12 | fooLens(idLens)(foo)
13 |
14 |
--------------------------------------------------------------------------------
/sonatype.sbt:
--------------------------------------------------------------------------------
1 | sonatypeProfileName := "io.strongtyped"
2 |
3 | credentials in Global ++= (for {
4 | username <- Option(System.getenv().get("SONATYPE_USERNAME"))
5 | password <- Option(System.getenv().get("SONATYPE_PASSWORD"))
6 | } yield Credentials("Sonatype Nexus Repository Manager", "oss.sonatype.org", username, password)).toSeq
7 |
8 |
9 | pomExtra in Global := {
10 | https://github.com/strongtyped/active-slick
11 |
12 |
13 | Apache-style
14 | http://www.apache.org/licenses/LICENSE-2.0
15 | repo
16 |
17 |
18 |
19 | https://github.com/strongtyped/active-slick.git
20 | scm:git:git@github.com:strongtyped/active-slick.git
21 |
22 |
23 |
24 | @renatocaval
25 | Renato Cavalcanti
26 | http://www.strongtyped.io/
27 |
28 |
29 | }
30 |
31 | publishTo in Global := {
32 | val nexus = "https://oss.sonatype.org/"
33 | if (isSnapshot.value)
34 | Some("snapshots" at nexus + "content/repositories/snapshots")
35 | else
36 | Some("releases" at nexus + "service/local/staging/deploy/maven2")
37 | }
--------------------------------------------------------------------------------
/src/asciidoctor/foundation-blue.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v2.1.2 | MIT License | git.io/normalize */
2 | /* ========================================================================== HTML5 display definitions ========================================================================== */
3 | /** Correct `block` display not defined in IE 8/9. */
4 | article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { display: block; }
5 |
6 | /** Correct `inline-block` display not defined in IE 8/9. */
7 | audio, canvas, video { display: inline-block; }
8 |
9 | /** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */
10 | audio:not([controls]) { display: none; height: 0; }
11 |
12 | /** Address `[hidden]` styling not present in IE 8/9. Hide the `template` element in IE, Safari, and Firefox < 22. */
13 | [hidden], template { display: none; }
14 |
15 | script { display: none !important; }
16 |
17 | /* ========================================================================== Base ========================================================================== */
18 | /** 1. Set default font family to sans-serif. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */
19 | html { font-family: sans-serif; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ }
20 |
21 | /** Remove default margin. */
22 | body { margin: 0; }
23 |
24 | /* ========================================================================== Links ========================================================================== */
25 | /** Remove the gray background color from active links in IE 10. */
26 | a { background: transparent; }
27 |
28 | /** Address `outline` inconsistency between Chrome and other browsers. */
29 | a:focus { outline: thin dotted; }
30 |
31 | /** Improve readability when focused and also mouse hovered in all browsers. */
32 | a:active, a:hover { outline: 0; }
33 |
34 | /* ========================================================================== Typography ========================================================================== */
35 | /** Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari 5, and Chrome. */
36 | h1 { font-size: 2em; margin: 0.67em 0; }
37 |
38 | /** Address styling not present in IE 8/9, Safari 5, and Chrome. */
39 | abbr[title] { border-bottom: 1px dotted; }
40 |
41 | /** Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */
42 | b, strong { font-weight: bold; }
43 |
44 | /** Address styling not present in Safari 5 and Chrome. */
45 | dfn { font-style: italic; }
46 |
47 | /** Address differences between Firefox and other browsers. */
48 | hr { -moz-box-sizing: content-box; box-sizing: content-box; height: 0; }
49 |
50 | /** Address styling not present in IE 8/9. */
51 | mark { background: #ff0; color: #000; }
52 |
53 | /** Correct font family set oddly in Safari 5 and Chrome. */
54 | code, kbd, pre, samp { font-family: monospace, serif; font-size: 1em; }
55 |
56 | /** Improve readability of pre-formatted text in all browsers. */
57 | pre { white-space: pre-wrap; }
58 |
59 | /** Set consistent quote types. */
60 | q { quotes: "\201C" "\201D" "\2018" "\2019"; }
61 |
62 | /** Address inconsistent and variable font size in all browsers. */
63 | small { font-size: 80%; }
64 |
65 | /** Prevent `sub` and `sup` affecting `line-height` in all browsers. */
66 | sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
67 |
68 | sup { top: -0.5em; }
69 |
70 | sub { bottom: -0.25em; }
71 |
72 | /* ========================================================================== Embedded content ========================================================================== */
73 | /** Remove border when inside `a` element in IE 8/9. */
74 | img { border: 0; }
75 |
76 | /** Correct overflow displayed oddly in IE 9. */
77 | svg:not(:root) { overflow: hidden; }
78 |
79 | /* ========================================================================== Figures ========================================================================== */
80 | /** Address margin not present in IE 8/9 and Safari 5. */
81 | figure { margin: 0; }
82 |
83 | /* ========================================================================== Forms ========================================================================== */
84 | /** Define consistent border, margin, and padding. */
85 | fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; }
86 |
87 | /** 1. Correct `color` not being inherited in IE 8/9. 2. Remove padding so people aren't caught out if they zero out fieldsets. */
88 | legend { border: 0; /* 1 */ padding: 0; /* 2 */ }
89 |
90 | /** 1. Correct font family not being inherited in all browsers. 2. Correct font size not being inherited in all browsers. 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. */
91 | button, input, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 2 */ margin: 0; /* 3 */ }
92 |
93 | /** Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet. */
94 | button, input { line-height: normal; }
95 |
96 | /** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. Correct `select` style inheritance in Firefox 4+ and Opera. */
97 | button, select { text-transform: none; }
98 |
99 | /** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. */
100 | button, html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ }
101 |
102 | /** Re-set default cursor for disabled elements. */
103 | button[disabled], html input[disabled] { cursor: default; }
104 |
105 | /** 1. Address box sizing set to `content-box` in IE 8/9. 2. Remove excess padding in IE 8/9. */
106 | input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ }
107 |
108 | /** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */
109 | input[type="search"] { -webkit-appearance: textfield; /* 1 */ -moz-box-sizing: content-box; -webkit-box-sizing: content-box; /* 2 */ box-sizing: content-box; }
110 |
111 | /** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */
112 | input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; }
113 |
114 | /** Remove inner padding and border in Firefox 4+. */
115 | button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }
116 |
117 | /** 1. Remove default vertical scrollbar in IE 8/9. 2. Improve readability and alignment in all browsers. */
118 | textarea { overflow: auto; /* 1 */ vertical-align: top; /* 2 */ }
119 |
120 | /* ========================================================================== Tables ========================================================================== */
121 | /** Remove most spacing between table cells. */
122 | table { border-collapse: collapse; border-spacing: 0; }
123 |
124 | meta.foundation-mq-small { font-family: "only screen and (min-width: 768px)"; width: 768px; }
125 |
126 | meta.foundation-mq-medium { font-family: "only screen and (min-width:1280px)"; width: 1280px; }
127 |
128 | meta.foundation-mq-large { font-family: "only screen and (min-width:1440px)"; width: 1440px; }
129 |
130 | *, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; }
131 |
132 | html, body { font-size: 100%; }
133 |
134 | body { background: white; color: #222222; padding: 0; margin: 0; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; line-height: 1; position: relative; cursor: auto; }
135 |
136 | a:hover { cursor: pointer; }
137 |
138 | img, object, embed { max-width: 100%; height: auto; }
139 |
140 | object, embed { height: 100%; }
141 |
142 | img { -ms-interpolation-mode: bicubic; }
143 |
144 | #map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { max-width: none !important; }
145 |
146 | .left { float: left !important; }
147 |
148 | .right { float: right !important; }
149 |
150 | .text-left { text-align: left !important; }
151 |
152 | .text-right { text-align: right !important; }
153 |
154 | .text-center { text-align: center !important; }
155 |
156 | .text-justify { text-align: justify !important; }
157 |
158 | .hide { display: none; }
159 |
160 | .antialiased, body { -webkit-font-smoothing: antialiased; }
161 |
162 | img { display: inline-block; vertical-align: middle; }
163 |
164 | textarea { height: auto; min-height: 50px; }
165 |
166 | select { width: 100%; }
167 |
168 | p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { font-size: 1.21875em; line-height: 1.6; }
169 |
170 | .subheader, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { line-height: 1.4; color: #980050; font-weight: 300; margin-top: 0.2em; margin-bottom: 0.5em; }
171 |
172 | /* Typography resets */
173 | div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; direction: ltr; }
174 |
175 | /* Default Link Styles */
176 | a { color: #647da1; text-decoration: none; line-height: inherit; }
177 | a:hover, a:focus { color: #640035; }
178 | a img { border: none; }
179 |
180 | /* Default paragraph styles */
181 | p { font-family: inherit; font-weight: normal; font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; text-rendering: optimizeLegibility; }
182 | p aside { font-size: 0.875em; line-height: 1.35; font-style: italic; }
183 |
184 | /* Default header styles */
185 | h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; color: #647da1; text-rendering: optimizeLegibility; margin-top: 1em; margin-bottom: 0.5em; line-height: 1.2125em; }
186 | h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small { font-size: 60%; color: gray; line-height: 0; }
187 |
188 | h1 { font-size: 2.125em; }
189 |
190 | h2 { font-size: 1.6875em; }
191 |
192 | h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.375em; }
193 |
194 | h4 { font-size: 1.125em; }
195 |
196 | h5 { font-size: 1.125em; }
197 |
198 | h6 { font-size: 1em; }
199 |
200 | hr { border: dotted #cccccc; border-width: 1px 0 0; clear: both; margin: 1.25em 0 1.1875em; height: 0; }
201 |
202 | /* Helpful Typography Defaults */
203 | em, i { font-style: italic; line-height: inherit; }
204 |
205 | strong, b { font-weight: bold; line-height: inherit; }
206 |
207 | small { font-size: 60%; line-height: inherit; }
208 |
209 | code { font-family: Consolas, "Liberation Mono", Courier, monospace; font-weight: bold; color: #320348; }
210 |
211 | /* Lists */
212 | ul, ol, dl { font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; list-style-position: outside; font-family: inherit; }
213 |
214 | ul, ol { margin-left: 1.5em; }
215 | ul.no-bullet, ol.no-bullet { margin-left: 1.5em; }
216 |
217 | /* Unordered Lists */
218 | ul li ul, ul li ol { margin-left: 1.25em; margin-bottom: 0; font-size: 1em; /* Override nested font-size change */ }
219 | ul.square li ul, ul.circle li ul, ul.disc li ul { list-style: inherit; }
220 | ul.square { list-style-type: square; }
221 | ul.circle { list-style-type: circle; }
222 | ul.disc { list-style-type: disc; }
223 | ul.no-bullet { list-style: none; }
224 |
225 | /* Ordered Lists */
226 | ol li ul, ol li ol { margin-left: 1.25em; margin-bottom: 0; }
227 |
228 | /* Definition Lists */
229 | dl dt { margin-bottom: 0.3125em; font-weight: bold; }
230 | dl dd { margin-bottom: 1.25em; }
231 |
232 | /* Abbreviations */
233 | abbr, acronym { text-transform: uppercase; font-size: 90%; color: #555555; border-bottom: 1px dotted #dddddd; cursor: help; }
234 |
235 | abbr { text-transform: none; }
236 |
237 | /* Blockquotes */
238 | blockquote { margin: 0 0 1.25em; padding: 0.5625em 1.25em 0 1.1875em; border-left: 1px solid #efefef; }
239 | blockquote cite { display: block; font-size: 0.8125em; color: #5e5e5e; }
240 | blockquote cite:before { content: "\2014 \0020"; }
241 | blockquote cite a, blockquote cite a:visited { color: #5e5e5e; }
242 |
243 | blockquote, blockquote p { line-height: 1.6; color: #777777; }
244 |
245 | /* Microformats */
246 | .vcard { display: inline-block; margin: 0 0 1.25em 0; border: 1px solid #dddddd; padding: 0.625em 0.75em; }
247 | .vcard li { margin: 0; display: block; }
248 | .vcard .fn { font-weight: bold; font-size: 0.9375em; }
249 |
250 | .vevent .summary { font-weight: bold; }
251 | .vevent abbr { cursor: auto; text-decoration: none; font-weight: bold; border: none; padding: 0 0.0625em; }
252 |
253 | @media only screen and (min-width: 768px) { h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; }
254 | h1 { font-size: 2.75em; }
255 | h2 { font-size: 2.3125em; }
256 | h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.6875em; }
257 | h4 { font-size: 1.4375em; } }
258 | /* Tables */
259 | table { background: white; margin-bottom: 1.25em; border: solid 1px #dddddd; }
260 | table thead, table tfoot { background: whitesmoke; font-weight: normal; }
261 | table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { padding: 0.5em 0.625em 0.625em; font-size: inherit; color: #333333; text-align: left; }
262 | table tr th, table tr td { padding: 0.5625em 0.625em; font-size: inherit; color: #555555; }
263 | table tr.even, table tr.alt, table tr:nth-of-type(even) { background: #f9f9f9; }
264 | table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { display: table-cell; line-height: 1.4; }
265 |
266 | h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; }
267 |
268 | .clearfix:before, .clearfix:after, .float-group:before, .float-group:after { content: " "; display: table; }
269 | .clearfix:after, .float-group:after { clear: both; }
270 |
271 | *:not(pre) > code { font-size: inherit; font-style: normal !important; letter-spacing: 0; padding: 0; line-height: inherit; }
272 |
273 | pre, pre > code { line-height: 1.4; color: black; font-family: monospace, serif; font-weight: normal; }
274 |
275 | .keyseq { color: #888888; }
276 |
277 | kbd { display: inline-block; color: #555555; font-size: 0.75em; line-height: 1.4; background-color: #f7f7f7; border: 1px solid #ccc; -webkit-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; margin: -0.15em 0.15em 0 0.15em; padding: 0.2em 0.6em 0.2em 0.5em; vertical-align: middle; white-space: nowrap; }
278 |
279 | .keyseq kbd:first-child { margin-left: 0; }
280 |
281 | .keyseq kbd:last-child { margin-right: 0; }
282 |
283 | .menuseq, .menu { color: #3b3b3b; }
284 |
285 | b.button:before, b.button:after { position: relative; top: -1px; font-weight: normal; }
286 |
287 | b.button:before { content: "["; padding: 0 3px 0 2px; }
288 |
289 | b.button:after { content: "]"; padding: 0 2px 0 3px; }
290 |
291 | #header, #content, #footnotes, #footer { width: 100%; margin-left: auto; margin-right: auto; margin-top: 0; margin-bottom: 0; max-width: 62.5em; *zoom: 1; position: relative; padding-left: 0.9375em; padding-right: 0.9375em; }
292 | #header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { content: " "; display: table; }
293 | #header:after, #content:after, #footnotes:after, #footer:after { clear: both; }
294 |
295 | #content { margin-top: 1.25em; }
296 |
297 | #content:before { content: none; }
298 |
299 | #header > h1:first-child { color: #647da1; margin-top: 2.25rem; margin-bottom: 0; }
300 | #header > h1:first-child + #toc { margin-top: 8px; border-top: 1px dotted #cccccc; }
301 | #header > h1:only-child, body.toc2 #header > h1:nth-last-child(2) { border-bottom: 1px dotted #cccccc; padding-bottom: 8px; }
302 | #header .details { border-bottom: 1px dotted #cccccc; line-height: 1.45; padding-top: 0.25em; padding-bottom: 0.25em; padding-left: 0.25em; color: #5e5e5e; display: -ms-flexbox; display: -webkit-flex; display: flex; -ms-flex-flow: row wrap; -webkit-flex-flow: row wrap; flex-flow: row wrap; }
303 | #header .details span:first-child { margin-left: -0.125em; }
304 | #header .details span.email a { color: #777777; }
305 | #header .details br { display: none; }
306 | #header .details br + span:before { content: "\00a0\2013\00a0"; }
307 | #header .details br + span.author:before { content: "\00a0\22c5\00a0"; color: #777777; }
308 | #header .details br + span#revremark:before { content: "\00a0|\00a0"; }
309 | #header #revnumber { text-transform: capitalize; }
310 | #header #revnumber:after { content: "\00a0"; }
311 |
312 | #content > h1:first-child:not([class]) { color: #647da1; border-bottom: 1px dotted #cccccc; padding-bottom: 8px; margin-top: 0; padding-top: 1rem; margin-bottom: 1.25rem; }
313 |
314 | #toc { border-bottom: 1px solid #cccccc; padding-bottom: 0.5em; }
315 | #toc > ul { margin-left: 0.125em; }
316 | #toc ul.sectlevel0 > li > a { font-style: italic; }
317 | #toc ul.sectlevel0 ul.sectlevel1 { margin: 0.5em 0; }
318 | #toc ul { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; list-style-type: none; }
319 | #toc a { text-decoration: none; }
320 | #toc a:active { text-decoration: underline; }
321 |
322 | #toctitle { color: #980050; font-size: 1.2em; }
323 |
324 | @media only screen and (min-width: 768px) { #toctitle { font-size: 1.375em; }
325 | body.toc2 { padding-left: 15em; padding-right: 0; }
326 | #toc.toc2 { margin-top: 0 !important; background-color: #f2f2f2; position: fixed; width: 15em; left: 0; top: 0; border-right: 1px solid #cccccc; border-top-width: 0 !important; border-bottom-width: 0 !important; z-index: 1000; padding: 1.25em 1em; height: 100%; overflow: auto; }
327 | #toc.toc2 #toctitle { margin-top: 0; font-size: 1.2em; }
328 | #toc.toc2 > ul { font-size: 0.9em; margin-bottom: 0; }
329 | #toc.toc2 ul ul { margin-left: 0; padding-left: 1em; }
330 | #toc.toc2 ul.sectlevel0 ul.sectlevel1 { padding-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; }
331 | body.toc2.toc-right { padding-left: 0; padding-right: 15em; }
332 | body.toc2.toc-right #toc.toc2 { border-right-width: 0; border-left: 1px solid #cccccc; left: auto; right: 0; } }
333 | @media only screen and (min-width: 1280px) { body.toc2 { padding-left: 20em; padding-right: 0; }
334 | #toc.toc2 { width: 20em; }
335 | #toc.toc2 #toctitle { font-size: 1.375em; }
336 | #toc.toc2 > ul { font-size: 0.95em; }
337 | #toc.toc2 ul ul { padding-left: 1.25em; }
338 | body.toc2.toc-right { padding-left: 0; padding-right: 20em; } }
339 | #content #toc { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; }
340 | #content #toc > :first-child { margin-top: 0; }
341 | #content #toc > :last-child { margin-bottom: 0; }
342 |
343 | #footer { max-width: 100%; background-color: #272727; padding: 1.25em; }
344 |
345 | #footer-text { color: #bcbcbc; line-height: 1.44; }
346 |
347 | .sect1 { padding-bottom: 0.625em; }
348 |
349 | @media only screen and (min-width: 768px) { .sect1 { padding-bottom: 1.25em; } }
350 | .sect1 + .sect1 { border-top: 1px solid #cccccc; }
351 |
352 | #content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { position: absolute; z-index: 1001; width: 1.5ex; margin-left: -1.5ex; display: block; text-decoration: none !important; visibility: hidden; text-align: center; font-weight: normal; }
353 | #content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { content: "\00A7"; font-size: 0.85em; display: block; padding-top: 0.1em; }
354 | #content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover { visibility: visible; }
355 | #content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link { color: #333333; text-decoration: none; }
356 | #content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover { color: #262626; }
357 |
358 | .audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock { margin-bottom: 1.25em; }
359 |
360 | .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { text-rendering: optimizeLegibility; text-align: left; }
361 |
362 | table.tableblock > caption.title { white-space: nowrap; overflow: visible; max-width: 0; }
363 |
364 | .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { color: #647da1; }
365 |
366 | table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { font-size: inherit; }
367 |
368 | .admonitionblock > table { border-collapse: separate; border: 0; background: none; width: 100%; }
369 | .admonitionblock > table td.icon { text-align: center; width: 80px; }
370 | .admonitionblock > table td.icon img { max-width: none; }
371 | .admonitionblock > table td.icon .title { font-weight: bold; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; text-transform: uppercase; }
372 | .admonitionblock > table td.content { padding-left: 1.125em; padding-right: 1.25em; border-left: 1px dotted #cccccc; color: #5e5e5e; }
373 | .admonitionblock > table td.content > :last-child > :last-child { margin-bottom: 0; }
374 |
375 | .exampleblock > .content { border-style: solid; border-width: 1px; border-color: #e6e6e6; margin-bottom: 1.25em; padding: 1.25em; background: white; -webkit-border-radius: 0; border-radius: 0; }
376 | .exampleblock > .content > :first-child { margin-top: 0; }
377 | .exampleblock > .content > :last-child { margin-bottom: 0; }
378 |
379 | .sidebarblock { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; }
380 | .sidebarblock > :first-child { margin-top: 0; }
381 | .sidebarblock > :last-child { margin-bottom: 0; }
382 | .sidebarblock > .content > .title { color: #980050; margin-top: 0; }
383 |
384 | .exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child { margin-bottom: 0; }
385 |
386 | .literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint { background: #efefef; }
387 | .sidebarblock .literalblock pre, .sidebarblock .listingblock pre:not(.highlight), .sidebarblock .listingblock pre[class="highlight"], .sidebarblock .listingblock pre[class^="highlight "], .sidebarblock .listingblock pre.CodeRay, .sidebarblock .listingblock pre.prettyprint { background: #f2f1f1; }
388 |
389 | .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { border: 1px solid #cccccc; -webkit-border-radius: 0; border-radius: 0; word-wrap: break-word; padding: 0.75em 0.75em 0.625em 0.75em; font-size: 0.8125em; }
390 | .literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap { overflow-x: auto; white-space: pre; word-wrap: normal; }
391 | @media only screen and (min-width: 768px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 0.90625em; } }
392 | @media only screen and (min-width: 1280px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 1em; } }
393 |
394 | .literalblock.output pre { color: #efefef; background-color: black; }
395 |
396 | .listingblock pre.highlightjs { padding: 0; }
397 | .listingblock pre.highlightjs > code { padding: 0.75em 0.75em 0.625em 0.75em; -webkit-border-radius: 0; border-radius: 0; }
398 |
399 | .listingblock > .content { position: relative; }
400 |
401 | .listingblock code[data-lang]:before { display: none; content: attr(data-lang); position: absolute; font-size: 0.75em; top: 0.425rem; right: 0.5rem; line-height: 1; text-transform: uppercase; color: #999; }
402 |
403 | .listingblock:hover code[data-lang]:before { display: block; }
404 |
405 | .listingblock.terminal pre .command:before { content: attr(data-prompt); padding-right: 0.5em; color: #999; }
406 |
407 | .listingblock.terminal pre .command:not([data-prompt]):before { content: "$"; }
408 |
409 | table.pyhltable { border-collapse: separate; border: 0; margin-bottom: 0; background: none; }
410 |
411 | table.pyhltable td { vertical-align: top; padding-top: 0; padding-bottom: 0; }
412 |
413 | table.pyhltable td.code { padding-left: .75em; padding-right: 0; }
414 |
415 | pre.pygments .lineno, table.pyhltable td:not(.code) { color: #999; padding-left: 0; padding-right: .5em; border-right: 1px solid #cccccc; }
416 |
417 | pre.pygments .lineno { display: inline-block; margin-right: .25em; }
418 |
419 | table.pyhltable .linenodiv { background: none !important; padding-right: 0 !important; }
420 |
421 | .quoteblock { margin: 0 1em 1.25em 1.5em; display: table; }
422 | .quoteblock > .title { margin-left: -1.5em; margin-bottom: 0.75em; }
423 | .quoteblock blockquote, .quoteblock blockquote p { color: #777777; font-size: 1.15rem; line-height: 1.75; word-spacing: 0.1em; letter-spacing: 0; font-style: italic; text-align: justify; }
424 | .quoteblock blockquote { margin: 0; padding: 0; border: 0; }
425 | .quoteblock blockquote:before { content: "\201c"; float: left; font-size: 2.75em; font-weight: bold; line-height: 0.6em; margin-left: -0.6em; color: #980050; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); }
426 | .quoteblock blockquote > .paragraph:last-child p { margin-bottom: 0; }
427 | .quoteblock .attribution { margin-top: 0.5em; margin-right: 0.5ex; text-align: right; }
428 | .quoteblock .quoteblock { margin-left: 0; margin-right: 0; padding: 0.5em 0; border-left: 3px solid #5e5e5e; }
429 | .quoteblock .quoteblock blockquote { padding: 0 0 0 0.75em; }
430 | .quoteblock .quoteblock blockquote:before { display: none; }
431 |
432 | .verseblock { margin: 0 1em 1.25em 1em; }
433 | .verseblock pre { font-family: "Open Sans", "DejaVu Sans", sans; font-size: 1.15rem; color: #777777; font-weight: 300; text-rendering: optimizeLegibility; }
434 | .verseblock pre strong { font-weight: 400; }
435 | .verseblock .attribution { margin-top: 1.25rem; margin-left: 0.5ex; }
436 |
437 | .quoteblock .attribution, .verseblock .attribution { font-size: 0.8125em; line-height: 1.45; font-style: italic; }
438 | .quoteblock .attribution br, .verseblock .attribution br { display: none; }
439 | .quoteblock .attribution cite, .verseblock .attribution cite { display: block; letter-spacing: -0.05em; color: #5e5e5e; }
440 |
441 | .quoteblock.abstract { margin: 0 0 1.25em 0; display: block; }
442 | .quoteblock.abstract blockquote, .quoteblock.abstract blockquote p { text-align: left; word-spacing: 0; }
443 | .quoteblock.abstract blockquote:before, .quoteblock.abstract blockquote p:first-of-type:before { display: none; }
444 |
445 | table.tableblock { max-width: 100%; border-collapse: separate; }
446 | table.tableblock td > .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child { margin-bottom: 0; }
447 |
448 | table.spread { width: 100%; }
449 |
450 | table.tableblock, th.tableblock, td.tableblock { border: 0 solid #dddddd; }
451 |
452 | table.grid-all th.tableblock, table.grid-all td.tableblock { border-width: 0 1px 1px 0; }
453 |
454 | table.grid-all tfoot > tr > th.tableblock, table.grid-all tfoot > tr > td.tableblock { border-width: 1px 1px 0 0; }
455 |
456 | table.grid-cols th.tableblock, table.grid-cols td.tableblock { border-width: 0 1px 0 0; }
457 |
458 | table.grid-all * > tr > .tableblock:last-child, table.grid-cols * > tr > .tableblock:last-child { border-right-width: 0; }
459 |
460 | table.grid-rows th.tableblock, table.grid-rows td.tableblock { border-width: 0 0 1px 0; }
461 |
462 | table.grid-all tbody > tr:last-child > th.tableblock, table.grid-all tbody > tr:last-child > td.tableblock, table.grid-all thead:last-child > tr > th.tableblock, table.grid-rows tbody > tr:last-child > th.tableblock, table.grid-rows tbody > tr:last-child > td.tableblock, table.grid-rows thead:last-child > tr > th.tableblock { border-bottom-width: 0; }
463 |
464 | table.grid-rows tfoot > tr > th.tableblock, table.grid-rows tfoot > tr > td.tableblock { border-width: 1px 0 0 0; }
465 |
466 | table.frame-all { border-width: 1px; }
467 |
468 | table.frame-sides { border-width: 0 1px; }
469 |
470 | table.frame-topbot { border-width: 1px 0; }
471 |
472 | th.halign-left, td.halign-left { text-align: left; }
473 |
474 | th.halign-right, td.halign-right { text-align: right; }
475 |
476 | th.halign-center, td.halign-center { text-align: center; }
477 |
478 | th.valign-top, td.valign-top { vertical-align: top; }
479 |
480 | th.valign-bottom, td.valign-bottom { vertical-align: bottom; }
481 |
482 | th.valign-middle, td.valign-middle { vertical-align: middle; }
483 |
484 | table thead th, table tfoot th { font-weight: normal; }
485 |
486 | tbody tr th { display: table-cell; line-height: 1.4; background: whitesmoke; }
487 |
488 | tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { color: #333333; font-weight: normal; }
489 |
490 | p.tableblock > code:only-child { background: none; padding: 0; }
491 |
492 | p.tableblock { font-size: 1em; }
493 |
494 | td > div.verse { white-space: pre; }
495 |
496 | ol { margin-left: 1.75em; }
497 |
498 | ul li ol { margin-left: 1.5em; }
499 |
500 | dl dd { margin-left: 1.125em; }
501 |
502 | dl dd:last-child, dl dd:last-child > :last-child { margin-bottom: 0; }
503 |
504 | ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist { margin-bottom: 0.625em; }
505 |
506 | ul.unstyled, ol.unnumbered, ul.checklist, ul.none { list-style-type: none; }
507 |
508 | ul.unstyled, ol.unnumbered, ul.checklist { margin-left: 0.625em; }
509 |
510 | ul.checklist li > p:first-child > .fa-square-o:first-child, ul.checklist li > p:first-child > .fa-check-square-o:first-child { width: 1em; font-size: 0.85em; }
511 |
512 | ul.checklist li > p:first-child > input[type="checkbox"]:first-child { width: 1em; position: relative; top: 1px; }
513 |
514 | ul.inline { margin: 0 auto 0.625em auto; margin-left: -1.375em; margin-right: 0; padding: 0; list-style: none; overflow: hidden; }
515 | ul.inline > li { list-style: none; float: left; margin-left: 1.375em; display: block; }
516 | ul.inline > li > * { display: block; }
517 |
518 | .unstyled dl dt { font-weight: normal; font-style: normal; }
519 |
520 | ol.arabic { list-style-type: decimal; }
521 |
522 | ol.decimal { list-style-type: decimal-leading-zero; }
523 |
524 | ol.loweralpha { list-style-type: lower-alpha; }
525 |
526 | ol.upperalpha { list-style-type: upper-alpha; }
527 |
528 | ol.lowerroman { list-style-type: lower-roman; }
529 |
530 | ol.upperroman { list-style-type: upper-roman; }
531 |
532 | ol.lowergreek { list-style-type: lower-greek; }
533 |
534 | .hdlist > table, .colist > table { border: 0; background: none; }
535 | .hdlist > table > tbody > tr, .colist > table > tbody > tr { background: none; }
536 |
537 | td.hdlist1 { padding-right: .75em; font-weight: bold; }
538 |
539 | td.hdlist1, td.hdlist2 { vertical-align: top; }
540 |
541 | .literalblock + .colist, .listingblock + .colist { margin-top: -0.5em; }
542 |
543 | .colist > table tr > td:first-of-type { padding: 0 0.75em; line-height: 1; }
544 | .colist > table tr > td:last-of-type { padding: 0.25em 0; }
545 |
546 | .thumb, .th { line-height: 0; display: inline-block; border: solid 4px white; -webkit-box-shadow: 0 0 0 1px #dddddd; box-shadow: 0 0 0 1px #dddddd; }
547 |
548 | .imageblock.left, .imageblock[style*="float: left"] { margin: 0.25em 0.625em 1.25em 0; }
549 | .imageblock.right, .imageblock[style*="float: right"] { margin: 0.25em 0 1.25em 0.625em; }
550 | .imageblock > .title { margin-bottom: 0; }
551 | .imageblock.thumb, .imageblock.th { border-width: 6px; }
552 | .imageblock.thumb > .title, .imageblock.th > .title { padding: 0 0.125em; }
553 |
554 | .image.left, .image.right { margin-top: 0.25em; margin-bottom: 0.25em; display: inline-block; line-height: 0; }
555 | .image.left { margin-right: 0.625em; }
556 | .image.right { margin-left: 0.625em; }
557 |
558 | a.image { text-decoration: none; }
559 |
560 | span.footnote, span.footnoteref { vertical-align: super; font-size: 0.875em; }
561 | span.footnote a, span.footnoteref a { text-decoration: none; }
562 | span.footnote a:active, span.footnoteref a:active { text-decoration: underline; }
563 |
564 | #footnotes { padding-top: 0.75em; padding-bottom: 0.75em; margin-bottom: 0.625em; }
565 | #footnotes hr { width: 20%; min-width: 6.25em; margin: -.25em 0 .75em 0; border-width: 1px 0 0 0; }
566 | #footnotes .footnote { padding: 0 0.375em; line-height: 1.3; font-size: 0.875em; margin-left: 1.2em; text-indent: -1.2em; margin-bottom: .2em; }
567 | #footnotes .footnote a:first-of-type { font-weight: bold; text-decoration: none; }
568 | #footnotes .footnote:last-of-type { margin-bottom: 0; }
569 |
570 | #content #footnotes { margin-top: -0.625em; margin-bottom: 0; padding: 0.75em 0; }
571 |
572 | .gist .file-data > table { border: 0; background: #fff; width: 100%; margin-bottom: 0; }
573 | .gist .file-data > table td.line-data { width: 99%; }
574 |
575 | div.unbreakable { page-break-inside: avoid; }
576 |
577 | .big { font-size: larger; }
578 |
579 | .small { font-size: smaller; }
580 |
581 | .underline { text-decoration: underline; }
582 |
583 | .overline { text-decoration: overline; }
584 |
585 | .line-through { text-decoration: line-through; }
586 |
587 | .aqua { color: #00bfbf; }
588 |
589 | .aqua-background { background-color: #00fafa; }
590 |
591 | .black { color: black; }
592 |
593 | .black-background { background-color: black; }
594 |
595 | .blue { color: #0000bf; }
596 |
597 | .blue-background { background-color: #0000fa; }
598 |
599 | .fuchsia { color: #bf00bf; }
600 |
601 | .fuchsia-background { background-color: #fa00fa; }
602 |
603 | .gray { color: #606060; }
604 |
605 | .gray-background { background-color: #7d7d7d; }
606 |
607 | .green { color: #006000; }
608 |
609 | .green-background { background-color: #007d00; }
610 |
611 | .lime { color: #00bf00; }
612 |
613 | .lime-background { background-color: #00fa00; }
614 |
615 | .maroon { color: #600000; }
616 |
617 | .maroon-background { background-color: #7d0000; }
618 |
619 | .navy { color: #000060; }
620 |
621 | .navy-background { background-color: #00007d; }
622 |
623 | .olive { color: #606000; }
624 |
625 | .olive-background { background-color: #7d7d00; }
626 |
627 | .purple { color: #600060; }
628 |
629 | .purple-background { background-color: #7d007d; }
630 |
631 | .red { color: #bf0000; }
632 |
633 | .red-background { background-color: #fa0000; }
634 |
635 | .silver { color: #909090; }
636 |
637 | .silver-background { background-color: #bcbcbc; }
638 |
639 | .teal { color: #006060; }
640 |
641 | .teal-background { background-color: #007d7d; }
642 |
643 | .white { color: #bfbfbf; }
644 |
645 | .white-background { background-color: #fafafa; }
646 |
647 | .yellow { color: #bfbf00; }
648 |
649 | .yellow-background { background-color: #fafa00; }
650 |
651 | span.icon > .fa { cursor: default; }
652 |
653 | .admonitionblock td.icon [class^="fa icon-"] { font-size: 2.5em; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); cursor: default; }
654 | .admonitionblock td.icon .icon-note:before { content: "\f05a"; color: #850046; }
655 | .admonitionblock td.icon .icon-tip:before { content: "\f0eb"; text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8); color: #111; }
656 | .admonitionblock td.icon .icon-warning:before { content: "\f071"; color: #bf6900; }
657 | .admonitionblock td.icon .icon-caution:before { content: "\f06d"; color: #bf3400; }
658 | .admonitionblock td.icon .icon-important:before { content: "\f06a"; color: #bf0000; }
659 |
660 | .conum[data-value] { display: inline-block; color: #fff !important; background-color: #555555; -webkit-border-radius: 100px; border-radius: 100px; text-align: center; font-size: 0.75em; width: 1.67em; height: 1.67em; line-height: 1.67em; font-family: "Open Sans", "DejaVu Sans", sans-serif; font-style: normal; font-weight: bold; }
661 | .conum[data-value] * { color: #fff !important; }
662 | .conum[data-value] + b { display: none; }
663 | .conum[data-value]:after { content: attr(data-value); }
664 | pre .conum[data-value] { position: relative; top: -0.125em; }
665 |
666 | b.conum * { color: inherit !important; }
667 |
668 | .conum:not([data-value]):empty { display: none; }
669 |
670 | #header > h1 { border-bottom-style: solid; }
671 |
672 | #toctitle { color: #333333; }
673 |
674 | .listingblock pre, .literalblock pre { background: -moz-linear-gradient(top, white 0%, #f4f4f4 100%); background: -webkit-linear-gradient(top, white 0%, #f4f4f4 100%); background: linear-gradient(to bottom, #ffffff 0%, #f4f4f4 100%); -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); }
675 |
676 | .sidebarblock { background: none; border: none; -webkit-box-shadow: 0 0 5px rgba(177, 0, 93, 0.5); box-shadow: 0 0 5px rgba(177, 0, 93, 0.5); }
677 | .sidebarblock > .content > .title { color: #181818; }
678 |
--------------------------------------------------------------------------------
/src/asciidoctor/index.adoc:
--------------------------------------------------------------------------------
1 | = ActiveSlick
2 | :toc: macro
3 | :linkcss:
4 | :icons: font
5 | :source-highlighter: pygments
6 | :pygments-style: monokai
7 | :pygments-linenums-mode: inline
8 | :author: Renato Cavalcanti - Strong[Typed]
9 |
10 |
11 | ActiveSlick is a library that offers CRUD operations for Slick 3.x projects. The main goal is to provide some basic operations to manage the lifecycle of persisted objects (new/persisted/deleted/stale) and enable the implementation of the Active Record Pattern on Slick mapped case classes.
12 |
13 | toc::[]
14 |
15 | == Main features
16 | - Basic CRUD and auxiliary methods - add/update/save, delete, findById, count and fetchAll (backed by Reactive Streams).
17 | - Generic Id type.
18 | - Optimistic locking my means of incremental version.
19 | - Before insert and update hooks.
20 | - **ActiveRecord** to enable the Active Record Pattern on mapped case classes via class extensions (pimp-my-library style)
21 |
22 | == Project artifact
23 |
24 | The artifacts are published to Sonatype Repository. Simply add the following to your build.sbt.
25 |
26 | As of version 0.3.x we don't support Slick 2.0 anymore. The differences between Slick 2.x and Slick 3.x are so huge that it makes impossible to support two versions.
27 |
28 | [source,scala]
29 | ----
30 | libraryDependencies += "io.strongtyped" %% "active-slick" % "0.3.5"
31 | ----
32 | Source code for version 0.3.5 can be found at:
33 | https://github.com/strongtyped/active-slick/tree/v0.3.5
34 |
35 |
36 | The version supporting Slick 2.0 is still available on Sonatype Repo. However, this documentation only covers the current series (i.e.: 0.3.x).
37 | [source,scala]
38 | ----
39 | libraryDependencies += "io.strongtyped" %% "active-slick" % "0.2.2"
40 | ----
41 |
42 | Source code for version 0.2.2 can be found at:
43 | https://github.com/strongtyped/active-slick/tree/v0.2.2
44 |
45 |
46 | == Motivation
47 |
48 | Slick is able to map result sets to case classes or Tuples because of its isomorphism. This is done thanks to built-in Scala features. However, there is no direct link between case class fields and database columns. Everything is done based on isomorphic projections.
49 |
50 | As a consequence, managing of Entities IDs must be done by hand, over and over again. One needs to save a model, ask Slick to return the generated ID and add it explicitly to the case class.
51 |
52 | The following code fragment demonstrates how this is typically done in a Slick application.
53 |
54 |
55 | [source,scala]
56 | ----
57 | include::../../modules/samples/src/main/scala/io/strongtyped/active/slick/docexamples/MappingWithoutActiveSlick.scala[tags=adoc]
58 | ----
59 |
60 | <1> Both **Coffee** and **CoffeeTable** have an **Id** representing the primary key. **Coffee** has a field of type **Option[Int]**
61 | <2> **CoffeeTable** has a method returning a **Rep[Int]**.
62 | <3> However, in order to inserta new Coffee we need some boilerplate to get back the generated ID.
63 |
64 | If we manage to connect both **ID** we can provide some generic functionality to manage Entities. Inserting a new `Entity` is only one of the possible use cases. With a well known ID, we can distinguish if an `Entity` has been already persisted or not, we can easilly implement byId methods (`deleteById`, `findById`) and we can add extensions methods like: `save`, `udpate`, `delete`.
65 |
66 |
67 | == History
68 | The first intention of this project was to implement Slick DAOs or Repositories using only Scala features and the Scala compiler (no code generation).
69 |
70 | In the time of Slick 2.0, this library required the usage of the Cake Pattern to wire together all the parts wihtout pre-defining a driver. Since Slick 3.0 imposes a huge refactoring, we decided to turn ActiveSlick inside out and eliminate the need of the Cake Pattern.
71 |
72 | As a result, all special traits/classes like `TableId`, `TableWithIdQuery`, `EntityTableQuery`, etc, are gone. These classes could only compile if a driver was in scope. However, only the final user of the library can decide which driver to use. To overcome that situation we had to define everything inside components and wire them all together in a cake.
73 |
74 | This approach was way to complex and hard to document. Users had to understand how to compose the traits and which classes to implement and/or mixin in order the get the desired effect.
75 |
76 | From v.0.3.x onwards, one can build a DAO/Repository and enable an ActiveRecord extension using plain old Slick mappings. Some configuration is needed, but it's far more obvious to configure it than to compose and bake a cake.
77 |
78 | == EntityActions
79 |
80 | `EntityActions` is the main class we can use to build a DAO/Repository. It offers default implmentation for the following methods.
81 |
82 |
83 |
84 | [source,scala]
85 | ----
86 | include::../../modules/core/src/main/scala/io/strongtyped/active/slick/CrudActions.scala[tags=adoc]
87 |
88 | include::../../modules/core/src/main/scala/io/strongtyped/active/slick/EntityActionsLike.scala[tags=adoc]
89 | ----
90 |
91 | TIP: `EntityActions` implements two basic traits: `CrudActions` and `EntityActionsLike`. All methods involving a `Model` are `Id` agnostic and are defined in `CrudActions`. Methods involving an `Entity` are defined by `EntityActionsLike`
92 |
93 | === Mapping using ActiveSlick
94 |
95 | The following code fragment illustrates how we can use ActiveSlick `EntityActions` to bind the `Entity` `Id` field and the `Table Id column`.
96 |
97 | [source,scala]
98 | ----
99 | include::../../modules/samples/src/main/scala/io/strongtyped/active/slick/docexamples/MappingWithActiveSlick.scala[tags=adoc]
100 | ----
101 |
102 | The mapping is exactly the same. However, we define it inside **CoffeeRepo** which implements **EntityActions**.
103 |
104 |
105 | === Building blocks
106 | The previous code fragment shows all necessary building blocks we need:
107 |
108 | <1> The first thing to note is the `import jdbcProfile.api._`. As said before Slick requires jdbc driver. If this line is not included many of the necessary Slick traits, classes and extension methods won't be available.
109 | <2> Define a `BaseTypedType` for the `Entity`'s Id by looking up on the implicit scope. This is probably the most intriging one. This is necessary to build queries involving the `Id`. Note that you don't need to make it implicit yourself. This is already done by the internals of `EntityActions` implementation.
110 | <3> Type Member for the `Entity` type. We choose to use **Type Member** instead of **Type Parameters** because it make the code a little bit cleaner when composing with other traits. (see `OptimisticLocking` trait and `SupplierDao` in `Schema.scala`).
111 | <4> Type Member for the Entity's `Id`
112 | <5> Type Member pointing to the Entity's Table. This is required by `EntityActions` as the predefined methods must know the type of the table.
113 | <6> The ubiquitous Slick `TableQuery`. This is required by `EntityActions` in order to implement the basic methods.
114 | <7> A method returning the `Id` column (ie: `Rep[Id]`). This is how we indicate which table column must be used for all operations involving the primary key.
115 | <8> Finally a `Lens` to get and set the `Id` on the `Entity`.
116 |
117 | We have now a well known column for our primary key. The next step is to define the method to extract the id from our model and to add a generated id back into the model.
118 |
119 | == Transactions
120 | As of version 0.3.0 and the introduction of `DBIO` in Slick 3.0, all methods return `DBIO` (not `Futures`). In Slick 3.0 the DB sessions and the transactional sessions are not passed as implicit parameters therefore it is the user that have to manage the sessions and transactions.
121 |
122 | If ActiveSlick were returning Futures instead, then the transactions will have to be managed internally by **ActiveSlick** which is of course not desirable.
123 |
124 | == Optimistic Locking
125 |
126 | Optimistic locking is supported by means for a version column of type `Long`.
127 |
128 | [source,scala]
129 | ----
130 | include::../../modules/samples/src/main/scala/io/strongtyped/active/slick/docexamples/OptimisticLockingExample.scala[tags=adoc]
131 | ...
132 | }
133 | ----
134 | The `OptimisticLocking` requires two new methods to implement in order to glue the `version` field to the `version` column.
135 |
136 | <1> A method returning the `version` column (ie: `Rep[Long]`). This is how we indicate which table column must be used store the version information.
137 | <2> A `Lens` to get and set the `version` on the `Entity`.
138 |
139 |
140 | [TIP]
141 | ====
142 | Note that we can mixin the trait `OptimisticLocking` without specifying the type of the `Entity` nor the `Id` type. This is due to the usage of Type Members.
143 |
144 | If we were using Type Parameters, we'll be forced to repeat the types each time as in:
145 | `EntityActions[Coffee, Int](H2Driver) with OptimisticLocking[Coffee, Int]`
146 | ====
147 |
148 | == Before Insert and Update hooks
149 |
150 | `EntityActions` provide two hooks to modify Entity data before insert and update. One can overwrite these methods to add extra checks or modify the `Entity` just before insert and updates.
151 |
152 | A possible usage could be to automatically update `lastUpdated` field for audit purposes.
153 | (see `EntityActionsBeforeInsertUpdateTest.scala` for a concrete example)
154 |
155 | In both cases, the returned DBIO is combined will be combined with the corresponding DBIO (from insert or update methods) and executed in the same transaction.
156 |
157 | [source,scala]
158 | ----
159 | def beforeInsert(entity: Entity)(implicit exc: ExecutionContext): DBIO[Entity] = {
160 | // default implementation does nothing
161 | DBIO.successful(entity)
162 | }
163 |
164 | def beforeUpdate(id: Id, entity: Entity)(implicit exc: ExecutionContext): DBIO[Entity] = {
165 | // default implementation does nothing
166 | DBIO.successful(entity)
167 | }
168 | ----
169 |
170 |
171 | toc::[]
172 |
--------------------------------------------------------------------------------
/src/asciidoctor/pygments-monokai.css:
--------------------------------------------------------------------------------
1 | .listingblock .pygments .hll { background-color: #49483e }
2 | .listingblock .pygments { background: #272822; color: #f8f8f2 }
3 | .listingblock .pygments .tok-c { color: #75715e } /* Comment */
4 | .listingblock .pygments .tok-err { color: #960050; background-color: #1e0010 } /* Error */
5 | .listingblock .pygments .tok-k { color: #66d9ef } /* Keyword */
6 | .listingblock .pygments .tok-l { color: #ae81ff } /* Literal */
7 | .listingblock .pygments .tok-n { color: #f8f8f2 } /* Name */
8 | .listingblock .pygments .tok-o { color: #f92672 } /* Operator */
9 | .listingblock .pygments .tok-p { color: #f8f8f2 } /* Punctuation */
10 | .listingblock .pygments .tok-cm { color: #75715e } /* Comment.Multiline */
11 | .listingblock .pygments .tok-cp { color: #75715e } /* Comment.Preproc */
12 | .listingblock .pygments .tok-c1 { color: #75715e } /* Comment.Single */
13 | .listingblock .pygments .tok-cs { color: #75715e } /* Comment.Special */
14 | .listingblock .pygments .tok-gd { color: #f92672 } /* Generic.Deleted */
15 | .listingblock .pygments .tok-ge { font-style: italic } /* Generic.Emph */
16 | .listingblock .pygments .tok-gi { color: #a6e22e } /* Generic.Inserted */
17 | .listingblock .pygments .tok-gs { font-weight: bold } /* Generic.Strong */
18 | .listingblock .pygments .tok-gu { color: #75715e } /* Generic.Subheading */
19 | .listingblock .pygments .tok-kc { color: #66d9ef } /* Keyword.Constant */
20 | .listingblock .pygments .tok-kd { color: #66d9ef } /* Keyword.Declaration */
21 | .listingblock .pygments .tok-kn { color: #f92672 } /* Keyword.Namespace */
22 | .listingblock .pygments .tok-kp { color: #66d9ef } /* Keyword.Pseudo */
23 | .listingblock .pygments .tok-kr { color: #66d9ef } /* Keyword.Reserved */
24 | .listingblock .pygments .tok-kt { color: #66d9ef } /* Keyword.Type */
25 | .listingblock .pygments .tok-ld { color: #e6db74 } /* Literal.Date */
26 | .listingblock .pygments .tok-m { color: #ae81ff } /* Literal.Number */
27 | .listingblock .pygments .tok-s { color: #e6db74 } /* Literal.String */
28 | .listingblock .pygments .tok-na { color: #a6e22e } /* Name.Attribute */
29 | .listingblock .pygments .tok-nb { color: #f8f8f2 } /* Name.Builtin */
30 | .listingblock .pygments .tok-nc { color: #a6e22e } /* Name.Class */
31 | .listingblock .pygments .tok-no { color: #66d9ef } /* Name.Constant */
32 | .listingblock .pygments .tok-nd { color: #a6e22e } /* Name.Decorator */
33 | .listingblock .pygments .tok-ni { color: #f8f8f2 } /* Name.Entity */
34 | .listingblock .pygments .tok-ne { color: #a6e22e } /* Name.Exception */
35 | .listingblock .pygments .tok-nf { color: #a6e22e } /* Name.Function */
36 | .listingblock .pygments .tok-nl { color: #f8f8f2 } /* Name.Label */
37 | .listingblock .pygments .tok-nn { color: #f8f8f2 } /* Name.Namespace */
38 | .listingblock .pygments .tok-nx { color: #a6e22e } /* Name.Other */
39 | .listingblock .pygments .tok-py { color: #f8f8f2 } /* Name.Property */
40 | .listingblock .pygments .tok-nt { color: #f92672 } /* Name.Tag */
41 | .listingblock .pygments .tok-nv { color: #f8f8f2 } /* Name.Variable */
42 | .listingblock .pygments .tok-ow { color: #f92672 } /* Operator.Word */
43 | .listingblock .pygments .tok-w { color: #f8f8f2 } /* Text.Whitespace */
44 | .listingblock .pygments .tok-mb { color: #ae81ff } /* Literal.Number.Bin */
45 | .listingblock .pygments .tok-mf { color: #ae81ff } /* Literal.Number.Float */
46 | .listingblock .pygments .tok-mh { color: #ae81ff } /* Literal.Number.Hex */
47 | .listingblock .pygments .tok-mi { color: #ae81ff } /* Literal.Number.Integer */
48 | .listingblock .pygments .tok-mo { color: #ae81ff } /* Literal.Number.Oct */
49 | .listingblock .pygments .tok-sb { color: #e6db74 } /* Literal.String.Backtick */
50 | .listingblock .pygments .tok-sc { color: #e6db74 } /* Literal.String.Char */
51 | .listingblock .pygments .tok-sd { color: #e6db74 } /* Literal.String.Doc */
52 | .listingblock .pygments .tok-s2 { color: #e6db74 } /* Literal.String.Double */
53 | .listingblock .pygments .tok-se { color: #ae81ff } /* Literal.String.Escape */
54 | .listingblock .pygments .tok-sh { color: #e6db74 } /* Literal.String.Heredoc */
55 | .listingblock .pygments .tok-si { color: #e6db74 } /* Literal.String.Interpol */
56 | .listingblock .pygments .tok-sx { color: #e6db74 } /* Literal.String.Other */
57 | .listingblock .pygments .tok-sr { color: #e6db74 } /* Literal.String.Regex */
58 | .listingblock .pygments .tok-s1 { color: #e6db74 } /* Literal.String.Single */
59 | .listingblock .pygments .tok-ss { color: #e6db74 } /* Literal.String.Symbol */
60 | .listingblock .pygments .tok-bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */
61 | .listingblock .pygments .tok-vc { color: #f8f8f2 } /* Name.Variable.Class */
62 | .listingblock .pygments .tok-vg { color: #f8f8f2 } /* Name.Variable.Global */
63 | .listingblock .pygments .tok-vi { color: #f8f8f2 } /* Name.Variable.Instance */
64 | .listingblock .pygments .tok-il { color: #ae81ff } /* Literal.Number.Integer.Long */
--------------------------------------------------------------------------------
/version.sbt:
--------------------------------------------------------------------------------
1 | val snapshotSuffix = "-SNAPSHOT"
2 |
3 | version in ThisBuild := "0.3.6" + snapshotSuffix
4 |
5 | isSnapshot := version.value.endsWith(snapshotSuffix)
6 |
--------------------------------------------------------------------------------