From af22829be941f11a71ee0ffe80cf83795cc68a40 Mon Sep 17 00:00:00 2001 From: enriquenieto Date: Mon, 16 Sep 2019 14:05:48 +0200 Subject: [PATCH 1/4] WIP fixing compilation problems in server module --- build.sbt | 14 +-- .../scalaexercises/evaluator/Decoders.scala | 0 .../evaluator/EvaluatorAPI.scala | 0 .../evaluator/EvaluatorClient.scala | 0 .../evaluator/EvaluatorResponses.scala | 0 .../evaluator/api/Evaluator.scala | 0 .../evaluator/free/algebra/EvaluatorOps.scala | 0 .../free/interpreters/Interpreter.scala | 0 .../evaluator/http/HttpClient.scala | 0 .../evaluator/http/HttpRequestBuilder.scala | 0 .../scalaexercises/evaluator/implicits.scala | 0 .../org/scalaexercises/evaluator/codecs.scala | 31 ++---- .../scalaexercises/evaluator/services.scala | 100 +++++++++--------- .../org/scalaexercises/evaluator/types.scala | 0 14 files changed, 65 insertions(+), 80 deletions(-) rename client/{shared => }/src/main/scala/org/scalaexercises/evaluator/Decoders.scala (100%) rename client/{shared => }/src/main/scala/org/scalaexercises/evaluator/EvaluatorAPI.scala (100%) rename client/{shared => }/src/main/scala/org/scalaexercises/evaluator/EvaluatorClient.scala (100%) rename client/{shared => }/src/main/scala/org/scalaexercises/evaluator/EvaluatorResponses.scala (100%) rename client/{shared => }/src/main/scala/org/scalaexercises/evaluator/api/Evaluator.scala (100%) rename client/{shared => }/src/main/scala/org/scalaexercises/evaluator/free/algebra/EvaluatorOps.scala (100%) rename client/{shared => }/src/main/scala/org/scalaexercises/evaluator/free/interpreters/Interpreter.scala (100%) rename client/{shared => }/src/main/scala/org/scalaexercises/evaluator/http/HttpClient.scala (100%) rename client/{shared => }/src/main/scala/org/scalaexercises/evaluator/http/HttpRequestBuilder.scala (100%) rename client/{shared => }/src/main/scala/org/scalaexercises/evaluator/implicits.scala (100%) rename shared/{shared => }/src/main/scala/org/scalaexercises/evaluator/types.scala (100%) diff --git a/build.sbt b/build.sbt index 9a700949..ab850687 100644 --- a/build.sbt +++ b/build.sbt @@ -1,14 +1,9 @@ -lazy val root = (project in file(".")) - .settings(mainClass in Universal := Some("org.scalaexercises.evaluator.EvaluatorServer")) - .settings(stage := (stage in Universal in `evaluator-server`).value) - .settings(noPublishSettings: _*) - .aggregate(`evaluator-server`, `evaluator-client`) - lazy val `evaluator-shared` = (project in file("shared")) .enablePlugins(AutomateHeaderPlugin) .settings(name := "evaluator-shared") lazy val `evaluator-client` = (project in file("client")) + .dependsOn(`evaluator-shared`) .enablePlugins(AutomateHeaderPlugin) .settings( name := "evaluator-client", @@ -16,6 +11,7 @@ lazy val `evaluator-client` = (project in file("client")) ) lazy val `evaluator-server` = (project in file("server")) + .dependsOn(`evaluator-shared`) .enablePlugins(JavaAppPackaging) .enablePlugins(AutomateHeaderPlugin) .enablePlugins(sbtdocker.DockerPlugin) @@ -40,6 +36,12 @@ lazy val `smoketests` = (project in file("smoketests")) ) .settings(buildInfoSettings: _*) +lazy val root = (project in file(".")) + .settings(mainClass in Universal := Some("org.scalaexercises.evaluator.EvaluatorServer")) + .settings(stage := (stage in Universal in `evaluator-server`).value) + .settings(noPublishSettings: _*) + .aggregate(`evaluator-server`, `evaluator-client`, `evaluator-shared`, `smoketests`) + addCommandAlias( "publishSignedAll", ";evaluator-sharedJS/publishSigned;evaluator-sharedJVM/publishSigned;evaluator-clientJS/publishSigned;evaluator-clientJVM/publishSigned" diff --git a/client/shared/src/main/scala/org/scalaexercises/evaluator/Decoders.scala b/client/src/main/scala/org/scalaexercises/evaluator/Decoders.scala similarity index 100% rename from client/shared/src/main/scala/org/scalaexercises/evaluator/Decoders.scala rename to client/src/main/scala/org/scalaexercises/evaluator/Decoders.scala diff --git a/client/shared/src/main/scala/org/scalaexercises/evaluator/EvaluatorAPI.scala b/client/src/main/scala/org/scalaexercises/evaluator/EvaluatorAPI.scala similarity index 100% rename from client/shared/src/main/scala/org/scalaexercises/evaluator/EvaluatorAPI.scala rename to client/src/main/scala/org/scalaexercises/evaluator/EvaluatorAPI.scala diff --git a/client/shared/src/main/scala/org/scalaexercises/evaluator/EvaluatorClient.scala b/client/src/main/scala/org/scalaexercises/evaluator/EvaluatorClient.scala similarity index 100% rename from client/shared/src/main/scala/org/scalaexercises/evaluator/EvaluatorClient.scala rename to client/src/main/scala/org/scalaexercises/evaluator/EvaluatorClient.scala diff --git a/client/shared/src/main/scala/org/scalaexercises/evaluator/EvaluatorResponses.scala b/client/src/main/scala/org/scalaexercises/evaluator/EvaluatorResponses.scala similarity index 100% rename from client/shared/src/main/scala/org/scalaexercises/evaluator/EvaluatorResponses.scala rename to client/src/main/scala/org/scalaexercises/evaluator/EvaluatorResponses.scala diff --git a/client/shared/src/main/scala/org/scalaexercises/evaluator/api/Evaluator.scala b/client/src/main/scala/org/scalaexercises/evaluator/api/Evaluator.scala similarity index 100% rename from client/shared/src/main/scala/org/scalaexercises/evaluator/api/Evaluator.scala rename to client/src/main/scala/org/scalaexercises/evaluator/api/Evaluator.scala diff --git a/client/shared/src/main/scala/org/scalaexercises/evaluator/free/algebra/EvaluatorOps.scala b/client/src/main/scala/org/scalaexercises/evaluator/free/algebra/EvaluatorOps.scala similarity index 100% rename from client/shared/src/main/scala/org/scalaexercises/evaluator/free/algebra/EvaluatorOps.scala rename to client/src/main/scala/org/scalaexercises/evaluator/free/algebra/EvaluatorOps.scala diff --git a/client/shared/src/main/scala/org/scalaexercises/evaluator/free/interpreters/Interpreter.scala b/client/src/main/scala/org/scalaexercises/evaluator/free/interpreters/Interpreter.scala similarity index 100% rename from client/shared/src/main/scala/org/scalaexercises/evaluator/free/interpreters/Interpreter.scala rename to client/src/main/scala/org/scalaexercises/evaluator/free/interpreters/Interpreter.scala diff --git a/client/shared/src/main/scala/org/scalaexercises/evaluator/http/HttpClient.scala b/client/src/main/scala/org/scalaexercises/evaluator/http/HttpClient.scala similarity index 100% rename from client/shared/src/main/scala/org/scalaexercises/evaluator/http/HttpClient.scala rename to client/src/main/scala/org/scalaexercises/evaluator/http/HttpClient.scala diff --git a/client/shared/src/main/scala/org/scalaexercises/evaluator/http/HttpRequestBuilder.scala b/client/src/main/scala/org/scalaexercises/evaluator/http/HttpRequestBuilder.scala similarity index 100% rename from client/shared/src/main/scala/org/scalaexercises/evaluator/http/HttpRequestBuilder.scala rename to client/src/main/scala/org/scalaexercises/evaluator/http/HttpRequestBuilder.scala diff --git a/client/shared/src/main/scala/org/scalaexercises/evaluator/implicits.scala b/client/src/main/scala/org/scalaexercises/evaluator/implicits.scala similarity index 100% rename from client/shared/src/main/scala/org/scalaexercises/evaluator/implicits.scala rename to client/src/main/scala/org/scalaexercises/evaluator/implicits.scala diff --git a/server/src/main/scala/org/scalaexercises/evaluator/codecs.scala b/server/src/main/scala/org/scalaexercises/evaluator/codecs.scala index bd092209..9ee17521 100644 --- a/server/src/main/scala/org/scalaexercises/evaluator/codecs.scala +++ b/server/src/main/scala/org/scalaexercises/evaluator/codecs.scala @@ -5,36 +5,21 @@ package org.scalaexercises.evaluator -import org.http4s._, org.http4s.dsl._ +import cats.effect.Sync import io.circe.{Decoder, Encoder, Json, Printer} -import org.http4s.headers.`Content-Type` import io.circe.jawn.CirceSupportParser.facade +import org.http4s +import org.http4s.circe._ +import org.http4s._ +import org.http4s.dsl._ +import org.http4s.headers.`Content-Type` /** Provides Json serialization codecs for the http4s services */ trait Http4sCodecInstances { - implicit val jsonDecoder: EntityDecoder[Json] = jawn.jawnDecoder(facade) - - implicit def jsonDecoderOf[A](implicit decoder: Decoder[A]): EntityDecoder[A] = - jsonDecoder.flatMapR { json => - decoder - .decodeJson(json) - .fold( - failure => - DecodeResult.failure( - InvalidMessageBodyFailure(s"Could not decode JSON: $json", Some(failure))), - DecodeResult.success(_) - ) - } - - implicit val jsonEntityEncoder: EntityEncoder[Json] = EntityEncoder[String] - .contramap[Json] { json => - Printer.noSpaces.pretty(json) - } - .withContentType(`Content-Type`(MediaType.`application/json`)) + implicit def entityDecoderOf[F[_]: Sync, A: Decoder]: EntityDecoder[F, A] = jsonOf[F, A] - implicit def jsonEncoderOf[A](implicit encoder: Encoder[A]): EntityEncoder[A] = - jsonEntityEncoder.contramap[A](encoder.apply) + implicit def entityEncoderOf[F[_]: Sync, A: Encoder]: EntityEncoder[F, A] = jsonEncoderOf[F, A] } diff --git a/server/src/main/scala/org/scalaexercises/evaluator/services.scala b/server/src/main/scala/org/scalaexercises/evaluator/services.scala index aa5dfe8f..05ecd000 100644 --- a/server/src/main/scala/org/scalaexercises/evaluator/services.scala +++ b/server/src/main/scala/org/scalaexercises/evaluator/services.scala @@ -5,11 +5,16 @@ package org.scalaexercises.evaluator -import monix.execution.Scheduler +import cats.effect.{ExitCode, IO, IOApp, Sync} +import cats.implicits._ +import codecs._ +import io.circe.generic.auto._ +import io.circe.syntax._ import org.http4s._ import org.http4s.dsl._ import org.http4s.server.blaze._ import org.log4s.getLogger +import org.http4s.syntax.kleisli.http4sKleisliResponseSyntax import scala.concurrent.duration._ import scala.language.postfixOps @@ -17,8 +22,6 @@ import scala.language.postfixOps object services { import EvalResponse.messages._ - import codecs._ - import io.circe.generic.auto._ private val logger = getLogger @@ -34,19 +37,19 @@ object services { Header("Access-Control-Max-Age", 1.day.toSeconds.toString()) ) - def evalService = - auth(HttpService { - case req @ POST -> Root / "eval" => - import io.circe.syntax._ - req - .decode[EvalRequest] { - evalRequest => - evaluator.eval[Any]( - code = evalRequest.code, - remotes = evalRequest.resolvers, - dependencies = evalRequest.dependencies - ) flatMap { - (result: EvalResult[_]) => + def service[F[_]: Sync] = new Http4sDsl[F] { + def httpApp = + HttpRoutes + .of[F] { + // Evaluator service + case req @ POST -> Root / "eval" => + req + .decode[EvalRequest] { evalRequest => + evaluator.eval[Any]( + code = evalRequest.code, + remotes = evalRequest.resolvers, + dependencies = evalRequest.dependencies + ) flatMap { (result: EvalResult[_]) => val response = result match { case EvalSuccess(cis, res, out) => EvalResponse( @@ -77,52 +80,47 @@ object services { EvalResponse(`Unforeseen Exception`, None, None, None, Map.empty) } Ok(response.asJson) + } } - } - .map((r: Response) => r.putHeaders(corsHeaders: _*)) - }) - - def loaderIOService = HttpService { - - case _ -> Root => - MethodNotAllowed() - - case GET -> Root / "loaderio-1318d1b3e06b7bc96dd5de5716f57496" => - Ok("loaderio-1318d1b3e06b7bc96dd5de5716f57496") - } - - // CORS middleware in http4s can't be combined with our `auth` middleware. We need to handle CORS calls ourselves. - def optionsService = HttpService { - case OPTIONS -> Root / "eval" => - Ok().putHeaders(corsHeaders: _*) + .map((r: Response[F]) => r.putHeaders(corsHeaders: _*)) + // LoaderIO service + case _ -> Root => + MethodNotAllowed() + case GET -> Root / "loaderio-1318d1b3e06b7bc96dd5de5716f57496" => + Ok("loaderio-1318d1b3e06b7bc96dd5de5716f57496") + // Options service + // CORS middleware in http4s can't be combined with our `auth` middleware. We need to handle CORS calls ourselves. + case OPTIONS -> Root / "eval" => + Ok().map(_.headers.put(corsHeaders: _*)) //putHeaders(corsHeaders: _*) + } + .orNotFound } } -object EvaluatorServer extends App { +object EvaluatorServer extends IOApp { import services._ private[this] val logger = getLogger - val ip = Option(System.getenv("HOST")).getOrElse("0.0.0.0") + lazy val ip = Option(System.getenv("HOST")).getOrElse("0.0.0.0") - val port = (Option(System.getenv("PORT")) orElse + lazy val port = (Option(System.getenv("PORT")) orElse Option(System.getProperty("http.port"))).map(_.toInt).getOrElse(8080) - logger.info(s"Initializing Evaluator at $ip:$port") - - // The order in which services are mounted is really important. They're executed from bottom to top, and they won't be - // checked for repeated methods or routes. That's why we set the `optionsService` before the `evalService` one, as if - // not, execution of a OPTIONS call to /eval would lead to `evalService`, even if that service doesn't recognize the - // OPTIONS verb. - BlazeBuilder - .bindHttp(port, ip) - .mountService(evalService) - .mountService(optionsService) - .mountService(loaderIOService) - .start - .run - .awaitShutdown() - + override def run(args: List[String]): IO[ExitCode] = { + logger.info(s"Initializing Evaluator at $ip:$port") + + // The order in which services are mounted is really important. They're executed from bottom to top, and they won't be + // checked for repeated methods or routes. That's why we set the `optionsService` before the `evalService` one, as if + // not, execution of a OPTIONS call to /eval would lead to `evalService`, even if that service doesn't recognize the + // OPTIONS verb. + BlazeServerBuilder[IO] + .bindHttp(port, ip) + .withHttpApp(service[IO].httpApp) + .serve + .compile + .lastOrError + } } diff --git a/shared/shared/src/main/scala/org/scalaexercises/evaluator/types.scala b/shared/src/main/scala/org/scalaexercises/evaluator/types.scala similarity index 100% rename from shared/shared/src/main/scala/org/scalaexercises/evaluator/types.scala rename to shared/src/main/scala/org/scalaexercises/evaluator/types.scala From 64185500e32025e592fa9718ebae4d7b57b362af Mon Sep 17 00:00:00 2001 From: enriquenieto Date: Wed, 18 Sep 2019 17:27:36 +0200 Subject: [PATCH 2/4] Updated server and dependencies --- .travis.yml | 4 +- project/ProjectPlugin.scala | 25 +++-- project/build.properties | 2 +- project/plugins.sbt | 2 +- .../org/scalaexercises/evaluator/auth.scala | 55 +++++----- .../org/scalaexercises/evaluator/codecs.scala | 14 ++- .../scalaexercises/evaluator/evaluation.scala | 95 +++++++++------- .../scalaexercises/evaluator/services.scala | 102 +++++++++--------- .../evaluator/EvalEndpointSpec.scala | 2 +- .../evaluator/EvaluatorSpec.scala | 2 +- .../org/scalaexercises/evaluator/types.scala | 6 +- 11 files changed, 165 insertions(+), 144 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5a78d965..0a9ffcbb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ services: - docker scala: - 2.11.11 -- 2.12.3 +- 2.12.10 jdk: - oraclejdk8 cache: @@ -30,7 +30,7 @@ after_success: sbt ++$TRAVIS_SCALA_VERSION dockerBuildAndPush; sbt ++$TRAVIS_SCALA_VERSION smoketests/test; fi -- if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" -a "$TRAVIS_SCALA_VERSION" = "2.12.3" ]; then +- if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" -a "$TRAVIS_SCALA_VERSION" = "2.12.10" ]; then sbt ++$TRAVIS_SCALA_VERSION publishSignedAll; echo "Deploying to Heroku"; docker login --username=noel.m@47deg.com --password=$heroku_token registry.heroku.com; diff --git a/project/ProjectPlugin.scala b/project/ProjectPlugin.scala index 6651ac86..e9ec7713 100644 --- a/project/ProjectPlugin.scala +++ b/project/ProjectPlugin.scala @@ -26,6 +26,7 @@ object ProjectPlugin extends AutoPlugin { lazy val roshttp = "2.2.4" lazy val slf4jSimple = "1.7.28" lazy val jwtCore = "4.0.0" + lazy val coursier = "2.0.0-RC3-4" } lazy val dockerSettings = Seq( @@ -81,9 +82,12 @@ object ProjectPlugin extends AutoPlugin { %%("http4s-circe", V.http4s), %("config"), %%("jwt-core", V.jwtCore), + %%("coursier", V.coursier), + %%("coursier-cache", V.coursier), + "io.get-coursier" %% "coursier-cats-interop" % V.coursier, %%("scalatest", V.scalatest) % "test" - ), - addSbtPlugin("io.get-coursier" % "sbt-coursier" % "2.0.0-RC3-2") + ) //, + //addSbtPlugin("io.get-coursier" % "sbt-coursier" % V.coursier) ) lazy val buildInfoSettings = Seq( @@ -135,7 +139,7 @@ object ProjectPlugin extends AutoPlugin { organizationEmail = "hello@47deg.com" ), orgLicenseSetting := ApacheLicense, - scalaVersion := "2.12.10", + scalaVersion := "2.11.11", scalaOrganization := "org.scala-lang", javacOptions ++= Seq("-encoding", "UTF-8", "-Xlint:-options"), fork in Test := false, @@ -143,13 +147,12 @@ object ProjectPlugin extends AutoPlugin { cancelable in Global := true, headerLicense := Some( HeaderLicense.Custom( - s"""|/* - | * scala-exercises - ${name.value} - | * Copyright (C) 2015-2016 47 Degrees, LLC. - | */ - | - |""".stripMargin - )), - headerMappings := headerMappings.value + (HeaderFileType.scala -> HeaderCommentStyle.CStyleBlockComment) + s"""| + | scala-exercises - ${name.value} + | Copyright (C) 2015-2019 47 Degrees, LLC. + | + |""".stripMargin + )) + //headerMappings := headerMappings.value + (HeaderFileType.scala -> HeaderCommentStyle.CStyleBlockComment) ) ++ shellPromptSettings } diff --git a/project/build.properties b/project/build.properties index c46277f9..1fc4b809 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.2.7 \ No newline at end of file +sbt.version=1.2.8 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt index 64c14493..21728879 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,5 +3,5 @@ addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.4.1") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.10") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.9.0") addSbtPlugin("se.marcuslonnberg" % "sbt-docker" % "1.5.0") -addSbtPlugin("com.47deg" % "sbt-org-policies" % "0.12.0-M1") +addSbtPlugin("com.47deg" % "sbt-org-policies" % "0.12.0-M2") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "3.0.1") diff --git a/server/src/main/scala/org/scalaexercises/evaluator/auth.scala b/server/src/main/scala/org/scalaexercises/evaluator/auth.scala index e5e36730..f8a45c93 100644 --- a/server/src/main/scala/org/scalaexercises/evaluator/auth.scala +++ b/server/src/main/scala/org/scalaexercises/evaluator/auth.scala @@ -1,19 +1,21 @@ /* - * scala-exercises - evaluator-server - * Copyright (C) 2015-2016 47 Degrees, LLC. + * + * scala-exercises - evaluator-server + * Copyright (C) 2015-2019 47 Degrees, LLC. + * */ package org.scalaexercises.evaluator -import org.http4s._, org.http4s.dsl._, org.http4s.server._ +import cats.effect.Sync import com.typesafe.config._ +import org.http4s._ +import org.http4s.syntax.kleisli.http4sKleisliResponseSyntax import org.http4s.util._ -import scala.util.{Failure, Success, Try} -import pdi.jwt.{Jwt, JwtAlgorithm, JwtClaim, JwtHeader, JwtOptions} - import org.log4s.getLogger +import pdi.jwt.{Jwt, JwtAlgorithm} -import scalaz.concurrent.Task +import scala.util.{Failure, Success} object auth { @@ -37,15 +39,14 @@ object auth { type HeaderT = `X-Scala-Eval-Api-Token` - def name: CaseInsensitiveString = "x-scala-eval-api-token".ci + def name: CaseInsensitiveString = CaseInsensitiveString("x-scala-eval-api-token") override def parse(s: String): ParseResult[`X-Scala-Eval-Api-Token`] = ParseResult.success(`X-Scala-Eval-Api-Token`(s)) - def matchHeader(header: Header): Option[HeaderT] = { + def matchHeader(header: Header): Option[HeaderT] = if (header.name == name) Some(`X-Scala-Eval-Api-Token`(header.value)) else None - } } @@ -55,20 +56,26 @@ object auth { writer.append(token) } - def apply(service: HttpService): HttpService = Service.lift { req => - req.headers.get(`X-Scala-Eval-Api-Token`) match { - case Some(header) => - Jwt.decodeRaw(header.value, secretKey, Seq(JwtAlgorithm.HS256)) match { - case Success(tokenIdentity) => - logger.info(s"Auth success with identity : $tokenIdentity") - service(req) - case Failure(ex) => - logger.warn(s"Auth failed : $ex") - Task.now(Response(Status.Unauthorized)) + def apply[F[_]: Sync](service: HttpApp[F]): HttpApp[F] = + HttpRoutes + .of[F] { + case req if req.headers.nonEmpty => { + req.headers.get(`X-Scala-Eval-Api-Token`) match { + case Some(header) => + Jwt.decodeRaw(header.token, secretKey, Seq(JwtAlgorithm.HS256)) match { + case Success(tokenIdentity) => { + logger.info(s"Auth success with identity : $tokenIdentity") + service(req) + } + case Failure(ex) => { + logger.warn(s"Auth failed : $ex") + Sync[F].pure(Response(Status.Unauthorized)) + } + } + } } - case None => Task.now(Response(Status.Unauthorized)) - } - - } + case _ => Sync[F].pure(Response(Status.Unauthorized)) + } + .orNotFound } diff --git a/server/src/main/scala/org/scalaexercises/evaluator/codecs.scala b/server/src/main/scala/org/scalaexercises/evaluator/codecs.scala index 9ee17521..84c71eb6 100644 --- a/server/src/main/scala/org/scalaexercises/evaluator/codecs.scala +++ b/server/src/main/scala/org/scalaexercises/evaluator/codecs.scala @@ -1,18 +1,16 @@ /* - * scala-exercises - evaluator-server - * Copyright (C) 2015-2016 47 Degrees, LLC. + * + * scala-exercises - evaluator-server + * Copyright (C) 2015-2019 47 Degrees, LLC. + * */ package org.scalaexercises.evaluator import cats.effect.Sync -import io.circe.{Decoder, Encoder, Json, Printer} -import io.circe.jawn.CirceSupportParser.facade -import org.http4s -import org.http4s.circe._ +import io.circe.{Decoder, Encoder} import org.http4s._ -import org.http4s.dsl._ -import org.http4s.headers.`Content-Type` +import org.http4s.circe._ /** Provides Json serialization codecs for the http4s services */ trait Http4sCodecInstances { diff --git a/server/src/main/scala/org/scalaexercises/evaluator/evaluation.scala b/server/src/main/scala/org/scalaexercises/evaluator/evaluation.scala index 38408d03..40792151 100644 --- a/server/src/main/scala/org/scalaexercises/evaluator/evaluation.scala +++ b/server/src/main/scala/org/scalaexercises/evaluator/evaluation.scala @@ -1,6 +1,8 @@ /* - * scala-exercises - evaluator-server - * Copyright (C) 2015-2016 47 Degrees, LLC. + * + * scala-exercises - evaluator-server + * Copyright (C) 2015-2019 47 Degrees, LLC. + * */ package org.scalaexercises.evaluator @@ -9,11 +11,13 @@ import java.io.{ByteArrayOutputStream, File} import java.math.BigInteger import java.net.URLClassLoader import java.security.MessageDigest -import java.util.concurrent.TimeoutException import java.util.jar.JarFile +import cats.effect.{Concurrent, ConcurrentEffect, ContextShift, Timer} +import cats.implicits._ import coursier._ -import monix.execution.Scheduler +import coursier.cache.{ArtifactError, FileCache} +import coursier.util.Sync import org.scalaexercises.evaluator.Eval.CompilerException import scala.concurrent.duration._ @@ -24,13 +28,11 @@ import scala.tools.nsc.reporters._ import scala.tools.nsc.{Global, Settings} import scala.util.Try import scala.util.control.NonFatal -import scalaz.Scalaz._ -import scalaz._ -import scalaz.concurrent.Task -class Evaluator(timeout: FiniteDuration = 20.seconds)( - implicit S: Scheduler -) { +class Evaluator[F[_]: Sync](timeout: FiniteDuration = 20.seconds)( + implicit CS: ContextShift[F], + F: ConcurrentEffect[F], + T: Timer[F]) { type Remote = String private[this] def convert(errors: (Position, String, String)): (String, List[CompilationInfo]) = { @@ -38,31 +40,35 @@ class Evaluator(timeout: FiniteDuration = 20.seconds)( (severity, CompilationInfo(msg, Some(RangePosition(pos.start, pos.point, pos.end))) :: Nil) } - def remoteToRepository(remote: Remote): Repository = - MavenRepository(remote) + def remoteToRepository(remote: Remote): Repository = MavenRepository(remote) def dependencyToModule(dependency: Dependency): coursier.Dependency = - coursier.Dependency( - Module(dependency.groupId, dependency.artifactId), + coursier.Dependency.of( + Module(Organization(dependency.groupId), ModuleName(dependency.artifactId)), dependency.version ) - def resolveArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): Task[Resolution] = { - val resolution = Resolution(dependencies.map(dependencyToModule).toSet) - val repositories: Seq[Repository] = Cache.ivy2Local +: remotes.map(remoteToRepository) - val fetch = Fetch.from(repositories, Cache.fetch()) - resolution.process.run(fetch) + val cache: FileCache[F] = FileCache[F].noCredentials + + def resolveArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): F[Resolution] = { + Resolve[F](cache) + .addDependencies(dependencies.map(dependencyToModule): _*) + .addRepositories(remotes.map(remoteToRepository): _*) + .addRepositories(coursier.LocalRepositories.ivy2Local) + .io } def fetchArtifacts( remotes: Seq[Remote], - dependencies: Seq[Dependency]): Task[coursier.FileError \/ List[File]] = + dependencies: Seq[Dependency]): F[Either[ArtifactError, List[File]]] = for { - resolution <- resolveArtifacts(remotes, dependencies) - artifacts <- Task.gatherUnordered( - resolution.artifacts.map(Cache.file(_).run) - ) - } yield artifacts.sequenceU + resolution <- resolveArtifacts(remotes, dependencies) + gatheredArtifacts <- resolution.artifacts().toList.traverse(cache.file(_).run) + artifacts = gatheredArtifacts.foldRight(Right(Nil): Either[ArtifactError, List[File]]) { + case (Right(file), acc) => acc.map(file :: _) + case (Left(ae), _) => Left(ae) + } + } yield artifacts def createEval(jars: Seq[File]) = { new Eval(jars = jars.toList) { @@ -124,22 +130,29 @@ class Evaluator(timeout: FiniteDuration = 20.seconds)( code: String, remotes: Seq[Remote] = Nil, dependencies: Seq[Dependency] = Nil - ): Task[EvalResult[T]] = { + ): F[EvalResult[T]] = { for { allJars <- fetchArtifacts(remotes, dependencies) result <- allJars match { - case \/-(jars) => - Task({ - evaluate(code, jars) - }).timed(timeout) - .handle({ - case err: TimeoutException => Timeout[T](timeout) - }) - case -\/(fileError) => - Task.now(UnresolvedDependency(fileError.describe)) + case Right(jars) => + val fallback: EvalResult[T] = Timeout[T](timeout) + val success: F[EvalResult[T]] = + timeoutTo(F.delay { evaluate(code, jars) }, timeout, fallback) + success + case Left(fileError) => + val failure: F[EvalResult[T]] = F.pure(UnresolvedDependency[T](fileError.describe)) + failure } } yield result } + + def timeoutTo[A](fa: F[A], after: FiniteDuration, fallback: A): F[A] = { + + Concurrent[F].race(fa, T.sleep(after)).flatMap { + case Left(a) => a.pure[F] + case Right(_) => fallback.pure[F] + } + } } /** @@ -170,7 +183,7 @@ private class StringCompiler( val settings = StringCompiler.this.settings val messages = new scala.collection.mutable.ListBuffer[List[String]] - def display(pos: Position, message: String, severity: Severity) { + def display(pos: Position, message: String, severity: Severity) = { severity.count += 1 val severityName = severity match { case ERROR => "error: " @@ -193,11 +206,11 @@ private class StringCompiler( }) } - def displayPrompt { + def displayPrompt = { // no. } - override def reset { + override def reset = { super.reset messages.clear() } @@ -205,7 +218,7 @@ private class StringCompiler( val global = new Global(settings, reporter) - def reset() { + def reset() = { targetDir match { case None => { output.asInstanceOf[VirtualDirectory].clear() @@ -239,7 +252,7 @@ private class StringCompiler( /** * Compile scala code. It can be found using the above class loader. */ - def apply(code: String) { + def apply(code: String) = { // if you're looking for the performance hit, it's 1/2 this line... val compiler = new global.Run val sourceFiles = List(new BatchSourceFile("(inline)", code)) @@ -366,7 +379,7 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) { * Check if code is Eval-able. * @throws CompilerException if not Eval-able. */ - def check(code: String) { + def check(code: String) = { val id = uniqueId(code) val className = "Evaluator__" + id val wrappedCode = wrapCodeInClass(className, code) diff --git a/server/src/main/scala/org/scalaexercises/evaluator/services.scala b/server/src/main/scala/org/scalaexercises/evaluator/services.scala index 05ecd000..0aac9ac1 100644 --- a/server/src/main/scala/org/scalaexercises/evaluator/services.scala +++ b/server/src/main/scala/org/scalaexercises/evaluator/services.scala @@ -1,17 +1,22 @@ /* - * scala-exercises - evaluator-server - * Copyright (C) 2015-2016 47 Degrees, LLC. + * + * scala-exercises - evaluator-server + * Copyright (C) 2015-2019 47 Degrees, LLC. + * */ package org.scalaexercises.evaluator -import cats.effect.{ExitCode, IO, IOApp, Sync} +import cats.effect.{ConcurrentEffect, ContextShift, ExitCode, IO, IOApp, Timer} import cats.implicits._ import codecs._ +import coursier.util.Sync +import coursier.interop.cats._ import io.circe.generic.auto._ import io.circe.syntax._ import org.http4s._ import org.http4s.dsl._ +import org.http4s.headers.Allow import org.http4s.server.blaze._ import org.log4s.getLogger import org.http4s.syntax.kleisli.http4sKleisliResponseSyntax @@ -23,11 +28,7 @@ object services { import EvalResponse.messages._ - private val logger = getLogger - - implicit val scheduler: Scheduler = Scheduler.io("scala-evaluator") - - val evaluator = new Evaluator(20 seconds) + def evaluator[F[_]: ConcurrentEffect: ContextShift: Timer: Sync] = new Evaluator[F](20 seconds) val corsHeaders = Seq( Header("Vary", "Origin,Access-Control-Request-Methods"), @@ -37,7 +38,7 @@ object services { Header("Access-Control-Max-Age", 1.day.toSeconds.toString()) ) - def service[F[_]: Sync] = new Http4sDsl[F] { + def service[F[_]: ConcurrentEffect: ContextShift: Timer: Sync] = new Http4sDsl[F] { def httpApp = HttpRoutes .of[F] { @@ -45,53 +46,54 @@ object services { case req @ POST -> Root / "eval" => req .decode[EvalRequest] { evalRequest => - evaluator.eval[Any]( - code = evalRequest.code, - remotes = evalRequest.resolvers, - dependencies = evalRequest.dependencies - ) flatMap { (result: EvalResult[_]) => - val response = result match { - case EvalSuccess(cis, res, out) => - EvalResponse( - `ok`, - Option(res.toString), - Option(res.asInstanceOf[AnyRef].getClass.getName), - Option(out), - cis) - case Timeout(_) => - EvalResponse(`Timeout Exceded`, None, None, None, Map.empty) - case UnresolvedDependency(msg) => - EvalResponse( - `Unresolved Dependency` + " : " + msg, - None, - None, - None, - Map.empty) - case EvalRuntimeError(cis, runtimeError) => - EvalResponse( - `Runtime Error`, - runtimeError map (_.error.getMessage), - runtimeError map (_.error.getClass.getName), - None, - cis) - case CompilationError(cis) => - EvalResponse(`Compilation Error`, None, None, None, cis) - case GeneralError(err) => - EvalResponse(`Unforeseen Exception`, None, None, None, Map.empty) + evaluator + .eval[Any]( + code = evalRequest.code, + remotes = evalRequest.resolvers, + dependencies = evalRequest.dependencies + ) + .flatMap { (result: EvalResult[_]) => + val response = result match { + case EvalSuccess(cis, res, out) => + EvalResponse( + `ok`, + Option(res.toString), + Option(res.asInstanceOf[AnyRef].getClass.getName), + Option(out), + cis) + case Timeout(_) => + EvalResponse(`Timeout Exceded`, None, None, None, Map.empty) + case UnresolvedDependency(msg) => + EvalResponse( + `Unresolved Dependency` + " : " + msg, + None, + None, + None, + Map.empty) + case EvalRuntimeError(cis, runtimeError) => + EvalResponse( + `Runtime Error`, + runtimeError map (_.error.getMessage), + runtimeError map (_.error.getClass.getName), + None, + cis) + case CompilationError(cis) => + EvalResponse(`Compilation Error`, None, None, None, cis) + case GeneralError(err) => + EvalResponse(`Unforeseen Exception`, None, None, None, Map.empty) + } + Ok(response.asJson) } - Ok(response.asJson) - } } .map((r: Response[F]) => r.putHeaders(corsHeaders: _*)) // LoaderIO service - case _ -> Root => - MethodNotAllowed() + case _ -> Root => MethodNotAllowed(Allow(GET, POST, OPTIONS)) case GET -> Root / "loaderio-1318d1b3e06b7bc96dd5de5716f57496" => Ok("loaderio-1318d1b3e06b7bc96dd5de5716f57496") // Options service // CORS middleware in http4s can't be combined with our `auth` middleware. We need to handle CORS calls ourselves. case OPTIONS -> Root / "eval" => - Ok().map(_.headers.put(corsHeaders: _*)) //putHeaders(corsHeaders: _*) + Ok().map(res => res.withHeaders(corsHeaders: _*)) //putHeaders(corsHeaders: _*) } .orNotFound } @@ -112,13 +114,9 @@ object EvaluatorServer extends IOApp { override def run(args: List[String]): IO[ExitCode] = { logger.info(s"Initializing Evaluator at $ip:$port") - // The order in which services are mounted is really important. They're executed from bottom to top, and they won't be - // checked for repeated methods or routes. That's why we set the `optionsService` before the `evalService` one, as if - // not, execution of a OPTIONS call to /eval would lead to `evalService`, even if that service doesn't recognize the - // OPTIONS verb. BlazeServerBuilder[IO] .bindHttp(port, ip) - .withHttpApp(service[IO].httpApp) + .withHttpApp(auth[IO](service[IO].httpApp)) .serve .compile .lastOrError diff --git a/server/src/test/scala/org/scalaexercises/evaluator/EvalEndpointSpec.scala b/server/src/test/scala/org/scalaexercises/evaluator/EvalEndpointSpec.scala index 270cac69..1bd878e6 100644 --- a/server/src/test/scala/org/scalaexercises/evaluator/EvalEndpointSpec.scala +++ b/server/src/test/scala/org/scalaexercises/evaluator/EvalEndpointSpec.scala @@ -17,7 +17,7 @@ import org.scalatest._ import pdi.jwt.{Jwt, JwtAlgorithm} import scodec.bits.ByteVector -import scalaz.stream.Process.emit +//import scalaz.stream.Process.emit class EvalEndpointSpec extends FunSpec with Matchers { diff --git a/server/src/test/scala/org/scalaexercises/evaluator/EvaluatorSpec.scala b/server/src/test/scala/org/scalaexercises/evaluator/EvaluatorSpec.scala index 8b86b6e4..15695ac3 100644 --- a/server/src/test/scala/org/scalaexercises/evaluator/EvaluatorSpec.scala +++ b/server/src/test/scala/org/scalaexercises/evaluator/EvaluatorSpec.scala @@ -5,7 +5,7 @@ package org.scalaexercises.evaluator -import monix.execution.Scheduler +//import monix.execution.Scheduler import org.scalaexercises.evaluator.helper._ import org.scalatest._ diff --git a/shared/src/main/scala/org/scalaexercises/evaluator/types.scala b/shared/src/main/scala/org/scalaexercises/evaluator/types.scala index 81d8046d..fe793f71 100644 --- a/shared/src/main/scala/org/scalaexercises/evaluator/types.scala +++ b/shared/src/main/scala/org/scalaexercises/evaluator/types.scala @@ -1,6 +1,8 @@ /* - * scala-exercises - evaluator-shared - * Copyright (C) 2015-2016 47 Degrees, LLC. + * + * scala-exercises - evaluator-shared + * Copyright (C) 2015-2019 47 Degrees, LLC. + * */ package org.scalaexercises.evaluator From 7082970113457989316ca7429e1eeb3c82771feb Mon Sep 17 00:00:00 2001 From: enriquenieto Date: Wed, 18 Sep 2019 18:01:43 +0200 Subject: [PATCH 3/4] Removed commented code --- .../scala/org/scalaexercises/evaluator/EvalEndpointSpec.scala | 4 ++-- .../scala/org/scalaexercises/evaluator/EvaluatorSpec.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/test/scala/org/scalaexercises/evaluator/EvalEndpointSpec.scala b/server/src/test/scala/org/scalaexercises/evaluator/EvalEndpointSpec.scala index 1bd878e6..14c613c9 100644 --- a/server/src/test/scala/org/scalaexercises/evaluator/EvalEndpointSpec.scala +++ b/server/src/test/scala/org/scalaexercises/evaluator/EvalEndpointSpec.scala @@ -17,7 +17,7 @@ import org.scalatest._ import pdi.jwt.{Jwt, JwtAlgorithm} import scodec.bits.ByteVector -//import scalaz.stream.Process.emit +import scalaz.stream.Process.emit class EvalEndpointSpec extends FunSpec with Matchers { @@ -186,5 +186,5 @@ class EvalEndpointSpec extends FunSpec with Matchers { `Accept-Ranges`(Nil)).status should be(HttpStatus.Unauthorized) } - } + } */ } diff --git a/server/src/test/scala/org/scalaexercises/evaluator/EvaluatorSpec.scala b/server/src/test/scala/org/scalaexercises/evaluator/EvaluatorSpec.scala index 15695ac3..8b86b6e4 100644 --- a/server/src/test/scala/org/scalaexercises/evaluator/EvaluatorSpec.scala +++ b/server/src/test/scala/org/scalaexercises/evaluator/EvaluatorSpec.scala @@ -5,7 +5,7 @@ package org.scalaexercises.evaluator -//import monix.execution.Scheduler +import monix.execution.Scheduler import org.scalaexercises.evaluator.helper._ import org.scalatest._ From 388d9d6bdeaeec922accb8033d02887124deec86 Mon Sep 17 00:00:00 2001 From: enriquenieto Date: Thu, 19 Sep 2019 09:39:09 +0200 Subject: [PATCH 4/4] Added suggestions to code --- .../main/scala/org/scalaexercises/evaluator/services.scala | 6 +++--- .../org/scalaexercises/evaluator/EvalEndpointSpec.scala | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/main/scala/org/scalaexercises/evaluator/services.scala b/server/src/main/scala/org/scalaexercises/evaluator/services.scala index 0aac9ac1..c0ba6a35 100644 --- a/server/src/main/scala/org/scalaexercises/evaluator/services.scala +++ b/server/src/main/scala/org/scalaexercises/evaluator/services.scala @@ -28,7 +28,7 @@ object services { import EvalResponse.messages._ - def evaluator[F[_]: ConcurrentEffect: ContextShift: Timer: Sync] = new Evaluator[F](20 seconds) + def evaluatorInstance[F[_]: ConcurrentEffect: ContextShift: Timer: Sync] = new Evaluator[F](20 seconds) val corsHeaders = Seq( Header("Vary", "Origin,Access-Control-Request-Methods"), @@ -39,7 +39,7 @@ object services { ) def service[F[_]: ConcurrentEffect: ContextShift: Timer: Sync] = new Http4sDsl[F] { - def httpApp = + def httpApp(evaluator: Evaluator[F]) = HttpRoutes .of[F] { // Evaluator service @@ -116,7 +116,7 @@ object EvaluatorServer extends IOApp { BlazeServerBuilder[IO] .bindHttp(port, ip) - .withHttpApp(auth[IO](service[IO].httpApp)) + .withHttpApp(auth[IO](service[IO].httpApp(evaluatorInstance[IO]))) .serve .compile .lastOrError diff --git a/server/src/test/scala/org/scalaexercises/evaluator/EvalEndpointSpec.scala b/server/src/test/scala/org/scalaexercises/evaluator/EvalEndpointSpec.scala index 14c613c9..270cac69 100644 --- a/server/src/test/scala/org/scalaexercises/evaluator/EvalEndpointSpec.scala +++ b/server/src/test/scala/org/scalaexercises/evaluator/EvalEndpointSpec.scala @@ -186,5 +186,5 @@ class EvalEndpointSpec extends FunSpec with Matchers { `Accept-Ranges`(Nil)).status should be(HttpStatus.Unauthorized) } - } */ + } }