From 901b02274fdfc08030443aac2f1760fc479b3816 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Fri, 29 Jun 2018 15:26:09 -0700 Subject: Add build support for ScalaJS --- src/main/scala/xyz/driver/core/app/DriverApp.scala | 294 --------------- src/main/scala/xyz/driver/core/app/init.scala | 119 ------ src/main/scala/xyz/driver/core/app/module.scala | 70 ---- src/main/scala/xyz/driver/core/auth.scala | 43 --- src/main/scala/xyz/driver/core/cache.scala | 110 ------ src/main/scala/xyz/driver/core/config.scala | 24 -- src/main/scala/xyz/driver/core/core.scala | 128 ------- .../xyz/driver/core/database/Converters.scala | 26 -- .../driver/core/database/MdcAsyncExecutor.scala | 53 --- .../core/database/PatchedHsqldbProfile.scala | 16 - .../xyz/driver/core/database/Repository.scala | 73 ---- .../core/database/SlickGetResultSupport.scala | 30 -- .../scala/xyz/driver/core/database/database.scala | 165 --------- .../scala/xyz/driver/core/database/package.scala | 61 ---- src/main/scala/xyz/driver/core/date.scala | 109 ------ src/main/scala/xyz/driver/core/domain.scala | 46 --- .../xyz/driver/core/file/FileSystemStorage.scala | 76 ---- .../scala/xyz/driver/core/file/GcsStorage.scala | 135 ------- .../scala/xyz/driver/core/file/S3Storage.scala | 87 ----- src/main/scala/xyz/driver/core/file/package.scala | 68 ---- src/main/scala/xyz/driver/core/future.scala | 87 ----- src/main/scala/xyz/driver/core/generators.scala | 138 ------- src/main/scala/xyz/driver/core/json.scala | 401 --------------------- .../driver/core/logging/MdcExecutionContext.scala | 31 -- .../scala/xyz/driver/core/logging/package.scala | 7 - src/main/scala/xyz/driver/core/messages.scala | 58 --- src/main/scala/xyz/driver/core/pubsub.scala | 145 -------- .../scala/xyz/driver/core/rest/DriverRoute.scala | 111 ------ .../core/rest/HttpRestServiceTransport.scala | 89 ----- .../xyz/driver/core/rest/PatchDirectives.scala | 104 ------ .../xyz/driver/core/rest/PooledHttpClient.scala | 67 ---- .../scala/xyz/driver/core/rest/ProxyRoute.scala | 26 -- .../scala/xyz/driver/core/rest/RestService.scala | 72 ---- .../driver/core/rest/SingleRequestHttpClient.scala | 29 -- src/main/scala/xyz/driver/core/rest/Swagger.scala | 127 ------- .../core/rest/auth/AlwaysAllowAuthorization.scala | 14 - .../xyz/driver/core/rest/auth/AuthProvider.scala | 73 ---- .../xyz/driver/core/rest/auth/Authorization.scala | 11 - .../core/rest/auth/AuthorizationResult.scala | 22 -- .../core/rest/auth/CachedTokenAuthorization.scala | 55 --- .../core/rest/auth/ChainedAuthorization.scala | 27 -- .../driver/core/rest/errors/serviceException.scala | 23 -- src/main/scala/xyz/driver/core/rest/package.scala | 286 --------------- .../xyz/driver/core/rest/serviceDiscovery.scala | 24 -- .../driver/core/rest/serviceRequestContext.scala | 74 ---- src/main/scala/xyz/driver/core/stats.scala | 58 --- .../xyz/driver/core/storage/BlobStorage.scala | 50 --- .../core/storage/FileSystemBlobStorage.scala | 82 ----- .../xyz/driver/core/storage/GcsBlobStorage.scala | 96 ----- .../xyz/driver/core/storage/channelStreams.scala | 112 ------ src/main/scala/xyz/driver/core/swagger.scala | 161 --------- src/main/scala/xyz/driver/core/time.scala | 175 --------- 52 files changed, 4568 deletions(-) delete mode 100644 src/main/scala/xyz/driver/core/app/DriverApp.scala delete mode 100644 src/main/scala/xyz/driver/core/app/init.scala delete mode 100644 src/main/scala/xyz/driver/core/app/module.scala delete mode 100644 src/main/scala/xyz/driver/core/auth.scala delete mode 100644 src/main/scala/xyz/driver/core/cache.scala delete mode 100644 src/main/scala/xyz/driver/core/config.scala delete mode 100644 src/main/scala/xyz/driver/core/core.scala delete mode 100644 src/main/scala/xyz/driver/core/database/Converters.scala delete mode 100644 src/main/scala/xyz/driver/core/database/MdcAsyncExecutor.scala delete mode 100644 src/main/scala/xyz/driver/core/database/PatchedHsqldbProfile.scala delete mode 100644 src/main/scala/xyz/driver/core/database/Repository.scala delete mode 100644 src/main/scala/xyz/driver/core/database/SlickGetResultSupport.scala delete mode 100644 src/main/scala/xyz/driver/core/database/database.scala delete mode 100644 src/main/scala/xyz/driver/core/database/package.scala delete mode 100644 src/main/scala/xyz/driver/core/date.scala delete mode 100644 src/main/scala/xyz/driver/core/domain.scala delete mode 100644 src/main/scala/xyz/driver/core/file/FileSystemStorage.scala delete mode 100644 src/main/scala/xyz/driver/core/file/GcsStorage.scala delete mode 100644 src/main/scala/xyz/driver/core/file/S3Storage.scala delete mode 100644 src/main/scala/xyz/driver/core/file/package.scala delete mode 100644 src/main/scala/xyz/driver/core/future.scala delete mode 100644 src/main/scala/xyz/driver/core/generators.scala delete mode 100644 src/main/scala/xyz/driver/core/json.scala delete mode 100644 src/main/scala/xyz/driver/core/logging/MdcExecutionContext.scala delete mode 100644 src/main/scala/xyz/driver/core/logging/package.scala delete mode 100644 src/main/scala/xyz/driver/core/messages.scala delete mode 100644 src/main/scala/xyz/driver/core/pubsub.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/DriverRoute.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/HttpRestServiceTransport.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/PatchDirectives.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/PooledHttpClient.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/ProxyRoute.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/RestService.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/SingleRequestHttpClient.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/Swagger.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/auth/AlwaysAllowAuthorization.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/auth/AuthProvider.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/auth/Authorization.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/auth/AuthorizationResult.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/auth/CachedTokenAuthorization.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/auth/ChainedAuthorization.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/errors/serviceException.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/package.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/serviceDiscovery.scala delete mode 100644 src/main/scala/xyz/driver/core/rest/serviceRequestContext.scala delete mode 100644 src/main/scala/xyz/driver/core/stats.scala delete mode 100644 src/main/scala/xyz/driver/core/storage/BlobStorage.scala delete mode 100644 src/main/scala/xyz/driver/core/storage/FileSystemBlobStorage.scala delete mode 100644 src/main/scala/xyz/driver/core/storage/GcsBlobStorage.scala delete mode 100644 src/main/scala/xyz/driver/core/storage/channelStreams.scala delete mode 100644 src/main/scala/xyz/driver/core/swagger.scala delete mode 100644 src/main/scala/xyz/driver/core/time.scala (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/core/app/DriverApp.scala b/src/main/scala/xyz/driver/core/app/DriverApp.scala deleted file mode 100644 index 6dd98e3..0000000 --- a/src/main/scala/xyz/driver/core/app/DriverApp.scala +++ /dev/null @@ -1,294 +0,0 @@ -package xyz.driver.core.app - -import akka.actor.ActorSystem -import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport -import akka.http.scaladsl.model._ -import akka.http.scaladsl.model.headers._ -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.RouteResult._ -import akka.http.scaladsl.server._ -import akka.http.scaladsl.{Http, HttpExt} -import akka.stream.ActorMaterializer -import com.typesafe.config.Config -import com.typesafe.scalalogging.Logger -import io.swagger.models.Scheme -import org.slf4j.{LoggerFactory, MDC} -import xyz.driver.core -import xyz.driver.core.rest._ -import xyz.driver.core.stats.SystemStats -import xyz.driver.core.time.Time -import xyz.driver.core.time.provider.{SystemTimeProvider, TimeProvider} -import xyz.driver.tracing.TracingDirectives._ -import xyz.driver.tracing._ - -import scala.concurrent.duration._ -import scala.concurrent.{Await, ExecutionContext} -import scala.util.Try -import scalaz.Scalaz.stringInstance -import scalaz.syntax.equal._ - -class DriverApp( - appName: String, - version: String, - gitHash: String, - modules: Seq[Module], - time: TimeProvider = new SystemTimeProvider(), - log: Logger = Logger(LoggerFactory.getLogger(classOf[DriverApp])), - config: Config = core.config.loadDefaultConfig, - interface: String = "::0", - baseUrl: String = "localhost:8080", - scheme: String = "http", - port: Int = 8080, - tracer: Tracer = NoTracer)(implicit actorSystem: ActorSystem, executionContext: ExecutionContext) { - self => - - implicit private lazy val materializer: ActorMaterializer = ActorMaterializer()(actorSystem) - private lazy val http: HttpExt = Http()(actorSystem) - val appEnvironment: String = config.getString("application.environment") - - def run(): Unit = { - activateServices(modules) - scheduleServicesDeactivation(modules) - bindHttp(modules) - Console.print(s"${this.getClass.getName} App is started\n") - } - - def stop(): Unit = { - http.shutdownAllConnectionPools().onComplete { _ => - Await.result(tracer.close(), 15.seconds) // flush out any remaining traces from the buffer - val terminated = Await.result(actorSystem.terminate(), 30.seconds) - val addressTerminated = if (terminated.addressTerminated) "is" else "is not" - Console.print(s"${this.getClass.getName} App $addressTerminated stopped ") - } - } - - protected lazy val allowedCorsDomainSuffixes: Set[HttpOrigin] = { - import scala.collection.JavaConverters._ - config - .getConfigList("application.cors.allowedOrigins") - .asScala - .map { c => - HttpOrigin(c.getString("scheme"), Host(c.getString("hostSuffix"))) - }(scala.collection.breakOut) - } - - protected lazy val defaultCorsAllowedMethods: Set[HttpMethod] = { - import scala.collection.JavaConverters._ - config.getStringList("application.cors.allowedMethods").asScala.toSet.flatMap(HttpMethods.getForKey) - } - - protected lazy val defaultCorsAllowedOrigin: Origin = { - Origin(allowedCorsDomainSuffixes.to[collection.immutable.Seq]) - } - - protected def corsAllowedOriginHeader(origin: Option[Origin]): HttpHeader = { - val allowedOrigin = - origin - .filter { requestOrigin => - allowedCorsDomainSuffixes.exists { allowedOriginSuffix => - requestOrigin.origins.exists(o => - o.scheme == allowedOriginSuffix.scheme && - o.host.host.address.endsWith(allowedOriginSuffix.host.host.address())) - } - } - .getOrElse(defaultCorsAllowedOrigin) - - `Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigin.origins: _*)) - } - - protected def respondWithAllCorsHeaders: Directive0 = { - respondWithCorsAllowedHeaders tflatMap { _ => - respondWithCorsAllowedMethodHeaders(defaultCorsAllowedMethods) tflatMap { _ => - optionalHeaderValueByType[Origin](()) flatMap { origin => - respondWithHeader(corsAllowedOriginHeader(origin)) - } - } - } - } - - private def extractHeader(request: HttpRequest)(headerName: String): Option[String] = - request.headers.find(_.name().toLowerCase === headerName).map(_.value()) - - protected def defaultOptionsRoute: Route = options { - respondWithAllCorsHeaders { - complete("OK") - } - } - - def appRoute: Route = { - val serviceTypes = modules.flatMap(_.routeTypes) - val swaggerService = new Swagger(baseUrl, Scheme.forValue(scheme) :: Nil, version, serviceTypes, config, log) - val swaggerRoute = swaggerService.routes ~ swaggerService.swaggerUI - val versionRt = versionRoute(version, gitHash, time.currentTime()) - val basicRoutes = new DriverRoute { - override def log: Logger = self.log - override def route: Route = versionRt ~ healthRoute ~ swaggerRoute - } - val combinedRoute = - Route.seal(modules.map(_.route).foldLeft(basicRoutes.routeWithDefaults)(_ ~ _) ~ defaultOptionsRoute) - - (extractHost & extractClientIP & trace(tracer) & handleRejections(authenticationRejectionHandler)) { - case (origin, ip) => - ctx => - val trackingId = extractTrackingId(ctx.request) - MDC.put("trackingId", trackingId) - - val updatedStacktrace = - (extractStacktrace(ctx.request) ++ Array(appName)).mkString("->") - MDC.put("stack", updatedStacktrace) - - storeRequestContextToMdc(ctx.request, origin, ip) - - log.info(s"""Received request ${ctx.request.method.value} ${ctx.request.uri} (trace: $trackingId)""") - - val contextWithTrackingId = - ctx.withRequest( - ctx.request - .addHeader(RawHeader(ContextHeaders.TrackingIdHeader, trackingId)) - .addHeader(RawHeader(ContextHeaders.StacktraceHeader, updatedStacktrace))) - - val logResponses = mapRouteResult { - case c @ Complete(response) => - log.info( - s"Responded to ${ctx.request.method.value} ${ctx.request.uri} " + - s"with ${response.status.toString} (trace: $trackingId)") - c - case r @ Rejected(rejections) => - log.warn( - s"Request ${ctx.request.method.value} ${ctx.request.uri} " + - s"(trace: $trackingId) is rejected:\n${rejections.mkString(",\n")}") - r - } - - respondWithAllCorsHeaders(logResponses(combinedRoute))(contextWithTrackingId) - } - } - - protected def authenticationRejectionHandler: RejectionHandler = - RejectionHandler - .newBuilder() - .handle { - case AuthenticationFailedRejection(_, challenge) => - complete(HttpResponse(StatusCodes.Unauthorized, entity = challenge.realm)) - } - .result() - - protected def bindHttp(modules: Seq[Module]): Unit = { - val _ = http.bindAndHandle(route2HandlerFlow(appRoute), interface, port)(materializer) - } - - private def storeRequestContextToMdc(request: HttpRequest, origin: String, ip: RemoteAddress): Unit = { - - MDC.put("origin", origin) - MDC.put("ip", ip.toOption.map(_.getHostAddress).getOrElse("unknown")) - MDC.put("remoteHost", ip.toOption.map(_.getHostName).getOrElse("unknown")) - - MDC.put( - "xForwardedFor", - extractHeader(request)("x-forwarded-for") - .orElse(extractHeader(request)("x_forwarded_for")) - .getOrElse("unknown")) - MDC.put("remoteAddress", extractHeader(request)("remote-address").getOrElse("unknown")) - MDC.put("userAgent", extractHeader(request)("user-agent").getOrElse("unknown")) - } - - protected def versionRoute(version: String, gitHash: String, startupTime: Time): Route = { - import spray.json._ - import DefaultJsonProtocol._ - import SprayJsonSupport._ - - path("version") { - val currentTime = time.currentTime().millis - complete( - Map( - "version" -> version.toJson, - "gitHash" -> gitHash.toJson, - "modules" -> modules.map(_.name).toJson, - "dependencies" -> collectAppDependencies().toJson, - "startupTime" -> startupTime.millis.toString.toJson, - "serverTime" -> currentTime.toString.toJson, - "uptime" -> (currentTime - startupTime.millis).toString.toJson - ).toJson) - } - } - - protected def collectAppDependencies(): Map[String, String] = { - - def serviceWithLocation(serviceName: String): (String, String) = - serviceName -> Try(config.getString(s"services.$serviceName.baseUrl")).getOrElse("not-detected") - - modules.flatMap(module => module.serviceDiscovery.getUsedServices.map(serviceWithLocation).toSeq).toMap - } - - protected def healthRoute: Route = { - import spray.json._ - import DefaultJsonProtocol._ - import SprayJsonSupport._ - import spray.json._ - - val memoryUsage = SystemStats.memoryUsage - val gcStats = SystemStats.garbageCollectorStats - - path("health") { - complete( - Map( - "availableProcessors" -> SystemStats.availableProcessors.toJson, - "memoryUsage" -> Map( - "free" -> memoryUsage.free.toJson, - "total" -> memoryUsage.total.toJson, - "max" -> memoryUsage.max.toJson - ).toJson, - "gcStats" -> Map( - "garbageCollectionTime" -> gcStats.garbageCollectionTime.toJson, - "totalGarbageCollections" -> gcStats.totalGarbageCollections.toJson - ).toJson, - "fileSystemSpace" -> SystemStats.fileSystemSpace.map { f => - Map( - "path" -> f.path.toJson, - "freeSpace" -> f.freeSpace.toJson, - "totalSpace" -> f.totalSpace.toJson, - "usableSpace" -> f.usableSpace.toJson) - }.toJson, - "operatingSystem" -> SystemStats.operatingSystemStats.toJson - )) - } - } - - /** - * Initializes services - */ - protected def activateServices(services: Seq[Module]): Unit = { - services.foreach { service => - Console.print(s"Service ${service.name} starts ...") - try { - service.activate() - } catch { - case t: Throwable => - log.error(s"Service ${service.name} failed to activate", t) - Console.print(" Failed! (check log)") - } - Console.print(" Done\n") - } - } - - /** - * Schedules services to be deactivated on the app shutdown - */ - protected def scheduleServicesDeactivation(services: Seq[Module]): Unit = { - Runtime.getRuntime.addShutdownHook(new Thread() { - override def run(): Unit = { - services.foreach { service => - Console.print(s"Service ${service.name} shutting down ...\n") - try { - service.deactivate() - } catch { - case t: Throwable => - log.error(s"Service ${service.name} failed to deactivate", t) - Console.print(" Failed! (check log)") - } - Console.print(s"Service ${service.name} is shut down\n") - } - } - }) - } -} diff --git a/src/main/scala/xyz/driver/core/app/init.scala b/src/main/scala/xyz/driver/core/app/init.scala deleted file mode 100644 index 119c91a..0000000 --- a/src/main/scala/xyz/driver/core/app/init.scala +++ /dev/null @@ -1,119 +0,0 @@ -package xyz.driver.core.app - -import java.nio.file.{Files, Paths} -import java.util.concurrent.{Executor, Executors} - -import akka.actor.ActorSystem -import akka.stream.ActorMaterializer -import com.typesafe.config.{Config, ConfigFactory} -import com.typesafe.scalalogging.Logger -import org.slf4j.LoggerFactory -import xyz.driver.core.logging.MdcExecutionContext -import xyz.driver.core.time.provider.{SystemTimeProvider, TimeProvider} -import xyz.driver.tracing.{GoogleTracer, NoTracer, Tracer} - -import scala.concurrent.ExecutionContext -import scala.util.Try - -object init { - - type RequiredBuildInfo = { - val name: String - val version: String - val gitHeadCommit: scala.Option[String] - } - - case class ApplicationContext(config: Config, time: TimeProvider, log: Logger) - - /** NOTE: This needs to be the first that is run when application starts. - * Otherwise if another command causes the logger to be instantiated, - * it will default to logback.xml, and not honor this configuration - */ - def configureLogging(): Unit = { - scala.sys.env.get("JSON_LOGGING") match { - case Some("true") => - System.setProperty("logback.configurationFile", "deployed-logback.xml") - case _ => - System.setProperty("logback.configurationFile", "logback.xml") - } - } - - def getEnvironmentSpecificConfig(): Config = { - scala.sys.env.get("APPLICATION_CONFIG_TYPE") match { - case Some("deployed") => - ConfigFactory.load(this.getClass.getClassLoader, "deployed-application.conf") - case _ => - xyz.driver.core.config.loadDefaultConfig - } - } - - def configureTracer(actorSystem: ActorSystem, applicationContext: ApplicationContext): Tracer = { - - val serviceAccountKeyFile = - Paths.get(applicationContext.config.getString("tracing.google.serviceAccountKeyfile")) - - if (Files.exists(serviceAccountKeyFile)) { - val materializer = ActorMaterializer()(actorSystem) - new GoogleTracer( - projectId = applicationContext.config.getString("tracing.google.projectId"), - serviceAccountFile = serviceAccountKeyFile - )(actorSystem, materializer) - } else { - applicationContext.log.warn(s"Tracing file $serviceAccountKeyFile was not found, using NoTracer!") - NoTracer - } - } - - def serviceActorSystem(serviceName: String, executionContext: ExecutionContext, config: Config): ActorSystem = { - val actorSystem = - ActorSystem(s"$serviceName-actors", Option(config), Option.empty[ClassLoader], Option(executionContext)) - - Runtime.getRuntime.addShutdownHook(new Thread() { - override def run(): Unit = Try(actorSystem.terminate()) - }) - - actorSystem - } - - def toMdcExecutionContext(executor: Executor) = - new MdcExecutionContext(ExecutionContext.fromExecutor(executor)) - - def newFixedMdcExecutionContext(capacity: Int): MdcExecutionContext = - toMdcExecutionContext(Executors.newFixedThreadPool(capacity)) - - def defaultApplicationContext(): ApplicationContext = { - val config = getEnvironmentSpecificConfig() - - val time = new SystemTimeProvider() - val log = Logger(LoggerFactory.getLogger(classOf[DriverApp])) - - ApplicationContext(config, time, log) - } - - def createDefaultApplication( - modules: Seq[Module], - buildInfo: RequiredBuildInfo, - actorSystem: ActorSystem, - tracer: Tracer, - context: ApplicationContext): DriverApp = { - val scheme = context.config.getString("application.scheme") - val baseUrl = context.config.getString("application.baseUrl") - val port = context.config.getInt("application.port") - - new DriverApp( - buildInfo.name, - buildInfo.version, - buildInfo.gitHeadCommit.getOrElse("None"), - modules = modules, - context.time, - context.log, - context.config, - interface = "0.0.0.0", - baseUrl, - scheme, - port, - tracer - )(actorSystem, actorSystem.dispatcher) - } - -} diff --git a/src/main/scala/xyz/driver/core/app/module.scala b/src/main/scala/xyz/driver/core/app/module.scala deleted file mode 100644 index 7be38eb..0000000 --- a/src/main/scala/xyz/driver/core/app/module.scala +++ /dev/null @@ -1,70 +0,0 @@ -package xyz.driver.core.app - -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Directives.complete -import akka.http.scaladsl.server.{Route, RouteConcatenation} -import com.typesafe.config.Config -import com.typesafe.scalalogging.Logger -import xyz.driver.core.database.Database -import xyz.driver.core.rest.{DriverRoute, NoServiceDiscovery, SavingUsedServiceDiscovery, ServiceDiscovery} - -import scala.reflect.runtime.universe._ - -trait Module { - val name: String - def route: Route - def routeTypes: Seq[Type] - - val serviceDiscovery: ServiceDiscovery with SavingUsedServiceDiscovery = new NoServiceDiscovery() - - def activate(): Unit = {} - def deactivate(): Unit = {} -} - -class EmptyModule extends Module { - override val name: String = "Nothing" - - override def route: Route = complete(StatusCodes.OK) - override def routeTypes: Seq[Type] = Seq.empty[Type] -} - -class SimpleModule(override val name: String, theRoute: Route, routeType: Type) extends Module { - private val driverRoute: DriverRoute = new DriverRoute { - override def route: Route = theRoute - override val log: Logger = xyz.driver.core.logging.NoLogger - } - - override def route: Route = driverRoute.routeWithDefaults - override def routeTypes: Seq[Type] = Seq(routeType) -} - -trait SingleDatabaseModule { self: Module => - - val databaseName: String - val config: Config - - def database = Database.fromConfig(config, databaseName) - - override def deactivate(): Unit = { - try { - database.database.close() - } finally { - self.deactivate() - } - } -} - -/** - * Module implementation which may be used to compose multiple modules - * - * @param name more general name of the composite module, - * must be provided as there is no good way to automatically - * generalize the name from the composed modules' names - * @param modules modules to compose into a single one - */ -class CompositeModule(override val name: String, modules: Seq[Module]) extends Module with RouteConcatenation { - override def route: Route = RouteConcatenation.concat(modules.map(_.route): _*) - override def routeTypes: Seq[Type] = modules.flatMap(_.routeTypes) - override def activate(): Unit = modules.foreach(_.activate()) - override def deactivate(): Unit = modules.reverse.foreach(_.deactivate()) -} diff --git a/src/main/scala/xyz/driver/core/auth.scala b/src/main/scala/xyz/driver/core/auth.scala deleted file mode 100644 index 896bd89..0000000 --- a/src/main/scala/xyz/driver/core/auth.scala +++ /dev/null @@ -1,43 +0,0 @@ -package xyz.driver.core - -import xyz.driver.core.domain.Email -import xyz.driver.core.time.Time -import scalaz.Equal - -object auth { - - trait Permission - - final case class Role(id: Id[Role], name: Name[Role]) { - - def oneOf(roles: Role*): Boolean = roles.contains(this) - - def oneOf(roles: Set[Role]): Boolean = roles.contains(this) - } - - object Role { - implicit def idEqual: Equal[Role] = Equal.equal[Role](_ == _) - } - - trait User { - def id: Id[User] - } - - final case class AuthToken(value: String) - - final case class AuthTokenUserInfo( - id: Id[User], - email: Email, - emailVerified: Boolean, - audience: String, - roles: Set[Role], - expirationTime: Time) - extends User - - final case class RefreshToken(value: String) - final case class PermissionsToken(value: String) - - final case class PasswordHash(value: String) - - final case class AuthCredentials(identifier: String, password: String) -} diff --git a/src/main/scala/xyz/driver/core/cache.scala b/src/main/scala/xyz/driver/core/cache.scala deleted file mode 100644 index 3500a2a..0000000 --- a/src/main/scala/xyz/driver/core/cache.scala +++ /dev/null @@ -1,110 +0,0 @@ -package xyz.driver.core - -import java.util.concurrent.{Callable, TimeUnit} - -import com.google.common.cache.{CacheBuilder, Cache => GuavaCache} -import com.typesafe.scalalogging.Logger - -import scala.concurrent.duration.{Duration, _} -import scala.concurrent.{ExecutionContext, Future} - -object cache { - - /** - * FutureCache is used to represent an in-memory, in-process, asynchronous cache. - * - * Every cache operation is atomic. - * - * This implementation evicts failed results, - * and doesn't interrupt the underlying request that has been fired off. - */ - class AsyncCache[K, V](name: String, cache: GuavaCache[K, Future[V]])(implicit executionContext: ExecutionContext) { - - private[this] val log = Logger(s"AsyncCache.$name") - private[this] val underlying = cache.asMap() - - private[this] def evictOnFailure(key: K, f: Future[V]): Future[V] = { - f.failed foreach { - case ex: Throwable => - log.debug(s"Evict key $key due to exception $ex") - evict(key, f) - } - f // we return the original future to make evict(k, f) easier to work with. - } - - /** - * Equivalent to getOrElseUpdate - */ - def apply(key: K)(value: => Future[V]): Future[V] = getOrElseUpdate(key)(value) - - /** - * Gets the cached Future. - * - * @return None if a value hasn't been specified for that key yet - * Some(ksync computation) if the value has been specified. Just - * because this returns Some(..) doesn't mean that it has been - * satisfied, but if it hasn't been satisfied, it's probably - * in-flight. - */ - def get(key: K): Option[Future[V]] = Option(underlying.get(key)) - - /** - * Gets the cached Future, or if it hasn't been returned yet, computes it and - * returns that value. - */ - def getOrElseUpdate(key: K)(compute: => Future[V]): Future[V] = { - log.debug(s"Try to retrieve key $key from cache") - evictOnFailure(key, cache.get(key, new Callable[Future[V]] { - def call(): Future[V] = { - log.debug(s"Cache miss, load the key: $key") - compute - } - })) - } - - /** - * Unconditionally sets a value for a given key - */ - def set(key: K, value: Future[V]): Unit = { - cache.put(key, value) - evictOnFailure(key, value) - } - - /** - * Evicts the contents of a `key` if the old value is `value`. - * - * Since `scala.concurrent.Future` uses reference equality, you must use the - * same object reference to evict a value. - * - * @return true if the key was evicted - * false if the key was not evicted - */ - def evict(key: K, value: Future[V]): Boolean = underlying.remove(key, value) - - /** - * @return the number of results that have been computed successfully or are in flight. - */ - def size: Int = cache.size.toInt - } - - object AsyncCache { - val DEFAULT_CAPACITY: Long = 10000L - val DEFAULT_READ_EXPIRATION: Duration = 10 minutes - val DEFAULT_WRITE_EXPIRATION: Duration = 1 hour - - def apply[K <: AnyRef, V <: AnyRef]( - name: String, - capacity: Long = DEFAULT_CAPACITY, - readExpiration: Duration = DEFAULT_READ_EXPIRATION, - writeExpiration: Duration = DEFAULT_WRITE_EXPIRATION)( - implicit executionContext: ExecutionContext): AsyncCache[K, V] = { - val guavaCache = CacheBuilder - .newBuilder() - .maximumSize(capacity) - .expireAfterAccess(readExpiration.toSeconds, TimeUnit.SECONDS) - .expireAfterWrite(writeExpiration.toSeconds, TimeUnit.SECONDS) - .build[K, Future[V]]() - new AsyncCache(name, guavaCache) - } - } -} diff --git a/src/main/scala/xyz/driver/core/config.scala b/src/main/scala/xyz/driver/core/config.scala deleted file mode 100644 index be81408..0000000 --- a/src/main/scala/xyz/driver/core/config.scala +++ /dev/null @@ -1,24 +0,0 @@ -package xyz.driver.core - -import java.io.File -import com.typesafe.config.{Config, ConfigFactory} - -object config { - - def loadDefaultConfig: Config = { - val configDefaults = ConfigFactory.load(this.getClass.getClassLoader, "application.conf") - - scala.sys.env.get("APPLICATION_CONFIG").orElse(scala.sys.props.get("application.config")) match { - - case Some(filename) => - val configFile = new File(filename) - if (configFile.exists()) { - ConfigFactory.parseFile(configFile).withFallback(configDefaults) - } else { - throw new IllegalStateException(s"No config found at $filename") - } - - case None => configDefaults - } - } -} diff --git a/src/main/scala/xyz/driver/core/core.scala b/src/main/scala/xyz/driver/core/core.scala deleted file mode 100644 index 72237b9..0000000 --- a/src/main/scala/xyz/driver/core/core.scala +++ /dev/null @@ -1,128 +0,0 @@ -package xyz.driver - -import scalaz.{Equal, Monad, OptionT} -import eu.timepit.refined.api.{Refined, Validate} -import eu.timepit.refined.collection.NonEmpty -import xyz.driver.core.rest.errors.ExternalServiceException - -import scala.concurrent.{ExecutionContext, Future} - -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() - } - } - - object tagging { - private[core] trait Tagged[+V, +Tag] - - implicit class Taggable[V <: Any](val v: V) extends AnyVal { - def tagged[Tag]: V @@ Tag = v.asInstanceOf[V @@ Tag] - } - } - type @@[+V, +Tag] = V with tagging.Tagged[V, Tag] - - implicit class OptionTExtensions[H[_]: Monad, T](optionTValue: OptionT[H, T]) { - - def returnUnit: H[Unit] = optionTValue.fold[Unit](_ => (), ()) - - def continueIgnoringNone: OptionT[H, Unit] = - optionTValue.map(_ => ()).orElse(OptionT.some[H, Unit](())) - - def subflatMap[B](f: T => Option[B]): OptionT[H, B] = - OptionT.optionT[H](implicitly[Monad[H]].map(optionTValue.run)(_.flatMap(f))) - } - - implicit class MonadicExtensions[H[_]: Monad, T](monadicValue: H[T]) { - private implicit val monadT = implicitly[Monad[H]] - - def returnUnit: H[Unit] = monadT(monadicValue)(_ => ()) - - def toOptionT: OptionT[H, T] = - OptionT.optionT[H](monadT(monadicValue)(value => Option(value))) - - def toUnitOptionT: OptionT[H, Unit] = - OptionT.optionT[H](monadT(monadicValue)(_ => Option(()))) - } - - implicit class FutureExtensions[T](future: Future[T]) { - def passThroughExternalServiceException(implicit executionContext: ExecutionContext): Future[T] = - future.transform(identity, { - case ExternalServiceException(_, _, Some(e)) => e - case t: Throwable => t - }) - } -} - -package core { - - final case class Id[+Tag](value: String) extends AnyVal { - @inline def length: Int = value.length - override def toString: String = value - } - - @SuppressWarnings(Array("org.wartremover.warts.ImplicitConversion")) - object Id { - implicit def idEqual[T]: Equal[Id[T]] = Equal.equal[Id[T]](_ == _) - implicit def idOrdering[T]: Ordering[Id[T]] = Ordering.by[Id[T], String](_.value) - - sealed class Mapper[E, R] { - def apply[T >: E](id: Id[R]): Id[T] = Id[E](id.value) - def apply[T >: R](id: Id[E])(implicit dummy: DummyImplicit): Id[T] = Id[R](id.value) - } - object Mapper { - def apply[E, R] = new Mapper[E, R] - } - implicit def convertRE[R, E](id: Id[R])(implicit mapper: Mapper[E, R]): Id[E] = mapper[E](id) - implicit def convertER[E, R](id: Id[E])(implicit mapper: Mapper[E, R]): Id[R] = mapper[R](id) - } - - final case class Name[+Tag](value: String) extends AnyVal { - @inline def length: Int = value.length - override def toString: String = value - } - - object Name { - implicit def nameEqual[T]: Equal[Name[T]] = Equal.equal[Name[T]](_ == _) - implicit def nameOrdering[T]: Ordering[Name[T]] = Ordering.by(_.value) - - implicit def nameValidator[T, P](implicit stringValidate: Validate[String, P]): Validate[Name[T], P] = { - Validate.instance[Name[T], P, stringValidate.R]( - name => stringValidate.validate(name.value), - name => stringValidate.showExpr(name.value)) - } - } - - final case class NonEmptyName[+Tag](value: String Refined NonEmpty) { - @inline def length: Int = value.value.length - override def toString: String = value.value - } - - object NonEmptyName { - implicit def nonEmptyNameEqual[T]: Equal[NonEmptyName[T]] = - Equal.equal[NonEmptyName[T]](_.value.value == _.value.value) - - implicit def nonEmptyNameOrdering[T]: Ordering[NonEmptyName[T]] = Ordering.by(_.value.value) - } - - final case class Revision[T](id: String) - - object Revision { - implicit def revisionEqual[T]: Equal[Revision[T]] = Equal.equal[Revision[T]](_.id == _.id) - } - - final case class Base64(value: String) -} diff --git a/src/main/scala/xyz/driver/core/database/Converters.scala b/src/main/scala/xyz/driver/core/database/Converters.scala deleted file mode 100644 index ad79abf..0000000 --- a/src/main/scala/xyz/driver/core/database/Converters.scala +++ /dev/null @@ -1,26 +0,0 @@ -package xyz.driver.core.database - -import xyz.driver.core.rest.errors.DatabaseException - -import scala.reflect.ClassTag - -/** - * Helper methods for converting between table rows and Scala objects - */ -trait Converters { - def fromStringOrThrow[ADT](entityStr: String, mapper: (String => Option[ADT]), entityName: String): ADT = - mapper(entityStr).getOrElse(throw DatabaseException(s"Invalid $entityName in database: $entityStr")) - - def expectValid[ADT](mapper: String => Option[ADT], query: String)(implicit ct: ClassTag[ADT]): ADT = - fromStringOrThrow[ADT](query, mapper, ct.toString()) - - def expectExistsAndValid[ADT](mapper: String => Option[ADT], query: Option[String], contextMsg: String = "")( - implicit ct: ClassTag[ADT]): ADT = { - expectValid[ADT](mapper, query.getOrElse(throw DatabaseException(contextMsg))) - } - - def expectValidOrEmpty[ADT](mapper: String => Option[ADT], query: Option[String], contextMsg: String = "")( - implicit ct: ClassTag[ADT]): Option[ADT] = { - query.map(expectValid[ADT](mapper, _)) - } -} diff --git a/src/main/scala/xyz/driver/core/database/MdcAsyncExecutor.scala b/src/main/scala/xyz/driver/core/database/MdcAsyncExecutor.scala deleted file mode 100644 index 5939efb..0000000 --- a/src/main/scala/xyz/driver/core/database/MdcAsyncExecutor.scala +++ /dev/null @@ -1,53 +0,0 @@ -/** Code ported from "de.geekonaut" %% "slickmdc" % "1.0.0" - * License: @see https://github.com/AVGP/slickmdc/blob/master/LICENSE - * Blog post: @see http://50linesofco.de/post/2016-07-01-slick-and-slf4j-mdc-logging-in-scala.html - */ -package xyz.driver.core -package database - -import java.util.concurrent._ -import java.util.concurrent.atomic.AtomicInteger - -import scala.concurrent._ -import com.typesafe.scalalogging.StrictLogging -import slick.util.AsyncExecutor - -import logging.MdcExecutionContext - -/** Taken from the original Slick AsyncExecutor and simplified - * @see https://github.com/slick/slick/blob/3.1/slick/src/main/scala/slick/util/AsyncExecutor.scala - */ -object MdcAsyncExecutor extends StrictLogging { - - /** Create an AsyncExecutor with a fixed-size thread pool. - * - * @param name The name for the thread pool. - * @param numThreads The number of threads in the pool. - */ - def apply(name: String, numThreads: Int): AsyncExecutor = { - new AsyncExecutor { - val tf = new DaemonThreadFactory(name + "-") - - lazy val executionContext = { - new MdcExecutionContext(ExecutionContext.fromExecutor(Executors.newFixedThreadPool(numThreads, tf))) - } - - def close(): Unit = {} - } - } - - def default(name: String = "AsyncExecutor.default"): AsyncExecutor = apply(name, 20) - - private class DaemonThreadFactory(namePrefix: String) extends ThreadFactory { - private[this] val group = - Option(System.getSecurityManager).fold(Thread.currentThread.getThreadGroup)(_.getThreadGroup) - private[this] val threadNumber = new AtomicInteger(1) - - def newThread(r: Runnable): Thread = { - val t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement, 0) - if (!t.isDaemon) t.setDaemon(true) - if (t.getPriority != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY) - t - } - } -} diff --git a/src/main/scala/xyz/driver/core/database/PatchedHsqldbProfile.scala b/src/main/scala/xyz/driver/core/database/PatchedHsqldbProfile.scala deleted file mode 100644 index e2efd32..0000000 --- a/src/main/scala/xyz/driver/core/database/PatchedHsqldbProfile.scala +++ /dev/null @@ -1,16 +0,0 @@ -package xyz.driver.core.database - -import slick.jdbc.{HsqldbProfile, JdbcType} -import slick.ast.FieldSymbol -import slick.relational.RelationalProfile - -trait PatchedHsqldbProfile extends HsqldbProfile { - override def defaultSqlTypeName(tmd: JdbcType[_], sym: Option[FieldSymbol]): String = tmd.sqlType match { - case java.sql.Types.VARCHAR => - val size = sym.flatMap(_.findColumnOption[RelationalProfile.ColumnOption.Length]) - size.fold("LONGVARCHAR")(l => if (l.varying) s"VARCHAR(${l.length})" else s"CHAR(${l.length})") - case _ => super.defaultSqlTypeName(tmd, sym) - } -} - -object PatchedHsqldbProfile extends PatchedHsqldbProfile diff --git a/src/main/scala/xyz/driver/core/database/Repository.scala b/src/main/scala/xyz/driver/core/database/Repository.scala deleted file mode 100644 index 31c79ad..0000000 --- a/src/main/scala/xyz/driver/core/database/Repository.scala +++ /dev/null @@ -1,73 +0,0 @@ -package xyz.driver.core.database - -import scalaz.std.scalaFuture._ -import scalaz.{ListT, Monad, OptionT} -import slick.lifted.{AbstractTable, CanBeQueryCondition, RunnableCompiled} -import slick.{lifted => sl} - -import scala.concurrent.{ExecutionContext, Future} - -trait Repository { - type T[D] - implicit def monadT: Monad[T] - - def execute[D](operations: T[D]): Future[D] - def noAction[V](v: V): T[V] - def customAction[R](action: => Future[R]): T[R] - - def customAction[R](action: => OptionT[Future, R]): OptionT[T, R] = - OptionT[T, R](customAction(action.run)) -} - -class FutureRepository(executionContext: ExecutionContext) extends Repository { - implicit val exec: ExecutionContext = executionContext - override type T[D] = Future[D] - implicit val monadT: Monad[Future] = implicitly[Monad[Future]] - - def execute[D](operations: T[D]): Future[D] = operations - def noAction[V](v: V): T[V] = Future.successful(v) - def customAction[R](action: => Future[R]): T[R] = action -} - -class SlickRepository(database: Database, executionContext: ExecutionContext) extends Repository { - import database.profile.api._ - implicit val exec: ExecutionContext = executionContext - - override type T[D] = slick.dbio.DBIO[D] - - implicit protected class QueryOps[+E, U](query: Query[E, U, Seq]) { - def resultT: ListT[T, U] = ListT[T, U](query.result.map(_.toList)) - - def maybeFilter[V, R: CanBeQueryCondition](data: Option[V])(f: V => E => R): sl.Query[E, U, Seq] = - data.map(v => query.withFilter(f(v))).getOrElse(query) - } - - implicit protected class CompiledQueryOps[U](compiledQuery: RunnableCompiled[_, Seq[U]]) { - def resultT: ListT[T, U] = ListT.listT[T](compiledQuery.result.map(_.toList)) - } - - private val dbioMonad = new Monad[T] { - override def point[A](a: => A): T[A] = DBIO.successful(a) - - override def bind[A, B](fa: T[A])(f: A => T[B]): T[B] = fa.flatMap(f) - } - - override implicit def monadT: Monad[T] = dbioMonad - - override def execute[D](readOperations: T[D]): Future[D] = { - database.database.run(readOperations.transactionally) - } - - override def noAction[V](v: V): T[V] = DBIO.successful(v) - - override def customAction[R](action: => Future[R]): T[R] = DBIO.from(action) - - def affectsRows(updatesCount: Int): Option[Unit] = { - if (updatesCount > 0) Some(()) else None - } - - def insertReturning[AT <: AbstractTable[_], V](table: TableQuery[AT])( - row: AT#TableElementType): slick.dbio.DBIO[AT#TableElementType] = { - table.returning(table) += row - } -} diff --git a/src/main/scala/xyz/driver/core/database/SlickGetResultSupport.scala b/src/main/scala/xyz/driver/core/database/SlickGetResultSupport.scala deleted file mode 100644 index 8293371..0000000 --- a/src/main/scala/xyz/driver/core/database/SlickGetResultSupport.scala +++ /dev/null @@ -1,30 +0,0 @@ -package xyz.driver.core.database - -import slick.jdbc.GetResult -import xyz.driver.core.date.Date -import xyz.driver.core.time.Time -import xyz.driver.core.{Id, Name} - -trait SlickGetResultSupport { - implicit def GetId[U]: GetResult[Id[U]] = - GetResult(r => Id[U](r.nextString())) - implicit def GetIdOption[U]: GetResult[Option[Id[U]]] = - GetResult(_.nextStringOption().map(Id.apply[U])) - - implicit def GetName[U]: GetResult[Name[U]] = - GetResult(r => Name[U](r.nextString())) - implicit def GetNameOption[U]: GetResult[Option[Name[U]]] = - GetResult(_.nextStringOption().map(Name.apply[U])) - - implicit val GetTime: GetResult[Time] = - GetResult(r => Time(r.nextTimestamp.getTime)) - implicit val GetTimeOption: GetResult[Option[Time]] = - GetResult(_.nextTimestampOption().map(t => Time(t.getTime))) - - implicit val GetDate: GetResult[Date] = - GetResult(r => sqlDateToDate(r.nextDate())) - implicit val GetDateOption: GetResult[Option[Date]] = - GetResult(_.nextDateOption().map(sqlDateToDate)) -} - -object SlickGetResultSupport extends SlickGetResultSupport diff --git a/src/main/scala/xyz/driver/core/database/database.scala b/src/main/scala/xyz/driver/core/database/database.scala deleted file mode 100644 index ae06517..0000000 --- a/src/main/scala/xyz/driver/core/database/database.scala +++ /dev/null @@ -1,165 +0,0 @@ -package xyz.driver.core - -import slick.basic.DatabaseConfig -import slick.jdbc.JdbcProfile -import xyz.driver.core.date.Date -import xyz.driver.core.time.Time - -import scala.concurrent.Future -import com.typesafe.config.Config - -package database { - - import java.sql.SQLDataException - - import eu.timepit.refined.api.{Refined, Validate} - import eu.timepit.refined.refineV - - trait Database { - val profile: JdbcProfile - val database: JdbcProfile#Backend#Database - } - - object Database { - def fromConfig(config: Config, databaseName: String): Database = { - val dbConfig: DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig(databaseName, config) - - new Database { - val profile: JdbcProfile = dbConfig.profile - val database: JdbcProfile#Backend#Database = dbConfig.db - } - } - - def fromConfig(databaseName: String): Database = { - fromConfig(com.typesafe.config.ConfigFactory.load(), databaseName) - } - } - - trait ColumnTypes { - val profile: JdbcProfile - } - - trait NameColumnTypes extends ColumnTypes { - import profile.api._ - implicit def `xyz.driver.core.Name.columnType`[T]: BaseColumnType[Name[T]] - } - - object NameColumnTypes { - trait StringName extends NameColumnTypes { - import profile.api._ - - override implicit def `xyz.driver.core.Name.columnType`[T]: BaseColumnType[Name[T]] = - MappedColumnType.base[Name[T], String](_.value, Name[T]) - } - } - - trait DateColumnTypes extends ColumnTypes { - import profile.api._ - implicit def `xyz.driver.core.time.Date.columnType`: BaseColumnType[Date] - } - - object DateColumnTypes { - trait SqlDate extends DateColumnTypes { - import profile.api._ - override implicit def `xyz.driver.core.time.Date.columnType`: BaseColumnType[Date] = - MappedColumnType.base[Date, java.sql.Date](dateToSqlDate, sqlDateToDate) - } - } - - trait RefinedColumnTypes[T, Predicate] extends ColumnTypes { - import profile.api._ - implicit def `eu.timepit.refined.api.Refined`( - implicit columnType: BaseColumnType[T], - validate: Validate[T, Predicate]): BaseColumnType[T Refined Predicate] - } - - object RefinedColumnTypes { - trait RefinedValue[T, Predicate] extends RefinedColumnTypes[T, Predicate] { - import profile.api._ - override implicit def `eu.timepit.refined.api.Refined`( - implicit columnType: BaseColumnType[T], - validate: Validate[T, Predicate]): BaseColumnType[T Refined Predicate] = - MappedColumnType.base[T Refined Predicate, T]( - _.value, { dbValue => - refineV[Predicate](dbValue) match { - case Left(refinementError) => - throw new SQLDataException( - s"Value in the database doesn't match the refinement constraints: $refinementError") - case Right(refinedValue) => - refinedValue - } - } - ) - } - } - - trait IdColumnTypes extends ColumnTypes { - import profile.api._ - implicit def `xyz.driver.core.Id.columnType`[T]: BaseColumnType[Id[T]] - } - - object IdColumnTypes { - trait UUID extends IdColumnTypes { - import profile.api._ - - override implicit def `xyz.driver.core.Id.columnType`[T] = - MappedColumnType - .base[Id[T], java.util.UUID](id => java.util.UUID.fromString(id.value), uuid => Id[T](uuid.toString)) - } - trait SerialId extends IdColumnTypes { - import profile.api._ - - override implicit def `xyz.driver.core.Id.columnType`[T] = - MappedColumnType.base[Id[T], Long](_.value.toLong, serialId => Id[T](serialId.toString)) - } - trait NaturalId extends IdColumnTypes { - import profile.api._ - - override implicit def `xyz.driver.core.Id.columnType`[T] = - MappedColumnType.base[Id[T], String](_.value, Id[T]) - } - } - - trait TimestampColumnTypes extends ColumnTypes { - import profile.api._ - implicit def `xyz.driver.core.time.Time.columnType`: BaseColumnType[Time] - } - - object TimestampColumnTypes { - trait SqlTimestamp extends TimestampColumnTypes { - import profile.api._ - - override implicit def `xyz.driver.core.time.Time.columnType`: BaseColumnType[Time] = - MappedColumnType.base[Time, java.sql.Timestamp]( - time => new java.sql.Timestamp(time.millis), - timestamp => Time(timestamp.getTime)) - } - - trait PrimitiveTimestamp extends TimestampColumnTypes { - import profile.api._ - - override implicit def `xyz.driver.core.time.Time.columnType`: BaseColumnType[Time] = - MappedColumnType.base[Time, Long](_.millis, Time(_)) - } - } - - trait KeyMappers extends ColumnTypes { - import profile.api._ - - def uuidKeyMapper[T] = - MappedColumnType - .base[Id[T], java.util.UUID](id => java.util.UUID.fromString(id.value), uuid => Id[T](uuid.toString)) - def serialKeyMapper[T] = MappedColumnType.base[Id[T], Long](_.value.toLong, serialId => Id[T](serialId.toString)) - def naturalKeyMapper[T] = MappedColumnType.base[Id[T], String](_.value, Id[T]) - } - - trait DatabaseObject extends ColumnTypes { - def createTables(): Future[Unit] - def disconnect(): Unit - } - - abstract class DatabaseObjectAdapter extends DatabaseObject { - def createTables(): Future[Unit] = Future.successful(()) - def disconnect(): Unit = {} - } -} diff --git a/src/main/scala/xyz/driver/core/database/package.scala b/src/main/scala/xyz/driver/core/database/package.scala deleted file mode 100644 index aee14c6..0000000 --- a/src/main/scala/xyz/driver/core/database/package.scala +++ /dev/null @@ -1,61 +0,0 @@ -package xyz.driver.core - -import java.sql.{Date => SqlDate} -import java.util.Calendar - -import date.{Date, Month} -import slick.dbio._ -import slick.jdbc.JdbcProfile -import slick.relational.RelationalProfile - -package object database { - - type Schema = { - def create: DBIOAction[Unit, NoStream, Effect.Schema] - def drop: DBIOAction[Unit, NoStream, Effect.Schema] - } - - @deprecated( - "sbt-slick-codegen 0.11.0+ no longer needs to generate these methods. Please use the new `CodegenTables` trait when upgrading.", - "driver-core 1.8.12") - type GeneratedTables = { - // structure of Slick data model traits generated by sbt-slick-codegen - val profile: JdbcProfile - def schema: profile.SchemaDescription - - def createNamespaceSchema: StreamingDBIO[Vector[Unit], Unit] - def dropNamespaceSchema: StreamingDBIO[Vector[Unit], Unit] - } - - /** A structural type for schema traits generated by sbt-slick-codegen. - * This will compile with codegen versions before 0.11.0, but note - * that methods in [[GeneratedTables]] are no longer generated. - */ - type CodegenTables[Profile <: RelationalProfile] = { - val profile: Profile - def schema: profile.SchemaDescription - } - - private[database] def sqlDateToDate(sqlDate: SqlDate): Date = { - // NOTE: SQL date does not have a time component, so this date - // should only be interpreted in the running JVMs timezone. - val cal = Calendar.getInstance() - cal.setTime(sqlDate) - Date(cal.get(Calendar.YEAR), Month(cal.get(Calendar.MONTH)), cal.get(Calendar.DAY_OF_MONTH)) - } - - private[database] def dateToSqlDate(date: Date): SqlDate = { - val cal = Calendar.getInstance() - cal.set(date.year, date.month, date.day, 0, 0, 0) - new SqlDate(cal.getTime.getTime) - } - - @deprecated("Dal is deprecated. Please use Repository trait instead!", "1.8.26") - type Dal = Repository - - @deprecated("SlickDal is deprecated. Please use SlickRepository class instead!", "1.8.26") - type SlickDal = SlickRepository - - @deprecated("FutureDal is deprecated. Please use FutureRepository class instead!", "1.8.26") - type FutureDal = FutureRepository -} diff --git a/src/main/scala/xyz/driver/core/date.scala b/src/main/scala/xyz/driver/core/date.scala deleted file mode 100644 index 5454093..0000000 --- a/src/main/scala/xyz/driver/core/date.scala +++ /dev/null @@ -1,109 +0,0 @@ -package xyz.driver.core - -import java.util.Calendar - -import enumeratum._ -import scalaz.std.anyVal._ -import scalaz.syntax.equal._ - -import scala.collection.immutable.IndexedSeq -import scala.util.Try - -/** - * Driver Date type and related validators/extractors. - * Day, Month, and Year extractors are from ISO 8601 strings => driver...Date integers. - * TODO: Decouple extractors from ISO 8601, as we might want to parse other formats. - */ -object date { - - sealed trait DayOfWeek extends EnumEntry - object DayOfWeek extends Enum[DayOfWeek] { - case object Monday extends DayOfWeek - case object Tuesday extends DayOfWeek - case object Wednesday extends DayOfWeek - case object Thursday extends DayOfWeek - case object Friday extends DayOfWeek - case object Saturday extends DayOfWeek - case object Sunday extends DayOfWeek - - val values: IndexedSeq[DayOfWeek] = findValues - - val All: Set[DayOfWeek] = values.toSet - - def fromString(day: String): Option[DayOfWeek] = withNameInsensitiveOption(day) - } - - type Day = Int @@ Day.type - - object Day { - def apply(value: Int): Day = { - require(1 to 31 contains value, "Day must be in range 1 <= value <= 31") - value.asInstanceOf[Day] - } - - def unapply(dayString: String): Option[Int] = { - require(dayString.length === 2, s"ISO 8601 day string, DD, must have length 2: $dayString") - Try(dayString.toInt).toOption.map(apply) - } - } - - type Month = Int @@ Month.type - - object Month { - def apply(value: Int): Month = { - require(0 to 11 contains value, "Month is zero-indexed: 0 <= value <= 11") - value.asInstanceOf[Month] - } - val JANUARY = Month(Calendar.JANUARY) - val FEBRUARY = Month(Calendar.FEBRUARY) - val MARCH = Month(Calendar.MARCH) - val APRIL = Month(Calendar.APRIL) - val MAY = Month(Calendar.MAY) - val JUNE = Month(Calendar.JUNE) - val JULY = Month(Calendar.JULY) - val AUGUST = Month(Calendar.AUGUST) - val SEPTEMBER = Month(Calendar.SEPTEMBER) - val OCTOBER = Month(Calendar.OCTOBER) - val NOVEMBER = Month(Calendar.NOVEMBER) - val DECEMBER = Month(Calendar.DECEMBER) - - def unapply(monthString: String): Option[Month] = { - require(monthString.length === 2, s"ISO 8601 month string, MM, must have length 2: $monthString") - Try(monthString.toInt).toOption.map(isoM => apply(isoM - 1)) - } - } - - type Year = Int @@ Year.type - - object Year { - def apply(value: Int): Year = value.asInstanceOf[Year] - - def unapply(yearString: String): Option[Int] = { - require(yearString.length === 4, s"ISO 8601 year string, YYYY, must have length 4: $yearString") - Try(yearString.toInt).toOption.map(apply) - } - } - - final case class Date(year: Int, month: Month, day: Int) { - override def toString = f"$year%04d-${month + 1}%02d-$day%02d" - } - - object Date { - implicit def dateOrdering: Ordering[Date] = Ordering.fromLessThan { (date1, date2) => - if (date1.year != date2.year) { - date1.year < date2.year - } else if (date1.month != date2.month) { - date1.month < date2.month - } else { - date1.day < date2.day - } - } - - def fromString(dateString: String): Option[Date] = { - dateString.split('-') match { - case Array(Year(year), Month(month), Day(day)) => Some(Date(year, month, day)) - case _ => None - } - } - } -} diff --git a/src/main/scala/xyz/driver/core/domain.scala b/src/main/scala/xyz/driver/core/domain.scala deleted file mode 100644 index fa3b5c4..0000000 --- a/src/main/scala/xyz/driver/core/domain.scala +++ /dev/null @@ -1,46 +0,0 @@ -package xyz.driver.core - -import com.google.i18n.phonenumbers.PhoneNumberUtil -import scalaz.Equal -import scalaz.std.string._ -import scalaz.syntax.equal._ - -object domain { - - final case class Email(username: String, domain: String) { - override def toString: String = username + "@" + domain - } - - object Email { - implicit val emailEqual: Equal[Email] = Equal.equal { - case (left, right) => left.toString.toLowerCase === right.toString.toLowerCase - } - - def parse(emailString: String): Option[Email] = { - Some(emailString.split("@")) collect { - case Array(username, domain) => Email(username, domain) - } - } - } - - final case class PhoneNumber(countryCode: String = "1", number: String) { - override def toString: String = s"+$countryCode $number" - } - - object PhoneNumber { - - private val phoneUtil = PhoneNumberUtil.getInstance() - - def parse(phoneNumber: String): Option[PhoneNumber] = { - val phone = scala.util.Try(phoneUtil.parseAndKeepRawInput(phoneNumber, "US")).toOption - - val validated = phone match { - case None => None - case Some(pn) => - if (!phoneUtil.isValidNumber(pn)) None - else Some(pn) - } - validated.map(pn => PhoneNumber(pn.getCountryCode.toString, pn.getNationalNumber.toString)) - } - } -} diff --git a/src/main/scala/xyz/driver/core/file/FileSystemStorage.scala b/src/main/scala/xyz/driver/core/file/FileSystemStorage.scala deleted file mode 100644 index ce26fe4..0000000 --- a/src/main/scala/xyz/driver/core/file/FileSystemStorage.scala +++ /dev/null @@ -1,76 +0,0 @@ -package xyz.driver.core.file - -import akka.NotUsed -import akka.stream.scaladsl.{FileIO, Source} -import akka.util.ByteString -import java.io.File -import java.nio.file.{Files, Path, Paths} - -import xyz.driver.core.{Name, Revision} -import xyz.driver.core.time.Time - -import scala.concurrent.{ExecutionContext, Future} -import scalaz.{ListT, OptionT} - -@deprecated("Consider using xyz.driver.core.storage.FileSystemBlobStorage instead", "driver-core 1.8.14") -class FileSystemStorage(executionContext: ExecutionContext) extends FileStorage { - implicit private val execution = executionContext - - override def upload(localSource: File, destination: Path): Future[Unit] = Future { - checkSafeFileName(destination) { - val destinationFile = destination.toFile - - if (destinationFile.getParentFile.exists() || destinationFile.getParentFile.mkdirs()) { - if (localSource.renameTo(destinationFile)) () - else { - throw new Exception( - s"Failed to move file from `${localSource.getCanonicalPath}` to `${destinationFile.getCanonicalPath}`") - } - } else { - throw new Exception(s"Failed to create parent directories for file `${destinationFile.getCanonicalPath}`") - } - } - } - - override def download(filePath: Path): OptionT[Future, File] = - OptionT.optionT(Future { - Option(new File(filePath.toString)).filter(file => file.exists() && file.isFile) - }) - - override def stream(filePath: Path): OptionT[Future, Source[ByteString, NotUsed]] = - OptionT.optionT(Future { - if (Files.exists(filePath)) { - Some(FileIO.fromPath(filePath).mapMaterializedValue(_ => NotUsed)) - } else { - None - } - }) - - override def delete(filePath: Path): Future[Unit] = Future { - val file = new File(filePath.toString) - if (file.delete()) () - else { - throw new Exception(s"Failed to delete file $file" + (if (!file.exists()) ", file does not exist." else ".")) - } - } - - override def list(path: Path): ListT[Future, FileLink] = - ListT.listT(Future { - val file = new File(path.toString) - if (file.isDirectory) { - file.listFiles().toList.filter(_.isFile).map { file => - FileLink( - Name[File](file.getName), - Paths.get(file.getPath), - Revision[File](file.hashCode.toString), - Time(file.lastModified()), - file.length()) - } - } else List.empty[FileLink] - }) - - override def exists(path: Path): Future[Boolean] = Future { - Files.exists(path) - } - -} diff --git a/src/main/scala/xyz/driver/core/file/GcsStorage.scala b/src/main/scala/xyz/driver/core/file/GcsStorage.scala deleted file mode 100644 index 5c94645..0000000 --- a/src/main/scala/xyz/driver/core/file/GcsStorage.scala +++ /dev/null @@ -1,135 +0,0 @@ -package xyz.driver.core.file - -import akka.NotUsed -import akka.stream.scaladsl.Source -import akka.util.ByteString -import com.google.cloud.ReadChannel -import java.io.{BufferedOutputStream, File, FileInputStream, FileOutputStream} -import java.net.URL -import java.nio.ByteBuffer -import java.nio.file.{Path, Paths} -import java.util.concurrent.TimeUnit - -import com.google.cloud.storage.Storage.BlobListOption -import com.google.cloud.storage.{Option => _, _} -import xyz.driver.core.time.Time -import xyz.driver.core.{Name, Revision, generators} - -import scala.collection.JavaConverters._ -import scala.concurrent.duration.Duration -import scala.concurrent.{ExecutionContext, Future} -import scalaz.{ListT, OptionT} - -@deprecated("Consider using xyz.driver.core.storage.GcsBlobStorage instead", "driver-core 1.8.14") -class GcsStorage( - storageClient: Storage, - bucketName: Name[Bucket], - executionContext: ExecutionContext, - chunkSize: Int = 4096) - extends SignedFileStorage { - implicit private val execution: ExecutionContext = executionContext - - override def upload(localSource: File, destination: Path): Future[Unit] = Future { - checkSafeFileName(destination) { - val blobId = BlobId.of(bucketName.value, destination.toString) - def acl = Bucket.BlobWriteOption.predefinedAcl(Storage.PredefinedAcl.PUBLIC_READ) - - storageClient.get(bucketName.value).create(blobId.getName, new FileInputStream(localSource), acl) - } - } - - override def download(filePath: Path): OptionT[Future, File] = { - OptionT.optionT(Future { - Option(storageClient.get(bucketName.value, filePath.toString)).filterNot(_.getSize == 0).map { - blob => - val tempDir = System.getProperty("java.io.tmpdir") - val randomFolderName = generators.nextUuid().toString - val tempDestinationFile = new File(Paths.get(tempDir, randomFolderName, filePath.toString).toString) - - if (!tempDestinationFile.getParentFile.mkdirs()) { - throw new Exception(s"Failed to create temp directory to download file `$tempDestinationFile`") - } else { - val target = new BufferedOutputStream(new FileOutputStream(tempDestinationFile)) - try target.write(blob.getContent()) - finally target.close() - tempDestinationFile - } - } - }) - } - - override def stream(filePath: Path): OptionT[Future, Source[ByteString, NotUsed]] = - OptionT.optionT(Future { - def readChunk(rc: ReadChannel): Option[ByteString] = { - val buffer = ByteBuffer.allocate(chunkSize) - val length = rc.read(buffer) - if (length > 0) { - buffer.flip() - Some(ByteString.fromByteBuffer(buffer)) - } else { - None - } - } - - Option(storageClient.get(bucketName.value, filePath.toString)).map { blob => - Source.unfoldResource[ByteString, ReadChannel]( - create = () => blob.reader(), - read = channel => readChunk(channel), - close = channel => channel.close() - ) - } - }) - - override def delete(filePath: Path): Future[Unit] = Future { - storageClient.delete(BlobId.of(bucketName.value, filePath.toString)) - } - - override def list(directoryPath: Path): ListT[Future, FileLink] = - ListT.listT(Future { - val directory = s"$directoryPath/" - val page = storageClient.list( - bucketName.value, - BlobListOption.currentDirectory(), - BlobListOption.prefix(directory) - ) - - page - .iterateAll() - .asScala - .filter(_.getName != directory) - .map(blobToFileLink(directoryPath, _)) - .toList - }) - - protected def blobToFileLink(path: Path, blob: Blob): FileLink = { - def nullError(property: String) = throw new IllegalStateException(s"Blob $blob at $path does not have $property") - val name = Option(blob.getName).getOrElse(nullError("a name")) - val generation = Option(blob.getGeneration).getOrElse(nullError("a generation")) - val updateTime = Option(blob.getUpdateTime).getOrElse(nullError("an update time")) - val size = Option(blob.getSize).getOrElse(nullError("a size")) - - FileLink( - Name(name.split('/').last), - Paths.get(name), - Revision(generation.toString), - Time(updateTime), - size - ) - } - - override def exists(path: Path): Future[Boolean] = Future { - val blob = Option( - storageClient.get( - bucketName.value, - path.toString - )) - blob.isDefined - } - - override def signedFileUrl(filePath: Path, duration: Duration): OptionT[Future, URL] = - OptionT.optionT(Future { - Option(storageClient.get(bucketName.value, filePath.toString)).filterNot(_.getSize == 0).map { blob => - blob.signUrl(duration.toSeconds, TimeUnit.SECONDS) - } - }) -} diff --git a/src/main/scala/xyz/driver/core/file/S3Storage.scala b/src/main/scala/xyz/driver/core/file/S3Storage.scala deleted file mode 100644 index 5158d4d..0000000 --- a/src/main/scala/xyz/driver/core/file/S3Storage.scala +++ /dev/null @@ -1,87 +0,0 @@ -package xyz.driver.core.file - -import akka.NotUsed -import akka.stream.scaladsl.{Source, StreamConverters} -import akka.util.ByteString -import java.io.File -import java.nio.file.{Path, Paths} -import java.util.UUID.randomUUID - -import com.amazonaws.services.s3.AmazonS3 -import com.amazonaws.services.s3.model.{Bucket, GetObjectRequest, ListObjectsV2Request} -import xyz.driver.core.{Name, Revision} -import xyz.driver.core.time.Time - -import scala.concurrent.{ExecutionContext, Future} -import scalaz.{ListT, OptionT} - -@deprecated( - "Blob storage functionality has been reimplemented in xyz.driver.core.storage.BlobStorage. " + - "It has not been ported to S3 storage. Please raise an issue if this required for your use-case.", - "driver-core 1.8.14" -) -class S3Storage(s3: AmazonS3, bucket: Name[Bucket], executionContext: ExecutionContext, chunkSize: Int = 4096) - extends FileStorage { - implicit private val execution = executionContext - - override def upload(localSource: File, destination: Path): Future[Unit] = Future { - checkSafeFileName(destination) { - val _ = s3.putObject(bucket.value, destination.toString, localSource).getETag - } - } - - override def download(filePath: Path): OptionT[Future, File] = - OptionT.optionT(Future { - val tempDir = System.getProperty("java.io.tmpdir") - val randomFolderName = randomUUID().toString - val tempDestinationFile = new File(Paths.get(tempDir, randomFolderName, filePath.toString).toString) - - if (!tempDestinationFile.getParentFile.mkdirs()) { - throw new Exception(s"Failed to create temp directory to download file `$tempDestinationFile`") - } else { - Option(s3.getObject(new GetObjectRequest(bucket.value, filePath.toString), tempDestinationFile)).map { _ => - tempDestinationFile - } - } - }) - - override def stream(filePath: Path): OptionT[Future, Source[ByteString, NotUsed]] = - OptionT.optionT(Future { - Option(s3.getObject(new GetObjectRequest(bucket.value, filePath.toString))).map { elem => - StreamConverters.fromInputStream(() => elem.getObjectContent(), chunkSize).mapMaterializedValue(_ => NotUsed) - } - }) - - override def delete(filePath: Path): Future[Unit] = Future { - s3.deleteObject(bucket.value, filePath.toString) - } - - override def list(path: Path): ListT[Future, FileLink] = - ListT.listT(Future { - import scala.collection.JavaConverters._ - val req = new ListObjectsV2Request().withBucketName(bucket.value).withPrefix(path.toString).withMaxKeys(2) - - def isInSubFolder(path: Path)(fileLink: FileLink) = - fileLink.location.toString.replace(path.toString + "/", "").contains("/") - - Iterator.continually(s3.listObjectsV2(req)).takeWhile { result => - req.setContinuationToken(result.getNextContinuationToken) - result.isTruncated - } flatMap { result => - result.getObjectSummaries.asScala.toList.map { summary => - FileLink( - Name[File](summary.getKey), - Paths.get(path.toString + "/" + summary.getKey), - Revision[File](summary.getETag), - Time(summary.getLastModified.getTime), - summary.getSize - ) - } filterNot isInSubFolder(path) - } toList - }) - - override def exists(path: Path): Future[Boolean] = Future { - s3.doesObjectExist(bucket.value, path.toString) - } - -} diff --git a/src/main/scala/xyz/driver/core/file/package.scala b/src/main/scala/xyz/driver/core/file/package.scala deleted file mode 100644 index 58955e5..0000000 --- a/src/main/scala/xyz/driver/core/file/package.scala +++ /dev/null @@ -1,68 +0,0 @@ -package xyz.driver.core - -import java.io.File -import java.nio.file.Path - -import xyz.driver.core.time.Time - -import scala.concurrent.Future -import scalaz.{ListT, OptionT} - -package file { - - import akka.NotUsed - import akka.stream.scaladsl.Source - import akka.util.ByteString - import java.net.URL - - import scala.concurrent.duration.Duration - - final case class FileLink( - name: Name[File], - location: Path, - revision: Revision[File], - lastModificationDate: Time, - fileSize: Long - ) - - trait FileService { - - def getFileLink(id: Name[File]): FileLink - - def getFile(fileLink: FileLink): File - } - - trait FileStorage { - - def upload(localSource: File, destination: Path): Future[Unit] - - def download(filePath: Path): OptionT[Future, File] - - def stream(filePath: Path): OptionT[Future, Source[ByteString, NotUsed]] - - def delete(filePath: Path): Future[Unit] - - /** List contents of a directory */ - def list(directoryPath: Path): ListT[Future, FileLink] - - def exists(path: Path): Future[Boolean] - - /** List of characters to avoid in S3 (I would say file names in general) - * - * @see http://stackoverflow.com/questions/7116450/what-are-valid-s3-key-names-that-can-be-accessed-via-the-s3-rest-api - */ - private val illegalChars = "\\^`><{}][#%~|&@:,$=+?; " - - protected def checkSafeFileName[T](filePath: Path)(f: => T): T = { - filePath.toString.find(c => illegalChars.contains(c)) match { - case Some(illegalCharacter) => - throw new IllegalArgumentException(s"File name cannot contain character `$illegalCharacter`") - case None => f - } - } - } - - trait SignedFileStorage extends FileStorage { - def signedFileUrl(filePath: Path, duration: Duration): OptionT[Future, URL] - } -} diff --git a/src/main/scala/xyz/driver/core/future.scala b/src/main/scala/xyz/driver/core/future.scala deleted file mode 100644 index 1ee3576..0000000 --- a/src/main/scala/xyz/driver/core/future.scala +++ /dev/null @@ -1,87 +0,0 @@ -package xyz.driver.core - -import com.typesafe.scalalogging.Logger - -import scala.concurrent.{ExecutionContext, Future, Promise} -import scala.util.{Failure, Success, Try} - -object future { - val log = Logger("Driver.Future") - - implicit class RichFuture[T](f: Future[T]) { - def mapAll[U](pf: PartialFunction[Try[T], U])(implicit executionContext: ExecutionContext): Future[U] = { - val p = Promise[U]() - f.onComplete(r => p.complete(Try(pf(r)))) - p.future - } - - def failFastZip[U](that: Future[U])(implicit executionContext: ExecutionContext): Future[(T, U)] = { - future.failFastZip(f, that) - } - } - - def failFastSequence[T](t: Iterable[Future[T]])(implicit ec: ExecutionContext): Future[Seq[T]] = { - t.foldLeft(Future.successful(Nil: List[T])) { (f, i) => - failFastZip(f, i).map { case (tail, h) => h :: tail } - } - .map(_.reverse) - } - - /** - * Standard scala zip waits forever on the left side, even if the right side fails - */ - def failFastZip[T, U](ft: Future[T], fu: Future[U])(implicit ec: ExecutionContext): Future[(T, U)] = { - type State = Either[(T, Promise[U]), (U, Promise[T])] - val middleState = Promise[State]() - - ft.onComplete { - case f @ Failure(err) => - if (!middleState.tryFailure(err)) { - // the right has already succeeded - middleState.future.foreach { - case Right((_, pt)) => pt.complete(f) - case Left((t1, _)) => // This should never happen - log.error(s"Logic error: tried to set Failure($err) but Left($t1) already set") - } - } - case Success(t) => - // Create the next promise: - val pu = Promise[U]() - if (!middleState.trySuccess(Left((t, pu)))) { - // we can't set, so the other promise beat us here. - middleState.future.foreach { - case Right((_, pt)) => pt.success(t) - case Left((t1, _)) => // This should never happen - log.error(s"Logic error: tried to set Left($t) but Left($t1) already set") - } - } - } - fu.onComplete { - case f @ Failure(err) => - if (!middleState.tryFailure(err)) { - // we can't set, so the other promise beat us here. - middleState.future.foreach { - case Left((_, pu)) => pu.complete(f) - case Right((u1, _)) => // This should never happen - log.error(s"Logic error: tried to set Failure($err) but Right($u1) already set") - } - } - case Success(u) => - // Create the next promise: - val pt = Promise[T]() - if (!middleState.trySuccess(Right((u, pt)))) { - // we can't set, so the other promise beat us here. - middleState.future.foreach { - case Left((_, pu)) => pu.success(u) - case Right((u1, _)) => // This should never happen - log.error(s"Logic error: tried to set Right($u) but Right($u1) already set") - } - } - } - - middleState.future.flatMap { - case Left((t, pu)) => pu.future.map((t, _)) - case Right((u, pt)) => pt.future.map((_, u)) - } - } -} diff --git a/src/main/scala/xyz/driver/core/generators.scala b/src/main/scala/xyz/driver/core/generators.scala deleted file mode 100644 index d57980e..0000000 --- a/src/main/scala/xyz/driver/core/generators.scala +++ /dev/null @@ -1,138 +0,0 @@ -package xyz.driver.core - -import enumeratum._ -import java.math.MathContext -import java.util.UUID - -import xyz.driver.core.time.{Time, TimeOfDay, TimeRange} -import xyz.driver.core.date.{Date, DayOfWeek} - -import scala.reflect.ClassTag -import scala.util.Random -import eu.timepit.refined.refineV -import eu.timepit.refined.api.Refined -import eu.timepit.refined.collection._ - -object generators { - - private val random = new Random - import random._ - private val secureRandom = new java.security.SecureRandom() - - private val DefaultMaxLength = 10 - private val StringLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ".toSet - private val NonAmbigiousCharacters = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789" - private val Numbers = "0123456789" - - private def nextTokenString(length: Int, chars: IndexedSeq[Char]): String = { - val builder = new StringBuilder - for (_ <- 0 until length) { - builder += chars(secureRandom.nextInt(chars.length)) - } - builder.result() - } - - /** Creates a random invitation token. - * - * This token is meant fo human input and avoids using ambiguous characters such as 'O' and '0'. It - * therefore contains less entropy and is not meant to be used as a cryptographic secret. */ - @deprecated( - "The term 'token' is too generic and security and readability conventions are not well defined. " + - "Services should implement their own version that suits their security requirements.", - "1.11.0" - ) - def nextToken(length: Int): String = nextTokenString(length, NonAmbigiousCharacters) - - @deprecated( - "The term 'token' is too generic and security and readability conventions are not well defined. " + - "Services should implement their own version that suits their security requirements.", - "1.11.0" - ) - def nextNumericToken(length: Int): String = nextTokenString(length, Numbers) - - def nextInt(maxValue: Int, minValue: Int = 0): Int = random.nextInt(maxValue - minValue) + minValue - - def nextBoolean(): Boolean = random.nextBoolean() - - def nextDouble(): Double = random.nextDouble() - - def nextId[T](): Id[T] = Id[T](nextUuid().toString) - - def nextId[T](maxLength: Int): Id[T] = Id[T](nextString(maxLength)) - - def nextNumericId[T](): Id[T] = Id[T](nextLong.abs.toString) - - def nextNumericId[T](maxValue: Int): Id[T] = Id[T](nextInt(maxValue).toString) - - def nextName[T](maxLength: Int = DefaultMaxLength): Name[T] = Name[T](nextString(maxLength)) - - def nextNonEmptyName[T](maxLength: Int = DefaultMaxLength): NonEmptyName[T] = - NonEmptyName[T](nextNonEmptyString(maxLength)) - - def nextUuid(): UUID = java.util.UUID.randomUUID - - def nextRevision[T](): Revision[T] = Revision[T](nextUuid().toString) - - def nextString(maxLength: Int = DefaultMaxLength): String = - (oneOf[Char](StringLetters) +: arrayOf(oneOf[Char](StringLetters), maxLength - 1)).mkString - - def nextNonEmptyString(maxLength: Int = DefaultMaxLength): String Refined NonEmpty = { - refineV[NonEmpty]( - (oneOf[Char](StringLetters) +: arrayOf(oneOf[Char](StringLetters), maxLength - 1)).mkString - ).right.get - } - - 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 nextTimeOfDay: TimeOfDay = TimeOfDay(java.time.LocalTime.MIN.plusSeconds(nextLong), java.util.TimeZone.getDefault) - - def nextTimeRange(): TimeRange = { - val oneTime = nextTime() - val anotherTime = nextTime() - - TimeRange( - Time(scala.math.min(oneTime.millis, anotherTime.millis)), - Time(scala.math.max(oneTime.millis, anotherTime.millis))) - } - - def nextDate(): Date = nextTime().toDate(java.util.TimeZone.getTimeZone("UTC")) - - def nextDayOfWeek(): DayOfWeek = oneOf(DayOfWeek.All) - - def nextBigDecimal(multiplier: Double = 1000000.00, precision: Int = 2): BigDecimal = - BigDecimal(multiplier * nextDouble, new MathContext(precision)) - - def oneOf[T](items: T*): T = oneOf(items.toSet) - - def oneOf[T](items: Set[T]): T = items.toSeq(nextInt(items.size)) - - def oneOf[T <: EnumEntry](enum: Enum[T]): T = oneOf(enum.values: _*) - - def arrayOf[T: ClassTag](generator: => T, maxLength: Int = DefaultMaxLength, minLength: Int = 0): Array[T] = - Array.fill(nextInt(maxLength, minLength))(generator) - - def seqOf[T](generator: => T, maxLength: Int = DefaultMaxLength, minLength: Int = 0): Seq[T] = - Seq.fill(nextInt(maxLength, minLength))(generator) - - def vectorOf[T](generator: => T, maxLength: Int = DefaultMaxLength, minLength: Int = 0): Vector[T] = - Vector.fill(nextInt(maxLength, minLength))(generator) - - def listOf[T](generator: => T, maxLength: Int = DefaultMaxLength, minLength: Int = 0): List[T] = - List.fill(nextInt(maxLength, minLength))(generator) - - def setOf[T](generator: => T, maxLength: Int = DefaultMaxLength, minLength: Int = 0): Set[T] = - seqOf(generator, maxLength, minLength).toSet - - def mapOf[K, V]( - keyGenerator: => K, - valueGenerator: => V, - maxLength: Int = DefaultMaxLength, - minLength: Int = 0): Map[K, V] = - seqOf(nextPair(keyGenerator, valueGenerator), maxLength, minLength).toMap -} diff --git a/src/main/scala/xyz/driver/core/json.scala b/src/main/scala/xyz/driver/core/json.scala deleted file mode 100644 index de1df31..0000000 --- a/src/main/scala/xyz/driver/core/json.scala +++ /dev/null @@ -1,401 +0,0 @@ -package xyz.driver.core - -import java.net.InetAddress -import java.util.{TimeZone, UUID} - -import akka.http.scaladsl.marshalling.{Marshaller, Marshalling} -import akka.http.scaladsl.model.Uri.Path -import akka.http.scaladsl.server.PathMatcher.{Matched, Unmatched} -import akka.http.scaladsl.server._ -import akka.http.scaladsl.unmarshalling.Unmarshaller -import enumeratum._ -import eu.timepit.refined.api.{Refined, Validate} -import eu.timepit.refined.collection.NonEmpty -import eu.timepit.refined.refineV -import spray.json._ -import xyz.driver.core.auth.AuthCredentials -import xyz.driver.core.date.{Date, DayOfWeek, Month} -import xyz.driver.core.domain.{Email, PhoneNumber} -import xyz.driver.core.rest.errors._ -import xyz.driver.core.time.{Time, TimeOfDay} - -import scala.reflect.runtime.universe._ -import scala.util.Try - -object json { - import DefaultJsonProtocol._ - - private def UuidInPath[T]: PathMatcher1[Id[T]] = - PathMatchers.JavaUUID.map((id: UUID) => Id[T](id.toString.toLowerCase)) - - def IdInPath[T]: PathMatcher1[Id[T]] = UuidInPath[T] | new PathMatcher1[Id[T]] { - def apply(path: Path) = path match { - case Path.Segment(segment, tail) => Matched(tail, Tuple1(Id[T](segment))) - case _ => Unmatched - } - } - - implicit def paramUnmarshaller[T](implicit reader: JsonReader[T]): Unmarshaller[String, T] = - Unmarshaller.firstOf( - Unmarshaller.strict((JsString(_: String)) andThen reader.read), - stringToValueUnmarshaller[T] - ) - - implicit def idFormat[T]: RootJsonFormat[Id[T]] = new RootJsonFormat[Id[T]] { - def write(id: Id[T]) = JsString(id.value) - - def read(value: JsValue): Id[T] = value match { - case JsString(id) if Try(UUID.fromString(id)).isSuccess => Id[T](id.toLowerCase) - case JsString(id) => Id[T](id) - case _ => throw DeserializationException("Id expects string") - } - } - - implicit def taggedFormat[F, T](implicit underlying: JsonFormat[F]): JsonFormat[F @@ T] = new JsonFormat[F @@ T] { - import tagging._ - - override def write(obj: F @@ T): JsValue = underlying.write(obj) - - override def read(json: JsValue): F @@ T = underlying.read(json).tagged[T] - } - - def NameInPath[T]: PathMatcher1[Name[T]] = new PathMatcher1[Name[T]] { - def apply(path: Path) = path match { - case Path.Segment(segment, tail) => Matched(tail, Tuple1(Name[T](segment))) - case _ => Unmatched - } - } - - implicit def nameFormat[T] = new RootJsonFormat[Name[T]] { - def write(name: Name[T]) = JsString(name.value) - - def read(value: JsValue): Name[T] = value match { - case JsString(name) => Name[T](name) - case _ => throw DeserializationException("Name expects string") - } - } - - def TimeInPath: PathMatcher1[Time] = - PathMatcher("""[+-]?\d*""".r) flatMap { string => - try Some(Time(string.toLong)) - catch { case _: IllegalArgumentException => None } - } - - implicit val timeFormat = new RootJsonFormat[Time] { - def write(time: Time) = JsObject("timestamp" -> JsNumber(time.millis)) - - def read(value: JsValue): Time = value match { - case JsObject(fields) => - fields - .get("timestamp") - .flatMap { - case JsNumber(millis) => Some(Time(millis.toLong)) - case _ => None - } - .getOrElse(throw DeserializationException("Time expects number")) - case _ => throw DeserializationException("Time expects number") - } - } - - implicit object localTimeFormat extends JsonFormat[java.time.LocalTime] { - private val formatter = TimeOfDay.getFormatter - def read(json: JsValue): java.time.LocalTime = json match { - case JsString(chars) => - java.time.LocalTime.parse(chars) - case _ => deserializationError(s"Expected time string got ${json.toString}") - } - - def write(obj: java.time.LocalTime): JsValue = { - JsString(obj.format(formatter)) - } - } - - implicit object timeZoneFormat extends JsonFormat[java.util.TimeZone] { - override def write(obj: TimeZone): JsValue = { - JsString(obj.getID()) - } - - override def read(json: JsValue): TimeZone = json match { - case JsString(chars) => - java.util.TimeZone.getTimeZone(chars) - case _ => deserializationError(s"Expected time zone string got ${json.toString}") - } - } - - implicit val timeOfDayFormat: RootJsonFormat[TimeOfDay] = jsonFormat2(TimeOfDay.apply) - - implicit val dayOfWeekFormat: JsonFormat[DayOfWeek] = new enumeratum.EnumJsonFormat(DayOfWeek) - - implicit val dateFormat = new RootJsonFormat[Date] { - def write(date: Date) = JsString(date.toString) - def read(value: JsValue): Date = value match { - case JsString(dateString) => - Date - .fromString(dateString) - .getOrElse( - throw DeserializationException(s"Misformated ISO 8601 Date. Expected YYYY-MM-DD, but got $dateString.")) - case _ => throw DeserializationException(s"Date expects a string, but got $value.") - } - } - - implicit val monthFormat = new RootJsonFormat[Month] { - def write(month: Month) = JsNumber(month) - def read(value: JsValue): Month = value match { - case JsNumber(month) if 0 <= month && month <= 11 => Month(month.toInt) - case _ => throw DeserializationException("Expected a number from 0 to 11") - } - } - - def RevisionInPath[T]: PathMatcher1[Revision[T]] = - PathMatcher("""[\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12}""".r) flatMap { string => - Some(Revision[T](string)) - } - - implicit def revisionFromStringUnmarshaller[T]: Unmarshaller[String, Revision[T]] = - Unmarshaller.strict[String, Revision[T]](Revision[T]) - - implicit def revisionFormat[T]: RootJsonFormat[Revision[T]] = new RootJsonFormat[Revision[T]] { - def write(revision: Revision[T]) = JsString(revision.id.toString) - - def read(value: JsValue): Revision[T] = value match { - case JsString(revision) => Revision[T](revision) - case _ => throw DeserializationException("Revision expects uuid string") - } - } - - implicit val base64Format = new RootJsonFormat[Base64] { - def write(base64Value: Base64) = JsString(base64Value.value) - - def read(value: JsValue): Base64 = value match { - case JsString(base64Value) => Base64(base64Value) - case _ => throw DeserializationException("Base64 format expects string") - } - } - - implicit val emailFormat = new RootJsonFormat[Email] { - def write(email: Email) = JsString(email.username + "@" + email.domain) - def read(json: JsValue): Email = json match { - - case JsString(value) => - Email.parse(value).getOrElse { - deserializationError("Expected '@' symbol in email string as Email, but got " + json.toString) - } - - case _ => - deserializationError("Expected string as Email, but got " + json.toString) - } - } - - implicit val phoneNumberFormat = jsonFormat2(PhoneNumber.apply) - - implicit val authCredentialsFormat = new RootJsonFormat[AuthCredentials] { - override def read(json: JsValue): AuthCredentials = { - json match { - case JsObject(fields) => - val emailField = fields.get("email") - val identifierField = fields.get("identifier") - val passwordField = fields.get("password") - - (emailField, identifierField, passwordField) match { - case (_, _, None) => - deserializationError("password field must be set") - case (Some(JsString(em)), _, Some(JsString(pw))) => - val email = Email.parse(em).getOrElse(throw deserializationError(s"failed to parse email $em")) - AuthCredentials(email.toString, pw) - case (_, Some(JsString(id)), Some(JsString(pw))) => AuthCredentials(id.toString, pw.toString) - case (None, None, _) => deserializationError("identifier must be provided") - case _ => deserializationError(s"failed to deserialize ${json.prettyPrint}") - } - case _ => deserializationError(s"failed to deserialize ${json.prettyPrint}") - } - } - - override def write(obj: AuthCredentials): JsValue = JsObject( - "identifier" -> JsString(obj.identifier), - "password" -> JsString(obj.password) - ) - } - - implicit object inetAddressFormat extends JsonFormat[InetAddress] { - override def read(json: JsValue): InetAddress = json match { - case JsString(ipString) => - Try(InetAddress.getByName(ipString)) - .getOrElse(deserializationError(s"Invalid IP Address: $ipString")) - case _ => deserializationError(s"Expected string for IP Address, got $json") - } - - override def write(obj: InetAddress): JsValue = - JsString(obj.getHostAddress) - } - - object enumeratum { - - def enumUnmarshaller[T <: EnumEntry](enum: Enum[T]): Unmarshaller[String, T] = - Unmarshaller.strict { value => - enum.withNameOption(value).getOrElse(unrecognizedValue(value, enum.values)) - } - - trait HasJsonFormat[T <: EnumEntry] { enum: Enum[T] => - - implicit val format: JsonFormat[T] = new EnumJsonFormat(enum) - - implicit val unmarshaller: Unmarshaller[String, T] = - Unmarshaller.strict { value => - enum.withNameOption(value).getOrElse(unrecognizedValue(value, enum.values)) - } - } - - class EnumJsonFormat[T <: EnumEntry](enum: Enum[T]) extends JsonFormat[T] { - override def read(json: JsValue): T = json match { - case JsString(name) => enum.withNameOption(name).getOrElse(unrecognizedValue(name, enum.values)) - case _ => deserializationError("Expected string as enumeration value, but got " + json.toString) - } - - override def write(obj: T): JsValue = JsString(obj.entryName) - } - - private def unrecognizedValue(value: String, possibleValues: Seq[Any]): Nothing = - deserializationError(s"Unexpected value $value. Expected one of: ${possibleValues.mkString("[", ", ", "]")}") - } - - class EnumJsonFormat[T](mapping: (String, T)*) extends RootJsonFormat[T] { - private val map = mapping.toMap - - override def write(value: T): JsValue = { - map.find(_._2 == value).map(_._1) match { - case Some(name) => JsString(name) - case _ => serializationError(s"Value $value is not found in the mapping $map") - } - } - - override def read(json: JsValue): T = json match { - case JsString(name) => - map.getOrElse(name, throw DeserializationException(s"Value $name is not found in the mapping $map")) - case _ => deserializationError("Expected string as enumeration value, but got " + json.toString) - } - } - - class ValueClassFormat[T: TypeTag](writeValue: T => BigDecimal, create: BigDecimal => T) extends JsonFormat[T] { - def write(valueClass: T) = JsNumber(writeValue(valueClass)) - def read(json: JsValue): T = json match { - case JsNumber(value) => create(value) - case _ => deserializationError(s"Expected number as ${typeOf[T].getClass.getName}, but got " + json.toString) - } - } - - class GadtJsonFormat[T: TypeTag]( - typeField: String, - typeValue: PartialFunction[T, String], - jsonFormat: PartialFunction[String, JsonFormat[_ <: T]]) - extends RootJsonFormat[T] { - - def write(value: T): JsValue = { - - val valueType = typeValue.applyOrElse(value, { v: T => - deserializationError(s"No Value type for this type of ${typeOf[T].getClass.getName}: " + v.toString) - }) - - val valueFormat = - jsonFormat.applyOrElse(valueType, { f: String => - deserializationError(s"No Json format for this type of $valueType") - }) - - valueFormat.asInstanceOf[JsonFormat[T]].write(value) match { - case JsObject(fields) => JsObject(fields ++ Map(typeField -> JsString(valueType))) - case _ => serializationError(s"${typeOf[T].getClass.getName} serialized not to a JSON object") - } - } - - def read(json: JsValue): T = json match { - case JsObject(fields) => - val valueJson = JsObject(fields.filterNot(_._1 == typeField)) - fields(typeField) match { - case JsString(valueType) => - val valueFormat = jsonFormat.applyOrElse(valueType, { t: String => - deserializationError(s"Unknown ${typeOf[T].getClass.getName} type ${fields(typeField)}") - }) - valueFormat.read(valueJson) - case _ => - deserializationError(s"Unknown ${typeOf[T].getClass.getName} type ${fields(typeField)}") - } - case _ => - deserializationError(s"Expected Json Object as ${typeOf[T].getClass.getName}, but got " + json.toString) - } - } - - object GadtJsonFormat { - - def create[T: TypeTag](typeField: String)(typeValue: PartialFunction[T, String])( - jsonFormat: PartialFunction[String, JsonFormat[_ <: T]]) = { - - new GadtJsonFormat[T](typeField, typeValue, jsonFormat) - } - } - - /** - * Provides the JsonFormat for the Refined types provided by the Refined library. - * - * @see https://github.com/fthomas/refined - */ - implicit def refinedJsonFormat[T, Predicate]( - implicit valueFormat: JsonFormat[T], - validate: Validate[T, Predicate]): JsonFormat[Refined[T, Predicate]] = - new JsonFormat[Refined[T, Predicate]] { - def write(x: T Refined Predicate): JsValue = valueFormat.write(x.value) - def read(value: JsValue): T Refined Predicate = { - refineV[Predicate](valueFormat.read(value))(validate) match { - case Right(refinedValue) => refinedValue - case Left(refinementError) => deserializationError(refinementError) - } - } - } - - def NonEmptyNameInPath[T]: PathMatcher1[NonEmptyName[T]] = new PathMatcher1[NonEmptyName[T]] { - def apply(path: Path) = path match { - case Path.Segment(segment, tail) => - refineV[NonEmpty](segment) match { - case Left(_) => Unmatched - case Right(nonEmptyString) => Matched(tail, Tuple1(NonEmptyName[T](nonEmptyString))) - } - case _ => Unmatched - } - } - - implicit def nonEmptyNameFormat[T](implicit nonEmptyStringFormat: JsonFormat[Refined[String, NonEmpty]]) = - new RootJsonFormat[NonEmptyName[T]] { - def write(name: NonEmptyName[T]) = JsString(name.value.value) - - def read(value: JsValue): NonEmptyName[T] = - NonEmptyName[T](nonEmptyStringFormat.read(value)) - } - - implicit val serviceExceptionFormat: RootJsonFormat[ServiceException] = - GadtJsonFormat.create[ServiceException]("type") { - case _: InvalidInputException => "InvalidInputException" - case _: InvalidActionException => "InvalidActionException" - case _: ResourceNotFoundException => "ResourceNotFoundException" - case _: ExternalServiceException => "ExternalServiceException" - case _: ExternalServiceTimeoutException => "ExternalServiceTimeoutException" - case _: DatabaseException => "DatabaseException" - } { - case "InvalidInputException" => jsonFormat(InvalidInputException, "message") - case "InvalidActionException" => jsonFormat(InvalidActionException, "message") - case "ResourceNotFoundException" => jsonFormat(ResourceNotFoundException, "message") - case "ExternalServiceException" => - jsonFormat(ExternalServiceException, "serviceName", "serviceMessage", "serviceException") - case "ExternalServiceTimeoutException" => jsonFormat(ExternalServiceTimeoutException, "message") - case "DatabaseException" => jsonFormat(DatabaseException, "message") - } - - val jsValueToStringMarshaller: Marshaller[JsValue, String] = - Marshaller.strict[JsValue, String](value => Marshalling.Opaque[String](() => value.compactPrint)) - - def valueToStringMarshaller[T](implicit jsonFormat: JsonWriter[T]): Marshaller[T, String] = - jsValueToStringMarshaller.compose[T](jsonFormat.write) - - val stringToJsValueUnmarshaller: Unmarshaller[String, JsValue] = - Unmarshaller.strict[String, JsValue](value => value.parseJson) - - def stringToValueUnmarshaller[T](implicit jsonFormat: JsonReader[T]): Unmarshaller[String, T] = - stringToJsValueUnmarshaller.map[T](jsonFormat.read) -} diff --git a/src/main/scala/xyz/driver/core/logging/MdcExecutionContext.scala b/src/main/scala/xyz/driver/core/logging/MdcExecutionContext.scala deleted file mode 100644 index df21b48..0000000 --- a/src/main/scala/xyz/driver/core/logging/MdcExecutionContext.scala +++ /dev/null @@ -1,31 +0,0 @@ -/** Code ported from "de.geekonaut" %% "slickmdc" % "1.0.0" - * License: @see https://github.com/AVGP/slickmdc/blob/master/LICENSE - * Blog post: @see http://50linesofco.de/post/2016-07-01-slick-and-slf4j-mdc-logging-in-scala.html - */ -package xyz.driver.core.logging - -import org.slf4j.MDC -import scala.concurrent.ExecutionContext - -/** - * Execution context proxy for propagating SLF4J diagnostic context from caller thread to execution thread. - */ -class MdcExecutionContext(executionContext: ExecutionContext) extends ExecutionContext { - override def execute(runnable: Runnable): Unit = { - val callerMdc = MDC.getCopyOfContextMap - executionContext.execute(new Runnable { - def run(): Unit = { - // copy caller thread diagnostic context to execution thread - Option(callerMdc).foreach(MDC.setContextMap) - try { - runnable.run() - } finally { - // the thread might be reused, so we clean up for the next use - MDC.clear() - } - } - }) - } - - override def reportFailure(cause: Throwable): Unit = executionContext.reportFailure(cause) -} diff --git a/src/main/scala/xyz/driver/core/logging/package.scala b/src/main/scala/xyz/driver/core/logging/package.scala deleted file mode 100644 index 2b6fc11..0000000 --- a/src/main/scala/xyz/driver/core/logging/package.scala +++ /dev/null @@ -1,7 +0,0 @@ -package xyz.driver.core - -import org.slf4j.helpers.NOPLogger - -package object logging { - val NoLogger = com.typesafe.scalalogging.Logger(NOPLogger.NOP_LOGGER) -} diff --git a/src/main/scala/xyz/driver/core/messages.scala b/src/main/scala/xyz/driver/core/messages.scala deleted file mode 100644 index 6b1bc7e..0000000 --- a/src/main/scala/xyz/driver/core/messages.scala +++ /dev/null @@ -1,58 +0,0 @@ -package xyz.driver.core - -import java.util.Locale - -import com.typesafe.config.{Config, ConfigException} -import com.typesafe.scalalogging.Logger - -/** - * Scala internationalization (i18n) support - */ -object messages { - - object Messages { - def messages(config: Config, log: Logger, locale: Locale = Locale.US): Messages = { - val map = config.getConfig(locale.getLanguage) - Messages(map, locale, log) - } - } - - final case class Messages(map: Config, locale: Locale, log: Logger) { - - /** - * Returns message for the key - * - * @param key key - * @return message - */ - def apply(key: String): String = { - try { - map.getString(key) - } catch { - case _: ConfigException => - log.error(s"Message with key '$key' not found for locale '${locale.getLanguage}'") - 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.replace(s"{$index}", value.toString) - } - - val template = apply(key) - format(template, params) - } - } -} diff --git a/src/main/scala/xyz/driver/core/pubsub.scala b/src/main/scala/xyz/driver/core/pubsub.scala deleted file mode 100644 index 6d2667f..0000000 --- a/src/main/scala/xyz/driver/core/pubsub.scala +++ /dev/null @@ -1,145 +0,0 @@ -package xyz.driver.core - -import akka.http.scaladsl.marshalling._ -import akka.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller} -import akka.stream.Materializer -import com.google.api.core.{ApiFutureCallback, ApiFutures} -import com.google.cloud.pubsub.v1._ -import com.google.protobuf.ByteString -import com.google.pubsub.v1._ -import com.typesafe.scalalogging.Logger - -import scala.concurrent.{ExecutionContext, Future, Promise} -import scala.util.{Failure, Try} - -object pubsub { - - trait PubsubPublisher[Message] { - - type Result - - def publish(message: Message): Future[Result] - } - - class GooglePubsubPublisher[Message](projectId: String, topic: String, log: Logger, autoCreate: Boolean = true)( - implicit messageMarshaller: Marshaller[Message, String], - ex: ExecutionContext - ) extends PubsubPublisher[Message] { - - type Result = Id[PubsubMessage] - - private val topicName = ProjectTopicName.of(projectId, topic) - - private val publisher = { - if (autoCreate) { - val adminClient = TopicAdminClient.create() - val topicExists = Try(adminClient.getTopic(topicName)).isSuccess - if (!topicExists) { - adminClient.createTopic(topicName) - } - } - Publisher.newBuilder(topicName).build() - } - - override def publish(message: Message): Future[Id[PubsubMessage]] = { - - Marshal(message).to[String].flatMap { messageString => - val data = ByteString.copyFromUtf8(messageString) - val pubsubMessage = PubsubMessage.newBuilder().setData(data).build() - - val promise = Promise[Id[PubsubMessage]]() - - val messageIdFuture = publisher.publish(pubsubMessage) - - ApiFutures.addCallback( - messageIdFuture, - new ApiFutureCallback[String]() { - override def onSuccess(messageId: String): Unit = { - log.info(s"Published a message with topic $topic, message id $messageId: $messageString") - promise.complete(Try(Id[PubsubMessage](messageId))) - } - - override def onFailure(t: Throwable): Unit = { - log.warn(s"Failed to publish a message with topic $topic: $message", t) - promise.complete(Failure(t)) - } - } - ) - - promise.future - } - } - } - - class FakePubsubPublisher[Message](topicName: String, log: Logger)( - implicit messageMarshaller: Marshaller[Message, String], - ex: ExecutionContext) - extends PubsubPublisher[Message] { - - type Result = Id[PubsubMessage] - - def publish(message: Message): Future[Result] = - Marshal(message).to[String].map { messageString => - log.info(s"Published a message to a fake pubsub with topic $topicName: $messageString") - generators.nextId[PubsubMessage]() - } - } - - trait PubsubSubscriber { - - def stopListening(): Unit - } - - class GooglePubsubSubscriber[Message]( - projectId: String, - subscriptionId: String, - receiver: Message => Future[Unit], - log: Logger, - autoCreateSettings: Option[GooglePubsubSubscriber.SubscriptionSettings] = None - )(implicit messageMarshaller: Unmarshaller[String, Message], mat: Materializer, ex: ExecutionContext) - extends PubsubSubscriber { - - private val subscriptionName = ProjectSubscriptionName.of(projectId, subscriptionId) - - private val messageReceiver = new MessageReceiver() { - override def receiveMessage(message: PubsubMessage, consumer: AckReplyConsumer): Unit = { - val messageString = message.getData.toStringUtf8 - Unmarshal(messageString).to[Message].flatMap { messageBody => - log.info(s"Received a message ${message.getMessageId} for subscription $subscriptionId: $messageString") - receiver(messageBody).transform(v => { consumer.ack(); v }, t => { consumer.nack(); t }) - } - } - } - - private val subscriber = { - autoCreateSettings.foreach { subscriptionSettings => - val adminClient = SubscriptionAdminClient.create() - val subscriptionExists = Try(adminClient.getSubscription(subscriptionName)).isSuccess - if (!subscriptionExists) { - val topicName = ProjectTopicName.of(projectId, subscriptionSettings.topic) - adminClient.createSubscription( - subscriptionName, - topicName, - subscriptionSettings.pushConfig, - subscriptionSettings.ackDeadlineSeconds) - } - } - - Subscriber.newBuilder(subscriptionName, messageReceiver).build() - } - - subscriber.startAsync() - - override def stopListening(): Unit = { - subscriber.stopAsync() - } - } - - object GooglePubsubSubscriber { - final case class SubscriptionSettings(topic: String, pushConfig: PushConfig, ackDeadlineSeconds: Int) - } - - class FakePubsubSubscriber extends PubsubSubscriber { - def stopListening(): Unit = () - } -} diff --git a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala deleted file mode 100644 index 55f39ba..0000000 --- a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala +++ /dev/null @@ -1,111 +0,0 @@ -package xyz.driver.core.rest - -import java.sql.SQLException - -import akka.http.scaladsl.model.{StatusCodes, _} -import akka.http.scaladsl.model.headers._ -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server._ -import com.typesafe.scalalogging.Logger -import org.slf4j.MDC -import xyz.driver.core.rest -import xyz.driver.core.rest.errors._ - -import scala.compat.Platform.ConcurrentModificationException - -trait DriverRoute { - def log: Logger - - def route: Route - - def routeWithDefaults: Route = { - (defaultResponseHeaders & handleExceptions(ExceptionHandler(exceptionHandler))) { - route - } - } - - protected def defaultResponseHeaders: Directive0 = { - extractRequest flatMap { request => - // Needs to happen before any request processing, so all the log messages - // associated with processing of this request are having this `trackingId` - val trackingId = rest.extractTrackingId(request) - val tracingHeader = RawHeader(ContextHeaders.TrackingIdHeader, trackingId) - MDC.put("trackingId", trackingId) - - // This header will eliminate the risk of LB trying to reuse a connection - // that already timed out on the server side by completely rejecting keep-alive - val rejectKeepAlive = Connection("close") - - respondWithHeaders(tracingHeader, rejectKeepAlive) - } - } - - /** - * Override me for custom exception handling - * - * @return Exception handling route for exception type - */ - protected def exceptionHandler: PartialFunction[Throwable, Route] = { - case serviceException: ServiceException => - serviceExceptionHandler(serviceException) - - case is: IllegalStateException => - ctx => - log.warn(s"Request is not allowed to ${ctx.request.method} ${ctx.request.uri}", is) - errorResponse(StatusCodes.BadRequest, message = is.getMessage, is)(ctx) - - case cm: ConcurrentModificationException => - ctx => - log.warn(s"Concurrent modification of the resource ${ctx.request.method} ${ctx.request.uri}", cm) - errorResponse(StatusCodes.Conflict, "Resource was changed concurrently, try requesting a newer version", cm)( - ctx) - - case se: SQLException => - ctx => - log.warn(s"Database exception for the resource ${ctx.request.method} ${ctx.request.uri}", se) - errorResponse(StatusCodes.InternalServerError, "Data access error", se)(ctx) - - case t: Exception => - ctx => - log.warn(s"Request to ${ctx.request.method} ${ctx.request.uri} could not be handled normally", t) - errorResponse(StatusCodes.InternalServerError, t.getMessage, t)(ctx) - } - - protected def serviceExceptionHandler(serviceException: ServiceException): Route = { - val statusCode = serviceException match { - case e: InvalidInputException => - log.info("Invalid client input error", e) - StatusCodes.BadRequest - case e: InvalidActionException => - log.info("Invalid client action error", e) - StatusCodes.Forbidden - case e: ResourceNotFoundException => - log.info("Resource not found error", e) - StatusCodes.NotFound - case e: ExternalServiceException => - log.error("Error while calling another service", e) - StatusCodes.InternalServerError - case e: ExternalServiceTimeoutException => - log.error("Service timeout error", e) - StatusCodes.GatewayTimeout - case e: DatabaseException => - log.error("Database error", e) - StatusCodes.InternalServerError - } - - { (ctx: RequestContext) => - import xyz.driver.core.json.serviceExceptionFormat - val entity = - HttpEntity(ContentTypes.`application/json`, serviceExceptionFormat.write(serviceException).toString()) - errorResponse(statusCode, entity, serviceException)(ctx) - } - } - - protected def errorResponse[T <: Exception](statusCode: StatusCode, message: String, exception: T): Route = - errorResponse(statusCode, HttpEntity(message), exception) - - protected def errorResponse[T <: Exception](statusCode: StatusCode, entity: ResponseEntity, exception: T): Route = { - complete(HttpResponse(statusCode, entity = entity)) - } - -} diff --git a/src/main/scala/xyz/driver/core/rest/HttpRestServiceTransport.scala b/src/main/scala/xyz/driver/core/rest/HttpRestServiceTransport.scala deleted file mode 100644 index 788729a..0000000 --- a/src/main/scala/xyz/driver/core/rest/HttpRestServiceTransport.scala +++ /dev/null @@ -1,89 +0,0 @@ -package xyz.driver.core.rest - -import akka.actor.ActorSystem -import akka.http.scaladsl.model._ -import akka.http.scaladsl.model.headers.RawHeader -import akka.http.scaladsl.unmarshalling.Unmarshal -import akka.stream.Materializer -import akka.stream.scaladsl.TcpIdleTimeoutException -import com.typesafe.scalalogging.Logger -import org.slf4j.MDC -import xyz.driver.core.Name -import xyz.driver.core.rest.errors.{ExternalServiceException, ExternalServiceTimeoutException} -import xyz.driver.core.time.provider.TimeProvider - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success} - -class HttpRestServiceTransport( - applicationName: Name[App], - applicationVersion: String, - actorSystem: ActorSystem, - executionContext: ExecutionContext, - log: Logger, - time: TimeProvider) - extends ServiceTransport { - - protected implicit val execution: ExecutionContext = executionContext - - protected val httpClient: HttpClient = new SingleRequestHttpClient(applicationName, applicationVersion, actorSystem) - - def sendRequestGetResponse(context: ServiceRequestContext)(requestStub: HttpRequest): Future[HttpResponse] = { - - val requestTime = time.currentTime() - - val request = requestStub - .withHeaders(context.contextHeaders.toSeq.map { - case (ContextHeaders.TrackingIdHeader, _) => - RawHeader(ContextHeaders.TrackingIdHeader, context.trackingId) - case (ContextHeaders.StacktraceHeader, _) => - RawHeader( - ContextHeaders.StacktraceHeader, - Option(MDC.get("stack")) - .orElse(context.contextHeaders.get(ContextHeaders.StacktraceHeader)) - .getOrElse("")) - case (header, headerValue) => RawHeader(header, headerValue) - }: _*) - - log.debug(s"Sending request to ${request.method} ${request.uri}") - - val response = httpClient.makeRequest(request) - - response.onComplete { - case Success(r) => - val responseLatency = requestTime.durationTo(time.currentTime()) - log.debug(s"Response from ${request.uri} to request $requestStub is successful in $responseLatency ms: $r") - - case Failure(t: Throwable) => - val responseLatency = requestTime.durationTo(time.currentTime()) - log.warn(s"Failed to receive response from ${request.method} ${request.uri} in $responseLatency ms", t) - }(executionContext) - - response.recoverWith { - case _: TcpIdleTimeoutException => - val serviceCalled = s"${requestStub.method} ${requestStub.uri}" - Future.failed(ExternalServiceTimeoutException(serviceCalled)) - case t: Throwable => Future.failed(t) - } - } - - def sendRequest(context: ServiceRequestContext)(requestStub: HttpRequest)( - implicit mat: Materializer): Future[Unmarshal[ResponseEntity]] = { - - sendRequestGetResponse(context)(requestStub) flatMap { response => - if (response.status == StatusCodes.NotFound) { - Future.successful(Unmarshal(HttpEntity.Empty: ResponseEntity)) - } else if (response.status.isFailure()) { - val serviceCalled = s"${requestStub.method} ${requestStub.uri}" - Unmarshal(response.entity).to[String] flatMap { errorString => - import spray.json._ - import xyz.driver.core.json._ - val serviceException = util.Try(serviceExceptionFormat.read(errorString.parseJson)).toOption - Future.failed(ExternalServiceException(serviceCalled, errorString, serviceException)) - } - } else { - Future.successful(Unmarshal(response.entity)) - } - } - } -} diff --git a/src/main/scala/xyz/driver/core/rest/PatchDirectives.scala b/src/main/scala/xyz/driver/core/rest/PatchDirectives.scala deleted file mode 100644 index f33bf9d..0000000 --- a/src/main/scala/xyz/driver/core/rest/PatchDirectives.scala +++ /dev/null @@ -1,104 +0,0 @@ -package xyz.driver.core.rest - -import akka.http.javadsl.server.Rejections -import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport -import akka.http.scaladsl.model.{ContentTypeRange, HttpCharsets, MediaType} -import akka.http.scaladsl.server._ -import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} -import spray.json._ - -import scala.concurrent.Future -import scala.util.{Failure, Success, Try} - -trait PatchDirectives extends Directives with SprayJsonSupport { - - /** Media type for patches to JSON values, as specified in [[https://tools.ietf.org/html/rfc7396 RFC 7396]]. */ - val `application/merge-patch+json`: MediaType.WithFixedCharset = - MediaType.applicationWithFixedCharset("merge-patch+json", HttpCharsets.`UTF-8`) - - /** Wraps a JSON value that represents a patch. - * The patch must given in the format specified in [[https://tools.ietf.org/html/rfc7396 RFC 7396]]. */ - case class PatchValue(value: JsValue) { - - /** Applies this patch to a given original JSON value. In other words, merges the original with this "diff". */ - def applyTo(original: JsValue): JsValue = mergeJsValues(original, value) - } - - /** Witness that the given patch may be applied to an original domain value. - * @tparam A type of the domain value - * @param patch the patch that may be applied to a domain value - * @param format a JSON format that enables serialization and deserialization of a domain value */ - case class Patchable[A](patch: PatchValue, format: RootJsonFormat[A]) { - - /** Applies the patch to a given domain object. The result will be a combination - * of the original value, updates with the fields specified in this witness' patch. */ - def applyTo(original: A): A = { - val serialized = format.write(original) - val merged = patch.applyTo(serialized) - val deserialized = format.read(merged) - deserialized - } - } - - implicit def patchValueUnmarshaller: FromEntityUnmarshaller[PatchValue] = - Unmarshaller.byteStringUnmarshaller - .andThen(sprayJsValueByteStringUnmarshaller) - .forContentTypes(ContentTypeRange(`application/merge-patch+json`)) - .map(js => PatchValue(js)) - - implicit def patchableUnmarshaller[A]( - implicit patchUnmarshaller: FromEntityUnmarshaller[PatchValue], - format: RootJsonFormat[A]): FromEntityUnmarshaller[Patchable[A]] = { - patchUnmarshaller.map(patch => Patchable[A](patch, format)) - } - - protected def mergeObjects(oldObj: JsObject, newObj: JsObject, maxLevels: Option[Int] = None): JsObject = { - JsObject((oldObj.fields.keys ++ newObj.fields.keys).map({ key => - val oldValue = oldObj.fields.getOrElse(key, JsNull) - val newValue = newObj.fields.get(key).fold(oldValue)(mergeJsValues(oldValue, _, maxLevels.map(_ - 1))) - key -> newValue - })(collection.breakOut): _*) - } - - protected def mergeJsValues(oldValue: JsValue, newValue: JsValue, maxLevels: Option[Int] = None): JsValue = { - def mergeError(typ: String): Nothing = - deserializationError(s"Expected $typ value, got $newValue") - - if (maxLevels.exists(_ < 0)) oldValue - else { - (oldValue, newValue) match { - case (_: JsString, newString @ (JsString(_) | JsNull)) => newString - case (_: JsString, _) => mergeError("string") - case (_: JsNumber, newNumber @ (JsNumber(_) | JsNull)) => newNumber - case (_: JsNumber, _) => mergeError("number") - case (_: JsBoolean, newBool @ (JsBoolean(_) | JsNull)) => newBool - case (_: JsBoolean, _) => mergeError("boolean") - case (_: JsArray, newArr @ (JsArray(_) | JsNull)) => newArr - case (_: JsArray, _) => mergeError("array") - case (oldObj: JsObject, newObj: JsObject) => mergeObjects(oldObj, newObj) - case (_: JsObject, JsNull) => JsNull - case (_: JsObject, _) => mergeError("object") - case (JsNull, _) => newValue - } - } - } - - def mergePatch[T](patchable: Patchable[T], retrieve: => Future[Option[T]]): Directive1[T] = - Directive { inner => requestCtx => - onSuccess(retrieve)({ - case Some(oldT) => - Try(patchable.applyTo(oldT)) - .transform[Route]( - mergedT => scala.util.Success(inner(Tuple1(mergedT))), { - case jsonException: DeserializationException => - Success(reject(Rejections.malformedRequestContent(jsonException.getMessage, jsonException))) - case t => Failure(t) - } - ) - .get // intentionally re-throw all other errors - case None => reject() - })(requestCtx) - } -} - -object PatchDirectives extends PatchDirectives diff --git a/src/main/scala/xyz/driver/core/rest/PooledHttpClient.scala b/src/main/scala/xyz/driver/core/rest/PooledHttpClient.scala deleted file mode 100644 index 2854257..0000000 --- a/src/main/scala/xyz/driver/core/rest/PooledHttpClient.scala +++ /dev/null @@ -1,67 +0,0 @@ -package xyz.driver.core.rest - -import akka.actor.ActorSystem -import akka.http.scaladsl.Http -import akka.http.scaladsl.model.headers.`User-Agent` -import akka.http.scaladsl.model.{HttpRequest, HttpResponse, Uri} -import akka.http.scaladsl.settings.{ClientConnectionSettings, ConnectionPoolSettings} -import akka.stream.scaladsl.{Keep, Sink, Source} -import akka.stream.{ActorMaterializer, OverflowStrategy, QueueOfferResult, ThrottleMode} -import xyz.driver.core.Name - -import scala.concurrent.{ExecutionContext, Future, Promise} -import scala.concurrent.duration._ -import scala.util.{Failure, Success} - -class PooledHttpClient( - baseUri: Uri, - applicationName: Name[App], - applicationVersion: String, - requestRateLimit: Int = 64, - requestQueueSize: Int = 1024)(implicit actorSystem: ActorSystem, executionContext: ExecutionContext) - extends HttpClient { - - private val host = baseUri.authority.host.toString() - private val port = baseUri.effectivePort - private val scheme = baseUri.scheme - - protected implicit val materializer: ActorMaterializer = ActorMaterializer()(actorSystem) - - private val clientConnectionSettings: ClientConnectionSettings = - ClientConnectionSettings(actorSystem).withUserAgentHeader( - Option(`User-Agent`(applicationName.value + "/" + applicationVersion))) - - private val connectionPoolSettings: ConnectionPoolSettings = ConnectionPoolSettings(actorSystem) - .withConnectionSettings(clientConnectionSettings) - - private val pool = if (scheme.equalsIgnoreCase("https")) { - Http().cachedHostConnectionPoolHttps[Promise[HttpResponse]](host, port, settings = connectionPoolSettings) - } else { - Http().cachedHostConnectionPool[Promise[HttpResponse]](host, port, settings = connectionPoolSettings) - } - - private val queue = Source - .queue[(HttpRequest, Promise[HttpResponse])](requestQueueSize, OverflowStrategy.dropNew) - .via(pool) - .throttle(requestRateLimit, 1.second, maximumBurst = requestRateLimit, ThrottleMode.shaping) - .toMat(Sink.foreach({ - case ((Success(resp), p)) => p.success(resp) - case ((Failure(e), p)) => p.failure(e) - }))(Keep.left) - .run - - def makeRequest(request: HttpRequest): Future[HttpResponse] = { - val responsePromise = Promise[HttpResponse]() - - queue.offer(request -> responsePromise).flatMap { - case QueueOfferResult.Enqueued => - responsePromise.future - case QueueOfferResult.Dropped => - Future.failed(new Exception(s"Request queue to the host $host is overflown")) - case QueueOfferResult.Failure(ex) => - Future.failed(ex) - case QueueOfferResult.QueueClosed => - Future.failed(new Exception("Queue was closed (pool shut down) while running the request")) - } - } -} diff --git a/src/main/scala/xyz/driver/core/rest/ProxyRoute.scala b/src/main/scala/xyz/driver/core/rest/ProxyRoute.scala deleted file mode 100644 index c0e9f99..0000000 --- a/src/main/scala/xyz/driver/core/rest/ProxyRoute.scala +++ /dev/null @@ -1,26 +0,0 @@ -package xyz.driver.core.rest - -import akka.http.scaladsl.server.{RequestContext, Route, RouteResult} -import com.typesafe.config.Config -import xyz.driver.core.Name - -import scala.concurrent.ExecutionContext - -trait ProxyRoute extends DriverRoute { - implicit val executionContext: ExecutionContext - val config: Config - val httpClient: HttpClient - - protected def proxyToService(serviceName: Name[Service]): Route = { ctx: RequestContext => - val httpScheme = config.getString(s"services.${serviceName.value}.httpScheme") - val baseUrl = config.getString(s"services.${serviceName.value}.baseUrl") - - val originalUri = ctx.request.uri - val originalRequest = ctx.request - - val newUri = originalUri.withScheme(httpScheme).withHost(baseUrl) - val newRequest = originalRequest.withUri(newUri) - - httpClient.makeRequest(newRequest).map(RouteResult.Complete) - } -} diff --git a/src/main/scala/xyz/driver/core/rest/RestService.scala b/src/main/scala/xyz/driver/core/rest/RestService.scala deleted file mode 100644 index 8d46d72..0000000 --- a/src/main/scala/xyz/driver/core/rest/RestService.scala +++ /dev/null @@ -1,72 +0,0 @@ -package xyz.driver.core.rest - -import akka.http.scaladsl.model._ -import akka.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller} -import akka.stream.Materializer - -import scala.concurrent.{ExecutionContext, Future} -import scalaz.{ListT, OptionT} - -trait RestService extends Service { - - import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ - import spray.json._ - - protected implicit val exec: ExecutionContext - protected implicit val materializer: Materializer - - implicit class ResponseEntityFoldable(entity: Unmarshal[ResponseEntity]) { - def fold[T](default: => T)(implicit um: Unmarshaller[ResponseEntity, T]): Future[T] = - if (entity.value.isKnownEmpty()) Future.successful[T](default) else entity.to[T] - } - - protected def unitResponse(request: Future[Unmarshal[ResponseEntity]]): OptionT[Future, Unit] = - OptionT[Future, Unit](request.flatMap(_.to[String]).map(_ => Option(()))) - - protected def optionalResponse[T](request: Future[Unmarshal[ResponseEntity]])( - implicit um: Unmarshaller[ResponseEntity, Option[T]]): OptionT[Future, T] = - OptionT[Future, T](request.flatMap(_.fold(Option.empty[T]))) - - protected def listResponse[T](request: Future[Unmarshal[ResponseEntity]])( - implicit um: Unmarshaller[ResponseEntity, List[T]]): ListT[Future, T] = - ListT[Future, T](request.flatMap(_.fold(List.empty[T]))) - - protected def jsonEntity(json: JsValue): RequestEntity = - HttpEntity(ContentTypes.`application/json`, json.compactPrint) - - protected def mergePatchJsonEntity(json: JsValue): RequestEntity = - HttpEntity(PatchDirectives.`application/merge-patch+json`, json.compactPrint) - - protected def get(baseUri: Uri, path: String, query: Seq[(String, String)] = Seq.empty) = - HttpRequest(HttpMethods.GET, endpointUri(baseUri, path, query)) - - protected def post(baseUri: Uri, path: String, httpEntity: RequestEntity) = - HttpRequest(HttpMethods.POST, endpointUri(baseUri, path), entity = httpEntity) - - protected def postJson(baseUri: Uri, path: String, json: JsValue) = - HttpRequest(HttpMethods.POST, endpointUri(baseUri, path), entity = jsonEntity(json)) - - protected def put(baseUri: Uri, path: String, httpEntity: RequestEntity) = - HttpRequest(HttpMethods.PUT, endpointUri(baseUri, path), entity = httpEntity) - - protected def putJson(baseUri: Uri, path: String, json: JsValue) = - HttpRequest(HttpMethods.PUT, endpointUri(baseUri, path), entity = jsonEntity(json)) - - protected def patch(baseUri: Uri, path: String, httpEntity: RequestEntity) = - HttpRequest(HttpMethods.PATCH, endpointUri(baseUri, path), entity = httpEntity) - - protected def patchJson(baseUri: Uri, path: String, json: JsValue) = - HttpRequest(HttpMethods.PATCH, endpointUri(baseUri, path), entity = jsonEntity(json)) - - protected def mergePatchJson(baseUri: Uri, path: String, json: JsValue) = - HttpRequest(HttpMethods.PATCH, endpointUri(baseUri, path), entity = mergePatchJsonEntity(json)) - - protected def delete(baseUri: Uri, path: String, query: Seq[(String, String)] = Seq.empty) = - HttpRequest(HttpMethods.DELETE, endpointUri(baseUri, path, query)) - - protected def endpointUri(baseUri: Uri, path: String): Uri = - baseUri.withPath(Uri.Path(path)) - - protected def endpointUri(baseUri: Uri, path: String, query: Seq[(String, String)]): Uri = - baseUri.withPath(Uri.Path(path)).withQuery(Uri.Query(query: _*)) -} diff --git a/src/main/scala/xyz/driver/core/rest/SingleRequestHttpClient.scala b/src/main/scala/xyz/driver/core/rest/SingleRequestHttpClient.scala deleted file mode 100644 index 964a5a2..0000000 --- a/src/main/scala/xyz/driver/core/rest/SingleRequestHttpClient.scala +++ /dev/null @@ -1,29 +0,0 @@ -package xyz.driver.core.rest - -import akka.actor.ActorSystem -import akka.http.scaladsl.Http -import akka.http.scaladsl.model.headers.`User-Agent` -import akka.http.scaladsl.model.{HttpRequest, HttpResponse} -import akka.http.scaladsl.settings.{ClientConnectionSettings, ConnectionPoolSettings} -import akka.stream.ActorMaterializer -import xyz.driver.core.Name - -import scala.concurrent.Future - -class SingleRequestHttpClient(applicationName: Name[App], applicationVersion: String, actorSystem: ActorSystem) - extends HttpClient { - - protected implicit val materializer: ActorMaterializer = ActorMaterializer()(actorSystem) - private val client = Http()(actorSystem) - - private val clientConnectionSettings: ClientConnectionSettings = - ClientConnectionSettings(actorSystem).withUserAgentHeader( - Option(`User-Agent`(applicationName.value + "/" + applicationVersion))) - - private val connectionPoolSettings: ConnectionPoolSettings = ConnectionPoolSettings(actorSystem) - .withConnectionSettings(clientConnectionSettings) - - def makeRequest(request: HttpRequest): Future[HttpResponse] = { - client.singleRequest(request, settings = connectionPoolSettings) - } -} diff --git a/src/main/scala/xyz/driver/core/rest/Swagger.scala b/src/main/scala/xyz/driver/core/rest/Swagger.scala deleted file mode 100644 index a3d942c..0000000 --- a/src/main/scala/xyz/driver/core/rest/Swagger.scala +++ /dev/null @@ -1,127 +0,0 @@ -package xyz.driver.core.rest - -import akka.http.scaladsl.model.{ContentType, ContentTypes, HttpEntity} -import akka.http.scaladsl.server.Route -import akka.http.scaladsl.server.directives.FileAndResourceDirectives.ResourceFile -import akka.stream.ActorAttributes -import akka.stream.scaladsl.{Framing, StreamConverters} -import akka.util.ByteString -import com.github.swagger.akka.SwaggerHttpService -import com.github.swagger.akka.model._ -import com.typesafe.config.Config -import com.typesafe.scalalogging.Logger -import io.swagger.models.Scheme -import io.swagger.util.Json - -import scala.reflect.runtime.universe -import scala.reflect.runtime.universe.Type -import scala.util.control.NonFatal - -class Swagger( - override val host: String, - override val schemes: List[Scheme], - version: String, - val apiTypes: Seq[Type], - val config: Config, - val logger: Logger) - extends SwaggerHttpService { - - lazy val mirror = universe.runtimeMirror(getClass.getClassLoader) - - override val apiClasses = apiTypes.map { tpe => - mirror.runtimeClass(tpe.typeSymbol.asClass) - }.toSet - - // Note that the reason for overriding this is a subtle chain of causality: - // - // 1. Some of our endpoints require a single trailing slash and will not - // function if it is omitted - // 2. Swagger omits trailing slashes in its generated api doc - // 3. To work around that, a space is added after the trailing slash in the - // swagger Path annotations - // 4. This space is removed manually in the code below - // - // TODO: Ideally we'd like to drop this custom override and fix the issue in - // 1, by dropping the slash requirement and accepting api endpoints with and - // without trailing slashes. This will require inspecting and potentially - // fixing all service endpoints. - override def generateSwaggerJson: String = { - import io.swagger.models.{Swagger => JSwagger} - - import scala.collection.JavaConverters._ - try { - val swagger: JSwagger = reader.read(apiClasses.asJava) - - // Removing trailing spaces - swagger.setPaths( - swagger.getPaths.asScala - .map { - case (key, path) => - key.trim -> path - } - .toMap - .asJava) - - Json.pretty().writeValueAsString(swagger) - } catch { - case NonFatal(t) => - logger.error("Issue with creating swagger.json", t) - throw t - } - } - - override val basePath: String = config.getString("swagger.basePath") - override val apiDocsPath: String = config.getString("swagger.docsPath") - - override val info = Info( - config.getString("swagger.apiInfo.description"), - version, - 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.empty[String, AnyRef] - ) - - /** A very simple templating extractor. Gets a resource from the classpath and subsitutes any `{{key}}` with a value. */ - private def getTemplatedResource( - resourceName: String, - contentType: ContentType, - substitution: (String, String)): Route = get { - Option(this.getClass.getClassLoader.getResource(resourceName)) flatMap ResourceFile.apply match { - case Some(ResourceFile(url, length @ _, _)) => - extractSettings { settings => - val stream = StreamConverters - .fromInputStream(() => url.openStream()) - .withAttributes(ActorAttributes.dispatcher(settings.fileIODispatcher)) - .via(Framing.delimiter(ByteString("\n"), 4096, true).map(_.utf8String)) - .map { line => - line.replaceAll(s"\\{\\{${substitution._1}\\}\\}", substitution._2) - } - .map(line => ByteString(line + "\n")) - complete( - HttpEntity(contentType, stream) - ) - } - case None => reject - } - } - - def swaggerUI: Route = - pathEndOrSingleSlash { - getTemplatedResource( - "swagger-ui/index.html", - ContentTypes.`text/html(UTF-8)`, - "title" -> config.getString("swagger.apiInfo.title")) - } ~ getFromResourceDirectory("swagger-ui") - -} diff --git a/src/main/scala/xyz/driver/core/rest/auth/AlwaysAllowAuthorization.scala b/src/main/scala/xyz/driver/core/rest/auth/AlwaysAllowAuthorization.scala deleted file mode 100644 index 5007774..0000000 --- a/src/main/scala/xyz/driver/core/rest/auth/AlwaysAllowAuthorization.scala +++ /dev/null @@ -1,14 +0,0 @@ -package xyz.driver.core.rest.auth - -import xyz.driver.core.auth.{Permission, User} -import xyz.driver.core.rest.ServiceRequestContext - -import scala.concurrent.Future - -class AlwaysAllowAuthorization[U <: User] extends Authorization[U] { - override def userHasPermissions(user: U, permissions: Seq[Permission])( - implicit ctx: ServiceRequestContext): Future[AuthorizationResult] = { - val permissionsMap = permissions.map(_ -> true).toMap - Future.successful(AuthorizationResult(authorized = permissionsMap, ctx.permissionsToken)) - } -} diff --git a/src/main/scala/xyz/driver/core/rest/auth/AuthProvider.scala b/src/main/scala/xyz/driver/core/rest/auth/AuthProvider.scala deleted file mode 100644 index 82edcc7..0000000 --- a/src/main/scala/xyz/driver/core/rest/auth/AuthProvider.scala +++ /dev/null @@ -1,73 +0,0 @@ -package xyz.driver.core.rest.auth - -import akka.http.scaladsl.model.headers.HttpChallenges -import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsRejected -import com.typesafe.scalalogging.Logger -import xyz.driver.core._ -import xyz.driver.core.auth.{Permission, User} -import xyz.driver.core.rest.{AuthorizedServiceRequestContext, ServiceRequestContext, serviceContext} - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success} - -import scalaz.Scalaz.futureInstance -import scalaz.OptionT - -abstract class AuthProvider[U <: User](val authorization: Authorization[U], log: Logger)( - implicit execution: ExecutionContext) { - - import akka.http.scaladsl.server._ - import Directives._ - - /** - * Specific implementation on how to extract user from request context, - * can either need to do a network call to auth server or extract everything from self-contained token - * - * @param ctx set of request values which can be relevant to authenticate user - * @return authenticated user - */ - def authenticatedUser(implicit ctx: ServiceRequestContext): OptionT[Future, U] - - /** - * Verifies if a service context is authenticated and authorized to have `permissions` - */ - def authorize( - context: ServiceRequestContext, - permissions: Permission*): Directive1[AuthorizedServiceRequestContext[U]] = { - onComplete { - (for { - authToken <- OptionT.optionT(Future.successful(context.authToken)) - user <- authenticatedUser(context) - authCtx = context.withAuthenticatedUser(authToken, user) - authorizationResult <- authorization.userHasPermissions(user, permissions)(authCtx).toOptionT - - cachedPermissionsAuthCtx = authorizationResult.token.fold(authCtx)(authCtx.withPermissionsToken) - allAuthorized = permissions.forall(authorizationResult.authorized.getOrElse(_, false)) - } yield (cachedPermissionsAuthCtx, allAuthorized)).run - } flatMap { - case Success(Some((authCtx, true))) => provide(authCtx) - case Success(Some((authCtx, false))) => - val challenge = - HttpChallenges.basic(s"User does not have the required permissions: ${permissions.mkString(", ")}") - log.warn( - s"User ${authCtx.authenticatedUser} does not have the required permissions: ${permissions.mkString(", ")}") - reject(AuthenticationFailedRejection(CredentialsRejected, challenge)) - case Success(None) => - val challenge = HttpChallenges.basic("Failed to authenticate user") - log.warn(s"Failed to authenticate user to verify ${permissions.mkString(", ")}") - reject(AuthenticationFailedRejection(CredentialsRejected, challenge)) - case Failure(t) => - log.warn(s"Wasn't able to verify token for authenticated user to verify ${permissions.mkString(", ")}", t) - reject(ValidationRejection(s"Wasn't able to verify token for authenticated user", Some(t))) - } - } - - /** - * Verifies if request is authenticated and authorized to have `permissions` - */ - def authorize(permissions: Permission*): Directive1[AuthorizedServiceRequestContext[U]] = { - serviceContext flatMap { ctx => - authorize(ctx, permissions: _*) - } - } -} diff --git a/src/main/scala/xyz/driver/core/rest/auth/Authorization.scala b/src/main/scala/xyz/driver/core/rest/auth/Authorization.scala deleted file mode 100644 index 1a5e9be..0000000 --- a/src/main/scala/xyz/driver/core/rest/auth/Authorization.scala +++ /dev/null @@ -1,11 +0,0 @@ -package xyz.driver.core.rest.auth - -import xyz.driver.core.auth.{Permission, User} -import xyz.driver.core.rest.ServiceRequestContext - -import scala.concurrent.Future - -trait Authorization[U <: User] { - def userHasPermissions(user: U, permissions: Seq[Permission])( - implicit ctx: ServiceRequestContext): Future[AuthorizationResult] -} diff --git a/src/main/scala/xyz/driver/core/rest/auth/AuthorizationResult.scala b/src/main/scala/xyz/driver/core/rest/auth/AuthorizationResult.scala deleted file mode 100644 index efe28c9..0000000 --- a/src/main/scala/xyz/driver/core/rest/auth/AuthorizationResult.scala +++ /dev/null @@ -1,22 +0,0 @@ -package xyz.driver.core.rest.auth - -import xyz.driver.core.auth.{Permission, PermissionsToken} - -import scalaz.Scalaz.mapMonoid -import scalaz.Semigroup -import scalaz.syntax.semigroup._ - -final case class AuthorizationResult(authorized: Map[Permission, Boolean], token: Option[PermissionsToken]) -object AuthorizationResult { - val unauthorized: AuthorizationResult = AuthorizationResult(authorized = Map.empty, None) - - implicit val authorizationSemigroup: Semigroup[AuthorizationResult] = new Semigroup[AuthorizationResult] { - private implicit val authorizedBooleanSemigroup = Semigroup.instance[Boolean](_ || _) - private implicit val permissionsTokenSemigroup = - Semigroup.instance[Option[PermissionsToken]]((a, b) => b.orElse(a)) - - override def append(a: AuthorizationResult, b: => AuthorizationResult): AuthorizationResult = { - AuthorizationResult(a.authorized |+| b.authorized, a.token |+| b.token) - } - } -} diff --git a/src/main/scala/xyz/driver/core/rest/auth/CachedTokenAuthorization.scala b/src/main/scala/xyz/driver/core/rest/auth/CachedTokenAuthorization.scala deleted file mode 100644 index 66de4ef..0000000 --- a/src/main/scala/xyz/driver/core/rest/auth/CachedTokenAuthorization.scala +++ /dev/null @@ -1,55 +0,0 @@ -package xyz.driver.core.rest.auth - -import java.nio.file.{Files, Path} -import java.security.{KeyFactory, PublicKey} -import java.security.spec.X509EncodedKeySpec - -import pdi.jwt.{Jwt, JwtAlgorithm} -import xyz.driver.core.auth.{Permission, User} -import xyz.driver.core.rest.ServiceRequestContext - -import scala.concurrent.Future -import scalaz.syntax.std.boolean._ - -class CachedTokenAuthorization[U <: User](publicKey: => PublicKey, issuer: String) extends Authorization[U] { - override def userHasPermissions(user: U, permissions: Seq[Permission])( - implicit ctx: ServiceRequestContext): Future[AuthorizationResult] = { - import spray.json._ - - def extractPermissionsFromTokenJSON(tokenObject: JsObject): Option[Map[String, Boolean]] = - tokenObject.fields.get("permissions").collect { - case JsObject(fields) => - fields.collect { - case (key, JsBoolean(value)) => key -> value - } - } - - val result = for { - token <- ctx.permissionsToken - jwt <- Jwt.decode(token.value, publicKey, Seq(JwtAlgorithm.RS256)).toOption - jwtJson = jwt.parseJson.asJsObject - - // Ensure jwt is for the currently authenticated user and the correct issuer, otherwise return None - _ <- jwtJson.fields.get("sub").contains(JsString(user.id.value)).option(()) - _ <- jwtJson.fields.get("iss").contains(JsString(issuer)).option(()) - - permissionsMap <- extractPermissionsFromTokenJSON(jwtJson) - - authorized = permissions.map(p => p -> permissionsMap.getOrElse(p.toString, false)).toMap - } yield AuthorizationResult(authorized, Some(token)) - - Future.successful(result.getOrElse(AuthorizationResult.unauthorized)) - } -} - -object CachedTokenAuthorization { - def apply[U <: User](publicKeyFile: Path, issuer: String): CachedTokenAuthorization[U] = { - lazy val publicKey: PublicKey = { - val publicKeyBase64Encoded = new String(Files.readAllBytes(publicKeyFile)).trim - val publicKeyBase64Decoded = java.util.Base64.getDecoder.decode(publicKeyBase64Encoded) - val spec = new X509EncodedKeySpec(publicKeyBase64Decoded) - KeyFactory.getInstance("RSA").generatePublic(spec) - } - new CachedTokenAuthorization[U](publicKey, issuer) - } -} diff --git a/src/main/scala/xyz/driver/core/rest/auth/ChainedAuthorization.scala b/src/main/scala/xyz/driver/core/rest/auth/ChainedAuthorization.scala deleted file mode 100644 index 131e7fc..0000000 --- a/src/main/scala/xyz/driver/core/rest/auth/ChainedAuthorization.scala +++ /dev/null @@ -1,27 +0,0 @@ -package xyz.driver.core.rest.auth - -import xyz.driver.core.auth.{Permission, User} -import xyz.driver.core.rest.ServiceRequestContext - -import scala.concurrent.{ExecutionContext, Future} -import scalaz.Scalaz.{futureInstance, listInstance} -import scalaz.syntax.semigroup._ -import scalaz.syntax.traverse._ - -class ChainedAuthorization[U <: User](authorizations: Authorization[U]*)(implicit execution: ExecutionContext) - extends Authorization[U] { - - override def userHasPermissions(user: U, permissions: Seq[Permission])( - implicit ctx: ServiceRequestContext): Future[AuthorizationResult] = { - def allAuthorized(permissionsMap: Map[Permission, Boolean]): Boolean = - permissions.forall(permissionsMap.getOrElse(_, false)) - - authorizations.toList.foldLeftM[Future, AuthorizationResult](AuthorizationResult.unauthorized) { - (authResult, authorization) => - if (allAuthorized(authResult.authorized)) Future.successful(authResult) - else { - authorization.userHasPermissions(user, permissions).map(authResult |+| _) - } - } - } -} diff --git a/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala b/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala deleted file mode 100644 index db289de..0000000 --- a/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala +++ /dev/null @@ -1,23 +0,0 @@ -package xyz.driver.core.rest.errors - -sealed abstract class ServiceException(val message: String) extends Exception(message) - -final case class InvalidInputException(override val message: String = "Invalid input") extends ServiceException(message) - -final case class InvalidActionException(override val message: String = "This action is not allowed") - extends ServiceException(message) - -final case class ResourceNotFoundException(override val message: String = "Resource not found") - extends ServiceException(message) - -final case class ExternalServiceException( - serviceName: String, - serviceMessage: String, - serviceException: Option[ServiceException]) - extends ServiceException(s"Error while calling '$serviceName': $serviceMessage") - -final case class ExternalServiceTimeoutException(serviceName: String) - extends ServiceException(s"$serviceName took too long to respond") - -final case class DatabaseException(override val message: String = "Database access error") - extends ServiceException(message) diff --git a/src/main/scala/xyz/driver/core/rest/package.scala b/src/main/scala/xyz/driver/core/rest/package.scala deleted file mode 100644 index f85c39a..0000000 --- a/src/main/scala/xyz/driver/core/rest/package.scala +++ /dev/null @@ -1,286 +0,0 @@ -package xyz.driver.core.rest - -import java.net.InetAddress - -import akka.http.scaladsl.marshalling.{ToEntityMarshaller, ToResponseMarshallable} -import akka.http.scaladsl.model.headers._ -import akka.http.scaladsl.model._ -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server._ -import akka.http.scaladsl.unmarshalling.Unmarshal -import akka.stream.Materializer -import akka.stream.scaladsl.Flow -import akka.util.ByteString -import xyz.driver.tracing.TracingDirectives - -import scala.concurrent.Future -import scala.util.Try -import scalaz.{Functor, OptionT} -import scalaz.Scalaz.{intInstance, stringInstance} -import scalaz.syntax.equal._ - -trait Service - -trait HttpClient { - def makeRequest(request: HttpRequest): Future[HttpResponse] -} - -trait ServiceTransport { - - def sendRequestGetResponse(context: ServiceRequestContext)(requestStub: HttpRequest): Future[HttpResponse] - - def sendRequest(context: ServiceRequestContext)(requestStub: HttpRequest)( - implicit mat: Materializer): Future[Unmarshal[ResponseEntity]] -} - -sealed trait SortingOrder -object SortingOrder { - case object Asc extends SortingOrder - case object Desc extends SortingOrder -} - -final case class SortingField(name: String, sortingOrder: SortingOrder) -final case class Sorting(sortingFields: Seq[SortingField]) - -final case class Pagination(pageSize: Int, pageNumber: Int) { - require(pageSize > 0, "Page size must be greater than zero") - require(pageNumber > 0, "Page number must be greater than zero") - - def offset: Int = pageSize * (pageNumber - 1) -} - -final case class ListResponse[+T](items: Seq[T], meta: ListResponse.Meta) - -object ListResponse { - - def apply[T](items: Seq[T], size: Int, pagination: Option[Pagination]): ListResponse[T] = - ListResponse( - items = items, - meta = ListResponse.Meta(size, pagination.fold(1)(_.pageNumber), pagination.fold(size)(_.pageSize))) - - final case class Meta(itemsCount: Int, pageNumber: Int, pageSize: Int) - - object Meta { - def apply(itemsCount: Int, pagination: Pagination): Meta = - Meta(itemsCount, pagination.pageNumber, pagination.pageSize) - } - -} - -object `package` { - implicit class OptionTRestAdditions[T](optionT: OptionT[Future, T]) { - def responseOrNotFound(successCode: StatusCodes.Success = StatusCodes.OK)( - implicit F: Functor[Future], - em: ToEntityMarshaller[T]): Future[ToResponseMarshallable] = { - optionT.fold[ToResponseMarshallable](successCode -> _, StatusCodes.NotFound -> None) - } - } - - object ContextHeaders { - val AuthenticationTokenHeader: String = "Authorization" - val PermissionsTokenHeader: String = "Permissions" - val AuthenticationHeaderPrefix: String = "Bearer" - val ClientFingerprintHeader: String = "X-Client-Fingerprint" - val TrackingIdHeader: String = "X-Trace" - val StacktraceHeader: String = "X-Stacktrace" - val OriginatingIpHeader: String = "X-Forwarded-For" - val ResourceCount: String = "X-Resource-Count" - val PageCount: String = "X-Page-Count" - val TraceHeaderName: String = TracingDirectives.TraceHeaderName - val SpanHeaderName: String = TracingDirectives.SpanHeaderName - } - - object AuthProvider { - val AuthenticationTokenHeader: String = ContextHeaders.AuthenticationTokenHeader - val PermissionsTokenHeader: String = ContextHeaders.PermissionsTokenHeader - val SetAuthenticationTokenHeader: String = "set-authorization" - val SetPermissionsTokenHeader: String = "set-permissions" - } - - val AllowedHeaders: Seq[String] = - Seq( - "Origin", - "X-Requested-With", - "Content-Type", - "Content-Length", - "Accept", - "X-Trace", - "Access-Control-Allow-Methods", - "Access-Control-Allow-Origin", - "Access-Control-Allow-Headers", - "Server", - "Date", - ContextHeaders.ClientFingerprintHeader, - ContextHeaders.TrackingIdHeader, - ContextHeaders.TraceHeaderName, - ContextHeaders.SpanHeaderName, - ContextHeaders.StacktraceHeader, - ContextHeaders.AuthenticationTokenHeader, - ContextHeaders.OriginatingIpHeader, - ContextHeaders.ResourceCount, - ContextHeaders.PageCount, - "X-Frame-Options", - "X-Content-Type-Options", - "Strict-Transport-Security", - AuthProvider.SetAuthenticationTokenHeader, - AuthProvider.SetPermissionsTokenHeader - ) - - def allowOrigin(originHeader: Option[Origin]): `Access-Control-Allow-Origin` = - `Access-Control-Allow-Origin`( - originHeader.fold[HttpOriginRange](HttpOriginRange.*)(h => HttpOriginRange(h.origins: _*))) - - def serviceContext: Directive1[ServiceRequestContext] = { - extractClientIP flatMap { remoteAddress => - extract(ctx => extractServiceContext(ctx.request, remoteAddress)) - } - } - - def respondWithCorsAllowedHeaders: Directive0 = { - respondWithHeaders( - List[HttpHeader]( - `Access-Control-Allow-Headers`(AllowedHeaders: _*), - `Access-Control-Expose-Headers`(AllowedHeaders: _*) - )) - } - - def respondWithCorsAllowedOriginHeaders(origin: Origin): Directive0 = { - respondWithHeader { - `Access-Control-Allow-Origin`(HttpOriginRange(origin.origins: _*)) - } - } - - def respondWithCorsAllowedMethodHeaders(methods: Set[HttpMethod]): Directive0 = { - respondWithHeaders( - List[HttpHeader]( - Allow(methods.to[collection.immutable.Seq]), - `Access-Control-Allow-Methods`(methods.to[collection.immutable.Seq]) - )) - } - - def extractServiceContext(request: HttpRequest, remoteAddress: RemoteAddress): ServiceRequestContext = - new ServiceRequestContext( - extractTrackingId(request), - extractOriginatingIP(request, remoteAddress), - extractContextHeaders(request)) - - def extractTrackingId(request: HttpRequest): String = { - request.headers - .find(_.name === ContextHeaders.TrackingIdHeader) - .fold(java.util.UUID.randomUUID.toString)(_.value()) - } - - def extractFingerprintHash(request: HttpRequest): Option[String] = { - request.headers - .find(_.name === ContextHeaders.ClientFingerprintHeader) - .map(_.value()) - } - - def extractOriginatingIP(request: HttpRequest, remoteAddress: RemoteAddress): Option[InetAddress] = { - request.headers - .find(_.name === ContextHeaders.OriginatingIpHeader) - .flatMap(ipName => Try(InetAddress.getByName(ipName.value)).toOption) - .orElse(remoteAddress.toOption) - } - - def extractStacktrace(request: HttpRequest): Array[String] = - request.headers.find(_.name == ContextHeaders.StacktraceHeader).fold("")(_.value()).split("->") - - def extractContextHeaders(request: HttpRequest): Map[String, String] = { - request.headers.filter { h => - h.name === ContextHeaders.AuthenticationTokenHeader || h.name === ContextHeaders.TrackingIdHeader || - h.name === ContextHeaders.PermissionsTokenHeader || h.name === ContextHeaders.StacktraceHeader || - h.name === ContextHeaders.TraceHeaderName || h.name === ContextHeaders.SpanHeaderName || - h.name === ContextHeaders.OriginatingIpHeader || h.name === ContextHeaders.ClientFingerprintHeader - } map { header => - if (header.name === ContextHeaders.AuthenticationTokenHeader) { - header.name -> header.value.stripPrefix(ContextHeaders.AuthenticationHeaderPrefix).trim - } else { - header.name -> header.value - } - } toMap - } - - private[rest] def escapeScriptTags(byteString: ByteString): ByteString = { - @annotation.tailrec - def dirtyIndices(from: Int, descIndices: List[Int]): List[Int] = { - val index = byteString.indexOf('/', from) - if (index === -1) descIndices.reverse - else { - val (init, tail) = byteString.splitAt(index) - if ((init endsWith "<") && (tail startsWith "/sc")) { - dirtyIndices(index + 1, index :: descIndices) - } else { - dirtyIndices(index + 1, descIndices) - } - } - } - - val indices = dirtyIndices(0, Nil) - - indices.headOption.fold(byteString) { head => - val builder = ByteString.newBuilder - builder ++= byteString.take(head) - - (indices :+ byteString.length).sliding(2).foreach { - case Seq(start, end) => - builder += ' ' - builder ++= byteString.slice(start, end) - case Seq(_) => // Should not match; sliding on at least 2 elements - assert(indices.nonEmpty, s"Indices should have been nonEmpty: $indices") - } - builder.result - } - } - - val sanitizeRequestEntity: Directive0 = { - mapRequest(request => request.mapEntity(entity => entity.transformDataBytes(Flow.fromFunction(escapeScriptTags)))) - } - - val paginated: Directive1[Pagination] = - parameters(("pageSize".as[Int] ? 100, "pageNumber".as[Int] ? 1)).as(Pagination) - - private def extractPagination(pageSizeOpt: Option[Int], pageNumberOpt: Option[Int]): Option[Pagination] = - (pageSizeOpt, pageNumberOpt) match { - case (Some(size), Some(number)) => Option(Pagination(size, number)) - case (None, None) => Option.empty[Pagination] - case (_, _) => throw new IllegalArgumentException("Pagination's parameters are incorrect") - } - - val optionalPagination: Directive1[Option[Pagination]] = - parameters(("pageSize".as[Int].?, "pageNumber".as[Int].?)).as(extractPagination) - - def paginationQuery(pagination: Pagination) = - Seq("pageNumber" -> pagination.pageNumber.toString, "pageSize" -> pagination.pageSize.toString) - - private def extractSorting(sortingString: Option[String]): Sorting = { - val sortingFields = sortingString.fold(Seq.empty[SortingField])( - _.split(",") - .filter(_.length > 0) - .map { sortingParam => - if (sortingParam.startsWith("-")) { - SortingField(sortingParam.substring(1), SortingOrder.Desc) - } else { - val fieldName = if (sortingParam.startsWith("+")) sortingParam.substring(1) else sortingParam - SortingField(fieldName, SortingOrder.Asc) - } - } - .toSeq) - - Sorting(sortingFields) - } - - val sorting: Directive1[Sorting] = parameter("sort".as[String].?).as(extractSorting) - - def sortingQuery(sorting: Sorting): Seq[(String, String)] = { - val sortingString = sorting.sortingFields - .map { sortingField => - sortingField.sortingOrder match { - case SortingOrder.Asc => sortingField.name - case SortingOrder.Desc => s"-${sortingField.name}" - } - } - .mkString(",") - Seq("sort" -> sortingString) - } -} diff --git a/src/main/scala/xyz/driver/core/rest/serviceDiscovery.scala b/src/main/scala/xyz/driver/core/rest/serviceDiscovery.scala deleted file mode 100644 index 55f1a2e..0000000 --- a/src/main/scala/xyz/driver/core/rest/serviceDiscovery.scala +++ /dev/null @@ -1,24 +0,0 @@ -package xyz.driver.core.rest - -import xyz.driver.core.Name - -trait ServiceDiscovery { - - def discover[T <: Service](serviceName: Name[Service]): T -} - -trait SavingUsedServiceDiscovery { - private val usedServices = new scala.collection.mutable.HashSet[String]() - - def saveServiceUsage(serviceName: Name[Service]): Unit = usedServices.synchronized { - usedServices += serviceName.value - } - - def getUsedServices: Set[String] = usedServices.synchronized { usedServices.toSet } -} - -class NoServiceDiscovery extends ServiceDiscovery with SavingUsedServiceDiscovery { - - def discover[T <: Service](serviceName: Name[Service]): T = - throw new IllegalArgumentException(s"Service with name $serviceName is unknown") -} diff --git a/src/main/scala/xyz/driver/core/rest/serviceRequestContext.scala b/src/main/scala/xyz/driver/core/rest/serviceRequestContext.scala deleted file mode 100644 index 775106e..0000000 --- a/src/main/scala/xyz/driver/core/rest/serviceRequestContext.scala +++ /dev/null @@ -1,74 +0,0 @@ -package xyz.driver.core.rest - -import java.net.InetAddress - -import xyz.driver.core.auth.{AuthToken, PermissionsToken, User} -import xyz.driver.core.generators - -import scalaz.Scalaz.{mapEqual, stringInstance} -import scalaz.syntax.equal._ - -class ServiceRequestContext( - val trackingId: String = generators.nextUuid().toString, - val originatingIp: Option[InetAddress] = None, - val contextHeaders: Map[String, String] = Map.empty[String, String]) { - def authToken: Option[AuthToken] = - contextHeaders.get(AuthProvider.AuthenticationTokenHeader).map(AuthToken.apply) - - def permissionsToken: Option[PermissionsToken] = - contextHeaders.get(AuthProvider.PermissionsTokenHeader).map(PermissionsToken.apply) - - def withAuthToken(authToken: AuthToken): ServiceRequestContext = - new ServiceRequestContext( - trackingId, - originatingIp, - contextHeaders.updated(AuthProvider.AuthenticationTokenHeader, authToken.value) - ) - - def withAuthenticatedUser[U <: User](authToken: AuthToken, user: U): AuthorizedServiceRequestContext[U] = - new AuthorizedServiceRequestContext( - trackingId, - originatingIp, - contextHeaders.updated(AuthProvider.AuthenticationTokenHeader, authToken.value), - user - ) - - override def hashCode(): Int = - Seq[Any](trackingId, originatingIp, contextHeaders) - .foldLeft(31)((result, obj) => 31 * result + obj.hashCode()) - - override def equals(obj: Any): Boolean = obj match { - case ctx: ServiceRequestContext => - trackingId === ctx.trackingId && - originatingIp == originatingIp && - contextHeaders === ctx.contextHeaders - case _ => false - } - - override def toString: String = s"ServiceRequestContext($trackingId, $contextHeaders)" -} - -class AuthorizedServiceRequestContext[U <: User]( - override val trackingId: String = generators.nextUuid().toString, - override val originatingIp: Option[InetAddress] = None, - override val contextHeaders: Map[String, String] = Map.empty[String, String], - val authenticatedUser: U) - extends ServiceRequestContext { - - def withPermissionsToken(permissionsToken: PermissionsToken): AuthorizedServiceRequestContext[U] = - new AuthorizedServiceRequestContext[U]( - trackingId, - originatingIp, - contextHeaders.updated(AuthProvider.PermissionsTokenHeader, permissionsToken.value), - authenticatedUser) - - override def hashCode(): Int = 31 * super.hashCode() + authenticatedUser.hashCode() - - override def equals(obj: Any): Boolean = obj match { - case ctx: AuthorizedServiceRequestContext[U] => super.equals(ctx) && ctx.authenticatedUser == authenticatedUser - case _ => false - } - - override def toString: String = - s"AuthorizedServiceRequestContext($trackingId, $contextHeaders, $authenticatedUser)" -} diff --git a/src/main/scala/xyz/driver/core/stats.scala b/src/main/scala/xyz/driver/core/stats.scala deleted file mode 100644 index dbcf6e4..0000000 --- a/src/main/scala/xyz/driver/core/stats.scala +++ /dev/null @@ -1,58 +0,0 @@ -package xyz.driver.core - -import java.io.File -import java.lang.management.ManagementFactory -import java.lang.reflect.Modifier - -object stats { - - final case class MemoryStats(free: Long, total: Long, max: Long) - - final case class GarbageCollectorStats(totalGarbageCollections: Long, garbageCollectionTime: Long) - - final case class FileRootSpace(path: String, totalSpace: Long, freeSpace: Long, usableSpace: Long) - - object SystemStats { - - def memoryUsage: MemoryStats = { - val runtime = Runtime.getRuntime - MemoryStats(runtime.freeMemory, runtime.totalMemory, runtime.maxMemory) - } - - def availableProcessors: Int = { - Runtime.getRuntime.availableProcessors() - } - - def garbageCollectorStats: GarbageCollectorStats = { - import scala.collection.JavaConverters._ - - val (totalGarbageCollections, garbageCollectionTime) = - ManagementFactory.getGarbageCollectorMXBeans.asScala.foldLeft(0L -> 0L) { - case ((total, collectionTime), gc) => - (total + math.max(0L, gc.getCollectionCount)) -> (collectionTime + math.max(0L, gc.getCollectionTime)) - } - - GarbageCollectorStats(totalGarbageCollections, garbageCollectionTime) - } - - def fileSystemSpace: Array[FileRootSpace] = { - File.listRoots() map { root => - FileRootSpace(root.getAbsolutePath, root.getTotalSpace, root.getFreeSpace, root.getUsableSpace) - } - } - - def operatingSystemStats: Map[String, String] = { - val operatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean - operatingSystemMXBean.getClass.getDeclaredMethods - .map(method => { method.setAccessible(true); method }) - .filter(method => method.getName.startsWith("get") && Modifier.isPublic(method.getModifiers)) - .map { method => - try { - method.getName -> String.valueOf(method.invoke(operatingSystemMXBean)) - } catch { - case t: Throwable => method.getName -> t.getMessage - } - } toMap - } - } -} diff --git a/src/main/scala/xyz/driver/core/storage/BlobStorage.scala b/src/main/scala/xyz/driver/core/storage/BlobStorage.scala deleted file mode 100644 index ee6c5d7..0000000 --- a/src/main/scala/xyz/driver/core/storage/BlobStorage.scala +++ /dev/null @@ -1,50 +0,0 @@ -package xyz.driver.core.storage - -import java.net.URL -import java.nio.file.Path - -import akka.stream.scaladsl.{Sink, Source} -import akka.util.ByteString -import akka.{Done, NotUsed} - -import scala.concurrent.Future -import scala.concurrent.duration.Duration - -/** Binary key-value store, typically implemented by cloud storage. */ -trait BlobStorage { - - /** Upload data by value. */ - def uploadContent(name: String, content: Array[Byte]): Future[String] - - /** Upload data from an existing file. */ - def uploadFile(name: String, content: Path): Future[String] - - def exists(name: String): Future[Boolean] - - /** List available keys. The prefix determines which keys should be listed - * and depends on the implementation (for instance, a file system backed - * blob store will treat a prefix as a directory path). */ - def list(prefix: String): Future[Set[String]] - - /** Get all the content of a given object. */ - def content(name: String): Future[Option[Array[Byte]]] - - /** Stream data asynchronously and with backpressure. */ - def download(name: String): Future[Option[Source[ByteString, NotUsed]]] - - /** Get a sink to upload data. */ - def upload(name: String): Future[Sink[ByteString, Future[Done]]] - - /** Delete a stored value. */ - def delete(name: String): Future[String] - - /** - * Path to specified resource. Checks that the resource exists and returns None if - * it is not found. Depending on the implementation, may throw. - */ - def url(name: String): Future[Option[URL]] -} - -trait SignedBlobStorage extends BlobStorage { - def signedDownloadUrl(name: String, duration: Duration): Future[Option[URL]] -} diff --git a/src/main/scala/xyz/driver/core/storage/FileSystemBlobStorage.scala b/src/main/scala/xyz/driver/core/storage/FileSystemBlobStorage.scala deleted file mode 100644 index e12c73d..0000000 --- a/src/main/scala/xyz/driver/core/storage/FileSystemBlobStorage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package xyz.driver.core.storage - -import java.net.URL -import java.nio.file.{Files, Path, StandardCopyOption} - -import akka.stream.scaladsl.{FileIO, Sink, Source} -import akka.util.ByteString -import akka.{Done, NotUsed} - -import scala.collection.JavaConverters._ -import scala.concurrent.{ExecutionContext, Future} - -/** A blob store that is backed by a local filesystem. All objects are stored relative to the given - * root path. Slashes ('/') in blob names are treated as usual path separators and are converted - * to directories. */ -class FileSystemBlobStorage(root: Path)(implicit ec: ExecutionContext) extends BlobStorage { - - private def ensureParents(file: Path): Path = { - Files.createDirectories(file.getParent()) - file - } - - private def file(name: String) = root.resolve(name) - - override def uploadContent(name: String, content: Array[Byte]): Future[String] = Future { - Files.write(ensureParents(file(name)), content) - name - } - override def uploadFile(name: String, content: Path): Future[String] = Future { - Files.copy(content, ensureParents(file(name)), StandardCopyOption.REPLACE_EXISTING) - name - } - - override def exists(name: String): Future[Boolean] = Future { - val path = file(name) - Files.exists(path) && Files.isReadable(path) - } - - override def list(prefix: String): Future[Set[String]] = Future { - val dir = file(prefix) - Files - .list(dir) - .iterator() - .asScala - .map(p => root.relativize(p)) - .map(_.toString) - .toSet - } - - override def content(name: String): Future[Option[Array[Byte]]] = exists(name) map { - case true => - Some(Files.readAllBytes(file(name))) - case false => None - } - - override def download(name: String): Future[Option[Source[ByteString, NotUsed]]] = Future { - if (Files.exists(file(name))) { - Some(FileIO.fromPath(file(name)).mapMaterializedValue(_ => NotUsed)) - } else { - None - } - } - - override def upload(name: String): Future[Sink[ByteString, Future[Done]]] = Future { - val f = ensureParents(file(name)) - FileIO.toPath(f).mapMaterializedValue(_.map(_ => Done)) - } - - override def delete(name: String): Future[String] = exists(name).map { e => - if (e) { - Files.delete(file(name)) - } - name - } - - override def url(name: String): Future[Option[URL]] = exists(name) map { - case true => - Some(root.resolve(name).toUri.toURL) - case false => - None - } -} diff --git a/src/main/scala/xyz/driver/core/storage/GcsBlobStorage.scala b/src/main/scala/xyz/driver/core/storage/GcsBlobStorage.scala deleted file mode 100644 index 95164c7..0000000 --- a/src/main/scala/xyz/driver/core/storage/GcsBlobStorage.scala +++ /dev/null @@ -1,96 +0,0 @@ -package xyz.driver.core.storage - -import java.io.{FileInputStream, InputStream} -import java.net.URL -import java.nio.file.Path - -import akka.Done -import akka.stream.scaladsl.Sink -import akka.util.ByteString -import com.google.api.gax.paging.Page -import com.google.auth.oauth2.ServiceAccountCredentials -import com.google.cloud.storage.Storage.BlobListOption -import com.google.cloud.storage.{Blob, BlobId, Bucket, Storage, StorageOptions} - -import scala.collection.JavaConverters._ -import scala.concurrent.duration.Duration -import scala.concurrent.{ExecutionContext, Future} - -class GcsBlobStorage(client: Storage, bucketId: String, chunkSize: Int = GcsBlobStorage.DefaultChunkSize)( - implicit ec: ExecutionContext) - extends BlobStorage with SignedBlobStorage { - - private val bucket: Bucket = client.get(bucketId) - require(bucket != null, s"Bucket $bucketId does not exist.") - - override def uploadContent(name: String, content: Array[Byte]): Future[String] = Future { - bucket.create(name, content).getBlobId.getName - } - - override def uploadFile(name: String, content: Path): Future[String] = Future { - bucket.create(name, new FileInputStream(content.toFile)).getBlobId.getName - } - - override def exists(name: String): Future[Boolean] = Future { - bucket.get(name) != null - } - - override def list(prefix: String): Future[Set[String]] = Future { - val page: Page[Blob] = bucket.list(BlobListOption.prefix(prefix)) - page - .iterateAll() - .asScala - .map(_.getName()) - .toSet - } - - override def content(name: String): Future[Option[Array[Byte]]] = Future { - Option(bucket.get(name)).map(blob => blob.getContent()) - } - - override def download(name: String) = Future { - Option(bucket.get(name)).map { blob => - ChannelStream.fromChannel(() => blob.reader(), chunkSize) - } - } - - override def upload(name: String): Future[Sink[ByteString, Future[Done]]] = Future { - val blob = bucket.create(name, Array.emptyByteArray) - ChannelStream.toChannel(() => blob.writer(), chunkSize) - } - - override def delete(name: String): Future[String] = Future { - client.delete(BlobId.of(bucketId, name)) - name - } - - override def signedDownloadUrl(name: String, duration: Duration): Future[Option[URL]] = Future { - Option(bucket.get(name)).map(blob => blob.signUrl(duration.length, duration.unit)) - } - - override def url(name: String): Future[Option[URL]] = Future { - val protocol: String = "https" - val resourcePath: String = s"storage.googleapis.com/${bucket.getName}/" - Option(bucket.get(name)).map { blob => - new URL(protocol, resourcePath, blob.getName) - } - } -} - -object GcsBlobStorage { - final val DefaultChunkSize = 8192 - - private def newClient(key: InputStream): Storage = - StorageOptions - .newBuilder() - .setCredentials(ServiceAccountCredentials.fromStream(key)) - .build() - .getService() - - def fromKeyfile(keyfile: Path, bucketId: String, chunkSize: Int = DefaultChunkSize)( - implicit ec: ExecutionContext): GcsBlobStorage = { - val client = newClient(new FileInputStream(keyfile.toFile)) - new GcsBlobStorage(client, bucketId, chunkSize) - } - -} diff --git a/src/main/scala/xyz/driver/core/storage/channelStreams.scala b/src/main/scala/xyz/driver/core/storage/channelStreams.scala deleted file mode 100644 index fc652be..0000000 --- a/src/main/scala/xyz/driver/core/storage/channelStreams.scala +++ /dev/null @@ -1,112 +0,0 @@ -package xyz.driver.core.storage - -import java.nio.ByteBuffer -import java.nio.channels.{ReadableByteChannel, WritableByteChannel} - -import akka.stream._ -import akka.stream.scaladsl.{Sink, Source} -import akka.stream.stage._ -import akka.util.ByteString -import akka.{Done, NotUsed} - -import scala.concurrent.{Future, Promise} -import scala.util.control.NonFatal - -class ChannelSource(createChannel: () => ReadableByteChannel, chunkSize: Int) - extends GraphStage[SourceShape[ByteString]] { - - val out = Outlet[ByteString]("ChannelSource.out") - val shape = SourceShape(out) - - override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape) { - val channel = createChannel() - - object Handler extends OutHandler { - override def onPull(): Unit = { - try { - val buffer = ByteBuffer.allocate(chunkSize) - if (channel.read(buffer) > 0) { - buffer.flip() - push(out, ByteString.fromByteBuffer(buffer)) - } else { - completeStage() - } - } catch { - case NonFatal(_) => - channel.close() - } - } - override def onDownstreamFinish(): Unit = { - channel.close() - } - } - - setHandler(out, Handler) - } - -} - -class ChannelSink(createChannel: () => WritableByteChannel, chunkSize: Int) - extends GraphStageWithMaterializedValue[SinkShape[ByteString], Future[Done]] { - - val in = Inlet[ByteString]("ChannelSink.in") - val shape = SinkShape(in) - - override def createLogicAndMaterializedValue(inheritedAttributes: Attributes): (GraphStageLogic, Future[Done]) = { - val promise = Promise[Done]() - val logic = new GraphStageLogic(shape) { - val channel = createChannel() - - object Handler extends InHandler { - override def onPush(): Unit = { - try { - val data = grab(in) - channel.write(data.asByteBuffer) - pull(in) - } catch { - case NonFatal(e) => - channel.close() - promise.failure(e) - } - } - - override def onUpstreamFinish(): Unit = { - channel.close() - completeStage() - promise.success(Done) - } - - override def onUpstreamFailure(ex: Throwable): Unit = { - channel.close() - promise.failure(ex) - } - } - - setHandler(in, Handler) - - override def preStart(): Unit = { - pull(in) - } - } - (logic, promise.future) - } - -} - -object ChannelStream { - - def fromChannel(channel: () => ReadableByteChannel, chunkSize: Int = 8192): Source[ByteString, NotUsed] = { - Source - .fromGraph(new ChannelSource(channel, chunkSize)) - .withAttributes(Attributes(ActorAttributes.IODispatcher)) - .async - } - - def toChannel(channel: () => WritableByteChannel, chunkSize: Int = 8192): Sink[ByteString, Future[Done]] = { - Sink - .fromGraph(new ChannelSink(channel, chunkSize)) - .withAttributes(Attributes(ActorAttributes.IODispatcher)) - .async - } - -} diff --git a/src/main/scala/xyz/driver/core/swagger.scala b/src/main/scala/xyz/driver/core/swagger.scala deleted file mode 100644 index 6567290..0000000 --- a/src/main/scala/xyz/driver/core/swagger.scala +++ /dev/null @@ -1,161 +0,0 @@ -package xyz.driver.core - -import java.lang.annotation.Annotation -import java.lang.reflect.Type -import java.util - -import com.fasterxml.jackson.databind.{BeanDescription, ObjectMapper} -import com.fasterxml.jackson.databind.`type`.ReferenceType -import io.swagger.converter._ -import io.swagger.jackson.AbstractModelConverter -import io.swagger.models.{Model, ModelImpl} -import io.swagger.models.properties._ -import io.swagger.util.{Json, PrimitiveType} -import spray.json._ - -object swagger { - - def configureCustomSwaggerModels( - customPropertiesExamples: Map[Class[_], Property], - customObjectsExamples: Map[Class[_], JsValue]) = { - ModelConverters - .getInstance() - .addConverter(new CustomSwaggerJsonConverter(Json.mapper(), customPropertiesExamples, customObjectsExamples)) - } - - object CustomSwaggerJsonConverter { - - def stringProperty(pattern: Option[String] = None, example: Option[String] = None): Property = { - make(new StringProperty()) { sp => - sp.required(true) - example.foreach(sp.example) - pattern.foreach(sp.pattern) - } - } - - def enumProperty[V](values: V*): Property = { - make(new StringProperty()) { sp => - for (v <- values) sp._enum(v.toString) - sp.setRequired(true) - } - } - - def numericProperty(example: Option[AnyRef] = None): Property = { - make(PrimitiveType.DECIMAL.createProperty()) { dp => - dp.setRequired(true) - example.foreach(dp.setExample) - } - } - - def booleanProperty(): Property = { - make(new BooleanProperty()) { bp => - bp.setRequired(true) - } - } - } - - @SuppressWarnings(Array("org.wartremover.warts.Null")) - class CustomSwaggerJsonConverter( - mapper: ObjectMapper, - customProperties: Map[Class[_], Property], - customObjects: Map[Class[_], JsValue]) - extends AbstractModelConverter(mapper) { - import CustomSwaggerJsonConverter._ - - override def resolveProperty( - `type`: Type, - context: ModelConverterContext, - annotations: Array[Annotation], - chain: util.Iterator[ModelConverter]): Property = { - val javaType = Json.mapper().constructType(`type`) - - Option(javaType.getRawClass) flatMap { cls => - customProperties.get(cls) - } orElse { - `type` match { - case rt: ReferenceType if isOption(javaType.getRawClass) && chain.hasNext => - val nextType = rt.getContentType - val nextResolved = Option(resolveProperty(nextType, context, annotations, chain)).getOrElse( - chain.next().resolveProperty(nextType, context, annotations, chain)) - nextResolved.setRequired(false) - Option(nextResolved) - case t if chain.hasNext => - val nextResolved = chain.next().resolveProperty(t, context, annotations, chain) - nextResolved.setRequired(true) - Option(nextResolved) - case _ => - Option.empty[Property] - } - } orNull - } - - @SuppressWarnings(Array("org.wartremover.warts.Null")) - override def resolve(`type`: Type, context: ModelConverterContext, chain: util.Iterator[ModelConverter]): Model = { - - val javaType = Json.mapper().constructType(`type`) - - (getEnumerationInstance(javaType.getRawClass) match { - case Some(_) => Option.empty[Model] // ignore scala enums - case None => - val customObjectModel = customObjects.get(javaType.getRawClass).map { objectExampleJson => - val properties = objectExampleJson.asJsObject.fields.mapValues(parseJsonValueToSwaggerProperty).flatMap { - case (key, value) => value.map(v => key -> v) - } - - val beanDesc = _mapper.getSerializationConfig.introspect[BeanDescription](javaType) - val name = _typeName(javaType, beanDesc) - - make(new ModelImpl()) { model => - model.name(name) - properties.foreach { case (field, property) => model.addProperty(field, property) } - } - } - - customObjectModel.orElse { - if (chain.hasNext) { - val next = chain.next() - Option(next.resolve(`type`, context, chain)) - } else { - Option.empty[Model] - } - } - }).orNull - } - - private def parseJsonValueToSwaggerProperty(jsValue: JsValue): Option[Property] = { - import scala.collection.JavaConverters._ - - jsValue match { - case JsArray(elements) => - elements.headOption.flatMap(parseJsonValueToSwaggerProperty).map { itemProperty => - new ArrayProperty(itemProperty) - } - case JsObject(subFields) => - val subProperties = subFields.mapValues(parseJsonValueToSwaggerProperty).flatMap { - case (key, value) => value.map(v => key -> v) - } - Option(new ObjectProperty(subProperties.asJava)) - case JsBoolean(_) => Option(booleanProperty()) - case JsNumber(value) => Option(numericProperty(example = Option(value))) - case JsString(value) => Option(stringProperty(example = Option(value))) - case _ => Option.empty[Property] - } - } - - private def getEnumerationInstance(cls: Class[_]): Option[Enumeration] = { - if (cls.getFields.map(_.getName).contains("MODULE$")) { - val javaUniverse = scala.reflect.runtime.universe - val m = javaUniverse.runtimeMirror(Thread.currentThread().getContextClassLoader) - val moduleMirror = m.reflectModule(m.staticModule(cls.getName)) - moduleMirror.instance match { - case enumInstance: Enumeration => Some(enumInstance) - case _ => None - } - } else { - None - } - } - - private def isOption(cls: Class[_]): Boolean = cls.equals(classOf[scala.Option[_]]) - } -} diff --git a/src/main/scala/xyz/driver/core/time.scala b/src/main/scala/xyz/driver/core/time.scala deleted file mode 100644 index 6dbd173..0000000 --- a/src/main/scala/xyz/driver/core/time.scala +++ /dev/null @@ -1,175 +0,0 @@ -package xyz.driver.core - -import java.text.SimpleDateFormat -import java.util._ -import java.util.concurrent.TimeUnit - -import xyz.driver.core.date.Month - -import scala.concurrent.duration._ -import scala.util.Try - -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 - - final case class Time(millis: Long) extends AnyVal { - - def isBefore(anotherTime: Time): Boolean = implicitly[Ordering[Time]].lt(this, anotherTime) - - def isAfter(anotherTime: Time): Boolean = implicitly[Ordering[Time]].gt(this, anotherTime) - - def advanceBy(duration: Duration): Time = Time(millis + duration.toMillis) - - def durationTo(anotherTime: Time): Duration = Duration.apply(anotherTime.millis - millis, TimeUnit.MILLISECONDS) - - def durationFrom(anotherTime: Time): Duration = Duration.apply(millis - anotherTime.millis, TimeUnit.MILLISECONDS) - - def toDate(timezone: TimeZone): date.Date = { - val cal = Calendar.getInstance(timezone) - cal.setTimeInMillis(millis) - date.Date(cal.get(Calendar.YEAR), date.Month(cal.get(Calendar.MONTH)), cal.get(Calendar.DAY_OF_MONTH)) - } - } - - /** - * Encapsulates a time and timezone without a specific date. - */ - final case class TimeOfDay(localTime: java.time.LocalTime, timeZone: TimeZone) { - - /** - * Is this time before another time on a specific day. Day light savings safe. These are zero-indexed - * for month/day. - */ - def isBefore(other: TimeOfDay, day: Int, month: Month, year: Int): Boolean = { - toCalendar(day, month, year).before(other.toCalendar(day, month, year)) - } - - /** - * Is this time after another time on a specific day. Day light savings safe. - */ - def isAfter(other: TimeOfDay, day: Int, month: Month, year: Int): Boolean = { - toCalendar(day, month, year).after(other.toCalendar(day, month, year)) - } - - def sameTimeAs(other: TimeOfDay, day: Int, month: Month, year: Int): Boolean = { - toCalendar(day, month, year).equals(other.toCalendar(day, month, year)) - } - - /** - * Enforces the same formatting as expected by [[java.sql.Time]] - * @return string formatted for `java.sql.Time` - */ - def timeString: String = { - localTime.format(TimeOfDay.getFormatter) - } - - /** - * @return a string parsable by [[java.util.TimeZone]] - */ - def timeZoneString: String = { - timeZone.getID - } - - /** - * @return this [[TimeOfDay]] as [[java.sql.Time]] object, [[java.sql.Time.valueOf]] will - * throw when the string is not valid, but this is protected by [[timeString]] method. - */ - def toTime: java.sql.Time = { - java.sql.Time.valueOf(timeString) - } - - private def toCalendar(day: Int, month: Int, year: Int): Calendar = { - val cal = Calendar.getInstance(timeZone) - cal.set(year, month, day, localTime.getHour, localTime.getMinute, localTime.getSecond) - cal.clear(Calendar.MILLISECOND) - cal - } - } - - object TimeOfDay { - def now(): TimeOfDay = { - TimeOfDay(java.time.LocalTime.now(), TimeZone.getDefault) - } - - /** - * Throws when [s] is not parsable by [[java.time.LocalTime.parse]], uses default [[java.util.TimeZone]] - */ - def parseTimeString(tz: TimeZone = TimeZone.getDefault)(s: String): TimeOfDay = { - TimeOfDay(java.time.LocalTime.parse(s), tz) - } - - def fromString(tz: TimeZone)(s: String): Option[TimeOfDay] = { - val op = Try(java.time.LocalTime.parse(s)).toOption - op.map(lt => TimeOfDay(lt, tz)) - } - - def fromStrings(zoneId: String)(s: String): Option[TimeOfDay] = { - val op = Try(TimeZone.getTimeZone(zoneId)).toOption - op.map(tz => TimeOfDay.parseTimeString(tz)(s)) - } - - /** - * Formatter that enforces `HH:mm:ss` which is expected by [[java.sql.Time]] - */ - def getFormatter: java.time.format.DateTimeFormatter = { - java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss") - } - } - - object Time { - - implicit def timeOrdering: Ordering[Time] = Ordering.by(_.millis) - } - - final case class TimeRange(start: Time, end: Time) { - def duration: Duration = FiniteDuration(end.millis - start.millis, MILLISECONDS) - } - - def startOfMonth(time: Time) = { - Time(make(new GregorianCalendar()) { cal => - cal.setTime(new Date(time.millis)) - cal.set(Calendar.DAY_OF_MONTH, cal.getActualMinimum(Calendar.DAY_OF_MONTH)) - }.getTime.getTime) - } - - def textualDate(timezone: TimeZone)(time: Time): String = - make(new SimpleDateFormat("MMMM d, yyyy"))(_.setTimeZone(timezone)).format(new Date(time.millis)) - - def textualTime(timezone: TimeZone)(time: Time): String = - make(new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a"))(_.setTimeZone(timezone)).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 TimeProvider { - def currentTime(): Time - } - - final class SystemTimeProvider extends TimeProvider { - def currentTime() = Time(System.currentTimeMillis()) - } - final val SystemTimeProvider = new SystemTimeProvider - - final class SpecificTimeProvider(time: Time) extends TimeProvider { - def currentTime() = time - } - } -} -- cgit v1.2.3