diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f683b1..21e15c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,9 @@ jobs: java-version: 11 - name: Run tests run: sbt +test + - name: Check Scala formatting + if: ${{ always() }} + run: sbt scalafmtCheckAll - name: Check assets can be published if: ${{ always() }} run: sbt publishLocal diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..be3ce7d --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,17 @@ +version = 2.4.2 +style = default +maxColumn = 120 +optIn.breakChainOnFirstMethodDot = false +assumeStandardLibraryStripMargin = true +align = more +continuationIndent.defnSite = 2 +rewrite.rules = [ + AsciiSortImports, + AvoidInfix, + PreferCurlyFors, + RedundantBraces, + RedundantParens, + SortModifiers +] +project.git = true +includeNoParensInSelectChains = true diff --git a/project/plugins.sbt b/project/plugins.sbt index e336017..48399c6 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,3 +4,4 @@ addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1") addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.15") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.9.0") addSbtPlugin("com.dwijnand" % "sbt-dynver" % "4.1.1") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2") diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/Config.scala b/src/main/scala/com/snowplowanalytics/iglu/server/Config.scala index 14c93f1..fc7b541 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/Config.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/Config.scala @@ -43,18 +43,20 @@ import generated.BuildInfo.version * be skipped if a schema with the same key already exists * @param webhooks List of webhooks triggered by specific actions or endpoints */ -case class Config(database: Config.StorageConfig, - repoServer: Config.Http, - debug: Option[Boolean], - patchesAllowed: Option[Boolean], - webhooks: Option[List[Webhook]]) +case class Config( + database: Config.StorageConfig, + repoServer: Config.Http, + debug: Option[Boolean], + patchesAllowed: Option[Boolean], + webhooks: Option[List[Webhook]] +) object Config { sealed trait ThreadPool extends Product with Serializable object ThreadPool { - case object Global extends ThreadPool - case object Cached extends ThreadPool + case object Global extends ThreadPool + case object Cached extends ThreadPool case class Fixed(size: Int) extends ThreadPool implicit val threadPoolReader: ConfigReader[ThreadPool] = @@ -64,7 +66,7 @@ object Config { case Right(s) if s.toLowerCase == "cached" => Right(Cached) case Left(err) => for { - obj <- cur.asObjectCursor + obj <- cur.asObjectCursor typeCur <- obj.atKey("type") typeStr <- typeCur.asString pool <- typeStr.toLowerCase match { @@ -75,7 +77,7 @@ object Config { } yield Fixed(sizeInt) case "global" => Right(Global) case "cached" => Right(Cached) - case _ => Left(err) + case _ => Left(err) } } yield pool } @@ -99,27 +101,31 @@ object Config { sealed trait ConnectionPool extends Product with Serializable object ConnectionPool { case class NoPool(threadPool: ThreadPool = ThreadPool.Cached) extends ConnectionPool - case class Hikari(connectionTimeout: Option[Int], - maxLifetime: Option[Int], - minimumIdle: Option[Int], - maximumPoolSize: Option[Int], - // Provided by doobie - connectionPool: ThreadPool = ThreadPool.Fixed(2), - transactionPool: ThreadPool = ThreadPool.Cached) extends ConnectionPool + case class Hikari( + connectionTimeout: Option[Int], + maxLifetime: Option[Int], + minimumIdle: Option[Int], + maximumPoolSize: Option[Int], + // Provided by doobie + connectionPool: ThreadPool = ThreadPool.Fixed(2), + transactionPool: ThreadPool = ThreadPool.Cached + ) extends ConnectionPool implicit val circePoolEncoder: Encoder[ConnectionPool] = Encoder.instance { case NoPool(threadPool) => - Json.fromFields(List( - ("type", Json.fromString("NoPool")), - ("threadPool", threadPool.asJson(ThreadPool.threadPoolCirceEncoder)) - )) + Json.fromFields( + List( + ("type", Json.fromString("NoPool")), + ("threadPool", threadPool.asJson(ThreadPool.threadPoolCirceEncoder)) + ) + ) case h: Hikari => deriveEncoder[Hikari].apply(h) } - implicit val nopoolHint = ProductHint[NoPool](ConfigFieldMapping(CamelCase, CamelCase)) - implicit val hikariHint = ProductHint[Hikari](ConfigFieldMapping(CamelCase, CamelCase)) + implicit val nopoolHint = ProductHint[NoPool](ConfigFieldMapping(CamelCase, CamelCase)) + implicit val hikariHint = ProductHint[Hikari](ConfigFieldMapping(CamelCase, CamelCase)) implicit val noPoolReader = deriveReader[NoPool] implicit val hikariReader = deriveReader[Hikari] @@ -129,15 +135,16 @@ object Config { objCur <- cur.asObjectCursor typeCur = objCur.atKeyOrUndefined("type") pool <- if (typeCur.isUndefined) Right(ConfigReader[NoPool].from(cur).getOrElse(ConnectionPool.NoPool())) - else for { - typeStr <- typeCur.asString - result <- typeStr.toLowerCase match { - case "hikari" => - ConfigReader[Hikari].from(cur) - case "nopool" => - ConfigReader[NoPool].from(cur) - } - } yield result + else + for { + typeStr <- typeCur.asString + result <- typeStr.toLowerCase match { + case "hikari" => + ConfigReader[Hikari].from(cur) + case "nopool" => + ConfigReader[NoPool].from(cur) + } + } yield result } yield pool } } @@ -150,15 +157,17 @@ object Config { /** * Configuration for PostgreSQL state storage. */ - case class Postgres(host: String, - port: Int, - dbname: String, - username: String, - password: String, - driver: String, - connectThreads: Option[Int], - maxPoolSize: Option[Int], // deprecated - pool: ConnectionPool = ConnectionPool.NoPool(ThreadPool.Cached)) extends StorageConfig { + case class Postgres( + host: String, + port: Int, + dbname: String, + username: String, + password: String, + driver: String, + connectThreads: Option[Int], + maxPoolSize: Option[Int], // deprecated + pool: ConnectionPool = ConnectionPool.NoPool(ThreadPool.Cached) + ) extends StorageConfig { /** Backward-compatibility */ val maximumPoolSize: Int = pool match { @@ -168,19 +177,26 @@ object Config { } val postgresReader: ConfigReader[Postgres] = - ConfigReader.forProduct9("host", "port","dbname", "username", - "password", "driver", "connectThreads", "maxPoolSize", "pool")(StorageConfig.Postgres.apply) + ConfigReader.forProduct9( + "host", + "port", + "dbname", + "username", + "password", + "driver", + "connectThreads", + "maxPoolSize", + "pool" + )(StorageConfig.Postgres.apply) implicit val storageConfigCirceEncoder: Encoder[StorageConfig] = deriveEncoder[StorageConfig].mapJson { json => - json.hcursor - .downField("Postgres") - .focus - .getOrElse(Json.Null) - .mapObject { o => JsonObject.fromMap(o.toMap.map { + json.hcursor.downField("Postgres").focus.getOrElse(Json.Null).mapObject { o => + JsonObject.fromMap(o.toMap.map { case ("password", _) => ("password", Json.fromString("******")) - case (k, v) => (k, v) - })} + case (k, v) => (k, v) + }) + } } } @@ -200,13 +216,13 @@ object Config { implicit val pureWebhookReader: ConfigReader[Webhook] = ConfigReader.fromCursor { cur => for { - objCur <- cur.asObjectCursor + objCur <- cur.asObjectCursor uriCursor <- objCur.atKey("uri") - uri <- ConfigReader[org.http4s.Uri].from(uriCursor) + uri <- ConfigReader[org.http4s.Uri].from(uriCursor) prefixes <- objCur.atKeyOrUndefined("vendor-prefixes") match { case keyCur if keyCur.isUndefined => List.empty.asRight - case keyCur => keyCur.asList.flatMap(_.traverse(cur => cur.asString)) + case keyCur => keyCur.asList.flatMap(_.traverse(cur => cur.asString)) } } yield Webhook.SchemaPublished(uri, Some(prefixes)) } @@ -216,9 +232,9 @@ object Config { objCur <- cur.asObjectCursor typeCur <- objCur.atKey("type") typeStr <- typeCur.asString - result <- typeStr match { + result <- typeStr match { case "postgres" => StorageConfig.postgresReader.from(cur) - case "dummy" => StorageConfig.Dummy.asRight + case "dummy" => StorageConfig.Dummy.asRight case _ => val message = s"type has value $typeStr instead of class1 or class2" objCur.failed[StorageConfig](error.CannotConvert(objCur.objValue.toString, "StorageConfig", message)) @@ -230,9 +246,9 @@ object Config { implicit val pureWebhooksReader: ConfigReader[List[Webhook]] = ConfigReader.fromCursor { cur => for { - objCur <- cur.asObjectCursor + objCur <- cur.asObjectCursor schemaPublishedCursors <- objCur.atKeyOrUndefined("schema-published").asList - webhooks <- schemaPublishedCursors.traverse(cur => pureWebhookReader.from(cur)) + webhooks <- schemaPublishedCursors.traverse(cur => pureWebhookReader.from(cur)) } yield webhooks } @@ -244,26 +260,26 @@ object Config { sealed trait ServerCommand { def config: Path def read: Either[String, Config] = - ConfigSource - .default(ConfigSource.file(config)) - .load[Config] - .leftMap(_.toList.map(_.description).mkString("\n")) + ConfigSource.default(ConfigSource.file(config)).load[Config].leftMap(_.toList.map(_.description).mkString("\n")) } object ServerCommand { - case class Run(config: Path) extends ServerCommand + case class Run(config: Path) extends ServerCommand case class Setup(config: Path, migrate: Option[MigrateFrom]) extends ServerCommand } val configOpt = Opts.option[Path]("config", "Path to server configuration HOCON") val migrateOpt = Opts .option[String]("migrate", "Migrate the DB from a particular version") - .mapValidated { s => MigrateFrom.parse(s).toValid(s"Cannot perform migration from version $s to $version").toValidatedNel } + .mapValidated { s => + MigrateFrom.parse(s).toValid(s"Cannot perform migration from version $s to $version").toValidatedNel + } .orNone val runCommand: Opts[ServerCommand] = configOpt.map(ServerCommand.Run.apply) val setupCommand: Opts[ServerCommand] = Opts.subcommand("setup", "Setup Iglu Server")((configOpt, migrateOpt).mapN(ServerCommand.Setup.apply)) - val serverCommand = Command[ServerCommand](generated.BuildInfo.name, generated.BuildInfo.version)(runCommand.orElse(setupCommand)) + val serverCommand = + Command[ServerCommand](generated.BuildInfo.name, generated.BuildInfo.version)(runCommand.orElse(setupCommand)) } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/Main.scala b/src/main/scala/com/snowplowanalytics/iglu/server/Main.scala index 4730df7..3987c1f 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/Main.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/Main.scala @@ -24,16 +24,16 @@ object Main extends SafeIOApp { val cli = for { command <- EitherT.fromEither[IO](Config.serverCommand.parse(args).leftMap(_.toString)) config <- EitherT.fromEither[IO](command.read) - result <- command match { + result <- command match { case _: Config.ServerCommand.Run => - EitherT.liftF[IO, String, ExitCode](Server.run(config).compile.lastOrError ) + EitherT.liftF[IO, String, ExitCode](Server.run(config).compile.lastOrError) case Config.ServerCommand.Setup(_, migration) => EitherT.liftF[IO, String, ExitCode](Server.setup(config, migration)) } } yield result cli.value.flatMap { - case Right(code) => IO.pure(code) + case Right(code) => IO.pure(code) case Left(cliError) => IO(System.err.println(cliError)).as(ExitCode.Error) } } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/SafeIOApp.scala b/src/main/scala/com/snowplowanalytics/iglu/server/SafeIOApp.scala index 46301df..256cafd 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/SafeIOApp.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/SafeIOApp.scala @@ -25,12 +25,14 @@ import scala.concurrent.ExecutionContext trait SafeIOApp extends IOApp.WithContext { private lazy val ec: ExecutionContext = - ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(List(2, Runtime.getRuntime.availableProcessors()).max, DaemonThreadFactory)) + ExecutionContext.fromExecutorService( + Executors.newFixedThreadPool(List(2, Runtime.getRuntime.availableProcessors()).max, DaemonThreadFactory) + ) - private final val log: Logger = LoggerFactory.getLogger(Main.getClass) + final private val log: Logger = LoggerFactory.getLogger(Main.getClass) // To overcome https://github.com/typelevel/cats-effect/issues/515 - private final val exitingEC: ExecutionContext = new ExecutionContext { + final private val exitingEC: ExecutionContext = new ExecutionContext { def execute(r: Runnable): Unit = ec.execute { () => try r.run() @@ -48,14 +50,14 @@ trait SafeIOApp extends IOApp.WithContext { Resource.eval(SyncIO(exitingEC)) private object DaemonThreadFactory extends ThreadFactory { - private val s: SecurityManager = System.getSecurityManager - private val poolNumber: AtomicInteger = new AtomicInteger(1) - private val group: ThreadGroup = if (s == null) Thread.currentThread().getThreadGroup else s.getThreadGroup - private val threadNumber: AtomicInteger = new AtomicInteger(1) - private val namePrefix: String = "ioapp-pool-" + poolNumber.getAndIncrement() + "-thread-" + private val s: SecurityManager = System.getSecurityManager + private val poolNumber: AtomicInteger = new AtomicInteger(1) + private val group: ThreadGroup = if (s == null) Thread.currentThread().getThreadGroup else s.getThreadGroup + private val threadNumber: AtomicInteger = new AtomicInteger(1) + private val namePrefix: String = "ioapp-pool-" + poolNumber.getAndIncrement() + "-thread-" def newThread(r: Runnable): Thread = { - val t: Thread = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0) + val t: Thread = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0) t.setDaemon(true) if (t.getPriority != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY) t diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/Server.scala b/src/main/scala/com/snowplowanalytics/iglu/server/Server.scala index 713482d..2dc64f6 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/Server.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/Server.scala @@ -14,13 +14,13 @@ */ package com.snowplowanalytics.iglu.server -import java.util.concurrent.{ Executors, ExecutorService } +import java.util.concurrent.{ExecutorService, Executors} import scala.concurrent.duration._ import scala.concurrent.ExecutionContext import cats.data.Kleisli -import cats.effect.{Blocker, ContextShift, Sync, ExitCode, IO, Timer, Resource} +import cats.effect.{Blocker, ContextShift, ExitCode, IO, Resource, Sync, Timer} import io.circe.syntax._ @@ -88,47 +88,50 @@ object Server { base -> constructor(storage, PermissionContext, swagger) } - def httpApp(storage: Storage[IO], - debug: Boolean, - patchesAllowed: Boolean, - webhook: Webhook.WebhookClient[IO], - cache: CachingMiddleware.ResponseCache[IO], - blocker: Blocker) - (implicit cs: ContextShift[IO]): HttpApp[IO] = { + def httpApp( + storage: Storage[IO], + debug: Boolean, + patchesAllowed: Boolean, + webhook: Webhook.WebhookClient[IO], + cache: CachingMiddleware.ResponseCache[IO], + blocker: Blocker + )(implicit cs: ContextShift[IO]): HttpApp[IO] = { val serverRoutes = httpRoutes(storage, debug, patchesAllowed, webhook, cache, blocker) Kleisli[IO, Request[IO], Response[IO]](req => Router(serverRoutes: _*).run(req).getOrElse(NotFound)) } - def httpRoutes(storage: Storage[IO], - debug: Boolean, - patchesAllowed: Boolean, - webhook: Webhook.WebhookClient[IO], - cache: CachingMiddleware.ResponseCache[IO], - blocker: Blocker) - (implicit cs: ContextShift[IO]): List[(String, HttpRoutes[IO])] = { + def httpRoutes( + storage: Storage[IO], + debug: Boolean, + patchesAllowed: Boolean, + webhook: Webhook.WebhookClient[IO], + cache: CachingMiddleware.ResponseCache[IO], + blocker: Blocker + )(implicit cs: ContextShift[IO]): List[(String, HttpRoutes[IO])] = { val services: List[(String, RoutesConstructor)] = List( "/api/meta" -> MetaService.asRoutes(debug, patchesAllowed), "/api/schemas" -> SchemaService.asRoutes(patchesAllowed, webhook), "/api/auth" -> AuthService.asRoutes, "/api/validation" -> ValidationService.asRoutes, - "/api/drafts" -> DraftService.asRoutes, + "/api/drafts" -> DraftService.asRoutes ) - val debugRoute = "/api/debug" -> DebugService.asRoutes(storage, ioSwagger.createRhoMiddleware()) + val debugRoute = "/api/debug" -> DebugService.asRoutes(storage, ioSwagger.createRhoMiddleware()) val staticRoute = "/static" -> StaticService.routes(blocker) - val routes = staticRoute :: services.map(addSwagger(storage)) + val routes = staticRoute :: services.map(addSwagger(storage)) val corsConfig = CORSConfig( anyOrigin = true, anyMethod = false, allowedMethods = Some(Set("GET", "POST", "PUT", "OPTIONS", "DELETE")), allowedHeaders = Some(Set("content-type", "apikey")), allowCredentials = true, - maxAge = 1.day.toSeconds) + maxAge = 1.day.toSeconds + ) (if (debug) debugRoute :: routes else routes).map { case (endpoint, route) => // Apply middleware - val httpRoutes = CachingMiddleware(cache)(BadRequestHandler(CORS(AutoSlash(route), corsConfig))) + val httpRoutes = CachingMiddleware(cache)(BadRequestHandler(CORS(AutoSlash(route), corsConfig))) val redactHeadersWhen = (Headers.SensitiveHeaders + "apikey".ci).contains _ (endpoint, Logger.httpRoutes[IO](true, true, redactHeadersWhen, Some(logger.debug(_)))(httpRoutes)) } @@ -136,7 +139,7 @@ object Server { def createThreadPool[F[_]: Sync](pool: Config.ThreadPool): Resource[F, ExecutionContext] = pool match { - case Config.ThreadPool.Global => // Assuming we already have shutdown hook thanks to IOApp + case Config.ThreadPool.Global => // Assuming we already have shutdown hook thanks to IOApp Resource.pure[F, ExecutionContext](scala.concurrent.ExecutionContext.global) case Config.ThreadPool.Cached => val alloc = Sync[F].delay(Executors.newCachedThreadPool) @@ -148,29 +151,39 @@ object Server { Resource.make(alloc)(free).map(ExecutionContext.fromExecutor) } - def buildServer(config: Config)(implicit cs: ContextShift[IO], timer: Timer[IO]): Resource[IO, BlazeServerBuilder[IO]] = + def buildServer( + config: Config + )(implicit cs: ContextShift[IO], timer: Timer[IO]): Resource[IO, BlazeServerBuilder[IO]] = for { - _ <- Resource.eval(logger.info(s"Initializing server with following configuration: ${config.asJson.noSpaces}")) - httpPool <- createThreadPool[IO](config.repoServer.threadPool) - client <- BlazeClientBuilder[IO](httpPool).resource + _ <- Resource.eval(logger.info(s"Initializing server with following configuration: ${config.asJson.noSpaces}")) + httpPool <- createThreadPool[IO](config.repoServer.threadPool) + client <- BlazeClientBuilder[IO](httpPool).resource webhookClient = Webhook.WebhookClient(config.webhooks.getOrElse(Nil), client) - storage <- Storage.initialize[IO](config.database) - cache <- CachingMiddleware.initResponseCache[IO](1000, CacheTtl) - blocker <- Blocker[IO] - builder = BlazeServerBuilder[IO](httpPool) + storage <- Storage.initialize[IO](config.database) + cache <- CachingMiddleware.initResponseCache[IO](1000, CacheTtl) + blocker <- Blocker[IO] + builder = BlazeServerBuilder(httpPool) .bindHttp(config.repoServer.port, config.repoServer.interface) - .withHttpApp(httpApp(storage, config.debug.getOrElse(false), config.patchesAllowed.getOrElse(false), webhookClient, cache, blocker)) + .withHttpApp( + httpApp( + storage, + config.debug.getOrElse(false), + config.patchesAllowed.getOrElse(false), + webhookClient, + cache, + blocker + ) + ) builderWithIdle = config.repoServer.idleTimeout match { case Some(t) => builder.withIdleTimeout(t.seconds) case None => builder } } yield builderWithIdle - def run(config: Config)(implicit cs: ContextShift[IO], timer: Timer[IO]): Stream[IO, ExitCode] = Stream.resource(buildServer(config)).flatMap(_.serve) - def setup(config: Config, migrate: Option[MigrateFrom])(implicit cs: ContextShift[IO]): IO[ExitCode] = { + def setup(config: Config, migrate: Option[MigrateFrom])(implicit cs: ContextShift[IO]): IO[ExitCode] = config.database match { case pg @ Config.StorageConfig.Postgres(_, _, dbname, _, _, _, _, _, _) => val xa = getTransactor(pg) @@ -186,7 +199,6 @@ object Server { case Config.StorageConfig.Dummy => logger.error(s"Nothing to setup with dummy storage").as(ExitCode.Error) } - } def getTransactor(config: Config.StorageConfig.Postgres)(implicit cs: ContextShift[IO]): Transactor[IO] = { val url = s"jdbc:postgresql://${config.host}:${config.port}/${config.dbname}" diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/Utils.scala b/src/main/scala/com/snowplowanalytics/iglu/server/Utils.scala index bb4bda1..d7e0976 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/Utils.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/Utils.scala @@ -17,11 +17,10 @@ package com.snowplowanalytics.iglu.server import io.circe.Encoder import io.circe.syntax._ -import fs2.{ Stream, text } +import fs2.{Stream, text} import com.snowplowanalytics.iglu.core.{SchemaKey, SchemaMap, SchemaVer} - object Utils { def toSchemaMap(schemaKey: SchemaKey): SchemaMap = diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/Webhook.scala b/src/main/scala/com/snowplowanalytics/iglu/server/Webhook.scala index f0a4255..e23fc01 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/Webhook.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/Webhook.scala @@ -33,15 +33,18 @@ object Webhook { case class SchemaPublished(uri: Uri, vendorPrefixes: Option[List[String]]) extends Webhook case class WebhookClient[F[_]](webhooks: List[Webhook], httpClient: Client[F]) { - def schemaPublished(schemaKey: SchemaKey, updated: Boolean)(implicit F: BracketThrow[F]): F[List[Either[String, Unit]]] = + def schemaPublished(schemaKey: SchemaKey, updated: Boolean)( + implicit F: BracketThrow[F] + ): F[List[Either[String, Unit]]] = webhooks.traverse { - case SchemaPublished(uri, prefixes) if prefixes.isEmpty || prefixes.getOrElse(List()).exists(schemaKey.vendor.startsWith(_)) => + case SchemaPublished(uri, prefixes) + if prefixes.isEmpty || prefixes.getOrElse(List()).exists(schemaKey.vendor.startsWith(_)) => val event = SchemaPublishedEvent(schemaKey, updated) - val req = Request[F]().withUri(uri).withBodyStream(Utils.toBytes(event)) + val req = Request[F]().withUri(uri).withBodyStream(Utils.toBytes(event)) httpClient.run(req).use { res: Response[F] => res.status match { case Status(code) if code < 200 || code > 299 => F.pure(code.toString.asLeft[Unit]) - case _ => F.pure(().asRight[String]) + case _ => F.pure(().asRight[String]) } } case _ => F.pure(().asRight) @@ -52,13 +55,14 @@ object Webhook { implicit val schemaPublishedEventEncoder: Encoder[SchemaPublishedEvent] = Encoder.instance { event => - Json.fromFields(List( - "schemaKey" -> Json.fromString(event.schemaKey.toSchemaUri), - "updated" -> Json.fromBoolean(event.updated) - )) + Json.fromFields( + List( + "schemaKey" -> Json.fromString(event.schemaKey.toSchemaUri), + "updated" -> Json.fromBoolean(event.updated) + ) + ) } implicit val webhookEncoder: Encoder[Webhook] = deriveEncoder[Webhook] } - diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/codecs/JsonCodecs.scala b/src/main/scala/com/snowplowanalytics/iglu/server/codecs/JsonCodecs.scala index 062e290..55f6108 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/codecs/JsonCodecs.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/codecs/JsonCodecs.scala @@ -25,12 +25,11 @@ import io.circe.fs2.byteStreamParser import org.http4s.circe._ import org.http4s.{Entity, EntityEncoder, Headers} -import com.snowplowanalytics.iglu.core.{ SelfDescribingSchema, SchemaKey } +import com.snowplowanalytics.iglu.core.{SchemaKey, SelfDescribingSchema} import com.snowplowanalytics.iglu.core.circe.CirceIgluCodecs._ import com.snowplowanalytics.iglu.server.model.{IgluResponse, Schema, SchemaDraft} import com.snowplowanalytics.iglu.server.service.MetaService.ServerInfo - trait JsonCodecs { case class JsonArrayStream[F[_], A](nonArray: Stream[F, A]) @@ -39,7 +38,8 @@ trait JsonCodecs { val W = implicitly[EntityEncoder[F, A]] new EntityEncoder[F, JsonArrayStream[F, A]] { override def toEntity(stream: JsonArrayStream[F, A]): Entity[F] = { - val commaSeparated = stream.nonArray.flatMap(W.toEntity(_).body).through(byteStreamParser[F]).map(_.noSpaces).intersperse(",") + val commaSeparated = + stream.nonArray.flatMap(W.toEntity(_).body).through(byteStreamParser[F]).map(_.noSpaces).intersperse(",") val wrapped = Stream.emit("[") ++ commaSeparated ++ Stream.emit("]") Entity(wrapped.through(utf8Encode)) } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/codecs/Swagger.scala b/src/main/scala/com/snowplowanalytics/iglu/server/codecs/Swagger.scala index 4a52d60..9188926 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/codecs/Swagger.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/codecs/Swagger.scala @@ -30,17 +30,18 @@ import com.snowplowanalytics.iglu.server.model.Schema object Swagger { - def Formats = DefaultSwaggerFormats - .withSerializers(typeOf[IgluResponse], exampleModel) - .withSerializers(typeOf[Schema.Repr.Canonical], exampleModel) - .withSerializers(typeOf[JsonCodecs.JsonArrayStream[cats.effect.IO, Schema.Repr]], exampleModel) - .withSerializers(typeOf[Json], exampleModel) - .withSerializers(typeOf[SchemaKey], exampleModel) + def Formats = + DefaultSwaggerFormats + .withSerializers(typeOf[IgluResponse], exampleModel) + .withSerializers(typeOf[Schema.Repr.Canonical], exampleModel) + .withSerializers(typeOf[JsonCodecs.JsonArrayStream[cats.effect.IO, Schema.Repr]], exampleModel) + .withSerializers(typeOf[Json], exampleModel) + .withSerializers(typeOf[SchemaKey], exampleModel) val exampleModel: Set[Model] = Set( ModelImpl( id = "IgluResponse", - id2 = "IgluResponse", // Somehow, only id2 works + id2 = "IgluResponse", // Somehow, only id2 works `type` = "object".some, description = "Iglu Server generic response".some, properties = Map( @@ -50,21 +51,17 @@ object Swagger { enums = Set() ) ), - example = - """{"message" : "Schema does not exist"}""".some + example = """{"message" : "Schema does not exist"}""".some ), - ModelImpl( id = "JsonArrayStream", id2 = "JsonArrayStream«F,Repr»", name = "Array".some, `type` = "array".some, description = "Generic JSON array JSON Schema representations".some, - properties = Map( ), - example = - """[]""".some + properties = Map(), + example = """[]""".some ), - ModelImpl( id = "Canonical", id2 = "Canonical", @@ -76,28 +73,23 @@ object Swagger { properties = Map("name" -> StringProperty(enums = Set())) ) ), - example = - """{"self": {"name": "event", "vendor": "com.acme", "format": "jsonschema", "version": "1-0-0"}}""".some + example = """{"self": {"name": "event", "vendor": "com.acme", "format": "jsonschema", "version": "1-0-0"}}""".some ), - ModelImpl( id = "SchemaKey", id2 = "SchemaKey", `type` = "string".some, description = "Canonical iglu URI".some, properties = Map(), - example = - """iglu:com.snowplowanalytics/geo_location/jsonschema/1-0-0""".some + example = """iglu:com.snowplowanalytics/geo_location/jsonschema/1-0-0""".some ), - ModelImpl( id = "Json", id2 = "Json", `type` = "string".some, description = "Any valid JSON".some, properties = Map(), - example = - """{"foo": null}""".some + example = """{"foo": null}""".some ) ) } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/codecs/UriParsers.scala b/src/main/scala/com/snowplowanalytics/iglu/server/codecs/UriParsers.scala index 8bcd9cf..84f039b 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/codecs/UriParsers.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/codecs/UriParsers.scala @@ -22,10 +22,10 @@ import cats.syntax.either._ import eu.timepit.refined.types.numeric.NonNegInt -import org.http4s.{ Response, Status, Query } +import org.http4s.{Query, Response, Status} import org.http4s.rho.bits._ -import com.snowplowanalytics.iglu.core.{ SchemaVer, ParseError } +import com.snowplowanalytics.iglu.core.{ParseError, SchemaVer} import com.snowplowanalytics.iglu.server.model.{DraftVersion, Schema} trait UriParsers { @@ -38,7 +38,9 @@ trait UriParsers { def parseRepresentationCanonical[F[_]](query: Query)(implicit F: Monad[F]): ResultResponse[F, Schema.Repr.Format] = parseRepresentation(query, Schema.Repr.Format.Canonical) - private def parseRepresentation[F[_]](query: Query, default: Schema.Repr.Format)(implicit F: Monad[F]): ResultResponse[F, Schema.Repr.Format] = { + private def parseRepresentation[F[_]](query: Query, default: Schema.Repr.Format)( + implicit F: Monad[F] + ): ResultResponse[F, Schema.Repr.Format] = { val result = query.params.get("repr") match { case Some(s) => Schema.Repr.Format.parse(s).toRight(s"Cannot recognize schema representation in query: $s") @@ -48,7 +50,7 @@ trait UriParsers { case ("0", "1") => Schema.Repr.Format.Canonical.asRight case ("1", "0") => Schema.Repr.Format.Meta.asRight case ("0", "0") => default.asRight - case (m, b) => s"Inconsistent metadata/body query parameters: $m/$b".asLeft + case (m, b) => s"Inconsistent metadata/body query parameters: $m/$b".asLeft } } @@ -56,9 +58,7 @@ trait UriParsers { case Right(format) => SuccessResponse[F, Schema.Repr.Format](format) case Left(error) => - val response = Response[F]() - .withStatus(Status.BadRequest) - .withBodyStream(Utils.toBytes(error)) + val response = Response[F]().withStatus(Status.BadRequest).withBodyStream(Utils.toBytes(error)) FailureResponse.pure[F](Monad[F].pure(response)) } } @@ -69,7 +69,7 @@ trait UriParsers { override def parse(s: String)(implicit F: Monad[F]): ResultResponse[F, Schema.Format] = Schema.Format.parse(s) match { case Some(format) => SuccessResponse(format) - case None => FailureResponse.pure[F](BadRequest.pure(s"Unknown schema format: '$s'")) + case None => FailureResponse.pure[F](BadRequest.pure(s"Unknown schema format: '$s'")) } } @@ -80,8 +80,12 @@ trait UriParsers { override def parse(s: String)(implicit F: Monad[F]): ResultResponse[F, SchemaVer.Full] = SchemaVer.parseFull(s) match { case Right(v) => SuccessResponse(v) - case Left(ParseError.InvalidSchemaVer) if s.isEmpty => FailureResponse.pure[F](NotFound.pure(s"Version cannot be empty, should be model to get SchemaList or full SchemaVer")) - case Left(e) => FailureResponse.pure[F](BadRequest.pure(s"Cannot parse version part '$s' as SchemaVer, ${e.code}")) + case Left(ParseError.InvalidSchemaVer) if s.isEmpty => + FailureResponse.pure[F]( + NotFound.pure(s"Version cannot be empty, should be model to get SchemaList or full SchemaVer") + ) + case Left(e) => + FailureResponse.pure[F](BadRequest.pure(s"Cannot parse version part '$s' as SchemaVer, ${e.code}")) } } @@ -90,7 +94,10 @@ trait UriParsers { override val typeTag: Some[TypeTag[DraftVersion]] = Some(implicitly[TypeTag[DraftVersion]]) override def parse(s: String)(implicit F: Monad[F]): ResultResponse[F, DraftVersion] = { - val int = try { Right(s.toInt) } catch { case _: NumberFormatException => Left(s"$s is not an integer") } + val int = + try { + Right(s.toInt) + } catch { case _: NumberFormatException => Left(s"$s is not an integer") } int.flatMap(NonNegInt.from).fold(err => FailureResponse.pure[F](BadRequest.pure(err)), SuccessResponse.apply) } } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/middleware/BadRequestHandler.scala b/src/main/scala/com/snowplowanalytics/iglu/server/middleware/BadRequestHandler.scala index eac08a8..5d85b3b 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/middleware/BadRequestHandler.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/middleware/BadRequestHandler.scala @@ -23,7 +23,7 @@ import com.snowplowanalytics.iglu.server.model.IgluResponse import io.circe.parser.parse -import org.http4s.{HttpRoutes, Request, Response, MediaType} +import org.http4s.{HttpRoutes, MediaType, Request, Response} import org.http4s.headers.`Content-Type` /** Wrap any non-JSON message into `{"message": original}` payload */ @@ -32,14 +32,17 @@ object BadRequestHandler { Kleisli { req: Request[G] => def handle(res: Response[G]): G[Response[G]] = if (res.status.code >= 400 && res.status.code < 500) - res.bodyText + res + .bodyText .compile .foldMonoid .fproduct(parse) .map { case (b, e) => Utils.toBytes(e.fold(_ => IgluResponse.Message(b).asJson, identity)) } - .map(s => Response(res.status).withBodyStream(s).withContentType(`Content-Type`(MediaType.application.json))) + .map(s => + Response(res.status).withBodyStream(s).withContentType(`Content-Type`(MediaType.application.json)) + ) else Effect[G].pure(res) - OptionT[G, Response[G]](http(req).value.flatMap { o => o.traverse(handle) }) + OptionT[G, Response[G]](http(req).value.flatMap(o => o.traverse(handle))) } } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/middleware/CachingMiddleware.scala b/src/main/scala/com/snowplowanalytics/iglu/server/middleware/CachingMiddleware.scala index af7a200..d2afa5c 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/middleware/CachingMiddleware.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/middleware/CachingMiddleware.scala @@ -15,7 +15,7 @@ package com.snowplowanalytics.iglu.server.middleware import cats.Applicative -import cats.data.{ Kleisli, OptionT } +import cats.data.{Kleisli, OptionT} import cats.implicits._ import cats.effect.{Resource, Sync, Async => CatsAsync} @@ -27,14 +27,13 @@ import org.http4s.{HttpRoutes, Method, Request, Response} import org.http4s.syntax.string._ import scalacache.caffeine.CaffeineCache -import scalacache.{Entry, Mode, removeAll, Flags} +import scalacache.{Entry, Flags, Mode, removeAll} import scalacache.CatsEffect.modes._ import scala.concurrent.duration.Duration -object CachingMiddleware { - def apply[F[_]](cache: ResponseCache[F])(service: HttpRoutes[F]) - (implicit F: CatsAsync[F]): HttpRoutes[F] = { +object CachingMiddleware { + def apply[F[_]](cache: ResponseCache[F])(service: HttpRoutes[F])(implicit F: CatsAsync[F]): HttpRoutes[F] = { val logger = Slf4jLogger.getLogger[F] Kleisli { req => val result = keyer(req) match { @@ -55,15 +54,14 @@ object CachingMiddleware { } def initResponseCache[F[_]: CatsAsync](size: Long, duration: Duration): Resource[F, ResponseCache[F]] = - Resource.make( - Sync[F].delay { - val underlyingCaffeineCache = - Caffeine.newBuilder().maximumSize(size).build[String, Entry[Option[Response[F]]]] - val cache = CaffeineCache(underlyingCaffeineCache) - ResponseCache[F](duration, cache) - })(_.destroy) + Resource.make(Sync[F].delay { + val underlyingCaffeineCache = + Caffeine.newBuilder().maximumSize(size).build[String, Entry[Option[Response[F]]]] + val cache = CaffeineCache(underlyingCaffeineCache) + ResponseCache[F](duration, cache) + })(_.destroy) - case class ResponseCache[F[_]] private(duration: Duration, schemas: CaffeineCache[Option[Response[F]]]) { + case class ResponseCache[F[_]] private (duration: Duration, schemas: CaffeineCache[Option[Response[F]]]) { def removeSchemas(implicit F: CatsAsync[F]): F[Unit] = removeAll[Option[Response[F]]]()(schemas, implicitly[Mode[F]]).void @@ -85,14 +83,16 @@ object CachingMiddleware { } } yield response - def destroy(implicit F: CatsAsync[F]): F[Unit] = schemas.close()(implicitly[Mode[F]]).void /** Lower duration for `/schemas/` endpoint */ def getDuration(key: String) = { - val path = key.split(":").toList.headOption - .flatMap { s => Either.catchNonFatal(java.net.URI.create(s)).toOption } + val path = key + .split(":") + .toList + .headOption + .flatMap(s => Either.catchNonFatal(java.net.URI.create(s)).toOption) .map(_.getPath) .map(_.stripSuffix("/")) val isSchemas = path.contains("/api/schemas") @@ -100,11 +100,11 @@ object CachingMiddleware { } } - private sealed trait CacheAction + sealed private trait CacheAction private object CacheAction { case class Get(key: String) extends CacheAction - case object DoNothing extends CacheAction - case object Clean extends CacheAction + case object DoNothing extends CacheAction + case object Clean extends CacheAction } /** Make sure that cache works only for GET requests of SchemaService */ @@ -117,5 +117,5 @@ object CachingMiddleware { CacheAction.Clean case _ => CacheAction.DoNothing - } + } } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/middleware/PermissionMiddleware.scala b/src/main/scala/com/snowplowanalytics/iglu/server/middleware/PermissionMiddleware.scala index 829689a..d29e875 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/middleware/PermissionMiddleware.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/middleware/PermissionMiddleware.scala @@ -24,12 +24,12 @@ import cats.syntax.applicative._ import cats.syntax.either._ import cats.syntax.flatMap._ -import org.http4s.{ Request, HttpRoutes, Response, Status } +import org.http4s.{HttpRoutes, Request, Response, Status} import org.http4s.server.AuthMiddleware import org.http4s.util.CaseInsensitiveString import org.http4s.rho.AuthedContext -import com.snowplowanalytics.iglu.server.model.{ Permission, IgluResponse } +import com.snowplowanalytics.iglu.server.model.{IgluResponse, Permission} import com.snowplowanalytics.iglu.server.storage.Storage /** @@ -45,22 +45,29 @@ object PermissionMiddleware { /** Build an authentication middleware on top of storage */ def apply[F[_]: Sync](storage: Storage[F]): AuthMiddleware[F, Permission] = - AuthMiddleware.noSpider(Kleisli { request => auth[F](storage)(request) }, badRequestHandler) // TODO: SchemaServiceSpec.e6 + AuthMiddleware.noSpider( + Kleisli(request => auth[F](storage)(request)), + badRequestHandler + ) // TODO: SchemaServiceSpec.e6 /** Extract API key from HTTP request */ def getApiKey[F[_]](request: Request[F]): Option[Either[Throwable, UUID]] = - request.headers.get(CaseInsensitiveString(ApiKey)) - .map { header => header.value } - .map { apiKey => Either.catchOnly[IllegalArgumentException](UUID.fromString(apiKey)) } + request.headers.get(CaseInsensitiveString(ApiKey)).map(header => header.value).map { apiKey => + Either.catchOnly[IllegalArgumentException](UUID.fromString(apiKey)) + } - def wrapService[F[_]: Sync](db: Storage[F], ctx: AuthedContext[F, Permission], service: HttpRoutes[F]): HttpRoutes[F] = + def wrapService[F[_]: Sync]( + db: Storage[F], + ctx: AuthedContext[F, Permission], + service: HttpRoutes[F] + ): HttpRoutes[F] = PermissionMiddleware[F](db).apply(ctx.toService(service)) private val SchemaNotFoundBody = Utils.toBytes(IgluResponse.SchemaNotFound: IgluResponse) - private val PermissionsIssue = Utils.toBytes(IgluResponse.Message("Not enough permissions"): IgluResponse) + private val PermissionsIssue = Utils.toBytes(IgluResponse.Message("Not enough permissions"): IgluResponse) /** Authenticate request against storage */ - private def auth[F[_]: Sync](storage: Storage[F])(request: Request[F]): OptionT[F, Permission] = { + private def auth[F[_]: Sync](storage: Storage[F])(request: Request[F]): OptionT[F, Permission] = getApiKey(request) match { case None => OptionT.pure(Permission.Noop) @@ -69,20 +76,22 @@ object PermissionMiddleware { case Some(_) => OptionT.none } - } /** Handle invalid apikey as BadRequest, everything else as NotFound * (because we don't reveal presence of private resources) * Function is called only on Some(_) */ private def badRequestHandler[F[_]](implicit F: Applicative[F]): Request[F] => F[Response[F]] = - s => getApiKey(s) match { - case Some(Left(error)) => - val body = Utils.toBytes[F, IgluResponse](IgluResponse.Message(s"Error parsing apikey HTTP header. ${error.getMessage}")) - F.pure(Response[F](Status.BadRequest, body = body)) - case _ if s.uri.renderString.contains("keygen") => // Horrible way to check if we're not using SchemaService - F.pure(Response[F](Status.Forbidden, body = PermissionsIssue)) - case Some(Right(_)) => - F.pure(Response[F](Status.NotFound, body = SchemaNotFoundBody)) - } + s => + getApiKey(s) match { + case Some(Left(error)) => + val body = Utils.toBytes[F, IgluResponse]( + IgluResponse.Message(s"Error parsing apikey HTTP header. ${error.getMessage}") + ) + F.pure(Response[F](Status.BadRequest, body = body)) + case _ if s.uri.renderString.contains("keygen") => // Horrible way to check if we're not using SchemaService + F.pure(Response[F](Status.Forbidden, body = PermissionsIssue)) + case Some(Right(_)) => + F.pure(Response[F](Status.NotFound, body = SchemaNotFoundBody)) + } } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/migrations/Bootstrap.scala b/src/main/scala/com/snowplowanalytics/iglu/server/migrations/Bootstrap.scala index 412ee83..3336e4e 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/migrations/Bootstrap.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/migrations/Bootstrap.scala @@ -22,7 +22,6 @@ import doobie.implicits._ import com.snowplowanalytics.iglu.server.storage.Postgres - object Bootstrap { val keyActionCreate = sql"""CREATE TYPE key_action AS ENUM ('CREATE', 'DELETE')""" @@ -30,8 +29,7 @@ object Bootstrap { val schemaActionCreate = sql"""CREATE TYPE schema_action AS ENUM ('READ', 'BUMP', 'CREATE', 'CREATE_VENDOR')""" - val permissionsCreate = ( - fr"CREATE TABLE" ++ Postgres.PermissionsTable ++ fr"""( + val permissionsCreate = (fr"CREATE TABLE" ++ Postgres.PermissionsTable ++ fr"""( apikey UUID NOT NULL, vendor VARCHAR(128), wildcard BOOL NOT NULL, @@ -40,8 +38,7 @@ object Bootstrap { PRIMARY KEY (apikey) );""") - val schemasCreate = ( - fr"CREATE TABLE" ++ Postgres.SchemasTable ++ fr"""( + val schemasCreate = (fr"CREATE TABLE" ++ Postgres.SchemasTable ++ fr"""( vendor VARCHAR(128) NOT NULL, name VARCHAR(128) NOT NULL, format VARCHAR(128) NOT NULL, @@ -56,8 +53,7 @@ object Bootstrap { body JSON NOT NULL )""") - val draftsCreate = ( - fr"CREATE TABLE" ++ Postgres.DraftsTable ++ fr"""( + val draftsCreate = (fr"CREATE TABLE" ++ Postgres.DraftsTable ++ fr"""( vendor VARCHAR(128) NOT NULL, name VARCHAR(128) NOT NULL, format VARCHAR(128) NOT NULL, @@ -73,10 +69,9 @@ object Bootstrap { val allStatements = List(keyActionCreate, schemaActionCreate, permissionsCreate, schemasCreate, draftsCreate) - def initialize[F[_]](xa: Transactor[F])(implicit F: Bracket[F, Throwable]) = { + def initialize[F[_]](xa: Transactor[F])(implicit F: Bracket[F, Throwable]) = allStatements .traverse[ConnectionIO, Int](sql => sql.updateWithLogHandler(LogHandler.jdkLogHandler).run) .map(_.combineAll) .transact(xa) - } } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/migrations/Fifth.scala b/src/main/scala/com/snowplowanalytics/iglu/server/migrations/Fifth.scala index 980c1b7..a502abc 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/migrations/Fifth.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/migrations/Fifth.scala @@ -20,7 +20,7 @@ import java.util.UUID import fs2.Stream -import cats.{ Applicative, MonadError } +import cats.{Applicative, MonadError} import cats.implicits._ import io.circe.Json @@ -34,30 +34,30 @@ import doobie.postgres.circe.json.implicits._ import eu.timepit.refined.types.numeric.NonNegInt -import com.snowplowanalytics.iglu.core.{ ParseError, SchemaKey, SchemaMap, SchemaVer, SelfDescribingSchema } +import com.snowplowanalytics.iglu.core.{ParseError, SchemaKey, SchemaMap, SchemaVer, SelfDescribingSchema} import com.snowplowanalytics.iglu.core.circe.implicits._ import com.snowplowanalytics.iglu.server.model.VersionCursor import com.snowplowanalytics.iglu.server.model.VersionCursor.Inconsistency -import model.{ Permission, Schema, SchemaDraft } +import model.{Permission, Schema, SchemaDraft} import storage.Postgres import storage.Storage.IncompatibleStorage - /** Steps required to migrate the DB from pre-0.6.0 structure to current one */ object Fifth { - val OldSchemasTable = Fragment.const("schemas") + val OldSchemasTable = Fragment.const("schemas") val OldPermissionsTable = Fragment.const("apikeys") case class SchemaFifth(map: SchemaMap, schema: Json, isPublic: Boolean, createdAt: Instant, updatedAt: Instant) def perform: ConnectionIO[Unit] = for { - _ <- checkContent(querySchemas) + _ <- checkContent(querySchemas) schemas <- checkConsistency(querySchemas) - _ <- checkContent(queryDrafts) - _ <- Bootstrap.allStatements + _ <- checkContent(queryDrafts) + _ <- Bootstrap + .allStatements .traverse[ConnectionIO, Int](_.updateWithLogHandler(LogHandler.jdkLogHandler).run) .map(_.combineAll) _ <- (migrateKeys ++ migrateSchemas(schemas) ++ migrateDrafts).compile.drain @@ -66,7 +66,7 @@ object Fifth { /** Perform query and check if entities are valid against current model, throw an exception otherwise */ def checkContent[A](query: Query0[Either[String, A]]): ConnectionIO[Unit] = { val errors = query.stream.flatMap { - case Right(_) => Stream.empty + case Right(_) => Stream.empty case Left(error) => Stream.emit(error) } errors.compile.toList.flatMap { @@ -78,46 +78,45 @@ object Fifth { } def checkConsistency(query: Query0[Either[String, SchemaFifth]]): ConnectionIO[List[SchemaFifth]] = - query.stream.compile - .toList - .map { schemas => schemas.parTraverse(_.toEitherNel) } - .flatMap { result => - result - .leftMap(errors => s"Inconsistent entries found: ${errors.toList.mkString(", ")}") - .flatTap(checkAllowance) match { - case Right(schemas) => - Applicative[ConnectionIO].pure(schemas) - case Left(errors) => - val exception = IncompatibleStorage(s"Inconsistent entities found: ${errors.toList.mkString(", ")}") - MonadError[ConnectionIO, Throwable].raiseError(exception) - } + query.stream.compile.toList.map(schemas => schemas.parTraverse(_.toEitherNel)).flatMap { result => + result + .leftMap(errors => s"Inconsistent entries found: ${errors.toList.mkString(", ")}") + .flatTap(checkAllowance) match { + case Right(schemas) => + Applicative[ConnectionIO].pure(schemas) + case Left(errors) => + val exception = IncompatibleStorage(s"Inconsistent entities found: ${errors.toList.mkString(", ")}") + MonadError[ConnectionIO, Throwable].raiseError(exception) } + } /** Traverse all schemas to find if any of them cannot be added */ def checkAllowance(schemas: List[SchemaFifth]) = { - val result = schemas - .foldLeft(List.empty[Either[String, SchemaFifth]]) { (accumulator, current) => - val SchemaFifth(map, schema, isPublic, createdAt, updatedAt) = current - val successful = accumulator.map(_.toOption).unite - isSchemaAllowed(successful, map, isPublic) match { - case Right(_) => accumulator :+ Right(SchemaFifth(map, schema, isPublic, createdAt, updatedAt)) - case Left(error) => accumulator :+ Left(s"${map.schemaKey.toPath}: ${error.show}") - } + val result = schemas.foldLeft(List.empty[Either[String, SchemaFifth]]) { (accumulator, current) => + val SchemaFifth(map, schema, isPublic, createdAt, updatedAt) = current + val successful = accumulator.map(_.toOption).unite + isSchemaAllowed(successful, map, isPublic) match { + case Right(_) => accumulator :+ Right(SchemaFifth(map, schema, isPublic, createdAt, updatedAt)) + case Left(error) => accumulator :+ Left(s"${map.schemaKey.toPath}: ${error.show}") } + } - result - .parTraverse(_.toEitherNel) - .void - .leftMap { errors => - s"Schemas not allowed: ${errors.toList.mkString(", ")}\n" + - "Make sure that older schemas exist and can be properly ordered by createdat" - } + result.parTraverse(_.toEitherNel).void.leftMap { errors => + s"Schemas not allowed: ${errors.toList.mkString(", ")}\n" + + "Make sure that older schemas exist and can be properly ordered by createdat" + } } - def isSchemaAllowed(previous: List[SchemaFifth], current: SchemaMap, isPublic: Boolean): Either[Inconsistency, Unit] = { - val schemas = previous.filter(x => x.map.schemaKey.vendor == current.schemaKey.vendor && x.map.schemaKey.name == current.schemaKey.name) + def isSchemaAllowed( + previous: List[SchemaFifth], + current: SchemaMap, + isPublic: Boolean + ): Either[Inconsistency, Unit] = { + val schemas = previous.filter(x => + x.map.schemaKey.vendor == current.schemaKey.vendor && x.map.schemaKey.name == current.schemaKey.name + ) val previousPublic = schemas.forall(_.isPublic) - val versions = schemas.map(_.map.schemaKey.version) + val versions = schemas.map(_.map.schemaKey.version) if ((previousPublic && isPublic) || (!previousPublic && !isPublic) || schemas.isEmpty) VersionCursor.isAllowed(current.schemaKey.version, versions, patchesAllowed = false) else @@ -126,26 +125,29 @@ object Fifth { def querySchemas = (fr"SELECT vendor, name, format, version, schema, createdat, updatedat, ispublic FROM" ++ OldSchemasTable ++ fr"WHERE draftnumber = '0' ORDER BY createdat") - .queryWithLogHandler[(String, String, String, String, String, Instant, Instant, Boolean)](LogHandler.jdkLogHandler) - .map { case (vendor, name, format, version, body, createdAt, updatedAt, isPublic) => - val schemaMap = for { - ver <- SchemaVer.parse(version) - key <- SchemaKey.fromUri(s"iglu:$vendor/$name/$format/${ver.asString}") - } yield SchemaMap(key) - for { - jsonBody <- parse(body).leftMap(_.show) - map <- schemaMap.leftMap(_.code) - schema <- SelfDescribingSchema.parse(jsonBody) match { - case Left(ParseError.InvalidSchema) => - jsonBody.asRight // Non self-describing JSON schema - case Left(e) => - s"Invalid self-describing payload for [${map.schemaKey.toSchemaUri}], ${e.code}".asLeft - case Right(schema) if schema.self == map => - schema.schema.asRight - case Right(schema) => - s"Self-describing payload [${schema.self.schemaKey.toSchemaUri}] does not match its DB reference [${map.schemaKey.toSchemaUri}]".asLeft - } - } yield SchemaFifth(map, schema, isPublic, createdAt, updatedAt) + .queryWithLogHandler[(String, String, String, String, String, Instant, Instant, Boolean)]( + LogHandler.jdkLogHandler + ) + .map { + case (vendor, name, format, version, body, createdAt, updatedAt, isPublic) => + val schemaMap = for { + ver <- SchemaVer.parse(version) + key <- SchemaKey.fromUri(s"iglu:$vendor/$name/$format/${ver.asString}") + } yield SchemaMap(key) + for { + jsonBody <- parse(body).leftMap(_.show) + map <- schemaMap.leftMap(_.code) + schema <- SelfDescribingSchema.parse(jsonBody) match { + case Left(ParseError.InvalidSchema) => + jsonBody.asRight // Non self-describing JSON schema + case Left(e) => + s"Invalid self-describing payload for [${map.schemaKey.toSchemaUri}], ${e.code}".asLeft + case Right(schema) if schema.self == map => + schema.schema.asRight + case Right(schema) => + s"Self-describing payload [${schema.self.schemaKey.toSchemaUri}] does not match its DB reference [${map.schemaKey.toSchemaUri}]".asLeft + } + } yield SchemaFifth(map, schema, isPublic, createdAt, updatedAt) } def migrateSchemas(schemas: List[SchemaFifth]) = @@ -156,21 +158,24 @@ object Fifth { def queryDrafts = (fr"SELECT vendor, name, format, draftnumber, schema, createdat, updatedat, ispublic FROM" ++ OldSchemasTable ++ fr"WHERE draftnumber != '0'") - .queryWithLogHandler[(String, String, String, String, String, Instant, Instant, Boolean)](LogHandler.jdkLogHandler) - .map { case (vendor, name, format, draftId, body, createdAt, updatedAt, isPublic) => - for { - verInt <- Either.catchOnly[NumberFormatException](draftId.toInt).leftMap(_.getMessage) - number <- NonNegInt.from(verInt) - jsonBody <- parse(body).leftMap(_.show) - draftId = SchemaDraft.DraftId(vendor, name, format, number) - meta = Schema.Metadata(createdAt, updatedAt, isPublic) - } yield SchemaDraft(draftId, meta, jsonBody) + .queryWithLogHandler[(String, String, String, String, String, Instant, Instant, Boolean)]( + LogHandler.jdkLogHandler + ) + .map { + case (vendor, name, format, draftId, body, createdAt, updatedAt, isPublic) => + for { + verInt <- Either.catchOnly[NumberFormatException](draftId.toInt).leftMap(_.getMessage) + number <- NonNegInt.from(verInt) + jsonBody <- parse(body).leftMap(_.show) + draftId = SchemaDraft.DraftId(vendor, name, format, number) + meta = Schema.Metadata(createdAt, updatedAt, isPublic) + } yield SchemaDraft(draftId, meta, jsonBody) } def migrateDrafts = for { row <- queryDrafts.stream - _ <- row match { + _ <- row match { case Right(draft) => Stream.eval_(addDraft(draft).run).void case Left(error) => @@ -179,24 +184,21 @@ object Fifth { } yield () def migrateKeys = { - val query = (fr"SELECT uid, vendor_prefix, permission FROM" ++ OldPermissionsTable) - .query[(UUID, String, String)] - .map { case (id, prefix, perm) => - val vendor = Permission.Vendor.parse(prefix) - val (schemaAction, keyAction) = perm match { - case "super" => (Permission.Master.schema, Permission.Master.key) - case "read" => (Some(Permission.SchemaAction.Read), Set.empty[Permission.KeyAction]) - case "write" => (Some(Permission.SchemaAction.CreateVendor), Set.empty[Permission.KeyAction]) - case _ => (Some(Permission.SchemaAction.Read), Set.empty[Permission.KeyAction]) // Should not happen - } - - (id, Permission(vendor, schemaAction, keyAction)) + val query = + (fr"SELECT uid, vendor_prefix, permission FROM" ++ OldPermissionsTable).query[(UUID, String, String)].map { + case (id, prefix, perm) => + val vendor = Permission.Vendor.parse(prefix) + val (schemaAction, keyAction) = perm match { + case "super" => (Permission.Master.schema, Permission.Master.key) + case "read" => (Some(Permission.SchemaAction.Read), Set.empty[Permission.KeyAction]) + case "write" => (Some(Permission.SchemaAction.CreateVendor), Set.empty[Permission.KeyAction]) + case _ => (Some(Permission.SchemaAction.Read), Set.empty[Permission.KeyAction]) // Should not happen + } + + (id, Permission(vendor, schemaAction, keyAction)) } - query - .stream - .evalMap { case (id, permission) => Postgres.Sql.addPermission(id, permission).run } - .void + query.stream.evalMap { case (id, permission) => Postgres.Sql.addPermission(id, permission).run }.void } def addSchema(schemaMap: SchemaMap, schema: Json, isPublic: Boolean, createdAt: Instant, updatedAt: Instant) = { @@ -211,6 +213,5 @@ object Fifth { (fr"INSERT INTO" ++ Postgres.DraftsTable ++ fr"(vendor, name, format, version, created_at, updated_at, is_public, body)" ++ fr"""VALUES (${draft.schemaMap.vendor}, ${draft.schemaMap.name}, ${draft.schemaMap.format}, ${draft.schemaMap.version.value}, ${draft.metadata.createdAt}, ${draft.metadata.updatedAt}, - ${draft.metadata.isPublic}, ${draft.body})""") - .updateWithLogHandler(LogHandler.jdkLogHandler) + ${draft.metadata.isPublic}, ${draft.body})""").updateWithLogHandler(LogHandler.jdkLogHandler) } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/migrations/MigrateFrom.scala b/src/main/scala/com/snowplowanalytics/iglu/server/migrations/MigrateFrom.scala index 5bef68b..8e1fa7f 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/migrations/MigrateFrom.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/migrations/MigrateFrom.scala @@ -28,6 +28,6 @@ object MigrateFrom { def parse(s: String): Option[MigrateFrom] = s match { case "0.5.0" => Some(`0.5.0`) - case _ => None + case _ => None } -} \ No newline at end of file +} diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/model/IgluResponse.scala b/src/main/scala/com/snowplowanalytics/iglu/server/model/IgluResponse.scala index 9441e14..f9ff1d4 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/model/IgluResponse.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/model/IgluResponse.scala @@ -34,19 +34,20 @@ trait IgluResponse extends Product with Serializable { object IgluResponse { - val NotFoundSchema = "The schema is not found" - val NotAuthorized = "Authentication error: not authorized" - val Mismatch = "Mismatch: the schema metadata does not match the payload and URI" - val DecodeError = "Cannot decode JSON schema" + val NotFoundSchema = "The schema is not found" + val NotAuthorized = "Authentication error: not authorized" + val Mismatch = "Mismatch: the schema metadata does not match the payload and URI" + val DecodeError = "Cannot decode JSON schema" val SchemaInvalidationMessage = "The schema does not conform to a JSON Schema v4 specification" - val DataInvalidationMessage = "The data for a field instance is invalid against its schema" - val NotFoundEndpoint = "The endpoint does not exist" + val DataInvalidationMessage = "The data for a field instance is invalid against its schema" + val NotFoundEndpoint = "The endpoint does not exist" - case object SchemaNotFound extends IgluResponse - case object EndpointNotFound extends IgluResponse - case object InvalidSchema extends IgluResponse + case object SchemaNotFound extends IgluResponse + case object EndpointNotFound extends IgluResponse + case object InvalidSchema extends IgluResponse case class SchemaMismatch(uriSchemaKey: SchemaKey, payloadSchemaKey: SchemaKey) extends IgluResponse - case class SchemaUploaded(updated: Boolean, location: SchemaKey) extends IgluResponse + case class SchemaUploaded(updated: Boolean, location: SchemaKey) extends IgluResponse + /** Generic human-readable message, used everywhere as a fallback */ case class Message(message: String) extends IgluResponse @@ -54,7 +55,7 @@ object IgluResponse { case object Forbidden extends IgluResponse - case class SchemaValidationReport(report: NonEmptyList[LinterMessage]) extends IgluResponse + case class SchemaValidationReport(report: NonEmptyList[LinterMessage]) extends IgluResponse case class InstanceValidationReport(report: NonEmptyList[ValidatorReport]) extends IgluResponse implicit val responsesEncoder: Encoder[IgluResponse] = @@ -68,54 +69,67 @@ object IgluResponse { case Forbidden => Json.fromFields(List("message" -> Json.fromString(NotAuthorized))) case ApiKeys(read, write) => - Json.fromFields(List( - "read" -> Json.fromString(read.toString), - "write" -> Json.fromString(write.toString) - )) + Json.fromFields( + List( + "read" -> Json.fromString(read.toString), + "write" -> Json.fromString(write.toString) + ) + ) case SchemaMismatch(uri, payload) => - Json.fromFields(List( - "uriSchemaKey" -> Json.fromString(uri.toSchemaUri), - "payloadSchemaKey" -> Json.fromString(payload.toSchemaUri), - "message" -> Json.fromString(Mismatch) - )) + Json.fromFields( + List( + "uriSchemaKey" -> Json.fromString(uri.toSchemaUri), + "payloadSchemaKey" -> Json.fromString(payload.toSchemaUri), + "message" -> Json.fromString(Mismatch) + ) + ) case SchemaUploaded(updated, location) => - Json.fromFields(List( - "message" -> Json.fromString(if (updated) "Schema updated" else "Schema created"), - "updated" -> Json.fromBoolean(updated), - "location" -> location.toSchemaUri.asJson, - "status" -> Json.fromInt(if (updated) 200 else 201) // TODO: remove after igluctl 0.7.0 released - )) + Json.fromFields( + List( + "message" -> Json.fromString(if (updated) "Schema updated" else "Schema created"), + "updated" -> Json.fromBoolean(updated), + "location" -> location.toSchemaUri.asJson, + "status" -> Json.fromInt(if (updated) 200 else 201) // TODO: remove after igluctl 0.7.0 released + ) + ) case InvalidSchema => Json.fromFields(List("message" -> Json.fromString(DecodeError))) case SchemaValidationReport(report) => - Json.fromFields(List( - "message" -> SchemaInvalidationMessage.asJson, - "report" -> Json.fromValues(report.toList.map { message => - Json.fromFields(List( - "message" -> message.message.asJson, - "level" -> message.level.toString.toUpperCase.asJson, - "pointer" -> message.jsonPointer.show.asJson - )) - }) - )) + Json.fromFields( + List( + "message" -> SchemaInvalidationMessage.asJson, + "report" -> Json.fromValues(report.toList.map { message => + Json.fromFields( + List( + "message" -> message.message.asJson, + "level" -> message.level.toString.toUpperCase.asJson, + "pointer" -> message.jsonPointer.show.asJson + ) + ) + }) + ) + ) case InstanceValidationReport(report) => - Json.fromFields(List( - "message" -> DataInvalidationMessage.asJson, - "report" -> report.asJson - )) + Json.fromFields( + List( + "message" -> DataInvalidationMessage.asJson, + "report" -> report.asJson + ) + ) } implicit val responsesDecoder: Decoder[IgluResponse] = Decoder.instance { cur => cur.downField("message").as[String] match { case Right(NotFoundSchema) => SchemaNotFound.asRight - case Right(NotAuthorized) => Forbidden.asRight - case Right(Mismatch) => for { - uriSchemaKey <- cur.downField("uriSchemaKey").as[SchemaKey] - payloadSchemaKey <- cur.downField("payloadSchemaKey").as[SchemaKey] - } yield SchemaMismatch(uriSchemaKey, payloadSchemaKey) + case Right(NotAuthorized) => Forbidden.asRight + case Right(Mismatch) => + for { + uriSchemaKey <- cur.downField("uriSchemaKey").as[SchemaKey] + payloadSchemaKey <- cur.downField("payloadSchemaKey").as[SchemaKey] + } yield SchemaMismatch(uriSchemaKey, payloadSchemaKey) case Right(DecodeError) => Message(DecodeError).asRight - case Right(message) => Message(message).asRight - case Left(e) => Message(e.show).asRight + case Right(message) => Message(message).asRight + case Left(e) => Message(e.show).asRight } } } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/model/Permission.scala b/src/main/scala/com/snowplowanalytics/iglu/server/model/Permission.scala index 79bcc0f..8ea2c83 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/model/Permission.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/model/Permission.scala @@ -33,9 +33,12 @@ import storage.Storage.IncompatibleStorage // Key A with x.y.z vendor CANNOT create key B with x.y vendor // Key A with any non-empty KeyAction set always has read key-permissions -case class Permission(vendor: Permission.Vendor, - schema: Option[Permission.SchemaAction], - key: Set[Permission.KeyAction]) { +case class Permission( + vendor: Permission.Vendor, + schema: Option[Permission.SchemaAction], + key: Set[Permission.KeyAction] +) { + /** Check if user has enough rights to read particular schema */ def canRead(schemaVendor: String): Boolean = this match { @@ -83,6 +86,7 @@ object Permission { * or just specified in `parts` */ case class Vendor(parts: List[String], wildcard: Boolean) { + /** Check if this `vendor` from permission is allowed to work with `requestedVendor` */ def check(requestedVendor: String): Boolean = { val requestedParts = requestedVendor.split("\\.").toList @@ -97,11 +101,12 @@ object Permission { def show: String = parts match { case Nil => "'wildcard vendor'" - case _ => parts.mkString(".") + case _ => parts.mkString(".") } } object Vendor { + /** Can be applied to any vendor */ val wildcard = Vendor(Nil, true) @@ -110,9 +115,9 @@ object Permission { def parse(string: String): Vendor = string match { - case "*" => wildcard + case "*" => wildcard case _ if string.trim.isEmpty => wildcard - case vendor => Vendor(vendor.split('.').toList, true) + case vendor => Vendor(vendor.split('.').toList, true) } implicit val vendorCirceEncoder: Encoder[Vendor] = @@ -122,16 +127,20 @@ object Permission { sealed trait SchemaAction extends Product with Serializable { def show: String = this match { case Permission.SchemaAction.CreateVendor => "CREATE_VENDOR" - case other => other.toString.toUpperCase + case other => other.toString.toUpperCase } } object SchemaAction { + /** Only get/view schemas */ case object Read extends SchemaAction + /** Bump schema versions within existing schema and read */ case object Bump extends SchemaAction + /** Create new schemas/names (but within attached vendor permission) */ case object Create extends SchemaAction + /** Do everything, including creating new "subvendor" (applied only for `Vendor` with `wildcard`) */ case object CreateVendor extends SchemaAction @@ -145,8 +154,7 @@ object Permission { All.find(_.show === string).toRight(s"String $string is not valid SchemaAction") implicit val doobieSchemaActionGet: Meta[SchemaAction] = - Meta[String] - .timap(x => SchemaAction.parse(x).fold(e => throw IncompatibleStorage(e), identity))(_.show) + Meta[String].timap(x => SchemaAction.parse(x).fold(e => throw IncompatibleStorage(e), identity))(_.show) implicit val schemaActionCirceEncoder: Encoder[SchemaAction] = Encoder.instance(_.show.asJson) @@ -165,8 +173,7 @@ object Permission { All.find(_.show === string).toRight(s"String $string is not valid KeyAction") implicit val doobieKeyActionGet: Meta[KeyAction] = - Meta[String] - .timap(x => parse(x).fold(e => throw IncompatibleStorage(e), identity))(_.show) + Meta[String].timap(x => parse(x).fold(e => throw IncompatibleStorage(e), identity))(_.show) implicit val keyActionCirceEncoder: Encoder[KeyAction] = Encoder.instance(_.show.asJson) @@ -187,8 +194,8 @@ object Permission { implicit val doobiePermissionRead: Read[Permission] = Read[(Option[String], Boolean, Option[SchemaAction], List[String])].map { case (ven, wildcard, schemaAction, keyAction) => - val vendor = ven.map(Vendor.parse).getOrElse(Vendor(Nil, wildcard)) + val vendor = ven.map(Vendor.parse).getOrElse(Vendor(Nil, wildcard)) val keyActions = keyAction.traverse(KeyAction.parse).fold(e => throw IncompatibleStorage(e), identity) Permission(vendor, schemaAction, keyActions.toSet) } -} \ No newline at end of file +} diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/model/Schema.scala b/src/main/scala/com/snowplowanalytics/iglu/server/model/Schema.scala index 53fe312..1757478 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/model/Schema.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/model/Schema.scala @@ -67,28 +67,31 @@ object Schema { /** Encoding of a schema */ sealed trait Repr object Repr { + /** Canonical self-describing representation */ case class Canonical(schema: SelfDescribingSchema[Json]) extends Repr + /** Non-vanilla representation for UIs/non-validation clients */ case class Full(schema: Schema) extends Repr + /** Just URI string (but schema is on the server) */ case class Uri(schemaKey: SchemaKey) extends Repr - def apply(schema: Schema): Repr = Full(schema) - def apply(uri: SchemaMap): Repr = Uri(uri.schemaKey) + def apply(schema: Schema): Repr = Full(schema) + def apply(uri: SchemaMap): Repr = Uri(uri.schemaKey) def apply(schema: SelfDescribingSchema[Json]): Repr = Canonical(schema) sealed trait Format extends Product with Serializable object Format { - case object Uri extends Format - case object Meta extends Format + case object Uri extends Format + case object Meta extends Format case object Canonical extends Format def parse(s: String): Option[Format] = s.toLowerCase match { - case "uri" => Some(Uri) - case "meta" => Some(Meta) + case "uri" => Some(Uri) + case "meta" => Some(Meta) case "canonical" => Some(Canonical) - case _ => None + case _ => None } } @@ -97,13 +100,13 @@ object Schema { sealed trait SchemaBody extends Product with Serializable object SchemaBody { case class SelfDescribing(schema: SelfDescribingSchema[Json]) extends SchemaBody - case class BodyOnly(schema: Json) extends SchemaBody + case class BodyOnly(schema: Json) extends SchemaBody implicit val schemaBodyCirceDecoder: Decoder[SchemaBody] = Decoder.instance { json => SelfDescribingSchema.parse(json.value) match { case Right(schema) => SelfDescribing(schema).asRight - case Left(_) => json.as[JsonObject].map(obj => BodyOnly(Json.fromJsonObject(obj))) + case Left(_) => json.as[JsonObject].map(obj => BodyOnly(Json.fromJsonObject(obj))) } } } @@ -114,16 +117,15 @@ object Schema { def parse(s: String): Option[Format] = s match { case "jsonschema" => Some(Jsonschema) - case _ => None + case _ => None } } - private def moveToFront[K, V](key: K, fields: List[(K, V)]) = { + private def moveToFront[K, V](key: K, fields: List[(K, V)]) = fields.span(_._1 != key) match { case (previous, matches :: next) => matches :: previous ++ next case _ => fields } - } private def orderedSchema(schema: Json): Json = schema.asObject match { @@ -134,27 +136,30 @@ object Schema { implicit val schemaEncoder: Encoder[Schema] = Encoder.instance { schema => - Json.obj( - "self" -> schema.schemaMap.asJson, - "metadata" -> schema.metadata.asJson(Metadata.metadataEncoder) - ).deepMerge(schema.body) + Json + .obj( + "self" -> schema.schemaMap.asJson, + "metadata" -> schema.metadata.asJson(Metadata.metadataEncoder) + ) + .deepMerge(schema.body) } implicit val representationEncoder: Encoder[Repr] = Encoder.instance { case Repr.Full(s) => orderedSchema(schemaEncoder.apply(s)) - case Repr.Uri(u) => Encoder[String].apply(u.toSchemaUri) - case Repr.Canonical(s) => orderedSchema(s.normalize.asObject match { - case Some(obj) => Json.fromJsonObject(("$schema", CanonicalUri.asJson) +: obj) - case None => s.normalize - }) + case Repr.Uri(u) => Encoder[String].apply(u.toSchemaUri) + case Repr.Canonical(s) => + orderedSchema(s.normalize.asObject match { + case Some(obj) => Json.fromJsonObject(("$schema", CanonicalUri.asJson) +: obj) + case None => s.normalize + }) } implicit val serverSchemaDecoder: Decoder[Schema] = Decoder.instance { cursor => for { - self <- cursor.value.as[SchemaMap] - meta <- cursor.downField("metadata").as[Metadata] + self <- cursor.value.as[SchemaMap] + meta <- cursor.downField("metadata").as[Metadata] bodyJson <- cursor.as[JsonObject] body = bodyJson.toList.filterNot { case (key, _) => key == "self" || key == "metadata" } } yield Schema(self, meta, Json.fromFields(body)) diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/model/SchemaDraft.scala b/src/main/scala/com/snowplowanalytics/iglu/server/model/SchemaDraft.scala index 447a747..6fa2148 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/model/SchemaDraft.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/model/SchemaDraft.scala @@ -19,7 +19,7 @@ import doobie._ import doobie.postgres.circe.json.implicits._ import doobie.postgres.implicits._ -import io.circe.{ Json, Encoder } +import io.circe.{Encoder, Json} import io.circe.generic.semiauto.deriveEncoder import io.circe.refined._ diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/model/VersionCursor.scala b/src/main/scala/com/snowplowanalytics/iglu/server/model/VersionCursor.scala index 9f1788d..d2048bf 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/model/VersionCursor.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/model/VersionCursor.scala @@ -23,19 +23,23 @@ import com.snowplowanalytics.iglu.core.SchemaVer sealed trait VersionCursor extends Product with Serializable object VersionCursor { + /** The very first schema of whole group */ case object Initial extends VersionCursor + /** First schema of a particular model */ - case class StartModel private(model: Int) extends VersionCursor + case class StartModel private (model: Int) extends VersionCursor + /** First schema (addition) of a particular revision of particular model */ - case class StartRevision private(model: Int, revision: Int) extends VersionCursor + case class StartRevision private (model: Int, revision: Int) extends VersionCursor + /** Non-initial schema */ - case class NonInitial private(schemaVer: SchemaVer.Full) extends VersionCursor + case class NonInitial private (schemaVer: SchemaVer.Full) extends VersionCursor sealed trait Inconsistency extends Product with Serializable object Inconsistency { - case object PreviousMissing extends Inconsistency - case object AlreadyExists extends Inconsistency + case object PreviousMissing extends Inconsistency + case object AlreadyExists extends Inconsistency case class Availability(isPublic: Boolean, previousPublic: Boolean) extends Inconsistency implicit val inconsistencyShowInstance: Show[Inconsistency] = @@ -49,15 +53,20 @@ object VersionCursor { } } - def isAllowed(version: SchemaVer.Full, existing: List[SchemaVer.Full], patchesAllowed: Boolean): Either[Inconsistency, Unit] = + def isAllowed( + version: SchemaVer.Full, + existing: List[SchemaVer.Full], + patchesAllowed: Boolean + ): Either[Inconsistency, Unit] = if (existing.contains(version) && !patchesAllowed) Inconsistency.AlreadyExists.asLeft - else if (previousExists(existing, get(version))) ().asRight else Inconsistency.PreviousMissing.asLeft + else if (previousExists(existing, get(version))) ().asRight + else Inconsistency.PreviousMissing.asLeft def get(version: SchemaVer.Full): VersionCursor = version match { case SchemaVer.Full(1, 0, 0) => Initial case SchemaVer.Full(m, 0, 0) => StartModel(m) case SchemaVer.Full(m, r, 0) => StartRevision(m, r) - case next => NonInitial(next) + case next => NonInitial(next) } /** @@ -74,9 +83,8 @@ object VersionCursor { val thisModel = existing.filter(_.model == m) thisModel.map(_.revision).contains(r - 1) case NonInitial(version) => - val thisModel = existing.filter(_.model == version.model) + val thisModel = existing.filter(_.model == version.model) val thisRevision = thisModel.filter(_.revision == version.revision) thisRevision.map(_.addition).contains(version.addition - 1) } } - diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/service/AuthService.scala b/src/main/scala/com/snowplowanalytics/iglu/server/service/AuthService.scala index 5bb527d..1eefdb1 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/service/AuthService.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/service/AuthService.scala @@ -19,7 +19,7 @@ import java.util.UUID import cats.implicits._ import cats.data.EitherT -import cats.effect.{ IO, Sync } +import cats.effect.{IO, Sync} import io.circe._ import io.circe.syntax._ @@ -37,9 +37,8 @@ import com.snowplowanalytics.iglu.server.model.Permission import com.snowplowanalytics.iglu.server.model.IgluResponse import com.snowplowanalytics.iglu.server.storage.Storage - class AuthService[F[+_]: Sync](swagger: SwaggerSyntax[F], ctx: AuthedContext[F, Permission], db: Storage[F]) - extends RhoRoutes[F] { + extends RhoRoutes[F] { import swagger._ import AuthService._ @@ -52,7 +51,7 @@ class AuthService[F[+_]: Sync](swagger: SwaggerSyntax[F], ctx: AuthedContext[F, POST / "keygen" >>> ctx.auth |>> { (req: Request[F], authInfo: Permission) => if (authInfo.key.contains(Permission.KeyAction.Create)) for { - vendorE <- vendorFromBody(req).value + vendorE <- vendorFromBody(req).value response <- vendorE match { case Right(vendor) if authInfo.canCreatePermission(vendor.asString) => for { @@ -69,7 +68,6 @@ class AuthService[F[+_]: Sync](swagger: SwaggerSyntax[F], ctx: AuthedContext[F, else Forbidden(IgluResponse.Message("Not sufficient privileges to create keys"): IgluResponse) } - def deleteKey(key: UUID, permission: Permission) = if (permission.key.contains(Permission.KeyAction.Delete)) { db.deletePermission(key) *> Ok(IgluResponse.Message(s"Keys have been deleted"): IgluResponse) @@ -82,24 +80,27 @@ object AuthService { implicit val schemaGenerateReq: Decoder[GenerateKey] = Decoder.instance { cursor => - cursor - .downField("vendorPrefix") - .as[String] - .map(Permission.Vendor.parse) - .map(GenerateKey.apply) + cursor.downField("vendorPrefix").as[String].map(Permission.Vendor.parse).map(GenerateKey.apply) } - def asRoutes(db: Storage[IO], ctx: AuthedContext[IO, Permission], rhoMiddleware: RhoMiddleware[IO]): HttpRoutes[IO] = { + def asRoutes( + db: Storage[IO], + ctx: AuthedContext[IO, Permission], + rhoMiddleware: RhoMiddleware[IO] + ): HttpRoutes[IO] = { val service = new AuthService(swaggerSyntax, ctx, db).toRoutes(rhoMiddleware) PermissionMiddleware.wrapService(db, ctx, service) } - def vendorFromBody[F[_]: Sync](request: Request[F]): EitherT[F, DecodeFailure, Permission.Vendor] = { + def vendorFromBody[F[_]: Sync](request: Request[F]): EitherT[F, DecodeFailure, Permission.Vendor] = request.attemptAs[Json].flatMap { json => - EitherT.fromEither[F](json.as[GenerateKey].fold( - e => InvalidMessageBodyFailure(e.show).asLeft[Permission.Vendor], - p => p.vendorPrefix.asRight[DecodeFailure]) + EitherT.fromEither[F]( + json + .as[GenerateKey] + .fold( + e => InvalidMessageBodyFailure(e.show).asLeft[Permission.Vendor], + p => p.vendorPrefix.asRight[DecodeFailure] + ) ) } - } } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/service/DebugService.scala b/src/main/scala/com/snowplowanalytics/iglu/server/service/DebugService.scala index 5415f3c..05cec09 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/service/DebugService.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/service/DebugService.scala @@ -14,15 +14,14 @@ */ package com.snowplowanalytics.iglu.server.service -import cats.effect.{ IO, Sync } +import cats.effect.{IO, Sync} import cats.implicits._ -import org.http4s.rho.{ RhoRoutes, RhoMiddleware } +import org.http4s.rho.{RhoMiddleware, RhoRoutes} import org.http4s.rho.swagger.SwaggerSyntax import org.http4s.rho.swagger.syntax.{io => swaggerSyntax} - -import com.snowplowanalytics.iglu.server.storage.{ Storage, InMemory } +import com.snowplowanalytics.iglu.server.storage.{InMemory, Storage} /** Service showing whole in-memory state. Use for development only */ class DebugService[F[_]: Sync](swagger: SwaggerSyntax[F], db: Storage[F]) extends RhoRoutes[F] { @@ -33,7 +32,7 @@ class DebugService[F[_]: Sync](swagger: SwaggerSyntax[F], db: Storage[F]) extend db match { case InMemory(ref) => for { - db <- ref.get + db <- ref.get response <- Ok(db.toString) } yield response case other => NotImplemented(s"Cannot show $other") diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/service/DraftService.scala b/src/main/scala/com/snowplowanalytics/iglu/server/service/DraftService.scala index acfa5b3..65e348d 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/service/DraftService.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/service/DraftService.scala @@ -21,26 +21,24 @@ import io.circe._ import org.http4s.HttpRoutes import org.http4s.circe._ -import org.http4s.rho.{ RhoRoutes, AuthedContext, RhoMiddleware } +import org.http4s.rho.{AuthedContext, RhoMiddleware, RhoRoutes} import org.http4s.rho.swagger.SwaggerSyntax import org.http4s.rho.swagger.syntax.io import com.snowplowanalytics.iglu.server.storage.Storage import com.snowplowanalytics.iglu.server.middleware.PermissionMiddleware -import com.snowplowanalytics.iglu.server.model.{IgluResponse, Permission, DraftVersion, SchemaDraft} +import com.snowplowanalytics.iglu.server.model.{DraftVersion, IgluResponse, Permission, SchemaDraft} import com.snowplowanalytics.iglu.server.codecs.UriParsers._ import com.snowplowanalytics.iglu.server.codecs.JsonCodecs._ - -class DraftService[F[+_]: Sync](swagger: SwaggerSyntax[F], - db: Storage[F], - ctx: AuthedContext[F, Permission]) extends RhoRoutes[F] { +class DraftService[F[+_]: Sync](swagger: SwaggerSyntax[F], db: Storage[F], ctx: AuthedContext[F, Permission]) + extends RhoRoutes[F] { import swagger._ import DraftService._ implicit val C: Clock[F] = Clock.create[F] - val version = pathVar[DraftVersion]("version", "Draft version") - val isPublic = paramD[Boolean]("isPublic", false, "Should schema be created as public") + val version = pathVar[DraftVersion]("version", "Draft version") + val isPublic = paramD[Boolean]("isPublic", false, "Should schema be created as public") val schemaBody = jsonOf[F, Json] "Get a particular draft by its URI" ** @@ -52,20 +50,25 @@ class DraftService[F[+_]: Sync](swagger: SwaggerSyntax[F], "List all available drafts" ** GET >>> ctx.auth |>> listDrafts _ - - def getDraft(vendor: String, name: String, format: String, version: DraftVersion, - permission: Permission) = { + def getDraft(vendor: String, name: String, format: String, version: DraftVersion, permission: Permission) = { val draftId = SchemaDraft.DraftId(vendor, name, format, version) if (permission.canRead(draftId.vendor)) { db.getDraft(draftId).flatMap { case Some(draft) => Ok(draft) - case _ => NotFound(IgluResponse.SchemaNotFound: IgluResponse) + case _ => NotFound(IgluResponse.SchemaNotFound: IgluResponse) } } else NotFound(IgluResponse.SchemaNotFound: IgluResponse) } - def putDraft(vendor: String, name: String, format: String, version: DraftVersion, - isPublic: Boolean, permission: Permission, body: Json) = { + def putDraft( + vendor: String, + name: String, + format: String, + version: DraftVersion, + isPublic: Boolean, + permission: Permission, + body: Json + ) = { val draftId = SchemaDraft.DraftId(vendor, name, format, version) if (permission.canCreateSchema(draftId.vendor)) db.addDraft(draftId, body, isPublic) *> NotImplemented("TODO") @@ -79,9 +82,11 @@ class DraftService[F[+_]: Sync](swagger: SwaggerSyntax[F], object DraftService { - def asRoutes(db: Storage[IO], - ctx: AuthedContext[IO, Permission], - rhoMiddleware: RhoMiddleware[IO]): HttpRoutes[IO] = { + def asRoutes( + db: Storage[IO], + ctx: AuthedContext[IO, Permission], + rhoMiddleware: RhoMiddleware[IO] + ): HttpRoutes[IO] = { val service = new DraftService(io, db, ctx).toRoutes(rhoMiddleware) PermissionMiddleware.wrapService(db, ctx, service) } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/service/MetaService.scala b/src/main/scala/com/snowplowanalytics/iglu/server/service/MetaService.scala index 2a450f7..2ccf11c 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/service/MetaService.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/service/MetaService.scala @@ -22,7 +22,7 @@ import cats.syntax.flatMap._ import io.circe._ import io.circe.generic.semiauto.deriveEncoder -import org.http4s.{ HttpRoutes, MediaType, Charset } +import org.http4s.{Charset, HttpRoutes, MediaType} import org.http4s.headers.`Content-Type` import org.http4s.rho.{AuthedContext, RhoMiddleware, RhoRoutes} import org.http4s.rho.swagger.SwaggerSyntax @@ -31,14 +31,16 @@ import org.http4s.rho.swagger.syntax.{io => swaggerSyntax} import com.snowplowanalytics.iglu.server.codecs.JsonCodecs._ import com.snowplowanalytics.iglu.server.generated.BuildInfo import com.snowplowanalytics.iglu.server.model.Permission -import com.snowplowanalytics.iglu.server.storage.{ Storage, Postgres, InMemory } +import com.snowplowanalytics.iglu.server.storage.{InMemory, Postgres, Storage} import com.snowplowanalytics.iglu.server.middleware.PermissionMiddleware -class MetaService[F[+_]: Sync](debug: Boolean, - patchesAllowed: Boolean, - swagger: SwaggerSyntax[F], - ctx: AuthedContext[F, Permission], - db: Storage[F]) extends RhoRoutes[F] { +class MetaService[F[+_]: Sync]( + debug: Boolean, + patchesAllowed: Boolean, + swagger: SwaggerSyntax[F], + ctx: AuthedContext[F, Permission], + db: Storage[F] +) extends RhoRoutes[F] { import swagger._ private val ok = Ok("OK").map(_.withContentType(`Content-Type`(MediaType.text.plain, Charset.`UTF-8`))) @@ -51,7 +53,7 @@ class MetaService[F[+_]: Sync](debug: Boolean, for { _ <- db match { case pg: Postgres[F] => pg.ping.void - case _ => Sync[F].unit + case _ => Sync[F].unit } response <- ok } yield response @@ -62,7 +64,7 @@ class MetaService[F[+_]: Sync](debug: Boolean, val database = db match { case _: Postgres[F] => "postgres" case _: InMemory[F] => "inmemory" - case _ => "unknown" + case _ => "unknown" } for { schemas <- db.getSchemasKeyOnly @@ -73,16 +75,21 @@ class MetaService[F[+_]: Sync](debug: Boolean, } object MetaService { - case class ServerInfo(version: String, - authInfo: Permission, - database: String, - schemaCount: Int, - debug: Boolean, - patchesAllowed: Boolean) + case class ServerInfo( + version: String, + authInfo: Permission, + database: String, + schemaCount: Int, + debug: Boolean, + patchesAllowed: Boolean + ) implicit val serverInfoEncoderInstance: Encoder[ServerInfo] = deriveEncoder[ServerInfo] - def asRoutes(debug: Boolean, patchesAllowed: Boolean)(db: Storage[IO], ctx: AuthedContext[IO, Permission], rhoMiddleware: RhoMiddleware[IO]): HttpRoutes[IO] = { + def asRoutes( + debug: Boolean, + patchesAllowed: Boolean + )(db: Storage[IO], ctx: AuthedContext[IO, Permission], rhoMiddleware: RhoMiddleware[IO]): HttpRoutes[IO] = { val service = new MetaService(debug, patchesAllowed, swaggerSyntax, ctx, db).toRoutes(rhoMiddleware) PermissionMiddleware.wrapService(db, ctx, service) } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/service/SchemaService.scala b/src/main/scala/com/snowplowanalytics/iglu/server/service/SchemaService.scala index 87a8293..1e1a4d9 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/service/SchemaService.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/service/SchemaService.scala @@ -38,24 +38,26 @@ import com.snowplowanalytics.iglu.server.model.Schema.SchemaBody import com.snowplowanalytics.iglu.server.model.Schema.Repr.{Format => SchemaFormat} import com.snowplowanalytics.iglu.server.model.VersionCursor.Inconsistency -class SchemaService[F[+_]: Sync](swagger: SwaggerSyntax[F], - ctx: AuthedContext[F, Permission], - db: Storage[F], - patchesAllowed: Boolean, - webhooks: Webhook.WebhookClient[F]) extends RhoRoutes[F] { +class SchemaService[F[+_]: Sync]( + swagger: SwaggerSyntax[F], + ctx: AuthedContext[F, Permission], + db: Storage[F], + patchesAllowed: Boolean, + webhooks: Webhook.WebhookClient[F] +) extends RhoRoutes[F] { import swagger._ import SchemaService._ implicit val C: Clock[F] = Clock.create[F] - val reprUri = genericQueryCapture(UriParsers.parseRepresentationUri[F]).withMetadata(ReprMetadata) + val reprUri = genericQueryCapture(UriParsers.parseRepresentationUri[F]).withMetadata(ReprMetadata) val reprCanonical = genericQueryCapture(UriParsers.parseRepresentationCanonical[F]).withMetadata(ReprMetadata) - val version = pathVar[SchemaVer.Full]("version", "SchemaVer") + val version = pathVar[SchemaVer.Full]("version", "SchemaVer") val isPublic = paramD[Boolean]("isPublic", false, "Should schema be created as public") val schemaOrJson = jsonOf[F, SchemaBody] - val schema = jsonOf[F, SelfDescribingSchema[Json]] + val schema = jsonOf[F, SelfDescribingSchema[Json]] private val validationService = new ValidationService[F](swagger, ctx, db) @@ -83,45 +85,43 @@ class SchemaService[F[+_]: Sync](swagger: SwaggerSyntax[F], "Publish new self-describing schema" ** POST +? isPublic >>> ctx.auth ^ schema |>> publishSchema _ - "Schema validation endpoint (deprecated)" ** POST / "validate" / 'vendor / 'name / "jsonschema" / 'version ^ jsonDecoder[F] |>> { - (_: String, _: String, _: String, json: Json) => - validationService.validateSchema(Schema.Format.Jsonschema, json) + (_: String, _: String, _: String, json: Json) => validationService.validateSchema(Schema.Format.Jsonschema, json) } - def getSchema(vendor: String, name: String, format: String, version: SchemaVer.Full, - schemaFormat: SchemaFormat, permission: Permission) = { + def getSchema( + vendor: String, + name: String, + format: String, + version: SchemaVer.Full, + schemaFormat: SchemaFormat, + permission: Permission + ) = db.getSchema(SchemaMap(vendor, name, format, version)).flatMap { - case Some(schema) if schema.metadata.isPublic => Ok(schema.withFormat(schemaFormat)) + case Some(schema) if schema.metadata.isPublic => Ok(schema.withFormat(schemaFormat)) case Some(schema) if permission.canRead(schema.schemaMap.schemaKey.vendor) => Ok(schema.withFormat(schemaFormat)) - case _ => NotFound(IgluResponse.SchemaNotFound: IgluResponse) + case _ => NotFound(IgluResponse.SchemaNotFound: IgluResponse) } - } - def deleteSchema(vendor: String, name: String, format: String, version: SchemaVer.Full, permission: Permission) = { + def deleteSchema(vendor: String, name: String, format: String, version: SchemaVer.Full, permission: Permission) = permission match { case Permission.Master if patchesAllowed => - db.deleteSchema(SchemaMap(vendor, name, format, version)) *> Ok(IgluResponse.Message("Schema deleted"): IgluResponse) + db.deleteSchema(SchemaMap(vendor, name, format, version)) *> Ok( + IgluResponse.Message("Schema deleted"): IgluResponse + ) case Permission.Master => MethodNotAllowed(IgluResponse.Message("DELETE is forbidden on production registry"): IgluResponse) case _ => Unauthorized(IgluResponse.Message("Not enough permissions"): IgluResponse) } - } def getSchemasByName(vendor: String, name: String, format: SchemaFormat, permission: Permission) = { - val query = db - .getSchemasByVendorName(vendor, name) - .filter(isReadable(permission)) - .map(_.withFormat(format)) + val query = db.getSchemasByVendorName(vendor, name).filter(isReadable(permission)).map(_.withFormat(format)) schemasOrNotFound(query) } def getSchemasByVendor(vendor: String, format: SchemaFormat, permission: Permission) = { - val query = db - .getSchemasByVendor(vendor, false) - .filter(isReadable(permission)) - .map(_.withFormat(format)) + val query = db.getSchemasByVendor(vendor, false).filter(isReadable(permission)).map(_.withFormat(format)) schemasOrNotFound(query) } @@ -137,9 +137,15 @@ class SchemaService[F[+_]: Sync](swagger: SwaggerSyntax[F], def publishSchema(isPublic: Boolean, permission: Permission, schema: SelfDescribingSchema[Json]) = addSchema(permission, schema, isPublic) - def putSchema(vendor: String, name: String, format: String, version: SchemaVer.Full, isPublic: Boolean, - permission: Permission, - json: SchemaBody) = json match { + def putSchema( + vendor: String, + name: String, + format: String, + version: SchemaVer.Full, + isPublic: Boolean, + permission: Permission, + json: SchemaBody + ) = json match { case SchemaBody.BodyOnly(body) => val schemaMap = SchemaMap(vendor, name, format, version) addSchema(permission, SelfDescribingSchema(schemaMap, body), isPublic) @@ -153,23 +159,21 @@ class SchemaService[F[+_]: Sync](swagger: SwaggerSyntax[F], val result = format match { case SchemaFormat.Uri => db.getSchemasKeyOnly - .map(_.filter(isReadablePair(permission)).map { case (map, meta) => Schema(map, meta, Json.Null).withFormat(SchemaFormat.Uri) }) + .map(_.filter(isReadablePair(permission)).map { + case (map, meta) => Schema(map, meta, Json.Null).withFormat(SchemaFormat.Uri) + }) case _ => - db.getSchemas - .map(_.filter(isReadable(permission)).map(_.withFormat(format))) + db.getSchemas.map(_.filter(isReadable(permission)).map(_.withFormat(format))) } result.flatMap(response => Ok(response)) } /** Make sure that SchemaList can be only non-empty list */ private def schemasOrNotFound(queryResult: fs2.Stream[F, Schema.Repr]) = - queryResult - .compile - .toList - .flatMap { - case Nil => NotFound(IgluResponse.Message(s"No schemas available"): IgluResponse) - case schemas => Ok(schemas) - } + queryResult.compile.toList.flatMap { + case Nil => NotFound(IgluResponse.Message(s"No schemas available"): IgluResponse) + case schemas => Ok(schemas) + } private def addSchema(permission: Permission, schema: SelfDescribingSchema[Json], isPublic: Boolean) = if (permission.canCreateSchema(schema.self.schemaKey.vendor)) @@ -179,8 +183,9 @@ class SchemaService[F[+_]: Sync](swagger: SwaggerSyntax[F], case Right(_) => for { existing <- db.getSchema(schema.self).map(_.isDefined) - _ <- if (existing) db.updateSchema(schema.self, schema.schema, isPublic) else db.addSchema(schema.self, schema.schema, isPublic) - payload = IgluResponse.SchemaUploaded(existing, schema.self.schemaKey): IgluResponse + _ <- if (existing) db.updateSchema(schema.self, schema.schema, isPublic) + else db.addSchema(schema.self, schema.schema, isPublic) + payload = IgluResponse.SchemaUploaded(existing, schema.self.schemaKey): IgluResponse _ <- webhooks.schemaPublished(schema.self.schemaKey, existing) response <- if (existing) Ok(payload) else Created(payload) } yield response @@ -198,26 +203,28 @@ object SchemaService { "Schema representation format (can be specified either by repr=uri/meta/canonical or legacy meta=1&body=1)" } - def asRoutes(patchesAllowed: Boolean, webhook: Webhook.WebhookClient[IO]) - (db: Storage[IO], - ctx: AuthedContext[IO, Permission], - rhoMiddleware: RhoMiddleware[IO]): HttpRoutes[IO] = { + def asRoutes( + patchesAllowed: Boolean, + webhook: Webhook.WebhookClient[IO] + )(db: Storage[IO], ctx: AuthedContext[IO, Permission], rhoMiddleware: RhoMiddleware[IO]): HttpRoutes[IO] = { val service = new SchemaService(swaggerSyntax, ctx, db, patchesAllowed, webhook).toRoutes(rhoMiddleware) PermissionMiddleware.wrapService(db, ctx, service) } - def isSchemaAllowed[F[_]: Sync](db: Storage[F], - schemaMap: SchemaMap, - patchesAllowed: Boolean, - isPublic: Boolean): F[Either[String, Unit]] = + def isSchemaAllowed[F[_]: Sync]( + db: Storage[F], + schemaMap: SchemaMap, + patchesAllowed: Boolean, + isPublic: Boolean + ): F[Either[String, Unit]] = for { - schemas <- db.getSchemasByVendorName(schemaMap.schemaKey.vendor, schemaMap.schemaKey.name).compile.toList - previousPublic = schemas.forall(_.metadata.isPublic) - versions = schemas.map(_.schemaMap.schemaKey.version) + schemas <- db.getSchemasByVendorName(schemaMap.schemaKey.vendor, schemaMap.schemaKey.name).compile.toList + previousPublic = schemas.forall(_.metadata.isPublic) + versions = schemas.map(_.schemaMap.schemaKey.version) } yield (if ((previousPublic && isPublic) || (!previousPublic && !isPublic) || schemas.isEmpty) - VersionCursor.isAllowed(schemaMap.schemaKey.version, versions, patchesAllowed) - else Inconsistency.Availability(isPublic, previousPublic).asLeft).leftMap(_.show) + VersionCursor.isAllowed(schemaMap.schemaKey.version, versions, patchesAllowed) + else Inconsistency.Availability(isPublic, previousPublic).asLeft).leftMap(_.show) /** Extract schemas from database, available for particular permission */ def isReadable(permission: Permission)(schema: Schema): Boolean = @@ -228,4 +235,4 @@ object SchemaService { case (schemaMap, metadata) => permission.canRead(schemaMap.schemaKey.vendor) || metadata.isPublic } -} \ No newline at end of file +} diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/service/StaticService.scala b/src/main/scala/com/snowplowanalytics/iglu/server/service/StaticService.scala index 73a4e3f..2254d4f 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/service/StaticService.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/service/StaticService.scala @@ -22,7 +22,7 @@ import org.http4s.dsl.io._ import com.snowplowanalytics.iglu.server.generated.BuildInfo object StaticService { - private val localUi = "/swagger-ui-dist" + private val localUi = "/swagger-ui-dist" private val swaggerUiDir = s"/META-INF/resources/webjars/swagger-ui/${BuildInfo.SwaggerUI}" /** @@ -36,7 +36,6 @@ object StaticService { case req @ GET -> Root / "swagger-ui" / path => fetchResource(swaggerUiDir + "/" + path, blocker, req) } - def fetchResource(path: String, blocker: Blocker, req: Request[IO])(implicit cs: ContextShift[IO]): IO[Response[IO]] = { + def fetchResource(path: String, blocker: Blocker, req: Request[IO])(implicit cs: ContextShift[IO]): IO[Response[IO]] = StaticFile.fromResource(path, blocker, Some(req)).getOrElseF(NotFound()) - } } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/service/ValidationService.scala b/src/main/scala/com/snowplowanalytics/iglu/server/service/ValidationService.scala index e40ebf5..89de52b 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/service/ValidationService.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/service/ValidationService.scala @@ -18,15 +18,15 @@ import io.circe.Json import org.http4s.HttpRoutes import org.http4s.circe._ -import org.http4s.rho.{RhoMiddleware, RhoRoutes, AuthedContext} +import org.http4s.rho.{AuthedContext, RhoMiddleware, RhoRoutes} import org.http4s.rho.swagger.SwaggerSyntax import org.http4s.rho.swagger.syntax.io -import cats.data.{NonEmptyList, ValidatedNel, Validated} +import cats.data.{NonEmptyList, Validated, ValidatedNel} import cats.effect.{IO, Sync} import cats.implicits._ -import com.snowplowanalytics.iglu.core.{SelfDescribingData, SelfDescribingSchema, SchemaMap} +import com.snowplowanalytics.iglu.core.{SchemaMap, SelfDescribingData, SelfDescribingSchema} import com.snowplowanalytics.iglu.core.circe.implicits._ import com.snowplowanalytics.iglu.client.validator.ValidatorError @@ -34,17 +34,16 @@ import com.snowplowanalytics.iglu.client.validator.CirceValidator import com.snowplowanalytics.iglu.schemaddl.jsonschema.circe.implicits._ import com.snowplowanalytics.iglu.schemaddl.jsonschema.{Pointer, SelfSyntaxChecker, Schema => SchemaAst} import com.snowplowanalytics.iglu.schemaddl.jsonschema.SanityLinter.lint -import com.snowplowanalytics.iglu.schemaddl.jsonschema.Linter.{ allLintersMap, Message, Level } +import com.snowplowanalytics.iglu.schemaddl.jsonschema.Linter.{Level, Message, allLintersMap} import com.snowplowanalytics.iglu.server.storage.Storage import com.snowplowanalytics.iglu.server.middleware.PermissionMiddleware -import com.snowplowanalytics.iglu.server.model.{ IgluResponse, Permission, Schema } +import com.snowplowanalytics.iglu.server.model.{IgluResponse, Permission, Schema} import com.snowplowanalytics.iglu.server.codecs.JsonCodecs._ import com.snowplowanalytics.iglu.server.codecs.UriParsers._ -class ValidationService[F[+_]: Sync](swagger: SwaggerSyntax[F], - ctx: AuthedContext[F, Permission], - db: Storage[F]) extends RhoRoutes[F] { +class ValidationService[F[+_]: Sync](swagger: SwaggerSyntax[F], ctx: AuthedContext[F, Permission], db: Storage[F]) + extends RhoRoutes[F] { import swagger._ import ValidationService._ @@ -68,7 +67,7 @@ class ValidationService[F[+_]: Sync](swagger: SwaggerSyntax[F], } } - def validateData(authInfo: Permission, instance: Json) = { + def validateData(authInfo: Permission, instance: Json) = SelfDescribingData.parse(instance) match { case Right(SelfDescribingData(key, data)) => for { @@ -92,14 +91,15 @@ class ValidationService[F[+_]: Sync](swagger: SwaggerSyntax[F], BadRequest(IgluResponse.Message(s"JSON payload is not self-describing, ${error.code}"): IgluResponse) } - } } object ValidationService { - def asRoutes(db: Storage[IO], - ctx: AuthedContext[IO, Permission], - rhoMiddleware: RhoMiddleware[IO]): HttpRoutes[IO] = { + def asRoutes( + db: Storage[IO], + ctx: AuthedContext[IO, Permission], + rhoMiddleware: RhoMiddleware[IO] + ): HttpRoutes[IO] = { val service = new ValidationService[IO](io, ctx, db).toRoutes(rhoMiddleware) PermissionMiddleware.wrapService(db, ctx, service) } @@ -107,7 +107,7 @@ object ValidationService { type LintReport[A] = ValidatedNel[Message, A] val NotSelfDescribing = Message(Pointer.Root, "JSON Schema is not self-describing", Level.Error) - val NotSchema = Message(Pointer.Root, "Cannot extract JSON Schema", Level.Error) + val NotSchema = Message(Pointer.Root, "Cannot extract JSON Schema", Level.Error) def validateJsonSchema(schema: Json): LintReport[SelfDescribingSchema[Json]] = { val generalCheck = @@ -117,15 +117,14 @@ object ValidationService { .parse(schema) .fold(_ => NotSelfDescribing.invalidNel[SelfDescribingSchema[Json]], _.validNel[Message]) - val lintReport: LintReport[Unit] = SchemaAst.parse(schema) - .fold(NotSchema.invalidNel[SchemaAst])(_.validNel[Message]) - .andThen { ast => - val result = lint(ast, allLintersMap.values.toList) - .toList - .flatMap { case (pointer, issues) => issues.toList.map(_.toMessage(pointer)) } + val lintReport: LintReport[Unit] = + SchemaAst.parse(schema).fold(NotSchema.invalidNel[SchemaAst])(_.validNel[Message]).andThen { ast => + val result = lint(ast, allLintersMap.values.toList).toList.flatMap { + case (pointer, issues) => issues.toList.map(_.toMessage(pointer)) + } NonEmptyList.fromList(result).fold(().validNel[Message])(_.invalid[Unit]) } - (generalCheck, lintReport, selfDescribingCheck).mapN { (_, _, schema) => schema } + (generalCheck, lintReport, selfDescribingCheck).mapN((_, _, schema) => schema) } } diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/storage/InMemory.scala b/src/main/scala/com/snowplowanalytics/iglu/server/storage/InMemory.scala index 07ba047..94477cb 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/storage/InMemory.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/storage/InMemory.scala @@ -22,14 +22,14 @@ import fs2.Stream import cats.Monad import cats.implicits._ -import cats.effect.{ Sync, Clock, Bracket } +import cats.effect.{Bracket, Clock, Sync} import cats.effect.concurrent.Ref import io.circe.Json import com.snowplowanalytics.iglu.core.SchemaMap -import com.snowplowanalytics.iglu.server.model.{ Permission, Schema, SchemaDraft } +import com.snowplowanalytics.iglu.server.model.{Permission, Schema, SchemaDraft} import com.snowplowanalytics.iglu.server.model.SchemaDraft.DraftId /** Ephemeral storage that will be lost after server shut down */ @@ -47,17 +47,23 @@ case class InMemory[F[_]](ref: Ref[F, InMemory.State]) extends Storage[F] { _ <- ref.set(newState) } yield () - def addSchema(schemaMap: SchemaMap, body: Json, isPublic: Boolean)(implicit C: Clock[F], M: Bracket[F, Throwable]): F[Unit] = + def addSchema(schemaMap: SchemaMap, body: Json, isPublic: Boolean)( + implicit C: Clock[F], + M: Bracket[F, Throwable] + ): F[Unit] = for { - db <- ref.get + db <- ref.get addedAtMillis <- C.realTime(TimeUnit.MILLISECONDS) addedAt = Instant.ofEpochMilli(addedAtMillis) - meta = Schema.Metadata(addedAt, addedAt, isPublic) - schema = Schema(schemaMap, meta, body) + meta = Schema.Metadata(addedAt, addedAt, isPublic) + schema = Schema(schemaMap, meta, body) _ <- ref.update(_.copy(schemas = db.schemas.updated(schemaMap, schema))) } yield () - def updateSchema(schemaMap: SchemaMap, body: Json, isPublic: Boolean)(implicit C: Clock[F], M: Bracket[F, Throwable]): F[Unit] = + def updateSchema(schemaMap: SchemaMap, body: Json, isPublic: Boolean)( + implicit C: Clock[F], + M: Bracket[F, Throwable] + ): F[Unit] = addSchema(schemaMap, body, isPublic) def getSchemas(implicit F: Bracket[F, Throwable]): F[List[Schema]] = @@ -69,31 +75,35 @@ case class InMemory[F[_]](ref: Ref[F, InMemory.State]) extends Storage[F] { def getDraft(draftId: DraftId)(implicit B: Bracket[F, Throwable]): F[Option[SchemaDraft]] = for { db <- ref.get } yield db.drafts.get(draftId) - def addDraft(draftId: DraftId, body: Json, isPublic: Boolean)(implicit C: Clock[F], M: Bracket[F, Throwable]): F[Unit] = + def addDraft(draftId: DraftId, body: Json, isPublic: Boolean)( + implicit C: Clock[F], + M: Bracket[F, Throwable] + ): F[Unit] = for { - db <- ref.get + db <- ref.get addedAtMillis <- C.realTime(TimeUnit.MILLISECONDS) addedAt = Instant.ofEpochMilli(addedAtMillis) - meta = Schema.Metadata(addedAt, addedAt, isPublic) - schema = SchemaDraft(draftId, meta, body) + meta = Schema.Metadata(addedAt, addedAt, isPublic) + schema = SchemaDraft(draftId, meta, body) _ <- ref.update(_.copy(drafts = db.drafts.updated(draftId, schema))) } yield () def getDrafts(implicit F: Monad[F]): Stream[F, SchemaDraft] = { - val drafts = ref.get.map(state => Stream.emits[F, SchemaDraft](state.drafts.values.toList.sortBy(_.metadata.createdAt))) + val drafts = + ref.get.map(state => Stream.emits[F, SchemaDraft](state.drafts.values.toList.sortBy(_.metadata.createdAt))) Stream.eval(drafts).flatten } def addPermission(apikey: UUID, permission: Permission)(implicit F: Bracket[F, Throwable]): F[Unit] = for { db <- ref.get - _ <- ref.update(_.copy(permission = db.permission.updated(apikey, permission))) + _ <- ref.update(_.copy(permission = db.permission.updated(apikey, permission))) } yield () def deletePermission(apikey: UUID)(implicit F: Bracket[F, Throwable]): F[Unit] = for { db <- ref.get - _ <- ref.update(_.copy(permission = db.permission - apikey)) + _ <- ref.update(_.copy(permission = db.permission - apikey)) } yield () } @@ -101,9 +111,11 @@ object InMemory { val DummyMasterKey: UUID = UUID.fromString("48b267d7-cd2b-4f22-bae4-0f002008b5ad") - case class State(schemas: Map[SchemaMap, Schema], - permission: Map[UUID, Permission], - drafts: Map[DraftId, SchemaDraft]) + case class State( + schemas: Map[SchemaMap, Schema], + permission: Map[UUID, Permission], + drafts: Map[DraftId, SchemaDraft] + ) object State { val empty: State = State(Map.empty, Map.empty, Map.empty) diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/storage/Postgres.scala b/src/main/scala/com/snowplowanalytics/iglu/server/storage/Postgres.scala index a66691b..eae761c 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/storage/Postgres.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/storage/Postgres.scala @@ -21,7 +21,7 @@ import io.circe.Json import cats.Monad import cats.implicits._ -import cats.effect.{ Clock, Bracket } +import cats.effect.{Bracket, Clock} import fs2.Stream @@ -32,8 +32,8 @@ import doobie.postgres.circe.json.implicits._ import org.typelevel.log4cats.Logger -import com.snowplowanalytics.iglu.core.{ SchemaMap, SchemaVer } -import com.snowplowanalytics.iglu.server.model.{ Permission, Schema, SchemaDraft } +import com.snowplowanalytics.iglu.core.{SchemaMap, SchemaVer} +import com.snowplowanalytics.iglu.server.model.{Permission, Schema, SchemaDraft} import com.snowplowanalytics.iglu.server.model.SchemaDraft.DraftId class Postgres[F[_]](xa: Transactor[F], logger: Logger[F]) extends Storage[F] { self => @@ -49,11 +49,17 @@ class Postgres[F[_]](xa: Transactor[F], logger: Logger[F]) extends Storage[F] { logger.debug(s"getPermission") *> Postgres.Sql.getPermission(apikey).option.transact(xa) - def addSchema(schemaMap: SchemaMap, body: Json, isPublic: Boolean)(implicit C: Clock[F], M: Bracket[F, Throwable]): F[Unit] = + def addSchema(schemaMap: SchemaMap, body: Json, isPublic: Boolean)( + implicit C: Clock[F], + M: Bracket[F, Throwable] + ): F[Unit] = logger.debug(s"addSchema ${schemaMap.schemaKey.toSchemaUri}") *> Postgres.Sql.addSchema(schemaMap, body, isPublic).run.void.transact(xa) - def updateSchema(schemaMap: SchemaMap, body: Json, isPublic: Boolean)(implicit C: Clock[F], M: Bracket[F, Throwable]): F[Unit] = + def updateSchema(schemaMap: SchemaMap, body: Json, isPublic: Boolean)( + implicit C: Clock[F], + M: Bracket[F, Throwable] + ): F[Unit] = logger.debug(s"updateSchema ${schemaMap.schemaKey.toSchemaUri}") *> Postgres.Sql.updateSchema(schemaMap, body, isPublic).run.void.transact(xa) @@ -68,7 +74,10 @@ class Postgres[F[_]](xa: Transactor[F], logger: Logger[F]) extends Storage[F] { def getDraft(draftId: DraftId)(implicit B: Bracket[F, Throwable]): F[Option[SchemaDraft]] = Postgres.Sql.getDraft(draftId).option.transact(xa) - def addDraft(draftId: DraftId, body: Json, isPublic: Boolean)(implicit C: Clock[F], M: Bracket[F, Throwable]): F[Unit] = + def addDraft(draftId: DraftId, body: Json, isPublic: Boolean)( + implicit C: Clock[F], + M: Bracket[F, Throwable] + ): F[Unit] = Postgres.Sql.addDraft(draftId, body, isPublic).run.void.transact(xa) def getDrafts(implicit F: Monad[F]): Stream[F, SchemaDraft] = @@ -80,10 +89,7 @@ class Postgres[F[_]](xa: Transactor[F], logger: Logger[F]) extends Storage[F] { def deletePermission(id: UUID)(implicit F: Bracket[F, Throwable]): F[Unit] = logger.debug(s"deletePermission") *> - Postgres.Sql.deletePermission(id) - .run - .void - .transact(xa) + Postgres.Sql.deletePermission(id).run.void.transact(xa) def ping(implicit F: Bracket[F, Throwable]): F[Storage[F]] = logger.debug(s"ping") *> @@ -101,12 +107,14 @@ class Postgres[F[_]](xa: Transactor[F], logger: Logger[F]) extends Storage[F] { object Postgres { - val SchemasTable = Fragment.const("iglu_schemas") - val DraftsTable = Fragment.const("iglu_drafts") + val SchemasTable = Fragment.const("iglu_schemas") + val DraftsTable = Fragment.const("iglu_drafts") val PermissionsTable = Fragment.const("iglu_permissions") - val schemaColumns = Fragment.const("vendor, name, format, model, revision, addition, created_at, updated_at, is_public, body") - val schemaKeyColumns = Fragment.const("vendor, name, format, model, revision, addition, created_at, updated_at, is_public") + val schemaColumns = + Fragment.const("vendor, name, format, model, revision, addition, created_at, updated_at, is_public, body") + val schemaKeyColumns = + Fragment.const("vendor, name, format, model, revision, addition, created_at, updated_at, is_public") val draftColumns = Fragment.const("vendor, name, format, version, created_at, updated_at, is_public, body") val Ordering = Fragment.const("ORDER BY created_at") @@ -130,7 +138,8 @@ object Postgres { object Sql { def getSchema(schemaMap: SchemaMap) = - (fr"SELECT" ++ schemaColumns ++ fr"FROM" ++ SchemasTable ++ fr"WHERE" ++ schemaMapFr(schemaMap) ++ fr"LIMIT 1").query[Schema] + (fr"SELECT" ++ schemaColumns ++ fr"FROM" ++ SchemasTable ++ fr"WHERE" ++ schemaMapFr(schemaMap) ++ fr"LIMIT 1") + .query[Schema] def deleteSchema(schemaMap: SchemaMap) = (fr"DELETE FROM" ++ SchemasTable ++ fr"WHERE" ++ schemaMapFr(schemaMap)).update @@ -145,35 +154,31 @@ object Postgres { val key = schemaMap.schemaKey val ver = key.version (fr"INSERT INTO" ++ SchemasTable ++ fr"(" ++ schemaColumns ++ fr")" ++ - fr"VALUES (${key.vendor}, ${key.name}, ${key.format}, ${ver.model}, ${ver.revision}, ${ver.addition}, current_timestamp, current_timestamp, $isPublic, $schema)") - .update + fr"VALUES (${key.vendor}, ${key.name}, ${key.format}, ${ver.model}, ${ver.revision}, ${ver.addition}, current_timestamp, current_timestamp, $isPublic, $schema)").update } - def updateSchema(schemaMap: SchemaMap, schema: Json, isPublic: Boolean): Update0 = { + def updateSchema(schemaMap: SchemaMap, schema: Json, isPublic: Boolean): Update0 = (fr"UPDATE" ++ SchemasTable ++ fr"SET created_at = current_timestamp, updated_at = current_timestamp, is_public = $isPublic, body = $schema" - ++ fr"WHERE" ++ schemaMapFr(schemaMap)) - .update - } + ++ fr"WHERE" ++ schemaMapFr(schemaMap)).update def getDraft(draftId: DraftId) = (fr"SELECT" ++ draftColumns ++ fr"FROM" ++ DraftsTable ++ fr"WHERE " ++ draftFr(draftId)).query[SchemaDraft] def addDraft(id: DraftId, body: Json, isPublic: Boolean) = (fr"INSERT INTO" ++ DraftsTable ++ fr"(" ++ draftColumns ++ fr")" ++ - fr"VALUES (${id.vendor}, ${id.name}, ${id.format}, ${id.version.value}, current_timestamp, current_timestamp, $isPublic, $body)") - .update + fr"VALUES (${id.vendor}, ${id.name}, ${id.format}, ${id.version.value}, current_timestamp, current_timestamp, $isPublic, $body)").update def getDrafts = (fr"SELECT * FROM" ++ DraftsTable ++ Ordering).query[SchemaDraft] def getPermission(id: UUID) = - (fr"SELECT vendor, wildcard, schema_action::schema_action, key_action::key_action[] FROM" ++ PermissionsTable ++ fr"WHERE apikey = $id").query[Permission] + (fr"SELECT vendor, wildcard, schema_action::schema_action, key_action::key_action[] FROM" ++ PermissionsTable ++ fr"WHERE apikey = $id") + .query[Permission] def addPermission(uuid: UUID, permission: Permission) = { - val vendor = permission.vendor.parts.mkString(".") + val vendor = permission.vendor.parts.mkString(".") val keyActions = permission.key.toList.map(_.show) - (fr"INSERT INTO" ++ PermissionsTable ++ sql"VALUES ($uuid, $vendor, ${permission.vendor.wildcard}, ${permission.schema}::schema_action, $keyActions::key_action[])") - .update + (fr"INSERT INTO" ++ PermissionsTable ++ sql"VALUES ($uuid, $vendor, ${permission.vendor.wildcard}, ${permission.schema}::schema_action, $keyActions::key_action[])").update } def deletePermission(id: UUID) = diff --git a/src/main/scala/com/snowplowanalytics/iglu/server/storage/Storage.scala b/src/main/scala/com/snowplowanalytics/iglu/server/storage/Storage.scala index 3c8b704..10e9fa2 100644 --- a/src/main/scala/com/snowplowanalytics/iglu/server/storage/Storage.scala +++ b/src/main/scala/com/snowplowanalytics/iglu/server/storage/Storage.scala @@ -20,7 +20,7 @@ import java.util.UUID import fs2.Stream import cats.Monad -import cats.effect.{Bracket, Blocker, Clock, ContextShift, Effect, Resource, Sync} +import cats.effect.{Blocker, Bracket, Clock, ContextShift, Effect, Resource, Sync} import cats.implicits._ import io.circe.Json @@ -45,18 +45,25 @@ trait Storage[F[_]] { def getSchemasByVendor(vendor: String, wildcard: Boolean)(implicit F: Bracket[F, Throwable]): Stream[F, Schema] = { val all = Stream.eval(getSchemas).flatMap(list => Stream.emits(list)) if (wildcard) all.filter(_.schemaMap.schemaKey.vendor.startsWith(vendor)) - else all.filter(_.schemaMap.schemaKey.vendor === vendor ) + else all.filter(_.schemaMap.schemaKey.vendor === vendor) } def deleteSchema(schemaMap: SchemaMap)(implicit F: Bracket[F, Throwable]): F[Unit] def getSchemasByVendorName(vendor: String, name: String)(implicit F: Bracket[F, Throwable]): Stream[F, Schema] = getSchemasByVendor(vendor, false).filter(_.schemaMap.schemaKey.name === name) def getSchemas(implicit F: Bracket[F, Throwable]): F[List[Schema]] + /** Optimization for `getSchemas` */ def getSchemasKeyOnly(implicit F: Bracket[F, Throwable]): F[List[(SchemaMap, Schema.Metadata)]] def getSchemaBody(schemaMap: SchemaMap)(implicit F: Bracket[F, Throwable]): F[Option[Json]] = getSchema(schemaMap).nested.map(_.body).value - def addSchema(schemaMap: SchemaMap, body: Json, isPublic: Boolean)(implicit C: Clock[F], M: Bracket[F, Throwable]): F[Unit] - def updateSchema(schemaMap: SchemaMap, body: Json, isPublic: Boolean)(implicit C: Clock[F], M: Bracket[F, Throwable]): F[Unit] + def addSchema(schemaMap: SchemaMap, body: Json, isPublic: Boolean)( + implicit C: Clock[F], + M: Bracket[F, Throwable] + ): F[Unit] + def updateSchema(schemaMap: SchemaMap, body: Json, isPublic: Boolean)( + implicit C: Clock[F], + M: Bracket[F, Throwable] + ): F[Unit] def addDraft(draftId: DraftId, body: Json, isPublic: Boolean)(implicit C: Clock[F], M: Bracket[F, Throwable]): F[Unit] def getDraft(draftId: DraftId)(implicit B: Bracket[F, Throwable]): F[Option[SchemaDraft]] @@ -84,18 +91,38 @@ object Storage { config match { case StorageConfig.Dummy => Resource.eval(storage.InMemory.empty) - case StorageConfig.Postgres(host, port, name, username, password, driver, _, _, Config.StorageConfig.ConnectionPool.NoPool(ec)) => + case StorageConfig.Postgres( + host, + port, + name, + username, + password, + driver, + _, + _, + Config.StorageConfig.ConnectionPool.NoPool(ec) + ) => val url = s"jdbc:postgresql://$host:$port/$name" for { blocker <- Server.createThreadPool(ec).map(Blocker.liftExecutionContext) xa: Transactor[F] = Transactor.fromDriverManager[F](driver, url, username, password, blocker) } yield Postgres[F](xa, logger) - case p @ StorageConfig.Postgres(host, port, name, username, password, driver, _, _, pool: Config.StorageConfig.ConnectionPool.Hikari) => + case p @ StorageConfig.Postgres( + host, + port, + name, + username, + password, + driver, + _, + _, + pool: Config.StorageConfig.ConnectionPool.Hikari + ) => val url = s"jdbc:postgresql://$host:$port/$name" for { connectEC <- Server.createThreadPool(pool.connectionPool) transactEC <- Server.createThreadPool(pool.transactionPool).map(Blocker.liftExecutionContext) - xa <- HikariTransactor.newHikariTransactor[F](driver, url, username, password, connectEC, transactEC) + xa <- HikariTransactor.newHikariTransactor[F](driver, url, username, password, connectEC, transactEC) _ <- Resource.eval { xa.configure { ds => Sync[F].delay { diff --git a/src/test/scala/com/snowplowanalytics/iglu/server/ConfigSpec.scala b/src/test/scala/com/snowplowanalytics/iglu/server/ConfigSpec.scala index 07012fe..7abdf73 100644 --- a/src/test/scala/com/snowplowanalytics/iglu/server/ConfigSpec.scala +++ b/src/test/scala/com/snowplowanalytics/iglu/server/ConfigSpec.scala @@ -23,7 +23,8 @@ import io.circe.literal._ import cats.syntax.either._ -class ConfigSpec extends org.specs2.Specification { def is = s2""" +class ConfigSpec extends org.specs2.Specification { + def is = s2""" parse run command without file $e1 parse run config from file $e2 parse run config with dummy DB from file $e3 @@ -32,59 +33,87 @@ class ConfigSpec extends org.specs2.Specification { def is = s2""" """ def e1 = { - val input = "--config foo.hocon" + val input = "--config foo.hocon" val expected = Config.ServerCommand.Run(Paths.get("foo.hocon")) - val result = Config.serverCommand.parse(input.split(" ").toList) + val result = Config.serverCommand.parse(input.split(" ").toList) result must beRight(expected) } def e2 = { - val config = getClass.getResource("/valid-pg-config.conf").toURI + val config = getClass.getResource("/valid-pg-config.conf").toURI val configPath = Paths.get(config) - val input = s"--config $configPath" - val pool = Config.StorageConfig.ConnectionPool.Hikari(Some(5000), Some(1000), Some(5), Some(3), Config.ThreadPool.Fixed(4), Config.ThreadPool.Cached) + val input = s"--config $configPath" + val pool = Config + .StorageConfig + .ConnectionPool + .Hikari(Some(5000), Some(1000), Some(5), Some(3), Config.ThreadPool.Fixed(4), Config.ThreadPool.Cached) val expected = Config( - Config.StorageConfig.Postgres( - "postgres", 5432, "igludb", "sp_user", "sp_password", "org.postgresql.Driver", - None, Some(5), pool - ), + Config + .StorageConfig + .Postgres( + "postgres", + 5432, + "igludb", + "sp_user", + "sp_password", + "org.postgresql.Driver", + None, + Some(5), + pool + ), Config.Http("0.0.0.0", 8080, Some(10), Config.ThreadPool.Global), Some(true), Some(true), - Some(List( - Webhook.SchemaPublished(uri"https://example.com/endpoint", Some(List.empty)), - Webhook.SchemaPublished(uri"https://example2.com/endpoint", Some(List("com", "org.acme", "org.snowplow"))) - )) + Some( + List( + Webhook.SchemaPublished(uri"https://example.com/endpoint", Some(List.empty)), + Webhook.SchemaPublished(uri"https://example2.com/endpoint", Some(List("com", "org.acme", "org.snowplow"))) + ) + ) ) - val result = Config - .serverCommand.parse(input.split(" ").toList) - .leftMap(_.toString) - .flatMap(_.read) + val result = Config.serverCommand.parse(input.split(" ").toList).leftMap(_.toString).flatMap(_.read) result must beRight(expected) } def e3 = { - val config = getClass.getResource("/valid-dummy-config.conf").toURI + val config = getClass.getResource("/valid-dummy-config.conf").toURI val configPath = Paths.get(config) - val input = s"--config $configPath" - val expected = Config(Config.StorageConfig.Dummy, Config.Http("0.0.0.0", 8080, None, Config.ThreadPool.Fixed(2)), Some(true), None, None) - val result = Config - .serverCommand.parse(input.split(" ").toList) - .leftMap(_.toString) - .flatMap(_.read) + val input = s"--config $configPath" + val expected = Config( + Config.StorageConfig.Dummy, + Config.Http("0.0.0.0", 8080, None, Config.ThreadPool.Fixed(2)), + Some(true), + None, + None + ) + val result = Config.serverCommand.parse(input.split(" ").toList).leftMap(_.toString).flatMap(_.read) result must beRight(expected) } def e4 = { val input = Config( - Config.StorageConfig.Postgres("postgres", 5432, "igludb", "sp_user", "sp_password", "org.postgresql.Driver", None, Some(5), Config.StorageConfig.ConnectionPool.NoPool(Config.ThreadPool.Fixed(2))), + Config + .StorageConfig + .Postgres( + "postgres", + 5432, + "igludb", + "sp_user", + "sp_password", + "org.postgresql.Driver", + None, + Some(5), + Config.StorageConfig.ConnectionPool.NoPool(Config.ThreadPool.Fixed(2)) + ), Config.Http("0.0.0.0", 8080, None, Config.ThreadPool.Global), Some(true), Some(true), - Some(List( - Webhook.SchemaPublished(uri"https://example.com/endpoint", Some(List.empty)), - Webhook.SchemaPublished(uri"https://example2.com/endpoint", Some(List("com", "org.acme", "org.snowplow"))) - )) + Some( + List( + Webhook.SchemaPublished(uri"https://example.com/endpoint", Some(List.empty)), + Webhook.SchemaPublished(uri"https://example2.com/endpoint", Some(List("com", "org.acme", "org.snowplow"))) + ) + ) ) val expected = json"""{ @@ -138,21 +167,30 @@ class ConfigSpec extends org.specs2.Specification { def is = s2""" } def e5 = { - val config = getClass.getResource("/valid-pg-minimal.conf").toURI + val config = getClass.getResource("/valid-pg-minimal.conf").toURI val configPath = Paths.get(config) - val input = s"--config $configPath" - val pool = Config.StorageConfig.ConnectionPool.NoPool(Config.ThreadPool.Cached) + val input = s"--config $configPath" + val pool = Config.StorageConfig.ConnectionPool.NoPool(Config.ThreadPool.Cached) val expected = Config( - Config.StorageConfig.Postgres( - "postgres", 5432, "igludb", "sp_user", "sp_password", "org.postgresql.Driver", - None, None, pool - ), + Config + .StorageConfig + .Postgres( + "postgres", + 5432, + "igludb", + "sp_user", + "sp_password", + "org.postgresql.Driver", + None, + None, + pool + ), Config.Http("0.0.0.0", 8080, None, Config.ThreadPool.Fixed(4)), - Some(false), None, None) - val result = Config - .serverCommand.parse(input.split(" ").toList) - .leftMap(_.toString) - .flatMap(_.read) + Some(false), + None, + None + ) + val result = Config.serverCommand.parse(input.split(" ").toList).leftMap(_.toString).flatMap(_.read) result must beRight(expected) } diff --git a/src/test/scala/com/snowplowanalytics/iglu/server/ServerSpec.scala b/src/test/scala/com/snowplowanalytics/iglu/server/ServerSpec.scala index 491b89e..3899af9 100644 --- a/src/test/scala/com/snowplowanalytics/iglu/server/ServerSpec.scala +++ b/src/test/scala/com/snowplowanalytics/iglu/server/ServerSpec.scala @@ -15,7 +15,7 @@ package com.snowplowanalytics.iglu.server import cats.implicits._ -import cats.effect.{ IO, ContextShift, Timer, Resource } +import cats.effect.{ContextShift, IO, Resource, Timer} import io.circe.Json import io.circe.literal._ @@ -27,17 +27,18 @@ import org.http4s.client.blaze.BlazeClientBuilder import org.specs2.Specification -import com.snowplowanalytics.iglu.core.{SelfDescribingSchema, SchemaMap, SchemaVer } +import com.snowplowanalytics.iglu.core.{SchemaMap, SchemaVer, SelfDescribingSchema} import com.snowplowanalytics.iglu.core.circe.implicits._ -import com.snowplowanalytics.iglu.server.storage.{ Storage, Postgres } -import com.snowplowanalytics.iglu.server.model.{ IgluResponse, Permission } +import com.snowplowanalytics.iglu.server.storage.{Postgres, Storage} +import com.snowplowanalytics.iglu.server.model.{IgluResponse, Permission} import com.snowplowanalytics.iglu.server.storage.InMemory import com.snowplowanalytics.iglu.server.codecs.JsonCodecs._ // Integration test requiring a database // docker run --name igludb -e POSTGRES_PASSWORD=iglusecret -e POSTGRES_DB=testdb -p 5432:5432 -d postgres -class ServerSpec extends Specification { def is = sequential ^ s2""" +class ServerSpec extends Specification { + def is = sequential ^ s2""" ${action(System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "off"))} Return 404 for non-existing schema $e1 Return 404 for unknown endpoint $e2 @@ -48,24 +49,24 @@ class ServerSpec extends Specification { def is = sequential ^ s2""" import ServerSpec._ def e1 = { - val req = Request[IO](Method.GET, uri"http://localhost:8080/api/schemas/com.acme/event/jsonschema/1-0-0") + val req = Request[IO](Method.GET, uri"http://localhost:8080/api/schemas/com.acme/event/jsonschema/1-0-0") val expected = TestResponse(404, IgluResponse.SchemaNotFound) val action = for { responses <- ServerSpec.executeRequests(List(req)) - results <- responses.traverse(res => TestResponse.build[IgluResponse](res)) + results <- responses.traverse(res => TestResponse.build[IgluResponse](res)) } yield results execute(action) must beEqualTo(List(expected)) } def e2 = { - val req = Request[IO](Method.GET, uri"http://localhost:8080/boom") + val req = Request[IO](Method.GET, uri"http://localhost:8080/boom") val expected = TestResponse(404, IgluResponse.Message("The endpoint does not exist")) val action = for { responses <- ServerSpec.executeRequests(List(req)) - results <- responses.traverse(res => TestResponse.build[IgluResponse](res)) + results <- responses.traverse(res => TestResponse.build[IgluResponse](res)) } yield results execute(action) must beEqualTo(List(expected)) @@ -84,25 +85,34 @@ class ServerSpec extends Specification { def is = sequential ^ s2""" ) val expected = List( - TestResponse(201, json"""{"message": "Schema created", "updated": false, "location": "iglu:com.acme/first/jsonschema/1-0-0", "status": 201}"""), + TestResponse( + 201, + json"""{"message": "Schema created", "updated": false, "location": "iglu:com.acme/first/jsonschema/1-0-0", "status": 201}""" + ), TestResponse(200, json"""["iglu:com.acme/first/jsonschema/1-0-0"]"""), - TestResponse(200, json"""{ + TestResponse( + 200, + json"""{ "$$schema" : "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#", "self": {"vendor": "com.acme", "name": "first", "format": "jsonschema", "version": "1-0-0"}, - "properties" : {}}"""), + "properties" : {}}""" + ), TestResponse(404, json"""{"message" : "The schema is not found"}""") ) val action = for { responses <- ServerSpec.executeRequests(reqs) - results <- responses.traverse(res => TestResponse.build[Json](res)) + results <- responses.traverse(res => TestResponse.build[Json](res)) } yield results execute(action) must beEqualTo(expected) } def e4 = { - val schema = SelfDescribingSchema[Json](SchemaMap("com.acme", "first", "jsonschema", SchemaVer.Full(1,0,0)), json"""{"properties": {}}""").normalize + val schema = SelfDescribingSchema[Json]( + SchemaMap("com.acme", "first", "jsonschema", SchemaVer.Full(1, 0, 0)), + json"""{"properties": {}}""" + ).normalize val reqs = List( Request[IO](Method.POST, uri"http://localhost:8080/api/schemas".withQueryParam("isPublic", "true")) @@ -110,18 +120,21 @@ class ServerSpec extends Specification { def is = sequential ^ s2""" .withHeaders(Header("apikey", InMemory.DummyMasterKey.toString)), Request[IO](Method.DELETE, uri"http://localhost:8080/api/schemas/com.acme/first/jsonschema/1-0-0") .withHeaders(Header("apikey", InMemory.DummyMasterKey.toString)), - Request[IO](Method.GET, uri"http://localhost:8080/api/schemas/"), + Request[IO](Method.GET, uri"http://localhost:8080/api/schemas/") ) val expected = List( - TestResponse(201, json"""{"message": "Schema created", "updated": false, "location": "iglu:com.acme/first/jsonschema/1-0-0", "status": 201}"""), + TestResponse( + 201, + json"""{"message": "Schema created", "updated": false, "location": "iglu:com.acme/first/jsonschema/1-0-0", "status": 201}""" + ), TestResponse(200, json"""{"message":"Schema deleted"}"""), TestResponse(200, json"""[]""") ) val action = for { responses <- ServerSpec.executeRequests(reqs) - results <- responses.traverse(res => TestResponse.build[Json](res)) + results <- responses.traverse(res => TestResponse.build[Json](res)) } yield results execute(action) must beEqualTo(expected) @@ -131,26 +144,28 @@ class ServerSpec extends Specification { def is = sequential ^ s2""" object ServerSpec { import scala.concurrent.ExecutionContext.global implicit val cs: ContextShift[IO] = IO.contextShift(global) - implicit val timer: Timer[IO] = IO.timer(global) + implicit val timer: Timer[IO] = IO.timer(global) val dbPoolConfig = Config.StorageConfig.ConnectionPool.Hikari(None, None, None, None) - val httpConfig = Config.Http("0.0.0.0", 8080, None, Config.ThreadPool.Cached) - val storageConfig = Config.StorageConfig.Postgres("localhost", 5432, "testdb", "postgres", "iglusecret", "org.postgresql.Driver", None, None, dbPoolConfig) + val httpConfig = Config.Http("0.0.0.0", 8080, None, Config.ThreadPool.Cached) + val storageConfig = Config + .StorageConfig + .Postgres("localhost", 5432, "testdb", "postgres", "iglusecret", "org.postgresql.Driver", None, None, dbPoolConfig) val config = Config(storageConfig, httpConfig, Some(false), Some(true), None) private val runServer = Server.buildServer(config).flatMap(_.resource) - private val client = BlazeClientBuilder[IO](global).resource - private val env = client <* runServer + private val client = BlazeClientBuilder[IO](global).resource + private val env = client <* runServer /** Execute requests against fresh server (only one execution per test is allowed) */ def executeRequests(requests: List[Request[IO]]): IO[List[Response[IO]]] = - env.use { client => requests.traverse(client.run(_).use(IO.pure)) } + env.use(client => requests.traverse(client.run(_).use(IO.pure))) val specification = Resource.make { Storage.initialize[IO](storageConfig).use(s => s.asInstanceOf[Postgres[IO]].drop) *> Server.setup(ServerSpec.config, None).void *> Storage.initialize[IO](storageConfig).use(_.addPermission(InMemory.DummyMasterKey, Permission.Master)) - } { _ => Storage.initialize[IO](storageConfig).use(s => s.asInstanceOf[Postgres[IO]].drop) } + }(_ => Storage.initialize[IO](storageConfig).use(s => s.asInstanceOf[Postgres[IO]].drop)) def execute[A](action: IO[A]): A = specification.use(_ => action).unsafeRunSync() @@ -159,6 +174,6 @@ object ServerSpec { object TestResponse { def build[E](actual: Response[IO])(implicit decoder: EntityDecoder[IO, E]): IO[TestResponse[E]] = - actual.as[E].map { body => TestResponse(actual.status.code, body) } + actual.as[E].map(body => TestResponse(actual.status.code, body)) } } diff --git a/src/test/scala/com/snowplowanalytics/iglu/server/SpecHelpers.scala b/src/test/scala/com/snowplowanalytics/iglu/server/SpecHelpers.scala index 20f554f..9824844 100644 --- a/src/test/scala/com/snowplowanalytics/iglu/server/SpecHelpers.scala +++ b/src/test/scala/com/snowplowanalytics/iglu/server/SpecHelpers.scala @@ -4,7 +4,7 @@ import java.util.UUID import java.time.Instant import cats.implicits._ -import cats.effect.{ IO, Clock } +import cats.effect.{Clock, IO} import fs2.Stream @@ -16,7 +16,7 @@ import org.http4s.rho.AuthedContext import com.snowplowanalytics.iglu.core.{SchemaMap, SchemaVer} import com.snowplowanalytics.iglu.server.model.Permission.{SchemaAction, Vendor} -import model.{ Schema, SchemaDraft, Permission } +import model.{Permission, Schema, SchemaDraft} import storage.{InMemory, Storage} object SpecHelpers { @@ -25,34 +25,38 @@ object SpecHelpers { val ctx = new AuthedContext[IO, Permission] - val now = Instant.ofEpochMilli(1537621061000L) - val masterKey = UUID.fromString("4ed2d87a-6da5-48e8-a23b-36a26e61f974") - val readKey = UUID.fromString("1eaad173-1da5-eef8-a2cb-3fa26e61f975") + val now = Instant.ofEpochMilli(1537621061000L) + val masterKey = UUID.fromString("4ed2d87a-6da5-48e8-a23b-36a26e61f974") + val readKey = UUID.fromString("1eaad173-1da5-eef8-a2cb-3fa26e61f975") val readKeyAcme = UUID.fromString("2abad125-0ba1-faf2-b2cc-4fa26e61f971") val schemaZero = json"""{"type": "object", "properties": {"one": {}}}""" - val selfSchemaZero = json"""{"$$schema" : "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#", "type": "object", "properties": {"one": {}}, "self": {"vendor" : "com.acme", "name" : "event", "format" : "jsonschema", "version" : "1-0-0"}}""" - val schemaOne = json"""{"type": "object", "properties": {"one": {}, "two": {}}}""" + val selfSchemaZero = + json"""{"$$schema" : "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#", "type": "object", "properties": {"one": {}}, "self": {"vendor" : "com.acme", "name" : "event", "format" : "jsonschema", "version" : "1-0-0"}}""" + val schemaOne = json"""{"type": "object", "properties": {"one": {}, "two": {}}}""" val schemaPrivate = json"""{"type": "object", "properties": {"password": {}}, "required": ["password"]}""" - val selfSchemaPrivate = json"""{"$$schema" : "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#", "type": "object", "properties": {"password": {}}, "required": ["password"], "self": {"vendor" : "com.acme", "name" : "secret", "format" : "jsonschema", "version" : "1-0-0"}}""" + val selfSchemaPrivate = + json"""{"$$schema" : "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#", "type": "object", "properties": {"password": {}}, "required": ["password"], "self": {"vendor" : "com.acme", "name" : "secret", "format" : "jsonschema", "version" : "1-0-0"}}""" // exampleState content val schemas = List( // public schemas Schema( - SchemaMap("com.acme", "event", "jsonschema", SchemaVer.Full(1,0,0)), + SchemaMap("com.acme", "event", "jsonschema", SchemaVer.Full(1, 0, 0)), Schema.Metadata(now, now, true), - schemaZero), + schemaZero + ), Schema( - SchemaMap("com.acme", "event", "jsonschema", SchemaVer.Full(1,0,1)), + SchemaMap("com.acme", "event", "jsonschema", SchemaVer.Full(1, 0, 1)), Schema.Metadata(now, now, true), - schemaOne), - + schemaOne + ), // private Schema( - SchemaMap("com.acme", "secret", "jsonschema", SchemaVer.Full(1,0,0)), + SchemaMap("com.acme", "secret", "jsonschema", SchemaVer.Full(1, 0, 0)), Schema.Metadata(now, now, false), - schemaPrivate) + schemaPrivate + ) ).map(s => (s.schemaMap, s)).toMap val drafts = List.empty[SchemaDraft].map(d => (d.schemaMap, d)).toMap @@ -60,22 +64,21 @@ object SpecHelpers { val exampleState = InMemory.State( schemas, Map( - masterKey -> Permission.Master, - readKey -> Permission.ReadOnlyAny, + masterKey -> Permission.Master, + readKey -> Permission.ReadOnlyAny, readKeyAcme -> Permission(Vendor(List("com", "acme"), false), Some(SchemaAction.Read), Set.empty) ), - drafts) + drafts + ) /** Run multiple requests against HTTP service and return all responses and result state */ - def state(ser: Storage[IO] => HttpRoutes[IO]) - (reqs: List[Request[IO]]): IO[(List[Response[IO]], InMemory.State)] = { + def state(ser: Storage[IO] => HttpRoutes[IO])(reqs: List[Request[IO]]): IO[(List[Response[IO]], InMemory.State)] = for { storage <- InMemory.getInMemory[IO](exampleState) service = ser(storage) responses <- reqs.traverse(service.run).value - state <- storage.ref.get + state <- storage.ref.get } yield (responses.getOrElse(List.empty), state) - } def toBytes(entity: Json) = Stream.emits(entity.noSpaces.stripMargin.getBytes).covary[IO] diff --git a/src/test/scala/com/snowplowanalytics/iglu/server/WebhookSpec.scala b/src/test/scala/com/snowplowanalytics/iglu/server/WebhookSpec.scala index d370e7d..794ab11 100644 --- a/src/test/scala/com/snowplowanalytics/iglu/server/WebhookSpec.scala +++ b/src/test/scala/com/snowplowanalytics/iglu/server/WebhookSpec.scala @@ -23,18 +23,23 @@ import org.http4s.client.Client import com.snowplowanalytics.iglu.core.{SchemaKey, SchemaVer} import com.snowplowanalytics.iglu.server.Webhook.WebhookClient -class WebhookSpec extends org.specs2.Specification { def is = s2""" +class WebhookSpec extends org.specs2.Specification { + def is = s2""" Return Unit results for successful requests $e1 Return status code results for failed requests $e2 """ def e1 = { - val response = WebhookSpec.webhookClient.schemaPublished(SchemaKey("com.acme", "event", "jsonschema", SchemaVer.Full(1,0,0)), true) + val response = WebhookSpec + .webhookClient + .schemaPublished(SchemaKey("com.acme", "event", "jsonschema", SchemaVer.Full(1, 0, 0)), true) response.unsafeRunSync() mustEqual List(().asRight, ().asRight) } def e2 = { - val response = WebhookSpec.badWebhookClient.schemaPublished(SchemaKey("com.acme", "event", "jsonschema", SchemaVer.Full(1,0,0)), true) + val response = WebhookSpec + .badWebhookClient + .schemaPublished(SchemaKey("com.acme", "event", "jsonschema", SchemaVer.Full(1, 0, 0)), true) response.unsafeRunSync() mustEqual List("502".asLeft, "502".asLeft) } } @@ -46,8 +51,9 @@ object WebhookSpec { ) val client: Client[IO] = Client.fromHttpApp(HttpApp[IO](r => Response[IO]().withEntity(r.body).pure[IO])) - val badClient: Client[IO] = Client.fromHttpApp(HttpApp[IO](r => Response[IO]().withStatus(Status.BadGateway).withEntity(r.body).pure[IO])) + val badClient: Client[IO] = + Client.fromHttpApp(HttpApp[IO](r => Response[IO]().withStatus(Status.BadGateway).withEntity(r.body).pure[IO])) - val webhookClient = WebhookClient(webhooks, client) + val webhookClient = WebhookClient(webhooks, client) val badWebhookClient = WebhookClient(webhooks, badClient) } diff --git a/src/test/scala/com/snowplowanalytics/iglu/server/model/PermissionSpec.scala b/src/test/scala/com/snowplowanalytics/iglu/server/model/PermissionSpec.scala index 6b26922..193f98d 100644 --- a/src/test/scala/com/snowplowanalytics/iglu/server/model/PermissionSpec.scala +++ b/src/test/scala/com/snowplowanalytics/iglu/server/model/PermissionSpec.scala @@ -1,8 +1,9 @@ package com.snowplowanalytics.iglu.server.model -import com.snowplowanalytics.iglu.server.model.Permission.{SchemaAction, Vendor, KeyAction} +import com.snowplowanalytics.iglu.server.model.Permission.{KeyAction, SchemaAction, Vendor} -class PermissionSpec extends org.specs2.Specification { def is = s2""" +class PermissionSpec extends org.specs2.Specification { + def is = s2""" canRead returns true for read-only key $e1 Vendor.check returns true for exactly same vendor $e2 Vendor.check returns false for child vendor without wildcard $e3 @@ -18,13 +19,11 @@ class PermissionSpec extends org.specs2.Specification { def is = s2""" permission.canRead("com.acme") must beTrue } - def e2 = { + def e2 = Vendor(List("com", "acme"), false).check("com.acme") must beTrue - } - def e3 = { + def e3 = Vendor(List("com", "acme"), false).check("com.acme.unrelated") must beFalse - } def e4 = { val permission = Permission(Vendor(List("com", "acme"), false), Some(SchemaAction.Read), Set.empty) @@ -42,11 +41,10 @@ class PermissionSpec extends org.specs2.Specification { def is = s2""" } def e7 = { - val permission = Permission(Vendor(List(),true),Some(SchemaAction.CreateVendor),Set()) + val permission = Permission(Vendor(List(), true), Some(SchemaAction.CreateVendor), Set()) permission.canCreateSchema("com.snowplowanalytics.snowplow.storage") must beTrue } - def e8 = { + def e8 = Vendor.parse(" ") must beEqualTo(Vendor.wildcard) - } } diff --git a/src/test/scala/com/snowplowanalytics/iglu/server/model/SchemaSpec.scala b/src/test/scala/com/snowplowanalytics/iglu/server/model/SchemaSpec.scala index bc3b2fa..6d02b8d 100644 --- a/src/test/scala/com/snowplowanalytics/iglu/server/model/SchemaSpec.scala +++ b/src/test/scala/com/snowplowanalytics/iglu/server/model/SchemaSpec.scala @@ -7,7 +7,8 @@ import io.circe.literal._ import com.snowplowanalytics.iglu.core.{SchemaMap, SchemaVer} import com.snowplowanalytics.iglu.server.model.Schema.Metadata -class SchemaSpec extends org.specs2.Specification { def is = s2""" +class SchemaSpec extends org.specs2.Specification { + def is = s2""" Decode Schema $e1 Decode SchemaBody $e2 """ @@ -31,9 +32,10 @@ class SchemaSpec extends org.specs2.Specification { def is = s2""" val expected = Schema( - SchemaMap("me.chuwy", "test-schema", "jsonschema", SchemaVer.Full(1,0,0)), + SchemaMap("me.chuwy", "test-schema", "jsonschema", SchemaVer.Full(1, 0, 0)), Metadata(Instant.parse("2019-01-12T22:12:54.777Z"), Instant.parse("2019-01-12T22:12:54.777Z"), true), - json"""{"type": "object"}""") + json"""{"type": "object"}""" + ) Schema.serverSchemaDecoder.decodeJson(input) must beRight(expected) } @@ -52,18 +54,19 @@ class SchemaSpec extends org.specs2.Specification { def is = s2""" }""" val bodyOnlyInput = json"""{ "type": "object" }""" - val invalidInput = json"""[{ "type": "object" }]""" + val invalidInput = json"""[{ "type": "object" }]""" - val selfDescribingResult = Schema.SchemaBody.schemaBodyCirceDecoder.decodeJson(selfDescribingInput) must beRight.like { - case _: Schema.SchemaBody.SelfDescribing => ok - case e => ko(s"Unexpected decoded value $e") - } + val selfDescribingResult = + Schema.SchemaBody.schemaBodyCirceDecoder.decodeJson(selfDescribingInput) must beRight.like { + case _: Schema.SchemaBody.SelfDescribing => ok + case e => ko(s"Unexpected decoded value $e") + } val bodyOnlyResult = Schema.SchemaBody.schemaBodyCirceDecoder.decodeJson(bodyOnlyInput) must beRight.like { case _: Schema.SchemaBody.BodyOnly => ok - case e => ko(s"Unexpected decoded value $e") + case e => ko(s"Unexpected decoded value $e") } val invalidBodyResult = Schema.SchemaBody.schemaBodyCirceDecoder.decodeJson(invalidInput) must beLeft - selfDescribingResult and bodyOnlyResult and invalidBodyResult + selfDescribingResult.and(bodyOnlyResult).and(invalidBodyResult) } } diff --git a/src/test/scala/com/snowplowanalytics/iglu/server/model/VersionCursorSpec.scala b/src/test/scala/com/snowplowanalytics/iglu/server/model/VersionCursorSpec.scala index c6caf0e..3f354b8 100644 --- a/src/test/scala/com/snowplowanalytics/iglu/server/model/VersionCursorSpec.scala +++ b/src/test/scala/com/snowplowanalytics/iglu/server/model/VersionCursorSpec.scala @@ -16,7 +16,8 @@ package com.snowplowanalytics.iglu.server.model import com.snowplowanalytics.iglu.core.SchemaVer -class VersionCursorSpec extends org.specs2.Specification { def is = s2""" +class VersionCursorSpec extends org.specs2.Specification { + def is = s2""" previousExists validates new revision $e1 previousExists validates new model if no schemas were created for this model yet $e2 previousExists rejects new model if previous model does not exist yet $e3 @@ -27,41 +28,40 @@ class VersionCursorSpec extends org.specs2.Specification { def is = s2""" """ def e1 = { - val existing = List(SchemaVer.Full(1,0,0), SchemaVer.Full(1,0,1)) - val current = VersionCursor.get(SchemaVer.Full(1,1,0)) + val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 0, 1)) + val current = VersionCursor.get(SchemaVer.Full(1, 1, 0)) VersionCursor.previousExists(existing, current) must beTrue } - def e2 = { - val existing = List(SchemaVer.Full(1,0,0), SchemaVer.Full(1,1,0), SchemaVer.Full(1,0,1)) - val current = VersionCursor.get(SchemaVer.Full(2,0,0)) + val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 1, 0), SchemaVer.Full(1, 0, 1)) + val current = VersionCursor.get(SchemaVer.Full(2, 0, 0)) VersionCursor.previousExists(existing, current) must beTrue } def e3 = { - val existing = List(SchemaVer.Full(1,0,0), SchemaVer.Full(1,1,0)) - val current = VersionCursor.get(SchemaVer.Full(3,0,0)) + val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 1, 0)) + val current = VersionCursor.get(SchemaVer.Full(3, 0, 0)) VersionCursor.previousExists(existing, current) must beFalse } def e4 = { - val existing = List(SchemaVer.Full(1,0,0), SchemaVer.Full(1,1,0), SchemaVer.Full(1,1,1)) - val current = VersionCursor.get(SchemaVer.Full(1,1,2)) + val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 1, 0), SchemaVer.Full(1, 1, 1)) + val current = VersionCursor.get(SchemaVer.Full(1, 1, 2)) VersionCursor.previousExists(existing, current) must beTrue } def e5 = { - val existing = List(SchemaVer.Full(1,0,0), SchemaVer.Full(1,1,0), SchemaVer.Full(1,1,1)) - val current = VersionCursor.get(SchemaVer.Full(1,1,3)) + val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 1, 0), SchemaVer.Full(1, 1, 1)) + val current = VersionCursor.get(SchemaVer.Full(1, 1, 3)) VersionCursor.previousExists(existing, current) must beFalse } - def e6 = { - VersionCursor.isAllowed(SchemaVer.Full(1,0,0), List(SchemaVer.Full(1,0,0)), true) must beRight(()) - } + def e6 = + VersionCursor.isAllowed(SchemaVer.Full(1, 0, 0), List(SchemaVer.Full(1, 0, 0)), true) must beRight(()) - def e7 = { - VersionCursor.isAllowed(SchemaVer.Full(1,0,0), List(SchemaVer.Full(1,0,0)), false) must beLeft(VersionCursor.Inconsistency.AlreadyExists) - } + def e7 = + VersionCursor.isAllowed(SchemaVer.Full(1, 0, 0), List(SchemaVer.Full(1, 0, 0)), false) must beLeft( + VersionCursor.Inconsistency.AlreadyExists + ) } diff --git a/src/test/scala/com/snowplowanalytics/iglu/server/service/AuthServiceSpec.scala b/src/test/scala/com/snowplowanalytics/iglu/server/service/AuthServiceSpec.scala index e955fbc..d126f04 100644 --- a/src/test/scala/com/snowplowanalytics/iglu/server/service/AuthServiceSpec.scala +++ b/src/test/scala/com/snowplowanalytics/iglu/server/service/AuthServiceSpec.scala @@ -6,7 +6,8 @@ import fs2.Stream import org.http4s._ import org.http4s.rho.swagger.syntax.io.createRhoMiddleware -class AuthServiceSpec extends org.specs2.Specification { def is = s2""" +class AuthServiceSpec extends org.specs2.Specification { + def is = s2""" /keygen generates read-only API key pair for master key and JSON payload $e1 /keygen does not work with deprecated form API $e2 /keygen doesn't authorize without apikey header $e3 @@ -16,18 +17,20 @@ class AuthServiceSpec extends org.specs2.Specification { def is = s2""" def e1 = { import model._ - val req = Request(Method.POST, + val req = Request( + Method.POST, Uri.uri("/keygen"), headers = Headers.of(Header("apikey", SpecHelpers.masterKey.toString)), - body = Stream.emits("""{"vendorPrefix": "me.chuwy"}""").evalMap(c => IO.pure(c.toByte))) + body = Stream.emits("""{"vendorPrefix": "me.chuwy"}""").evalMap(c => IO.pure(c.toByte)) + ) val expected = Permission( - Permission.Vendor(List("me", "chuwy"),true), + Permission.Vendor(List("me", "chuwy"), true), Some(Permission.SchemaAction.Read), Set() ) - val response = AuthServiceSpec.state(List(req)) + val response = AuthServiceSpec.state(List(req)) val (_, state) = response.unsafeRunSync() state.permission must haveValues(expected) } @@ -35,47 +38,52 @@ class AuthServiceSpec extends org.specs2.Specification { def is = s2""" def e2 = { import model._ - val req = Request(Method.POST, + val req = Request( + Method.POST, Uri.uri("/keygen"), headers = Headers.of(Header("apikey", SpecHelpers.masterKey.toString)), body = Stream.emits("""vendor_prefix=ru.chuwy""").evalMap(c => IO.pure(c.toByte)) ).withContentType(headers.`Content-Type`(MediaType.application.`x-www-form-urlencoded`)) val expected = Permission( - Permission.Vendor(List("ru", "chuwy"),true), + Permission.Vendor(List("ru", "chuwy"), true), Some(Permission.SchemaAction.Read), Set() ) - val response = AuthServiceSpec.state(List(req)) + val response = AuthServiceSpec.state(List(req)) val (_, state) = response.unsafeRunSync() - state.permission must not haveValues(expected) + (state.permission must not).haveValues(expected) } def e3 = { - val req = Request(Method.POST, + val req = Request( + Method.POST, Uri.uri("/keygen"), - body = Stream.emits("""{"vendorPrefix": "me.chuwy"}""").evalMap(c => IO.pure(c.toByte))) + body = Stream.emits("""{"vendorPrefix": "me.chuwy"}""").evalMap(c => IO.pure(c.toByte)) + ) - val response = AuthServiceSpec.state(List(req)) + val response = AuthServiceSpec.state(List(req)) val (responses, state) = response.unsafeRunSync() val stateHaventChanged = state must beEqualTo(SpecHelpers.exampleState) - val unauthorized = responses.map(_.status) must beEqualTo(List(Status.Forbidden)) + val unauthorized = responses.map(_.status) must beEqualTo(List(Status.Forbidden)) - stateHaventChanged and unauthorized + stateHaventChanged.and(unauthorized) } def e4 = { - val req = Request[IO](Method.DELETE, + val req = Request[IO]( + Method.DELETE, Uri.uri("/keygen").withQueryParam("key", SpecHelpers.readKey.toString), - headers = Headers.of(Header("apikey", SpecHelpers.masterKey.toString))) + headers = Headers.of(Header("apikey", SpecHelpers.masterKey.toString)) + ) - val response = AuthServiceSpec.state(List(req)) + val response = AuthServiceSpec.state(List(req)) val (responses, state) = response.unsafeRunSync() - val nokey = state.permission must not haveKey(SpecHelpers.readKey) - val deletedResponse = responses.map(_.status) must beEqualTo(List(Status.Ok)) + val nokey = (state.permission must not).haveKey(SpecHelpers.readKey) + val deletedResponse = responses.map(_.status) must beEqualTo(List(Status.Ok)) - nokey and deletedResponse + nokey.and(deletedResponse) } } diff --git a/src/test/scala/com/snowplowanalytics/iglu/server/service/SchemaServiceSpec.scala b/src/test/scala/com/snowplowanalytics/iglu/server/service/SchemaServiceSpec.scala index 2c4ba32..663a101 100644 --- a/src/test/scala/com/snowplowanalytics/iglu/server/service/SchemaServiceSpec.scala +++ b/src/test/scala/com/snowplowanalytics/iglu/server/service/SchemaServiceSpec.scala @@ -17,12 +17,12 @@ import org.http4s.circe._ import org.http4s.client.Client import org.http4s.rho.swagger.syntax.io.createRhoMiddleware -import com.snowplowanalytics.iglu.core.{SchemaMap, SchemaVer, SchemaKey, SelfDescribingSchema} +import com.snowplowanalytics.iglu.core.{SchemaKey, SchemaMap, SchemaVer, SelfDescribingSchema} import com.snowplowanalytics.iglu.server.codecs.JsonCodecs._ -import com.snowplowanalytics.iglu.server.model.{ IgluResponse, Schema } +import com.snowplowanalytics.iglu.server.model.{IgluResponse, Schema} - -class SchemaServiceSpec extends org.specs2.Specification { def is = s2""" +class SchemaServiceSpec extends org.specs2.Specification { + def is = s2""" GET Returns 404 for non-existing schema $e1 Returns 200 and schema for existing public schema $e3 @@ -56,11 +56,11 @@ class SchemaServiceSpec extends org.specs2.Specification { def is = s2""" val result = for { response <- SchemaServiceSpec.request(List(req), false) - body <- response.as[Json] + body <- response.as[Json] } yield (response.status, body) val (status, body) = result.unsafeRunSync() - status must beEqualTo(Status.Ok) and (body must beEqualTo(SpecHelpers.selfSchemaZero)) + (status must beEqualTo(Status.Ok)).and(body must beEqualTo(SpecHelpers.selfSchemaZero)) } def e4 = { @@ -68,15 +68,16 @@ class SchemaServiceSpec extends org.specs2.Specification { def is = s2""" Request( Method.GET, Uri.uri("/com.acme/secret/jsonschema/1-0-0"), - headers = Headers.of(Header("apikey", SpecHelpers.readKeyAcme.toString))) + headers = Headers.of(Header("apikey", SpecHelpers.readKeyAcme.toString)) + ) val result = for { response <- SchemaServiceSpec.request(List(req), false) - body <- response.as[Json] + body <- response.as[Json] } yield (response.status, body) val (status, body) = result.unsafeRunSync() - status must beEqualTo(Status.Ok) and (body must beEqualTo(SpecHelpers.selfSchemaPrivate)) + (status must beEqualTo(Status.Ok)).and(body must beEqualTo(SpecHelpers.selfSchemaPrivate)) } def e5 = { @@ -84,70 +85,63 @@ class SchemaServiceSpec extends org.specs2.Specification { def is = s2""" Request( Method.GET, Uri.uri("/com.acme/secret/jsonschema/1-0-0"), - headers = Headers.of(Header("apikey", UUID.randomUUID().toString))) + headers = Headers.of(Header("apikey", UUID.randomUUID().toString)) + ) val result = for { response <- SchemaServiceSpec.request(List(req), false) - body <- response.as[IgluResponse] + body <- response.as[IgluResponse] } yield (response.status, body) val (status, body) = result.unsafeRunSync() - status must beEqualTo(Status.NotFound) and (body must beEqualTo(IgluResponse.SchemaNotFound)) + (status must beEqualTo(Status.NotFound)).and(body must beEqualTo(IgluResponse.SchemaNotFound)) } def e9 = { val req: Request[IO] = - Request( - Method.GET, - Uri.uri("/").withQueryParam("metadata", "1")) + Request(Method.GET, Uri.uri("/").withQueryParam("metadata", "1")) val result = for { response <- SchemaServiceSpec.request(List(req), false) - body <- response.as[List[Schema]] + body <- response.as[List[Schema]] } yield (response.status, body) - val expectedBody = SpecHelpers.schemas - .filter { case (_, m) => m.metadata.isPublic } - .map(_._2) + val expectedBody = SpecHelpers.schemas.filter { case (_, m) => m.metadata.isPublic }.map(_._2) val (status, body) = result.unsafeRunSync() - status must beEqualTo(Status.Ok) and (body must beEqualTo(expectedBody)) + (status must beEqualTo(Status.Ok)).and(body must beEqualTo(expectedBody)) } def e10 = { val req: Request[IO] = - Request( - Method.GET, - Uri.uri("/").withQueryParam("body", "1")) + Request(Method.GET, Uri.uri("/").withQueryParam("body", "1")) val result = for { response <- SchemaServiceSpec.request(List(req), false) - body <- response.as[List[SelfDescribingSchema[Json]]] + body <- response.as[List[SelfDescribingSchema[Json]]] } yield (response.status, body.map(_.self)) - val expectedBody = SpecHelpers.schemas - .filter { case (_, m) => m.metadata.isPublic } - .map(_._2.schemaMap) + val expectedBody = SpecHelpers.schemas.filter { case (_, m) => m.metadata.isPublic }.map(_._2.schemaMap) val (status, body) = result.unsafeRunSync() - status must beEqualTo(Status.Ok) and (body must beEqualTo(expectedBody)) + (status must beEqualTo(Status.Ok)).and(body must beEqualTo(expectedBody)) } - def e6 = { val req: Request[IO] = Request( Method.GET, Uri.uri("/com.acme/secret/jsonschema/1-0-0"), - headers = Headers.of(Header("apikey", "not-uuid"))) + headers = Headers.of(Header("apikey", "not-uuid")) + ) val result = for { response <- SchemaServiceSpec.request(List(req), false) - body <- response.as[Json] + body <- response.as[Json] } yield (response.status, body) val (status, body) = result.unsafeRunSync() - status must beEqualTo(Status.BadRequest) and (body.noSpaces must contain("Invalid UUID")) + (status must beEqualTo(Status.BadRequest)).and(body.noSpaces must contain("Invalid UUID")) } def e7 = { @@ -156,36 +150,35 @@ class SchemaServiceSpec extends org.specs2.Specification { def is = s2""" val result = for { response <- SchemaServiceSpec.request(List(req), false) - body <- response.as[List[SchemaKey]] + body <- response.as[List[SchemaKey]] } yield (response.status, body) val expected = List( - SchemaKey("com.acme", "event", "jsonschema", SchemaVer.Full(1,0,0)), - SchemaKey("com.acme", "event", "jsonschema", SchemaVer.Full(1,0,1)) + SchemaKey("com.acme", "event", "jsonschema", SchemaVer.Full(1, 0, 0)), + SchemaKey("com.acme", "event", "jsonschema", SchemaVer.Full(1, 0, 1)) ) val (status, body) = result.unsafeRunSync() - status must beEqualTo(Status.Ok) and (body must beEqualTo(expected)) + (status must beEqualTo(Status.Ok)).and(body must beEqualTo(expected)) } def e8 = { val req: Request[IO] = - Request(Method.GET, uri"/") - .withHeaders(Headers.of(Header("apikey", SpecHelpers.masterKey.toString))) + Request(Method.GET, uri"/").withHeaders(Headers.of(Header("apikey", SpecHelpers.masterKey.toString))) val result = for { response <- SchemaServiceSpec.request(List(req), false) - body <- response.as[List[SchemaKey]] + body <- response.as[List[SchemaKey]] } yield (response.status, body) val expected = List( - SchemaKey("com.acme", "event", "jsonschema", SchemaVer.Full(1,0,0)), - SchemaKey("com.acme", "event", "jsonschema", SchemaVer.Full(1,0,1)), - SchemaKey("com.acme", "secret", "jsonschema", SchemaVer.Full(1,0,0)) + SchemaKey("com.acme", "event", "jsonschema", SchemaVer.Full(1, 0, 0)), + SchemaKey("com.acme", "event", "jsonschema", SchemaVer.Full(1, 0, 1)), + SchemaKey("com.acme", "secret", "jsonschema", SchemaVer.Full(1, 0, 0)) ) val (status, body) = result.unsafeRunSync() - status must beEqualTo(Status.Ok) and (body must beEqualTo(expected)) + (status must beEqualTo(Status.Ok)).and(body must beEqualTo(expected)) } def e2 = { @@ -214,10 +207,13 @@ class SchemaServiceSpec extends org.specs2.Specification { def is = s2""" val (requests, state) = SchemaServiceSpec.state(reqs, false).unsafeRunSync() val dbExpectation = state.schemas.mapValues(s => (s.metadata.isPublic, s.body)) must havePair( - (SchemaMap("com.acme", "nonexistent", "jsonschema", SchemaVer.Full(1,0,0)), (false, json"""{"type": "object"}""")) + ( + SchemaMap("com.acme", "nonexistent", "jsonschema", SchemaVer.Full(1, 0, 0)), + (false, json"""{"type": "object"}""") + ) ) val requestExpectation = requests.lastOption.map(_.status) must beSome(Status.Ok) - dbExpectation and requestExpectation + dbExpectation.and(requestExpectation) } def e11 = { @@ -265,11 +261,14 @@ class SchemaServiceSpec extends org.specs2.Specification { def is = s2""" val (requests, state) = SchemaServiceSpec.state(reqs, false).unsafeRunSync() val dbExpectation = state.schemas.mapValues(s => (s.metadata.isPublic, s.body)) must havePair( - (SchemaMap("com.acme", "nonexistent", "jsonschema", SchemaVer.Full(1,0,0)), (false, json"""{"type": "object"}""")) + ( + SchemaMap("com.acme", "nonexistent", "jsonschema", SchemaVer.Full(1, 0, 0)), + (false, json"""{"type": "object"}""") + ) ) val putRequestExpectation = requests.get(1).map(_.status) must beSome(Status.Conflict) val getRequestExpectation = requests.lastOption.map(_.status) must beSome(Status.Ok) - dbExpectation and putRequestExpectation and getRequestExpectation + dbExpectation.and(putRequestExpectation).and(getRequestExpectation) } def e12 = { @@ -288,17 +287,21 @@ class SchemaServiceSpec extends org.specs2.Specification { def is = s2""" val exampleSchema = Stream.emits(selfDescribingSchema.noSpaces.stripMargin.getBytes).covary[IO] val req = Request[IO](Method.PUT, Uri.uri("/com.acme/nonexistent/jsonschema/1-2-0")) - .withContentType(headers.`Content-Type`(MediaType.application.json)) - .withHeaders(Headers.of(Header("apikey", SpecHelpers.masterKey.toString))) - .withBodyStream(exampleSchema) + .withContentType(headers.`Content-Type`(MediaType.application.json)) + .withHeaders(Headers.of(Header("apikey", SpecHelpers.masterKey.toString))) + .withBodyStream(exampleSchema) val result = for { response <- SchemaServiceSpec.request(List(req), false) - body <- response.as[Json] + body <- response.as[Json] } yield (response.status, body) val (status, body) = result.unsafeRunSync() - status must beEqualTo(Status.Conflict) and (body.noSpaces must contain("Preceding SchemaVer in the group is missing, check that schemas published in proper order")) + (status must beEqualTo(Status.Conflict)).and( + body.noSpaces must contain( + "Preceding SchemaVer in the group is missing, check that schemas published in proper order" + ) + ) } def e13 = { @@ -346,10 +349,13 @@ class SchemaServiceSpec extends org.specs2.Specification { def is = s2""" val (requests, state) = SchemaServiceSpec.state(reqs, true).unsafeRunSync() val dbExpectation = state.schemas.mapValues(s => (s.metadata.isPublic, s.body)) must havePair( - (SchemaMap("com.acme", "nonexistent", "jsonschema", SchemaVer.Full(1,0,0)), (false, json"""{"type": "object", "additionalProperties": true}""")) + ( + SchemaMap("com.acme", "nonexistent", "jsonschema", SchemaVer.Full(1, 0, 0)), + (false, json"""{"type": "object", "additionalProperties": true}""") + ) ) val requestExpectation = requests.lastOption.map(_.status) must beSome(Status.Ok) - dbExpectation and requestExpectation + dbExpectation.and(requestExpectation) } def e14 = { @@ -359,17 +365,18 @@ class SchemaServiceSpec extends org.specs2.Specification { def is = s2""" val response = SchemaServiceSpec.request(List(req), false) val result = for { - r <- response + r <- response body <- r.bodyText.compile.foldMonoid } yield (r.status, body) // Text body transformed to JSON later in HttpApp val (status, body) = result.unsafeRunSync() - (status must beEqualTo(Status.BadRequest)) and (body must beEqualTo("Cannot parse version part 'boom' as SchemaVer, INVALID_SCHEMAVER")) + (status must beEqualTo(Status.BadRequest)) + .and(body must beEqualTo("Cannot parse version part 'boom' as SchemaVer, INVALID_SCHEMAVER")) } def e15 = { - val simpleSchema = json"""{"type": "object"}""" + val simpleSchema = json"""{"type": "object"}""" val exampleSchema = Stream.emits(simpleSchema.noSpaces.stripMargin.getBytes).covary[IO] val reqs: List[Request[IO]] = List( @@ -402,7 +409,7 @@ class SchemaServiceSpec extends org.specs2.Specification { def is = s2""" val result = for { response <- SchemaServiceSpec.request(reqs, false) - last <- response.bodyText.compile.foldMonoid + last <- response.bodyText.compile.foldMonoid } yield last result.unsafeRunSync() must beEqualTo(expected) @@ -414,20 +421,26 @@ object SchemaServiceSpec { val client: Client[IO] = Client.fromHttpApp(HttpApp[IO](r => Response[IO]().withEntity(r.body).pure[IO])) - def request(reqs: List[Request[IO]], patchesAllowed: Boolean): IO[Response[IO]] = { + def request(reqs: List[Request[IO]], patchesAllowed: Boolean): IO[Response[IO]] = for { storage <- InMemory.getInMemory[IO](SpecHelpers.exampleState) - service = SchemaService.asRoutes(patchesAllowed, Webhook.WebhookClient(List(), client))(storage, SpecHelpers.ctx, createRhoMiddleware()) + service = SchemaService.asRoutes(patchesAllowed, Webhook.WebhookClient(List(), client))( + storage, + SpecHelpers.ctx, + createRhoMiddleware() + ) responses <- reqs.traverse(service.run).value } yield responses.flatMap(_.lastOption).getOrElse(Response(Status.NotFound)) - } - def state(reqs: List[Request[IO]], patchesAllowed: Boolean): IO[(List[Response[IO]], InMemory.State)] = { + def state(reqs: List[Request[IO]], patchesAllowed: Boolean): IO[(List[Response[IO]], InMemory.State)] = for { storage <- InMemory.getInMemory[IO](SpecHelpers.exampleState) - service = SchemaService.asRoutes(patchesAllowed, Webhook.WebhookClient(List(), client))(storage, SpecHelpers.ctx, createRhoMiddleware()) + service = SchemaService.asRoutes(patchesAllowed, Webhook.WebhookClient(List(), client))( + storage, + SpecHelpers.ctx, + createRhoMiddleware() + ) responses <- reqs.traverse(service.run).value - state <- storage.ref.get + state <- storage.ref.get } yield (responses.getOrElse(List.empty), state) - } } diff --git a/src/test/scala/com/snowplowanalytics/iglu/server/service/ValidationServiceSpec.scala b/src/test/scala/com/snowplowanalytics/iglu/server/service/ValidationServiceSpec.scala index b157dfe..e790b23 100644 --- a/src/test/scala/com/snowplowanalytics/iglu/server/service/ValidationServiceSpec.scala +++ b/src/test/scala/com/snowplowanalytics/iglu/server/service/ValidationServiceSpec.scala @@ -17,7 +17,8 @@ import org.http4s.rho.swagger.syntax.io.createRhoMiddleware import SpecHelpers.toBytes import ValidationServiceSpec._ -class ValidationServiceSpec extends org.specs2.Specification { def is = s2""" +class ValidationServiceSpec extends org.specs2.Specification { + def is = s2""" POST /validate/schema/jsonschema returns linting errors for self-describing schema $e1 POST /validate/schema/jsonschema returns success message for valid self-describing schema $e2 POST /validate/schema/jsonschema reports about unknown self keyword without metaschema $e3 @@ -32,7 +33,6 @@ class ValidationServiceSpec extends org.specs2.Specification { def is = s2""" POST /validate/instance pretends a private schema does not exist if apikey is inappropriate $e10 """ - def e1 = { val selfDescribingSchema = json""" { @@ -44,7 +44,7 @@ class ValidationServiceSpec extends org.specs2.Specification { def is = s2""" }, "type": "object" }""" - val expected = json"""{ + val expected = json"""{ "message":"The schema does not conform to a JSON Schema v4 specification", "report":[ {"message":"self is unknown keyword for vanilla $$schema, use http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#", "level":"ERROR", "pointer":"/"}, @@ -78,7 +78,8 @@ class ValidationServiceSpec extends org.specs2.Specification { def is = s2""" "description": "schema with no issues", "properties": { } }""" - val expected = json"""{"message" : "The schema provided is a valid self-describing iglu:com.acme/nonexistent/jsonschema/1-0-0 schema"}""" + val expected = + json"""{"message" : "The schema provided is a valid self-describing iglu:com.acme/nonexistent/jsonschema/1-0-0 schema"}""" val request = Request[IO](Method.POST, uri"/validate/schema/jsonschema") .withContentType(headers.`Content-Type`(MediaType.application.json)) @@ -102,7 +103,7 @@ class ValidationServiceSpec extends org.specs2.Specification { def is = s2""" "description": "schema with no issues", "properties": { } }""" - val expected = json"""{ + val expected = json"""{ "message":"The schema does not conform to a JSON Schema v4 specification", "report":[ {"message":"self is unknown keyword for vanilla $$schema, use http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#", "level":"ERROR", "pointer":"/"}, @@ -121,7 +122,8 @@ class ValidationServiceSpec extends org.specs2.Specification { def is = s2""" } def e4 = { - val selfDescribingSchema = json"""{"type": "object", "description": "non-self-describing schema", "properties": {}}""" + val selfDescribingSchema = + json"""{"type": "object", "description": "non-self-describing schema", "properties": {}}""" val expected = json"""{ "message" : "The schema does not conform to a JSON Schema v4 specification", "report" : [ @@ -141,8 +143,8 @@ class ValidationServiceSpec extends org.specs2.Specification { def is = s2""" def e5 = { val expected = "The request body was malformed." - val request = Request[IO](Method.POST, Uri.uri("/validate/schema/jsonschema")) - .withBodyStream(Stream.emits("non-json".getBytes)) + val request = + Request[IO](Method.POST, Uri.uri("/validate/schema/jsonschema")).withBodyStream(Stream.emits("non-json".getBytes)) val response = sendRequestGetText(request) @@ -164,8 +166,7 @@ class ValidationServiceSpec extends org.specs2.Specification { def is = s2""" ] }""" - val request = Request[IO](Method.POST, Uri.uri("/validate/instance")) - .withBodyStream(toBytes(instance)) + val request = Request[IO](Method.POST, Uri.uri("/validate/instance")).withBodyStream(toBytes(instance)) val response = sendRequest(request) response must beEqualTo(expected) @@ -176,8 +177,7 @@ class ValidationServiceSpec extends org.specs2.Specification { def is = s2""" json"""{"schema" : "iglu:com.acme/event/jsonschema/1-0-0", "data" : {"one": null} } """ val expected = json"""{"message" : "Instance is valid iglu:com.acme/event/jsonschema/1-0-0"}""" - val request = Request[IO](Method.POST, Uri.uri("/validate/instance")) - .withBodyStream(toBytes(instance)) + val request = Request[IO](Method.POST, Uri.uri("/validate/instance")).withBodyStream(toBytes(instance)) val response = sendRequest(request) response must beEqualTo(expected) @@ -189,15 +189,14 @@ class ValidationServiceSpec extends org.specs2.Specification { def is = s2""" val expected = json"""{"message" : "The schema is not found"}""" - val request = Request[IO](Method.POST, Uri.uri("/validate/instance")) - .withBodyStream(toBytes(instance)) + val request = Request[IO](Method.POST, Uri.uri("/validate/instance")).withBodyStream(toBytes(instance)) val (responses, _) = ValidationServiceSpec.request(List(request)).unsafeRunSync() - val response = responses.last + val response = responses.last - val bodyExpectation = response.as[Json].unsafeRunSync() must beEqualTo(expected) + val bodyExpectation = response.as[Json].unsafeRunSync() must beEqualTo(expected) val statusExpectation = response.status.code must beEqualTo(404) - bodyExpectation and statusExpectation + bodyExpectation.and(statusExpectation) } def e9 = { @@ -221,11 +220,11 @@ class ValidationServiceSpec extends org.specs2.Specification { def is = s2""" .withBodyStream(toBytes(instance)) val (responses, _) = ValidationServiceSpec.request(List(request)).unsafeRunSync() - val response = responses.last + val response = responses.last - val bodyExpectation = response.as[Json].unsafeRunSync() must beEqualTo(expected) + val bodyExpectation = response.as[Json].unsafeRunSync() must beEqualTo(expected) val statusExpectation = response.status.code must beEqualTo(404) - bodyExpectation and statusExpectation + bodyExpectation.and(statusExpectation) } def e11 = { @@ -272,16 +271,11 @@ class ValidationServiceSpec extends org.specs2.Specification { def is = s2""" object ValidationServiceSpec { def request(reqs: List[Request[IO]]) = - SpecHelpers - .state(storage => ValidationService.asRoutes(storage, SpecHelpers.ctx, createRhoMiddleware()))(reqs) + SpecHelpers.state(storage => ValidationService.asRoutes(storage, SpecHelpers.ctx, createRhoMiddleware()))(reqs) def sendRequest(req: Request[IO]) = - request(List(req)) - .flatMap { case (responses, _) => responses.last.as[Json] } - .unsafeRunSync() + request(List(req)).flatMap { case (responses, _) => responses.last.as[Json] }.unsafeRunSync() def sendRequestGetText(req: Request[IO]) = - request(List(req)) - .flatMap { case (responses, _) => responses.last.bodyText.compile.foldMonoid } - .unsafeRunSync() + request(List(req)).flatMap { case (responses, _) => responses.last.bodyText.compile.foldMonoid }.unsafeRunSync() } diff --git a/src/test/scala/com/snowplowanalytics/iglu/server/storage/PostgresSpec.scala b/src/test/scala/com/snowplowanalytics/iglu/server/storage/PostgresSpec.scala index 5f48ade..610cf1c 100644 --- a/src/test/scala/com/snowplowanalytics/iglu/server/storage/PostgresSpec.scala +++ b/src/test/scala/com/snowplowanalytics/iglu/server/storage/PostgresSpec.scala @@ -29,7 +29,7 @@ import doobie.implicits._ import eu.timepit.refined.types.numeric.NonNegInt import com.snowplowanalytics.iglu.core.{SchemaMap, SchemaVer} -import com.snowplowanalytics.iglu.server.model.{ SchemaDraft, Permission } +import com.snowplowanalytics.iglu.server.model.{Permission, SchemaDraft} import com.snowplowanalytics.iglu.server.migrations.Bootstrap import scala.concurrent.ExecutionContext @@ -42,10 +42,12 @@ class PostgresSpec extends Specification with BeforeAll with IOChecker { implicit val cs = IO.contextShift(ExecutionContext.global) val transactor = Transactor.fromDriverManager[IO]( - "org.postgresql.Driver", "jdbc:postgresql://localhost:5432/testdb", "postgres", "iglusecret" + "org.postgresql.Driver", + "jdbc:postgresql://localhost:5432/testdb", + "postgres", + "iglusecret" ) - def beforeAll(): Unit = { val dropStatement = List( @@ -53,9 +55,8 @@ class PostgresSpec extends Specification with BeforeAll with IOChecker { fr"DROP TABLE IF EXISTS" ++ Postgres.SchemasTable, fr"DROP TABLE IF EXISTS" ++ Postgres.DraftsTable, fr"DROP TYPE IF EXISTS schema_action", - fr"DROP TYPE IF EXISTS key_action") - .map(_.update.run).sequence - + fr"DROP TYPE IF EXISTS key_action" + ).map(_.update.run).sequence val action = dropStatement.transact(transactor) *> Bootstrap.initialize(transactor) @@ -76,7 +77,11 @@ class PostgresSpec extends Specification with BeforeAll with IOChecker { } "typecheck addSchema" in { - check(Postgres.Sql.addSchema(SchemaMap("does", "not", "exist", SchemaVer.Full(1, 0, 0)), Json.fromFields(List.empty), true)) + check( + Postgres + .Sql + .addSchema(SchemaMap("does", "not", "exist", SchemaVer.Full(1, 0, 0)), Json.fromFields(List.empty), true) + ) } "typecheck getDraft" in { @@ -88,7 +93,14 @@ class PostgresSpec extends Specification with BeforeAll with IOChecker { } "typecheck addPermission" in { - check(Postgres.Sql.addPermission(UUID.fromString("6907ba19-b6e0-4126-a931-dd236eec2736"), Permission(Permission.Vendor(List("com", "acme"), false), None, Set.empty))) + check( + Postgres + .Sql + .addPermission( + UUID.fromString("6907ba19-b6e0-4126-a931-dd236eec2736"), + Permission(Permission.Vendor(List("com", "acme"), false), None, Set.empty) + ) + ) } "typecheck deletePermission" in {