aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorvlad <vlad@drivergrp.com>2016-07-15 19:41:26 -0400
committervlad <vlad@drivergrp.com>2016-07-15 19:41:26 -0400
commitc0d574dc6134e4f406875ea5a1301ba46602a6ec (patch)
tree606a56d184bd8c4d67f98b5aa3fafa3640a8190f
downloaddriver-core-c0d574dc6134e4f406875ea5a1301ba46602a6ec.tar.gz
driver-core-c0d574dc6134e4f406875ea5a1301ba46602a6ec.tar.bz2
driver-core-c0d574dc6134e4f406875ea5a1301ba46602a6ec.zip
Initial commit with standard lib, might be used a example of cake
-rw-r--r--.gitignore18
-rw-r--r--LICENSE202
-rw-r--r--README.md40
-rw-r--r--project/.sbtserver3
-rw-r--r--project/.sbtserver.lock0
-rw-r--r--project/Build.scala41
-rw-r--r--project/build.properties4
-rw-r--r--project/plugins.sbt0
-rw-r--r--project/sbt-ui.sbt3
-rw-r--r--src/main/resources/logback.xml22
-rw-r--r--src/main/scala/com/drivergrp/core/DriverApp.scala75
-rw-r--r--src/main/scala/com/drivergrp/core/Service.scala34
-rw-r--r--src/main/scala/com/drivergrp/core/Swagger.scala45
-rw-r--r--src/main/scala/com/drivergrp/core/config.scala43
-rw-r--r--src/main/scala/com/drivergrp/core/database.scala45
-rw-r--r--src/main/scala/com/drivergrp/core/execution.scala28
-rw-r--r--src/main/scala/com/drivergrp/core/generators.scala53
-rw-r--r--src/main/scala/com/drivergrp/core/id.scala24
-rw-r--r--src/main/scala/com/drivergrp/core/logging.scala132
-rw-r--r--src/main/scala/com/drivergrp/core/messages.scala74
-rw-r--r--src/main/scala/com/drivergrp/core/package.scala19
-rw-r--r--src/main/scala/com/drivergrp/core/rest.scala92
-rw-r--r--src/main/scala/com/drivergrp/core/stats.scala50
-rw-r--r--src/main/scala/com/drivergrp/core/time.scala79
24 files changed, 1126 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4530652
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+*.class
+*.log
+
+# sbt specific
+.cache
+.history
+.lib/
+dist/*
+target/
+lib_managed/
+src_managed/
+project/boot/
+project/plugins/project/
+.DS_Store
+# Scala-IDE specific
+.scala_dependencies
+.worksheet
+.idea \ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8f71f43
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6181c58
--- /dev/null
+++ b/README.md
@@ -0,0 +1,40 @@
+# Driver Core Library
+
+##Running
+
+The database pre-configured is an h2, so you just have to:
+
+1. Launch SBT:
+
+ $ sbt
+
+2. Compile everything and run:
+
+ > run
+
+#Testing
+
+1. Launch SBT:
+
+ $ sbt
+
+2. Compile everything and test:
+
+ > test
+
+## Dependency injection
+
+**Cake pattern** makes not clear which dependencies are missed if dependency lists are long and/or inheritance used to satisfy more that one dependency at once (probably just no tool support).
+[Nice overview of different cake aspects and App as a component might be found here](http://scabl.blogspot.com/2013/02/cbdi.html). Popular description is by [Jonas Boner](http://jonasboner.com/real-world-scala-dependency-injection-di/) and formal by [Martin Odersky](http://lampwww.epfl.ch/~odersky/papers/ScalableComponent.pdf).
+
+**Passing dependencies as parameters** requires passing all the dependencies through the dependency graph. Some optimizations and `wired` macro for automatic binding are described [here](http://di-in-scala.github.io).
+
+**Implicits** ambiguates usage of implicits, increasing compilation times, harder to debug.
+
+**Frameworks** introduce magic, limiting, invasive for codebase, need to maintain later versions.
+
+**Inheritance** exposes transitive dependencies implementation details to the higher-level components. This and how self-types work for circular dependencies is explained in [this artice](http://www.andrewrollins.com/2014/08/07/scala-cake-pattern-self-type-annotations-vs-inheritance/) and also [this](http://www.warski.org/blog/2014/02/using-scala-traits-as-modules-or-the-thin-cake-pattern/), however they are using `lazy val`s a lot which may lead to deadlocks at the applications start-up (for circular dependencies).
+
+**Reader monad** requires common `Modules` object with all dependencies, which introduces coupling, preventing easy refactoring (harder to figure out which dependencies are actually used) and makes tests run longer. Harder to differentiate function dependencies from parameters, constructors and self-types are not used, affecting readability. Not allowing scopes with `def`/`val`. [Implementation described here](http://blog.originate.com/blog/2013/10/21/reader-monad-for-dependency-injection/).
+
+__Cake pattern seems the preferable option, due to absence of implicits, inheritance and `lazy val`s, correct handling of circular dependencies, and usage in Scala compiler (hence, hopefuly, no hidden issues).__
diff --git a/project/.sbtserver b/project/.sbtserver
new file mode 100644
index 0000000..8dcde7c
--- /dev/null
+++ b/project/.sbtserver
@@ -0,0 +1,3 @@
+#Server Startup at 2016-07-06T23:09+0000
+#Wed Jul 06 16:09:57 PDT 2016
+server.uri=http\://0.0.0.0\:58105
diff --git a/project/.sbtserver.lock b/project/.sbtserver.lock
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/project/.sbtserver.lock
diff --git a/project/Build.scala b/project/Build.scala
new file mode 100644
index 0000000..60ae493
--- /dev/null
+++ b/project/Build.scala
@@ -0,0 +1,41 @@
+import sbt._
+import Keys._
+
+object BuildSettings {
+ val buildSettings = Defaults.coreDefaultSettings ++ Seq (
+ organization := "com.drivergrp",
+ name := "core",
+ version := "0.0.1",
+ scalaVersion := "2.11.6",
+ scalacOptions := Seq("-unchecked", "-deprecation", "-feature", "-encoding", "utf8"),
+ fork in run := true
+ )
+}
+
+object DriverBuild extends Build {
+ import BuildSettings._
+
+ val akkaHttpV = "2.4.8"
+
+ val dependencies = Seq(
+ "com.typesafe.akka" %% "akka-http-core" % akkaHttpV,
+ "com.typesafe.akka" %% "akka-http-experimental" % akkaHttpV,
+ "com.typesafe.akka" %% "akka-http-jackson-experimental" % akkaHttpV,
+ "com.typesafe.akka" %% "akka-http-spray-json-experimental" % akkaHttpV,
+ "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpV,
+ "org.scalatest" % "scalatest_2.11" % "2.2.1" % "test",
+ "com.typesafe.slick" %% "slick" % "3.0.0",
+ "com.typesafe" % "config" % "1.2.1",
+ "com.typesafe.scala-logging" %% "scala-logging" % "3.1.0",
+ "ch.qos.logback" % "logback-classic" % "1.1.3",
+ "org.slf4j" % "slf4j-nop" % "1.6.4",
+ "org.scalaz" %% "scalaz-core" % "7.2.4",
+ "com.github.swagger-akka-http" %% "swagger-akka-http" % "0.7.1"
+ )
+
+ lazy val core = Project (
+ "core",
+ file ("."),
+ settings = buildSettings ++ Seq (libraryDependencies ++= dependencies)
+ )
+} \ No newline at end of file
diff --git a/project/build.properties b/project/build.properties
new file mode 100644
index 0000000..4c003f6
--- /dev/null
+++ b/project/build.properties
@@ -0,0 +1,4 @@
+#Activator-generated Properties
+#Wed Jul 06 16:08:49 PDT 2016
+template.uuid=a675a7df-bee3-48df-9eaa-688d99e5814e
+sbt.version=0.13.8
diff --git a/project/plugins.sbt b/project/plugins.sbt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/project/plugins.sbt
diff --git a/project/sbt-ui.sbt b/project/sbt-ui.sbt
new file mode 100644
index 0000000..7c28b97
--- /dev/null
+++ b/project/sbt-ui.sbt
@@ -0,0 +1,3 @@
+// This plugin represents functionality that is to be added to sbt in the future
+
+addSbtPlugin("org.scala-sbt" % "sbt-core-next" % "0.1.1") \ No newline at end of file
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 0000000..1b96003
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+ <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+ <target>System.out</target>
+ <encoder>
+ <pattern>%date{MM/dd HH:mm:ss} %-5level[%.15thread] %logger{1} - %msg%n</pattern>
+ </encoder>
+ </appender>
+ <logger name="slick.backend.DatabaseComponent.actio" level="debug"/>
+ <logger name="slick.jdbc" level="error" />
+ <logger name="slick.ast" level="error" />
+ <logger name="slick.memory" level="error" />
+ <logger name="slick.relational" level="error" />
+ <logger name="slick.compiler" level="error" />
+ <logger name="com.wordnik" level="error" />
+ <logger name="com.github" level="error" />
+ <root level="debug">
+ <appender-ref ref="CONSOLE"/>
+ </root>
+
+</configuration>
diff --git a/src/main/scala/com/drivergrp/core/DriverApp.scala b/src/main/scala/com/drivergrp/core/DriverApp.scala
new file mode 100644
index 0000000..01fee03
--- /dev/null
+++ b/src/main/scala/com/drivergrp/core/DriverApp.scala
@@ -0,0 +1,75 @@
+package com.drivergrp.core
+
+import akka.actor.ActorSystem
+import akka.http.scaladsl.Http
+import akka.http.scaladsl.server.Directives._
+import akka.http.scaladsl.server.RouteResult._
+import akka.stream.ActorMaterializer
+import com.drivergrp.core.config.ConfigModule
+import com.drivergrp.core.logging.LoggerModule
+
+
+trait DriverApp {
+ this: ConfigModule with LoggerModule =>
+
+ def interface: String = "localhost"
+ def port: Int = 8080
+
+ def services: Seq[Service]
+
+
+ val servicesInstances = services
+ activateServices(servicesInstances)
+ scheduleServicesDeactivation(servicesInstances)
+
+ implicit val actorSystem = ActorSystem("spray-routing", config)
+ implicit val executionContext = actorSystem.dispatcher
+ implicit val materializer = ActorMaterializer()(actorSystem)
+
+ val serviceTypes = servicesInstances.flatMap(_.serviceTypes)
+ val swaggerService = new Swagger(actorSystem, serviceTypes, config)
+ val swaggerRoutes = swaggerService.routes ~ swaggerService.swaggerUI
+
+ Http()(actorSystem).bindAndHandle(
+ route2HandlerFlow(logRequestResult("log")(servicesInstances.map(_.route).foldLeft(swaggerRoutes) { _ ~ _ })),
+ interface, port)(materializer)
+
+
+ /**
+ * Initializes services
+ */
+ protected def activateServices(servicesInstances: Seq[Service]) = {
+ servicesInstances.foreach { service =>
+ Console.print(s"Service ${service.name} starts ...")
+ try {
+ service.activate()
+ } catch {
+ case t: Throwable =>
+ log.fatal(s"Service ${service.name} failed to activate", t)
+ Console.print(" Failed! (check log)")
+ }
+ Console.println(" Done")
+ }
+ }
+
+ /**
+ * Schedules services to be deactivated on the app shutdown
+ */
+ protected def scheduleServicesDeactivation(servicesInstances: Seq[Service]) = {
+ Runtime.getRuntime.addShutdownHook(new Thread() {
+ override def run(): Unit = {
+ servicesInstances.foreach { service =>
+ Console.print(s"Service ${service.name} shutting down ...")
+ try {
+ service.deactivate()
+ } catch {
+ case t: Throwable =>
+ log.fatal(s"Service ${service.name} failed to deactivate", t)
+ Console.print(" Failed! (check log)")
+ }
+ Console.println(" Done")
+ }
+ }
+ })
+ }
+}
diff --git a/src/main/scala/com/drivergrp/core/Service.scala b/src/main/scala/com/drivergrp/core/Service.scala
new file mode 100644
index 0000000..4404c9c
--- /dev/null
+++ b/src/main/scala/com/drivergrp/core/Service.scala
@@ -0,0 +1,34 @@
+package com.drivergrp.core
+
+import akka.http.scaladsl.server.{Route, RouteConcatenation}
+
+
+trait Service {
+ import scala.reflect.runtime.universe._
+
+ val name: String
+ def route: Route
+ def serviceTypes: Seq[Type]
+
+ def activate(): Unit = {}
+ def deactivate(): Unit = {}
+}
+
+/**
+ * Service implementation which may be used to composed a few
+ *
+ * @param name more general name of the composite service,
+ * must be provided as there is no good way to automatically
+ * generalize the name from the composed services' names
+ * @param services services to compose into a single one
+ */
+class CompositeService(val name: String, services: Seq[Service])
+ extends Service with RouteConcatenation {
+
+ def route: Route = services.map(_.route).reduce(_ ~ _)
+
+ def serviceTypes = services.flatMap(_.serviceTypes)
+
+ override def activate() = services.foreach(_.activate())
+ override def deactivate() = services.reverse.foreach(_.deactivate())
+} \ No newline at end of file
diff --git a/src/main/scala/com/drivergrp/core/Swagger.scala b/src/main/scala/com/drivergrp/core/Swagger.scala
new file mode 100644
index 0000000..428920c
--- /dev/null
+++ b/src/main/scala/com/drivergrp/core/Swagger.scala
@@ -0,0 +1,45 @@
+package com.drivergrp.core
+
+import akka.actor.ActorSystem
+import akka.stream.ActorMaterializer
+import com.github.swagger.akka.model._
+import com.github.swagger.akka.{HasActorSystem, SwaggerHttpService}
+import com.typesafe.config.Config
+
+import scala.reflect.runtime.universe._
+
+
+class Swagger(override val actorSystem: ActorSystem,
+ override val apiTypes: Seq[Type],
+ val config: Config) extends SwaggerHttpService with HasActorSystem {
+
+ val materializer = ActorMaterializer()(actorSystem)
+
+ override val host = "localhost:8080" //the url of your api, not swagger's json endpoint
+ override val basePath = config.getString("swagger.basePath")
+ override val apiDocsPath = config.getString("swagger.docsPath")
+
+ override val info = Info(
+ config.getString("swagger.apiInfo.description"),
+ config.getString("swagger.apiVersion"),
+ config.getString("swagger.apiInfo.title"),
+ config.getString("swagger.apiInfo.termsOfServiceUrl"),
+ contact = Some(Contact(
+ config.getString("swagger.apiInfo.contact.name"),
+ config.getString("swagger.apiInfo.contact.url"),
+ config.getString("swagger.apiInfo.contact.email")
+ )),
+ license = Some(License(
+ config.getString("swagger.apiInfo.license"),
+ config.getString("swagger.apiInfo.licenseUrl")
+ )),
+ vendorExtensions = Map())
+
+ def swaggerUI = get {
+ pathPrefix("") {
+ pathEndOrSingleSlash {
+ getFromResource("swagger-ui/index.html")
+ }
+ } ~ getFromResourceDirectory("swagger-ui")
+ }
+} \ No newline at end of file
diff --git a/src/main/scala/com/drivergrp/core/config.scala b/src/main/scala/com/drivergrp/core/config.scala
new file mode 100644
index 0000000..5a89752
--- /dev/null
+++ b/src/main/scala/com/drivergrp/core/config.scala
@@ -0,0 +1,43 @@
+package com.drivergrp.core
+
+import java.io.File
+import com.typesafe.config.{Config, ConfigFactory}
+
+
+object config {
+
+ trait ConfigModule {
+ def config: Config
+ }
+
+ /**
+ * Configuration implementation providing config which is specified as the parameter
+ * which might be used for testing purposes
+ *
+ * @param config fixed config to provide
+ */
+ class DefaultConfigModule(val config: Config) extends ConfigModule
+
+ /**
+ * Configuration implementation reading default typesafe config
+ */
+ trait TypesafeConfigModule extends ConfigModule {
+
+ private val internalConfig: Config = {
+ val configDefaults =
+ ConfigFactory.load(this.getClass.getClassLoader, "application.conf")
+
+ scala.sys.props.get("application.config") match {
+
+ case Some(filename) =>
+ ConfigFactory.parseFile(new File(filename)).withFallback(configDefaults)
+
+ case None => configDefaults
+ }
+ }
+
+ protected val rootConfig = internalConfig
+
+ val config = rootConfig
+ }
+}
diff --git a/src/main/scala/com/drivergrp/core/database.scala b/src/main/scala/com/drivergrp/core/database.scala
new file mode 100644
index 0000000..5eb9d28
--- /dev/null
+++ b/src/main/scala/com/drivergrp/core/database.scala
@@ -0,0 +1,45 @@
+package com.drivergrp.core
+
+import com.drivergrp.core.id.{Id, Name}
+
+import scala.concurrent.Future
+
+
+object database {
+
+ import slick.backend.DatabaseConfig
+ import slick.driver.JdbcProfile
+
+
+ trait DatabaseModule {
+ val profile: JdbcProfile
+ val database: JdbcProfile#Backend#Database
+ }
+
+ trait ConfigDatabaseModule extends DatabaseModule {
+
+ protected def databaseConfigKey: String
+
+ private val dbConfig: DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig(databaseConfigKey)
+
+ val profile: JdbcProfile = dbConfig.driver
+ val database: JdbcProfile#Backend#Database = dbConfig.db
+ }
+
+ trait DatabaseObject {
+ def createTables(): Future[Unit]
+ def disconnect(): Unit
+ }
+
+ trait IdColumnTypes {
+ this: DatabaseModule =>
+
+ import profile.api._
+
+ implicit def idColumnType[T] =
+ MappedColumnType.base[Id[T], Long]({ id => id: Long }, { id => Id[T](id) })
+
+ implicit def nameColumnType[T] =
+ MappedColumnType.base[Name[T], String]({ name => name: String }, { name => Name[T](name) })
+ }
+}
diff --git a/src/main/scala/com/drivergrp/core/execution.scala b/src/main/scala/com/drivergrp/core/execution.scala
new file mode 100644
index 0000000..7274f00
--- /dev/null
+++ b/src/main/scala/com/drivergrp/core/execution.scala
@@ -0,0 +1,28 @@
+package com.drivergrp.core
+
+
+object execution {
+
+ import scala.concurrent.ExecutionContext
+ import java.util.concurrent.Executors
+ import akka.actor.ActorSystem
+
+
+ trait ExecutionContextModule {
+
+ def executionContext: ExecutionContext
+ }
+
+ trait FixedThreadsExecutionContext extends ExecutionContextModule {
+
+ def threadsNumber: Int
+
+ val executionContext: ExecutionContext =
+ ExecutionContext.fromExecutor(Executors.newFixedThreadPool(threadsNumber))
+ }
+
+ trait ActorSystemModule {
+
+ def actorSystem: ActorSystem
+ }
+}
diff --git a/src/main/scala/com/drivergrp/core/generators.scala b/src/main/scala/com/drivergrp/core/generators.scala
new file mode 100644
index 0000000..89d6e56
--- /dev/null
+++ b/src/main/scala/com/drivergrp/core/generators.scala
@@ -0,0 +1,53 @@
+package com.drivergrp.core
+
+import java.math.MathContext
+
+import com.drivergrp.core.id.{Id, Name}
+import com.drivergrp.core.time.{Time, TimeRange}
+
+import scala.reflect.ClassTag
+import scala.util.Random
+
+
+object generators {
+
+ private val random = new Random
+ import random._
+
+ private val DefaultMaxLength = 100
+
+
+ def nextId[T](): Id[T] = Id[T](nextLong())
+
+ def nextName[T](maxLength: Int = DefaultMaxLength): Name[T] = Name[T](nextString(maxLength))
+
+ def nextString(maxLength: Int = DefaultMaxLength) = random.nextString(maxLength)
+
+ def nextOption[T](value: => T): Option[T] = if(nextBoolean) Option(value) else None
+
+ def nextPair[L, R](left: => L, right: => R): (L, R) = (left, right)
+
+ def nextTriad[F, S, T](first: => F, second: => S, third: => T): (F, S, T) = (first, second, third)
+
+ def nextTime(): Time = Time(math.abs(nextLong() % System.currentTimeMillis))
+
+ def nextTimeRange(): TimeRange = TimeRange(nextTime(), nextTime())
+
+ def nextBigDecimal(multiplier: Double = 1000000.00, precision: Int = 2): BigDecimal =
+ BigDecimal(multiplier * nextDouble, new MathContext(precision))
+
+ def oneOf[T](items: Seq[T]): T = items(nextInt(items.size))
+
+ def arrayOf[T : ClassTag](generator: => T, maxLength: Int = DefaultMaxLength): Array[T] = Array.fill(maxLength)(generator)
+
+ def seqOf[T](generator: => T, maxLength: Int = DefaultMaxLength): Seq[T] = Seq.fill(maxLength)(generator)
+
+ def vectorOf[T](generator: => T, maxLength: Int = DefaultMaxLength): Vector[T] = Vector.fill(maxLength)(generator)
+
+ def listOf[T](generator: => T, maxLength: Int = DefaultMaxLength): List[T] = List.fill(maxLength)(generator)
+
+ def setOf[T](generator: => T, maxLength: Int = DefaultMaxLength): Set[T] = seqOf(generator, maxLength).toSet
+
+ def mapOf[K, V](maxLength: Int, keyGenerator: => K, valueGenerator: => V): Map[K, V] =
+ seqOf(nextPair(keyGenerator, valueGenerator), maxLength).toMap
+} \ No newline at end of file
diff --git a/src/main/scala/com/drivergrp/core/id.scala b/src/main/scala/com/drivergrp/core/id.scala
new file mode 100644
index 0000000..29b8d99
--- /dev/null
+++ b/src/main/scala/com/drivergrp/core/id.scala
@@ -0,0 +1,24 @@
+package com.drivergrp.core
+
+import scalaz._
+
+
+object id {
+
+ trait Tagged[+V, +Tag]
+ type @@[+V, +Tag] = V with Tagged[V, Tag]
+
+ type Id[+Tag] = Long @@ Tag
+ object Id {
+ def apply[Tag](value: Long) = value.asInstanceOf[Id[Tag]]
+ }
+
+ implicit def idEqual[T]: Equal[Id[T]] = Equal.equal[Id[T]](_ == _)
+
+ type Name[+Tag] = String @@ Tag
+ object Name {
+ def apply[Tag](value: String) = value.asInstanceOf[Name[Tag]]
+ }
+
+ implicit def nameEqual[T]: Equal[Name[T]] = Equal.equal[Name[T]](_ == _)
+}
diff --git a/src/main/scala/com/drivergrp/core/logging.scala b/src/main/scala/com/drivergrp/core/logging.scala
new file mode 100644
index 0000000..35f3a1b
--- /dev/null
+++ b/src/main/scala/com/drivergrp/core/logging.scala
@@ -0,0 +1,132 @@
+package com.drivergrp.core
+
+import com.typesafe.scalalogging.{LazyLogging, StrictLogging}
+import org.slf4j.Marker
+
+
+object logging {
+
+ trait LoggerModule {
+
+ def log: Logger
+ }
+
+ trait NopLoggerModule extends LoggerModule {
+
+ def log: Logger = new NopLogger()
+ }
+
+ /**
+ * Defines `logger` as a lazy value initialized with an underlying `org.slf4j.Logger`
+ * named according to the class into which this trait is mixed.
+ */
+ trait LazyLoggerModule extends LoggerModule {
+ this: LazyLogging =>
+
+ override val log: Logger = new TypesafeScalaLogger(logger)
+ }
+
+ /**
+ * Defines `logger` as a value initialized with an underlying `org.slf4j.Logger`
+ * named according to the class into which this trait is mixed.
+ */
+ trait StrictLoggerModule extends LoggerModule {
+ this: StrictLogging =>
+
+ override val log: Logger = new TypesafeScalaLogger(logger)
+ }
+
+
+ trait Logger {
+
+ def fatal(message: String): Unit
+ def fatal(message: String, cause: Throwable): Unit
+ def fatal(message: String, args: AnyRef*): Unit
+ def fatal(marker: Marker, message: String): Unit
+ def fatal(marker: Marker, message: String, cause: Throwable): Unit
+ def fatal(marker: Marker, message: String, args: AnyRef*): Unit
+
+ def error(message: String): Unit
+ def error(message: String, cause: Throwable): Unit
+ def error(message: String, args: AnyRef*): Unit
+ def error(marker: Marker, message: String): Unit
+ def error(marker: Marker, message: String, cause: Throwable): Unit
+ def error(marker: Marker, message: String, args: AnyRef*): Unit
+
+ def audit(message: String): Unit
+ def audit(message: String, cause: Throwable): Unit
+ def audit(message: String, args: AnyRef*): Unit
+ def audit(marker: Marker, message: String): Unit
+ def audit(marker: Marker, message: String, cause: Throwable): Unit
+ def audit(marker: Marker, message: String, args: AnyRef*): Unit
+
+ def debug(message: String): Unit
+ def debug(message: String, cause: Throwable): Unit
+ def debug(message: String, args: AnyRef*): Unit
+ def debug(marker: Marker, message: String): Unit
+ def debug(marker: Marker, message: String, cause: Throwable): Unit
+ def debug(marker: Marker, message: String, args: AnyRef*): Unit
+ }
+
+ class TypesafeScalaLogger(scalaLogging: com.typesafe.scalalogging.Logger) extends Logger {
+
+ def fatal(message: String): Unit = scalaLogging.error(message)
+ def fatal(message: String, cause: Throwable): Unit = scalaLogging.error(message, cause)
+ def fatal(message: String, args: AnyRef*): Unit = scalaLogging.error(message, args)
+ def fatal(marker: Marker, message: String): Unit = scalaLogging.error(marker, message)
+ def fatal(marker: Marker, message: String, cause: Throwable): Unit = scalaLogging.error(marker, message, cause)
+ def fatal(marker: Marker, message: String, args: AnyRef*): Unit = scalaLogging.error(marker, message, args)
+
+ def error(message: String): Unit = scalaLogging.warn(message)
+ def error(message: String, cause: Throwable): Unit = scalaLogging.warn(message, cause)
+ def error(message: String, args: AnyRef*): Unit = scalaLogging.warn(message, args)
+ def error(marker: Marker, message: String): Unit = scalaLogging.warn(marker, message)
+ def error(marker: Marker, message: String, cause: Throwable): Unit = scalaLogging.warn(marker, message, cause)
+ def error(marker: Marker, message: String, args: AnyRef*): Unit = scalaLogging.warn(marker, message, args)
+
+ def audit(message: String): Unit = scalaLogging.info(message)
+ def audit(message: String, cause: Throwable): Unit = scalaLogging.info(message, cause)
+ def audit(message: String, args: AnyRef*): Unit = scalaLogging.info(message, args)
+ def audit(marker: Marker, message: String): Unit = scalaLogging.info(marker, message)
+ def audit(marker: Marker, message: String, cause: Throwable): Unit = scalaLogging.info(marker, message, cause)
+ def audit(marker: Marker, message: String, args: AnyRef*): Unit = scalaLogging.info(marker, message, args)
+
+ def debug(message: String): Unit = scalaLogging.debug(message)
+ def debug(message: String, cause: Throwable): Unit = scalaLogging.debug(message, cause)
+ def debug(message: String, args: AnyRef*): Unit = scalaLogging.debug(message, args)
+ def debug(marker: Marker, message: String): Unit = scalaLogging.debug(marker, message)
+ def debug(marker: Marker, message: String, cause: Throwable): Unit = scalaLogging.debug(marker, message, cause)
+ def debug(marker: Marker, message: String, args: AnyRef*): Unit = scalaLogging.debug(marker, message, args)
+ }
+
+ class NopLogger() extends Logger {
+
+ def fatal(message: String): Unit = {}
+ def fatal(message: String, cause: Throwable): Unit = {}
+ def fatal(message: String, args: AnyRef*): Unit = {}
+ def fatal(marker: Marker, message: String): Unit = {}
+ def fatal(marker: Marker, message: String, cause: Throwable): Unit = {}
+ def fatal(marker: Marker, message: String, args: AnyRef*): Unit = {}
+
+ def error(message: String): Unit = {}
+ def error(message: String, cause: Throwable): Unit = {}
+ def error(message: String, args: AnyRef*): Unit = {}
+ def error(marker: Marker, message: String): Unit = {}
+ def error(marker: Marker, message: String, cause: Throwable): Unit = {}
+ def error(marker: Marker, message: String, args: AnyRef*): Unit = {}
+
+ def audit(message: String): Unit = {}
+ def audit(message: String, cause: Throwable): Unit = {}
+ def audit(message: String, args: AnyRef*): Unit = {}
+ def audit(marker: Marker, message: String): Unit = {}
+ def audit(marker: Marker, message: String, cause: Throwable): Unit = {}
+ def audit(marker: Marker, message: String, args: AnyRef*): Unit = {}
+
+ def debug(message: String): Unit = {}
+ def debug(message: String, cause: Throwable): Unit = {}
+ def debug(message: String, args: AnyRef*): Unit = {}
+ def debug(marker: Marker, message: String): Unit = {}
+ def debug(marker: Marker, message: String, cause: Throwable): Unit = {}
+ def debug(marker: Marker, message: String, args: AnyRef*): Unit = {}
+ }
+}
diff --git a/src/main/scala/com/drivergrp/core/messages.scala b/src/main/scala/com/drivergrp/core/messages.scala
new file mode 100644
index 0000000..cf0bb6c
--- /dev/null
+++ b/src/main/scala/com/drivergrp/core/messages.scala
@@ -0,0 +1,74 @@
+package com.drivergrp.core
+
+
+import java.util.Locale
+
+import com.drivergrp.core.config.ConfigModule
+import com.drivergrp.core.logging.{Logger, LoggerModule}
+
+import scala.collection.JavaConverters._
+import scala.collection.concurrent.TrieMap
+
+/**
+ * Scala internationalization (i18n) support
+ */
+object messages {
+
+ trait MessagesModule {
+
+ def messages: Messages
+ }
+
+ trait ConfigMessagesModule extends MessagesModule {
+ this: ConfigModule with LoggerModule =>
+
+ private val loadedFromConfig = new TrieMap[Locale, Messages]()
+ val locale: Locale = Locale.US
+
+ val messages: Messages = {
+ loadedFromConfig.getOrElseUpdate(locale, {
+ val map = config.getConfig(locale.getISO3Language).root().unwrapped().asScala.mapValues(_.toString).toMap
+ Messages(map, locale, log)
+ })
+ }
+ }
+
+
+ case class Messages(map: Map[String, String], locale: Locale, log: Logger) {
+
+ /**
+ * Returns message for the key
+ *
+ * @param key key
+ * @return message
+ */
+ def apply(key: String): String = {
+ map.get(key) match {
+ case Some(message) => message
+ case None =>
+ log.error(s"Message with key $key not found for locale " + locale.getDisplayName)
+ key
+ }
+ }
+
+ /**
+ * Returns message for the key and formats that with parameters
+ *
+ * @example "Hello {0}!" with "Joe" will be "Hello Joe!"
+ *
+ * @param key key
+ * @param params params to be embedded
+ * @return formatted message
+ */
+ def apply(key: String, params: Any*): String = {
+
+ def format(formatString: String, params: Seq[Any]) =
+ params.zipWithIndex.foldLeft(formatString) {
+ case (res, (value, index)) => res.replaceAll(s"{$index}", value.toString)
+ }
+
+ val template = apply(key)
+ format(template, params)
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/scala/com/drivergrp/core/package.scala b/src/main/scala/com/drivergrp/core/package.scala
new file mode 100644
index 0000000..3c19431
--- /dev/null
+++ b/src/main/scala/com/drivergrp/core/package.scala
@@ -0,0 +1,19 @@
+package com.drivergrp
+
+
+package object core {
+ import scala.language.reflectiveCalls
+
+ def make[T](v: => T)(f: T => Unit): T = {
+ val value = v; f(value); value
+ }
+
+ def using[R <: { def close() }, P](r: => R)(f: R => P): P = {
+ val resource = r
+ try {
+ f(resource)
+ } finally {
+ resource.close()
+ }
+ }
+}
diff --git a/src/main/scala/com/drivergrp/core/rest.scala b/src/main/scala/com/drivergrp/core/rest.scala
new file mode 100644
index 0000000..e121640
--- /dev/null
+++ b/src/main/scala/com/drivergrp/core/rest.scala
@@ -0,0 +1,92 @@
+package com.drivergrp.core
+
+import akka.http.scaladsl.Http
+import akka.http.scaladsl.model.{HttpRequest, HttpResponse}
+import akka.http.scaladsl.server.{Directive, _}
+import akka.http.scaladsl.util.FastFuture._
+import akka.stream.ActorMaterializer
+import akka.util.Timeout
+import com.drivergrp.core.execution.{ActorSystemModule, ExecutionContextModule}
+import com.drivergrp.core.id.{Id, Name}
+import com.drivergrp.core.logging.LoggerModule
+import com.drivergrp.core.stats.StatsModule
+import com.drivergrp.core.time.TimeRange
+import com.drivergrp.core.time.provider.TimeModule
+import spray.json.{DeserializationException, JsNumber, JsString, JsValue, RootJsonFormat}
+
+import scala.concurrent.Future
+import scala.concurrent.duration._
+import scala.language.postfixOps
+import scala.util.{Failure, Success, Try}
+import scalaz.{Failure => _, Success => _, _}
+
+
+object rest {
+
+ trait RestService {
+ this: ActorSystemModule with LoggerModule with StatsModule with TimeModule with ExecutionContextModule =>
+
+ protected implicit val timeout = Timeout(5 seconds)
+
+ implicit val materializer = ActorMaterializer()(actorSystem)
+
+
+ def sendRequest(request: HttpRequest): Future[HttpResponse] = {
+
+ log.audit(s"Sending to ${request.uri} request $request")
+
+ val requestTime = time.currentTime()
+ val response = Http()(actorSystem).singleRequest(request)(materializer)
+
+ response.onComplete {
+ case Success(_) =>
+ val responseTime = time.currentTime()
+ log.audit(s"Response from ${request.uri} to request $request is successful")
+ stats.recordStats(Seq("request", request.uri.toString, "success"), TimeRange(requestTime, responseTime), 1)
+
+ case Failure(t) =>
+ val responseTime = time.currentTime()
+ log.audit(s"Failed to receive response from ${request.uri} to request $request")
+ log.error(s"Failed to receive response from ${request.uri} to request $request", t)
+ stats.recordStats(Seq("request", request.uri.toString, "fail"), TimeRange(requestTime, responseTime), 1)
+ } (executionContext)
+
+ response
+ }
+ }
+
+
+ object basicFormats {
+
+ implicit def idFormat[T] = new RootJsonFormat[Id[T]] {
+ def write(id: Id[T]) = JsNumber(id)
+
+ def read(value: JsValue) = value match {
+ case JsNumber(id) => Id[T](id.toLong)
+ case _ => throw new DeserializationException("Id expects number")
+ }
+ }
+
+ implicit def nameFormat[T] = new RootJsonFormat[Name[T]] {
+ def write(name: Name[T]) = JsNumber(name)
+
+ def read(value: JsValue): Name[T] = value match {
+ case JsString(name) => Name[T](name)
+ case _ => throw new DeserializationException("Name expects string")
+ }
+ }
+ }
+
+ trait OptionTDirectives {
+
+ /**
+ * "Unwraps" a `OptionT[Future, T]` and runs the inner route after future
+ * completion with the future's value as an extraction of type `Try[T]`.
+ */
+ def onComplete[T](optionT: OptionT[Future, T]): Directive1[Try[Option[T]]] =
+ Directive { inner ⇒ ctx ⇒
+ import ctx.executionContext
+ optionT.run.fast.transformWith(t ⇒ inner(Tuple1(t))(ctx))
+ }
+ }
+}
diff --git a/src/main/scala/com/drivergrp/core/stats.scala b/src/main/scala/com/drivergrp/core/stats.scala
new file mode 100644
index 0000000..2a173df
--- /dev/null
+++ b/src/main/scala/com/drivergrp/core/stats.scala
@@ -0,0 +1,50 @@
+package com.drivergrp.core
+
+import com.drivergrp.core.logging.LoggerModule
+import com.drivergrp.core.time.{Time, TimeRange}
+
+object stats {
+
+ type StatsKey = String
+ type StatsKeys = Seq[StatsKey]
+
+
+ trait StatsModule {
+
+ def stats: Stats
+ }
+
+ trait Stats {
+
+ def recordStats(keys: StatsKeys, interval: TimeRange, value: BigDecimal): Unit
+
+ def recordStats(keys: StatsKeys, interval: TimeRange, value: Int): Unit =
+ recordStats(keys, interval, BigDecimal(value))
+
+ def recordStats(key: StatsKey, interval: TimeRange, value: BigDecimal): Unit =
+ recordStats(Vector(key), interval, value)
+
+ def recordStats(key: StatsKey, interval: TimeRange, value: Int): Unit =
+ recordStats(Vector(key), interval, BigDecimal(value))
+
+ def recordStats(keys: StatsKeys, time: Time, value: BigDecimal): Unit =
+ recordStats(keys, TimeRange(time, time), value)
+
+ def recordStats(keys: StatsKeys, time: Time, value: Int): Unit =
+ recordStats(keys, TimeRange(time, time), BigDecimal(value))
+
+ def recordStats(key: StatsKey, time: Time, value: BigDecimal): Unit =
+ recordStats(Vector(key), TimeRange(time, time), value)
+
+ def recordStats(key: StatsKey, time: Time, value: Int): Unit =
+ recordStats(Vector(key), TimeRange(time, time), BigDecimal(value))
+ }
+
+ trait LogStats extends Stats {
+ this: LoggerModule =>
+
+ def recordStats(keys: StatsKeys, interval: TimeRange, value: BigDecimal): Unit = {
+ log.audit(s"${keys.mkString(".")}(${interval.start.millis}-${interval.end.millis})=${value.toString}")
+ }
+ }
+}
diff --git a/src/main/scala/com/drivergrp/core/time.scala b/src/main/scala/com/drivergrp/core/time.scala
new file mode 100644
index 0000000..645c991
--- /dev/null
+++ b/src/main/scala/com/drivergrp/core/time.scala
@@ -0,0 +1,79 @@
+package com.drivergrp.core
+
+import java.text.SimpleDateFormat
+import java.util.{Calendar, Date, GregorianCalendar}
+
+import scala.concurrent.duration.Duration
+
+object time {
+
+ // The most useful time units
+ val Second = 1000L
+ val Seconds = Second
+ val Minute = 60 * Seconds
+ val Minutes = Minute
+ val Hour = 60 * Minutes
+ val Hours = Hour
+ val Day = 24 * Hours
+ val Days = Day
+ val Week = 7 * Days
+ val Weeks = Week
+
+
+ case class Time(millis: Long) extends AnyVal {
+
+ def isBefore(anotherTime: Time): Boolean = millis < anotherTime.millis
+
+ def isAfter(anotherTime: Time): Boolean = millis > anotherTime.millis
+
+ def advanceBy(duration: Duration): Time = Time(millis + duration.length)
+ }
+
+ case class TimeRange(start: Time, end: Time)
+
+ implicit def timeOrdering: Ordering[Time] = Ordering.by(_.millis)
+
+
+ def startOfMonth(time: Time) = {
+ make(new GregorianCalendar()) { cal =>
+ cal.setTime(new Date(time.millis))
+ cal.set(Calendar.DAY_OF_MONTH, cal.getActualMinimum(Calendar.DAY_OF_MONTH))
+ Time(cal.getTime.getTime)
+ }
+ }
+
+ def textualDate(time: Time): String =
+ new SimpleDateFormat("MMMM d, yyyy").format(new Date(time.millis))
+
+ def textualTime(time: Time): String =
+ new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a").format(new Date(time.millis))
+
+
+ object provider {
+
+ /**
+ * Time providers are supplying code with current times
+ * and are extremely useful for testing to check how system is going
+ * to behave at specific moments in time.
+ *
+ * All the calls to receive current time must be made using time
+ * provider injected to the caller.
+ */
+
+ trait TimeModule {
+ def time: TimeProvider
+ }
+
+ trait TimeProvider {
+ def currentTime(): Time
+ }
+
+ final class SystemTimeProvider extends TimeProvider {
+ def currentTime() = Time(System.currentTimeMillis())
+ }
+
+ final class SpecificTimeProvider(time: Time) extends TimeProvider {
+ def currentTime() = time
+ }
+ }
+}