diff --git a/.scalafmt b/.scalafmt new file mode 100644 index 00000000..e5d869b0 --- /dev/null +++ b/.scalafmt @@ -0,0 +1,5 @@ +--style defaultWithAlign +--maxColumn 80 +--continuationIndentCallSite 2 +--continuationIndentDefnSite 2 +--alignByOpenParenCallSite false \ No newline at end of file diff --git a/build.sbt b/build.sbt index 3369f0f5..324faa65 100644 --- a/build.sbt +++ b/build.sbt @@ -1,35 +1,51 @@ -val http4sVersion = "0.14.1" +lazy val root = (project in file(".")) + .aggregate(`evaluator-server`, `evaluator-shared`, `evaluator-client`) -val circeVersion = "0.4.1" +lazy val `evaluator-shared` = (project in file("shared")) + .settings(name := "evaluator-shared") -lazy val evaluator = (project in file(".")) +lazy val `evaluator-client` = (project in file("client")) + .dependsOn(`evaluator-shared`) + .settings( + name := "evaluator-client", + libraryDependencies <++= libraryVersions { v => Seq( + "org.typelevel" %% "cats-free" % v('cats), + "io.circe" %% "circe-core" % v('circe), + "io.circe" %% "circe-generic" % v('circe), + "io.circe" %% "circe-parser" % v('circe), + "org.log4s" %% "log4s" % v('log4s), + "org.scalaj" %% "scalaj-http" % v('scalajhttp), + "org.slf4j" % "slf4j-simple" % v('slf4j), + // Testing libraries + "org.scalatest" %% "scalatest" % v('scalaTest) % "test" + ) + } + ) + +lazy val `evaluator-server` = (project in file("server")) + .dependsOn(`evaluator-shared`) .enablePlugins(JavaAppPackaging) .settings( - name := "evaluator", - scalaVersion := "2.11.8", - resolvers += Resolver.sonatypeRepo("snapshots"), - libraryDependencies ++= Seq( - "org.scala-exercises" %% "evaluator-types" % version.value, - "org.scala-lang" % "scala-compiler" % scalaVersion.value, - "io.monix" %% "monix" % "2.0-RC8", - "org.http4s" %% "http4s-dsl" % http4sVersion, - "org.http4s" %% "http4s-blaze-server" % http4sVersion, - "org.http4s" %% "http4s-blaze-client" % http4sVersion, - "org.http4s" %% "http4s-circe" % http4sVersion, - "io.circe" %% "circe-core" % circeVersion, - "io.circe" %% "circe-generic" % circeVersion, - "io.circe" %% "circe-parser" % circeVersion, - "com.typesafe" % "config" % "1.3.0", - "com.pauldijou" %% "jwt-core" % "0.8.0", - "org.log4s" %% "log4s" % "1.3.0", - "org.slf4j" % "slf4j-simple" % "1.7.21", - "io.get-coursier" %% "coursier" % "1.0.0-M12", - "io.get-coursier" %% "coursier-cache" % "1.0.0-M12", - "org.scalatest" %% "scalatest" % "2.2.4" % "test" + name := "evaluator-server", + libraryDependencies <++= libraryVersions { v => Seq( + "io.monix" %% "monix" % v('monix), + "org.http4s" %% "http4s-dsl" % v('http4s), + "org.http4s" %% "http4s-blaze-server" % v('http4s), + "org.http4s" %% "http4s-blaze-client" % v('http4s), + "org.http4s" %% "http4s-circe" % v('http4s), + "io.circe" %% "circe-core" % v('circe), + "io.circe" %% "circe-generic" % v('circe), + "io.circe" %% "circe-parser" % v('circe), + "com.typesafe" % "config" % v('config), + "com.pauldijou" %% "jwt-core" % v('jwtcore), + "org.log4s" %% "log4s" % v('log4s), + "org.slf4j" % "slf4j-simple" % v('slf4j), + "io.get-coursier" %% "coursier" % v('coursier), + "io.get-coursier" %% "coursier-cache" % v('coursier), + "org.scalatest" %% "scalatest" % v('scalaTest) % "test" ) + } ) + .settings(compilerDependencySettings: _*) -addCompilerPlugin( - "org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full -) - +onLoad in Global := (Command.process("project evaluator-server", _: State)) compose (onLoad in Global).value \ No newline at end of file diff --git a/client/src/main/scala/org/scalaexercises/evaluator/Decoders.scala b/client/src/main/scala/org/scalaexercises/evaluator/Decoders.scala new file mode 100644 index 00000000..fdfeff1e --- /dev/null +++ b/client/src/main/scala/org/scalaexercises/evaluator/Decoders.scala @@ -0,0 +1,16 @@ +package org.scalaexercises.evaluator + +import io.circe._, io.circe.jawn._, io.circe.syntax._ + +object Decoders { + + implicit val decodeRangePosition: Decoder[RangePosition] = + Decoder.forProduct3("start", "point", "end")(RangePosition.apply) + + implicit val decodeCompilationInfo: Decoder[CompilationInfo] = + Decoder.forProduct2("message", "pos")(CompilationInfo.apply) + + implicit val decodeEvalResponse: Decoder[EvalResponse] = + Decoder.forProduct4("msg", "value", "valueType", "compilationInfos")( + EvalResponse.apply) +} diff --git a/client/src/main/scala/org/scalaexercises/evaluator/EvaluatorResponses.scala b/client/src/main/scala/org/scalaexercises/evaluator/EvaluatorResponses.scala new file mode 100644 index 00000000..69808ff7 --- /dev/null +++ b/client/src/main/scala/org/scalaexercises/evaluator/EvaluatorResponses.scala @@ -0,0 +1,51 @@ +package org.scalaexercises.evaluator + +import cats.data.Xor +import cats.syntax.xor._ +import io.circe.Decoder +import io.circe.parser._ +import io.circe.generic.auto._ + +import scala.language.higherKinds +import scalaj.http.HttpResponse + +object EvaluatorResponses { + + type EvaluationResponse[A] = EvalException Xor EvaluationResult[A] + + case class EvaluationResult[A](result: A, + statusCode: Int, + headers: Map[String, IndexedSeq[String]]) + + sealed abstract class EvalException(msg: String, + cause: Option[Throwable] = None) + extends Throwable(msg) { + cause foreach initCause + } + + case class JsonParsingException(msg: String, json: String) + extends EvalException(msg) + + case class UnexpectedException(msg: String) extends EvalException(msg) + + def toEntity[A](response: HttpResponse[String])( + implicit D: Decoder[A]): EvaluationResponse[A] = response match { + case r if r.isSuccess ⇒ + decode[A](r.body).fold( + e ⇒ + JsonParsingException(e.getMessage, r.body).left[EvaluationResult[A]], + result ⇒ + Xor.Right(EvaluationResult(result, r.code, r.headers.toLowerCase)) + ) + case r ⇒ + UnexpectedException( + s"Failed invoking get with status : ${r.code}, body : \n ${r.body}") + .left[EvaluationResult[A]] + } + + implicit class HeadersLowerCase[A](headers: Map[String, A]) { + + def toLowerCase: Map[String, A] = headers.map(e ⇒ (e._1.toLowerCase, e._2)) + + } +} diff --git a/client/src/main/scala/org/scalaexercises/evaluator/api/Evaluator.scala b/client/src/main/scala/org/scalaexercises/evaluator/api/Evaluator.scala new file mode 100644 index 00000000..141502c3 --- /dev/null +++ b/client/src/main/scala/org/scalaexercises/evaluator/api/Evaluator.scala @@ -0,0 +1,21 @@ +package org.scalaexercises.evaluator.api + +import org.scalaexercises.evaluator.EvaluatorResponses.EvaluationResponse +import org.scalaexercises.evaluator.{Decoders, EvalRequest, EvalResponse} +import org.scalaexercises.evaluator.http.HttpClient +import io.circe.generic.auto._ +import io.circe.syntax._ + +class Evaluator { + + import Decoders._ + + private val httpClient = new HttpClient + + def eval(url: String, + authKey: String, + evalRequest: EvalRequest): EvaluationResponse[EvalResponse] = + httpClient + .post[EvalResponse](url, authKey, data = evalRequest.asJson.noSpaces) + +} diff --git a/client/src/main/scala/org/scalaexercises/evaluator/http/HttpClient.scala b/client/src/main/scala/org/scalaexercises/evaluator/http/HttpClient.scala new file mode 100644 index 00000000..c8be8057 --- /dev/null +++ b/client/src/main/scala/org/scalaexercises/evaluator/http/HttpClient.scala @@ -0,0 +1,29 @@ +package org.scalaexercises.evaluator.http + +import io.circe.Decoder +import org.scalaexercises.evaluator.EvaluatorResponses +import org.scalaexercises.evaluator.EvaluatorResponses.EvaluationResponse + +object HttpClient { + + val authHeaderName = "x-scala-eval-api-token" + type Headers = Map[String, String] + +} + +class HttpClient { + + import HttpClient._ + def post[A]( + url: String, + secretKey: String, + method: String = "post", + headers: Headers = Map.empty, + data: String + )(implicit D: Decoder[A]): EvaluationResponse[A] = + EvaluatorResponses.toEntity( + HttpRequestBuilder(url = url, httpVerb = method) + .withHeaders(headers + (authHeaderName -> secretKey)) + .withBody(data) + .run) +} diff --git a/client/src/main/scala/org/scalaexercises/evaluator/http/HttpRequestBuilder.scala b/client/src/main/scala/org/scalaexercises/evaluator/http/HttpRequestBuilder.scala new file mode 100644 index 00000000..75f103eb --- /dev/null +++ b/client/src/main/scala/org/scalaexercises/evaluator/http/HttpRequestBuilder.scala @@ -0,0 +1,26 @@ +package org.scalaexercises.evaluator.http + +import org.scalaexercises.evaluator.http.HttpClient._ + +import scalaj.http.Http + +case class HttpRequestBuilder( + url: String, + httpVerb: String, + headers: Headers = Map.empty[String, String], + body: Option[String] = None +) { + + def withHeaders(headers: Headers) = copy(headers = headers) + + def withBody(body: String) = copy(body = Option(body)) + + def run = { + val request = Http(url).method(httpVerb).headers(headers) + + body + .fold(request)( + request.postData(_).header("content-type", "application/json")) + .asString + } +} diff --git a/project/EvaluatorBuild.scala b/project/EvaluatorBuild.scala new file mode 100644 index 00000000..9063b427 --- /dev/null +++ b/project/EvaluatorBuild.scala @@ -0,0 +1,82 @@ +import org.scalafmt.sbt.ScalaFmtPlugin +import org.scalafmt.sbt.ScalaFmtPlugin.autoImport._ +import sbt.Keys._ +import sbt._ + +object EvaluatorBuild extends AutoPlugin { + + override def requires = plugins.JvmPlugin && ScalaFmtPlugin + + override def trigger = allRequirements + + object autoImport { + + val libraryVersions = settingKey[Map[Symbol, String]]("Common versions to be used for dependencies") + + def compilerDependencySettings = Seq( + libraryDependencies ++= Seq( + "org.scala-lang" % "scala-compiler" % scalaVersion.value, + compilerPlugin( + "org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full + ) + ) + ) + } + + import autoImport._ + + override def projectSettings = + baseSettings ++ + reformatOnCompileSettings ++ + dependencySettings ++ + miscSettings + + + private[this] def baseSettings = Seq( + version := "0.0.1-SNAPSHOT", + organization := "org.scala-exercises", + scalaVersion := "2.11.8", + scalafmtConfig in ThisBuild := Some(file(".scalafmt")), + + resolvers ++= Seq(Resolver.mavenLocal, Resolver.sonatypeRepo("snapshots"), Resolver.sonatypeRepo("releases")), + + parallelExecution in Test := false, + cancelable in Global := true, + + scalacOptions ++= Seq( + "-deprecation", "-feature", "-unchecked", "-encoding", "utf8"), + scalacOptions ++= Seq( + "-language:implicitConversions", + "-language:higherKinds"), + javacOptions ++= Seq("-encoding", "UTF-8", "-Xlint:-options") + ) + + private[this] def dependencySettings = Seq( + libraryVersions := Map( + 'cats -> "0.6.1", + 'circe -> "0.5.0-M2", + 'config -> "1.3.0", + 'coursier -> "1.0.0-M12", + 'http4s -> "0.14.1", + 'jwtcore -> "0.8.0", + 'log4s -> "1.3.0", + 'monix -> "2.0-RC8", + 'scalajhttp -> "2.3.0", + 'scalacheck -> "1.12.5", + 'scalaTest -> "2.2.6", + 'slf4j -> "1.7.21" + ) + ) + + private[this] def miscSettings = Seq( + shellPrompt := { s: State => + val c = scala.Console + val blue = c.RESET + c.BLUE + c.BOLD + val white = c.RESET + c.BOLD + + val projectName = Project.extract(s).currentProject.id + + s"$blue$projectName$white>${c.RESET}" + } + ) +} \ No newline at end of file diff --git a/project/build.properties b/project/build.properties index 176a863a..7d789d45 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.9 \ No newline at end of file +sbt.version=0.13.12 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt index 685959ee..48907822 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1 +1,2 @@ addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.1.1") +addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "0.2.11") diff --git a/src/main/resources/application.conf b/server/src/main/resources/application.conf similarity index 100% rename from src/main/resources/application.conf rename to server/src/main/resources/application.conf diff --git a/src/main/scala/auth.scala b/server/src/main/scala/auth.scala similarity index 85% rename from src/main/scala/auth.scala rename to server/src/main/scala/auth.scala index 82692237..b3e2f472 100644 --- a/src/main/scala/auth.scala +++ b/server/src/main/scala/auth.scala @@ -12,7 +12,7 @@ import scalaz.concurrent.Task object auth { - private [this] val logger = getLogger + private[this] val logger = getLogger val config = ConfigFactory.load() @@ -21,10 +21,11 @@ object auth { val secretKey = if (config.hasPath(SecretKeyPath)) { config.getString(SecretKeyPath) } else { - throw new IllegalStateException("Missing -Deval.auth.secretKey=[YOUR_KEY_HERE] or env var [EVAL_SECRET_KEY] ") + throw new IllegalStateException( + "Missing -Deval.auth.secretKey=[YOUR_KEY_HERE] or env var [EVAL_SECRET_KEY] ") } - def generateToken(value : String = "{}") = + def generateToken(value: String = "{}") = Jwt.encode(value, secretKey, JwtAlgorithm.HS256) object `X-Scala-Eval-Api-Token` extends HeaderKey.Singleton { @@ -43,7 +44,8 @@ object auth { } - final case class `X-Scala-Eval-Api-Token`(token: String) extends Header.Parsed { + final case class `X-Scala-Eval-Api-Token`(token: String) + extends Header.Parsed { override def key = `X-Scala-Eval-Api-Token` override def renderValue(writer: Writer): writer.type = writer.append(token) diff --git a/server/src/main/scala/codecs.scala b/server/src/main/scala/codecs.scala new file mode 100644 index 00000000..208defc2 --- /dev/null +++ b/server/src/main/scala/codecs.scala @@ -0,0 +1,40 @@ +package org.scalaexercises.evaluator + +import org.http4s._, org.http4s.dsl._ +import io.circe.{Encoder, Decoder, Json, Printer} +import org.http4s.headers.`Content-Type` +import io.circe.jawn.CirceSupportParser.facade + +/** 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 jsonEncoderOf[A]( + implicit encoder: Encoder[A]): EntityEncoder[A] = + jsonEntityEncoder.contramap[A](encoder.apply) + +} + +object codecs extends Http4sCodecInstances diff --git a/src/main/scala/evaluation.scala b/server/src/main/scala/evaluation.scala similarity index 55% rename from src/main/scala/evaluation.scala rename to server/src/main/scala/evaluation.scala index 6d7940d2..3e958935 100644 --- a/src/main/scala/evaluation.scala +++ b/server/src/main/scala/evaluation.scala @@ -5,10 +5,9 @@ package org.scalaexercises.evaluator - import scala.language.reflectiveCalls -import java.io.{ File, InputStream } +import java.io.{File, InputStream} import java.net.URLClassLoader import java.nio.file.Path import java.util.jar.JarFile @@ -16,10 +15,10 @@ import java.util.concurrent.TimeoutException import java.security.MessageDigest import java.math.BigInteger -import scala.tools.nsc.{ Global, Settings } +import scala.tools.nsc.{Global, Settings} import scala.tools.nsc.reporters._ -import scala.tools.nsc.io.{ VirtualDirectory, AbstractFile } -import scala.reflect.internal.util.{ Position, NoPosition, BatchSourceFile, AbstractFileClassLoader } +import scala.tools.nsc.io.{VirtualDirectory, AbstractFile} +import scala.reflect.internal.util.{Position, NoPosition, BatchSourceFile, AbstractFileClassLoader} import scalaz._; import Scalaz._ import scala.util.Try @@ -34,15 +33,16 @@ import coursier._ import org.scalaexercises.evaluator._ - class Evaluator(timeout: FiniteDuration = 20.seconds)( implicit S: Scheduler ) { type Remote = String - private[this] def convert(errors: (Position, String, String)): (String, List[CompilationInfo]) = { + private[this] def convert( + errors: (Position, String, String)): (String, List[CompilationInfo]) = { val (pos, msg, severity) = errors - (severity, CompilationInfo(msg, Some(RangePosition(pos.start, pos.point, pos.end))) :: Nil) + (severity, + CompilationInfo(msg, Some(RangePosition(pos.start, pos.point, pos.end))) :: Nil) } def remoteToRepository(remote: Remote): Repository = @@ -50,38 +50,46 @@ class Evaluator(timeout: FiniteDuration = 20.seconds)( def dependencyToModule(dependency: Dependency): coursier.Dependency = coursier.Dependency( - Module(dependency.groupId, dependency.artifactId), dependency.version + Module(dependency.groupId, dependency.artifactId), + dependency.version ) - def resolveArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): Task[Resolution] = { - val resolution = Resolution(dependencies.map(dependencyToModule).toSet) + def resolveArtifacts(remotes: Seq[Remote], + dependencies: Seq[Dependency]): Task[Resolution] = { + val resolution = Resolution(dependencies.map(dependencyToModule).toSet) val repositories: Seq[Repository] = remotes.map(remoteToRepository) - val fetch = Fetch.from(repositories, Cache.fetch()) + val fetch = Fetch.from(repositories, Cache.fetch()) resolution.process.run(fetch) } - def fetchArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): Task[coursier.FileError \/ List[File]] = for { - resolution <- resolveArtifacts(remotes, dependencies) - artifacts <- Task.gatherUnordered( - resolution.artifacts.map(Cache.file(_).run) - ) - } yield artifacts.sequenceU + def fetchArtifacts( + remotes: Seq[Remote], + dependencies: Seq[Dependency]): Task[coursier.FileError \/ List[File]] = + for { + resolution <- resolveArtifacts(remotes, dependencies) + artifacts <- Task.gatherUnordered( + resolution.artifacts.map(Cache.file(_).run) + ) + } yield artifacts.sequenceU def createEval(jars: Seq[File]) = { new Eval(jars = jars.toList) { @volatile var errors: Map[String, List[CompilationInfo]] = Map.empty - override lazy val compilerSettings: Settings = new EvalSettings(None){ + override lazy val compilerSettings: Settings = new EvalSettings(None) { if (!jars.isEmpty) { val newJars = jars.mkString(File.pathSeparator) classpath.value = newJars + File.pathSeparator + classpath.value } } - override lazy val compilerMessageHandler: Option[Reporter] = Some(new AbstractReporter { + override lazy val compilerMessageHandler: Option[Reporter] = Some( + new AbstractReporter { override val settings: Settings = compilerSettings override def displayPrompt(): Unit = () - override def display(pos: Position, msg: String, severity: this.type#Severity): Unit = { + override def display(pos: Position, + msg: String, + severity: this.type#Severity): Unit = { errors += convert((pos, msg, severity.toString)) } override def reset() = { @@ -96,7 +104,7 @@ class Evaluator(timeout: FiniteDuration = 20.seconds)( val eval = createEval(jars) val result = for { - _ ← Try(eval.check(code)) + _ ← Try(eval.check(code)) result ← Try(eval.execute[T](code, resetState = true, jars = jars)) } yield result @@ -104,11 +112,13 @@ class Evaluator(timeout: FiniteDuration = 20.seconds)( result match { case scala.util.Success(r) ⇒ EvalSuccess[T](errors, r, "") - case scala.util.Failure(t) ⇒ t match { - case e: Eval.CompilerException ⇒ CompilationError(errors) - case NonFatal(e) ⇒ EvalRuntimeError(errors, Option(RuntimeError(e, None))) - case e ⇒ GeneralError(e) - } + case scala.util.Failure(t) ⇒ + t match { + case e: Eval.CompilerException ⇒ CompilationError(errors) + case NonFatal(e) ⇒ + EvalRuntimeError(errors, Option(RuntimeError(e, None))) + case e ⇒ GeneralError(e) + } } } @@ -120,13 +130,16 @@ class Evaluator(timeout: FiniteDuration = 20.seconds)( 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 \/-(jars) => + Task({ + evaluate(code, jars) + }).timed(timeout) + .handle({ + case err: TimeoutException => Timeout[T](timeout) + }) + case -\/(fileError) => + Task.now(UnresolvedDependency(fileError.describe)) + } } yield result } } @@ -149,7 +162,8 @@ private class StringCompiler( val messages: Seq[List[String]] } - val reporter = messageHandler getOrElse new AbstractReporter with MessageCollector { + val reporter = messageHandler getOrElse new AbstractReporter + with MessageCollector { val settings = StringCompiler.this.settings val messages = new scala.collection.mutable.ListBuffer[List[String]] @@ -158,23 +172,22 @@ private class StringCompiler( val severityName = severity match { case ERROR => "error: " case WARNING => "warning: " - case _ => "" + case _ => "" } // the line number is not always available - val lineMessage = - try { - "line " + (pos.line - lineOffset) - } catch { - case _: Throwable => "" - } + val lineMessage = try { + "line " + (pos.line - lineOffset) + } catch { + case _: Throwable => "" + } messages += (severityName + lineMessage + ": " + message) :: - (if (pos.isDefined) { - pos.inUltimateSource(pos.source).lineContent.stripLineEnd :: - (" " * (pos.column - 1) + "^") :: - Nil - } else { - Nil - }) + (if (pos.isDefined) { + pos.inUltimateSource(pos.source).lineContent.stripLineEnd :: + (" " * (pos.column - 1) + "^") :: + Nil + } else { + Nil + }) } def displayPrompt { @@ -196,7 +209,8 @@ private class StringCompiler( } case Some(t) => { output.foreach { abstractFile => - if (abstractFile.file == null || abstractFile.file.getName.endsWith(".class")) { + if (abstractFile.file == null || abstractFile.file.getName.endsWith( + ".class")) { abstractFile.delete() } } @@ -206,7 +220,8 @@ private class StringCompiler( reporter.reset() } - def findClass(className: String, classLoader: ClassLoader): Option[Class[_]] = { + def findClass(className: String, + classLoader: ClassLoader): Option[Class[_]] = { synchronized { cache.get(className).orElse { try { @@ -225,7 +240,7 @@ private class StringCompiler( */ def apply(code: String) { // if you're looking for the performance hit, it's 1/2 this line... - val compiler = new global.Run + val compiler = new global.Run val sourceFiles = List(new BatchSourceFile("(inline)", code)) // ...and 1/2 this line: compiler.compileSources(sourceFiles) @@ -244,7 +259,10 @@ private class StringCompiler( /** * Compile a new class, load it, and return it. Thread-safe. */ - def apply(code: String, className: String, resetState: Boolean = true, classLoader: ClassLoader): Class[_] = { + def apply(code: String, + className: String, + resetState: Boolean = true, + classLoader: ClassLoader): Class[_] = { synchronized { if (resetState) reset() @@ -254,34 +272,37 @@ private class StringCompiler( } } - /** - * Evaluates files, strings, or input streams as Scala code, and returns the result. - * - * If `target` is `None`, the results are compiled to memory (and are therefore ephemeral). If - * `target` is `Some(path)`, the path must point to a directory, and classes will be saved into - * that directory. You can optionally pass a list of JARs to include to the classpath during - * compilation and evaluation. - * - * The flow of evaluation is: - * - wrap code in an `apply` method in a generated class - * - compile the class adding the jars to the classpath - * - contruct an instance of that class - * - return the result of `apply()` - */ + * Evaluates files, strings, or input streams as Scala code, and returns the result. + * + * If `target` is `None`, the results are compiled to memory (and are therefore ephemeral). If + * `target` is `Some(path)`, the path must point to a directory, and classes will be saved into + * that directory. You can optionally pass a list of JARs to include to the classpath during + * compilation and evaluation. + * + * The flow of evaluation is: + * - wrap code in an `apply` method in a generated class + * - compile the class adding the jars to the classpath + * - contruct an instance of that class + * - return the result of `apply()` + */ class Eval(target: Option[File] = None, jars: List[File] = Nil) { private lazy val compilerPath = try { classPathOfClass("scala.tools.nsc.Interpreter") } catch { case e: Throwable => - throw new RuntimeException("Unable to load Scala interpreter from classpath (scala-compiler jar is missing?)", e) + throw new RuntimeException( + "Unable to load Scala interpreter from classpath (scala-compiler jar is missing?)", + e) } private lazy val libPath = try { classPathOfClass("scala.AnyVal") } catch { case e: Throwable => - throw new RuntimeException("Unable to load scala base object from classpath (scala-library jar is missing?)", e) + throw new RuntimeException( + "Unable to load scala base object from classpath (scala-library jar is missing?)", + e) } // For derived classes to provide an alternate compiler message handler. @@ -300,44 +321,60 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) { ) /** - * Will generate a classname of the form Evaluater__, - * where unique is computed from the jvmID (a random number) - * and a digest of code - */ + * Will generate a classname of the form Evaluater__, + * where unique is computed from the jvmID (a random number) + * and a digest of code + */ def execute[T](code: String, resetState: Boolean, jars: Seq[File]): T = { - val id = uniqueId(code) + val id = uniqueId(code) val className = "Evaluator__" + id execute(className, code, resetState, jars) } - def execute[T](className: String, code: String, resetState: Boolean, jars: Seq[File]): T = { - val jarUrls = jars.map(jar => new java.net.URL(s"file://${jar.getAbsolutePath}")).toArray - val urlClassLoader = new URLClassLoader(jarUrls , compiler.getClass.getClassLoader) - val classLoader = new AbstractFileClassLoader(compilerOutputDir, urlClassLoader) + def execute[T](className: String, + code: String, + resetState: Boolean, + jars: Seq[File]): T = { + val jarUrls = jars + .map(jar => new java.net.URL(s"file://${jar.getAbsolutePath}")) + .toArray + val urlClassLoader = + new URLClassLoader(jarUrls, compiler.getClass.getClassLoader) + val classLoader = + new AbstractFileClassLoader(compilerOutputDir, urlClassLoader) val cls = compiler( - wrapCodeInClass(className, code), className, resetState, classLoader + wrapCodeInClass(className, code), + className, + resetState, + classLoader ) - cls.getConstructor().newInstance().asInstanceOf[() => T].apply().asInstanceOf[T] + cls + .getConstructor() + .newInstance() + .asInstanceOf[() => T] + .apply() + .asInstanceOf[T] } /** - * Check if code is Eval-able. - * @throws CompilerException if not Eval-able. - */ + * Check if code is Eval-able. + * @throws CompilerException if not Eval-able. + */ def check(code: String) { - val id = uniqueId(code) - val className = "Evaluator__" + id + val id = uniqueId(code) + val className = "Evaluator__" + id val wrappedCode = wrapCodeInClass(className, code) compiler(wrappedCode) } - private[this] def uniqueId(code: String, idOpt: Option[Int] = Some(Eval.jvmId)): String = { + private[this] def uniqueId(code: String, + idOpt: Option[Int] = Some(Eval.jvmId)): String = { val digest = MessageDigest.getInstance("SHA-1").digest(code.getBytes()) - val sha = new BigInteger(1, digest).toString(16) + val sha = new BigInteger(1, digest).toString(16) idOpt match { case Some(id) => sha + "_" + id - case _ => sha + case _ => sha } } @@ -367,9 +404,9 @@ class ${className} extends (() => Any) with java.io.Serializable { */ private def classPathOfClass(className: String) = { val resource = className.split('.').mkString("/", "/", ".class") - val path = getClass.getResource(resource).getPath + val path = getClass.getResource(resource).getPath if (path.indexOf("file:") >= 0) { - val indexOfFile = path.indexOf("file:") + 5 + val indexOfFile = path.indexOf("file:") + 5 val indexOfSeparator = path.lastIndexOf('!') List(path.substring(indexOfFile, indexOfSeparator)) } else { @@ -383,39 +420,53 @@ class ${className} extends (() => Any) with java.io.Serializable { * This is probably fragile. */ lazy val impliedClassPath: List[String] = { - def getClassPath(cl: ClassLoader, acc: List[List[String]] = List.empty): List[List[String]] = { + def getClassPath( + cl: ClassLoader, + acc: List[List[String]] = List.empty): List[List[String]] = { val cp = cl match { - case urlClassLoader: URLClassLoader => urlClassLoader.getURLs.filter(_.getProtocol == "file"). - map(u => new File(u.toURI).getPath).toList + case urlClassLoader: URLClassLoader => + urlClassLoader.getURLs + .filter(_.getProtocol == "file") + .map(u => new File(u.toURI).getPath) + .toList case _ => Nil } cl.getParent match { - case null => (cp :: acc).reverse + case null => (cp :: acc).reverse case parent => getClassPath(parent, cp :: acc) } } - val classPath = getClassPath(this.getClass.getClassLoader) + val classPath = getClassPath(this.getClass.getClassLoader) val currentClassPath = classPath.head // if there's just one thing in the classpath, and it's a jar, assume an executable jar. - currentClassPath ::: (if (currentClassPath.size == 1 && currentClassPath(0).endsWith(".jar")) { - val jarFile = currentClassPath(0) - val relativeRoot = new File(jarFile).getParentFile() - val nestedClassPath = new JarFile(jarFile).getManifest.getMainAttributes.getValue("Class-Path") - if (nestedClassPath eq null) { - Nil - } else { - nestedClassPath.split(" ").map { f => new File(relativeRoot, f).getAbsolutePath }.toList - } - } else { - Nil - }) ::: classPath.tail.flatten + currentClassPath ::: (if (currentClassPath.size == 1 && currentClassPath(0) + .endsWith(".jar")) { + val jarFile = currentClassPath(0) + val relativeRoot = + new File(jarFile).getParentFile() + val nestedClassPath = + new JarFile(jarFile).getManifest.getMainAttributes + .getValue("Class-Path") + if (nestedClassPath eq null) { + Nil + } else { + nestedClassPath + .split(" ") + .map { f => + new File(relativeRoot, f).getAbsolutePath + } + .toList + } + } else { + Nil + }) ::: classPath.tail.flatten } lazy val compilerOutputDir = target match { case Some(dir) => AbstractFile.getDirectory(dir) - case None => new VirtualDirectory("(memory)", None) + case None => new VirtualDirectory("(memory)", None) } class EvalSettings(targetDir: Option[File]) extends Settings { @@ -423,14 +474,15 @@ class ${className} extends (() => Any) with java.io.Serializable { outputDirs.setSingleOutput(compilerOutputDir) private[this] val pathList = compilerPath ::: libPath bootclasspath.value = pathList.mkString(File.pathSeparator) - classpath.value = (pathList ::: impliedClassPath).mkString(File.pathSeparator) + classpath.value = + (pathList ::: impliedClassPath).mkString(File.pathSeparator) } } - object Eval { private val jvmId = java.lang.Math.abs(new java.util.Random().nextInt()) - class CompilerException(val messages: List[List[String]]) extends Exception( - "Compiler exception " + messages.map(_.mkString("\n")).mkString("\n")) + class CompilerException(val messages: List[List[String]]) + extends Exception( + "Compiler exception " + messages.map(_.mkString("\n")).mkString("\n")) } diff --git a/server/src/main/scala/services.scala b/server/src/main/scala/services.scala new file mode 100644 index 00000000..23a676a8 --- /dev/null +++ b/server/src/main/scala/services.scala @@ -0,0 +1,88 @@ +package org.scalaexercises.evaluator + +import org.http4s._, org.http4s.dsl._, org.http4s.server._ +import org.http4s.server.blaze._ +import org.log4s.getLogger + +import monix.execution.Scheduler + +import scala.concurrent.duration._ +import scala.language.postfixOps + +import scalaz.concurrent.Task +import scalaz._ + +object services { + + import codecs._ + import io.circe.generic.auto._ + import EvalResponse.messages._ + + private val logger = getLogger + + implicit val scheduler: Scheduler = Scheduler.io("scala-evaluator") + + val evaluator = new Evaluator(20 seconds) + + 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 => + val response = result match { + case EvalSuccess(cis, result, out) => + EvalResponse( + `ok`, + Option(result.toString), + Option(result.asInstanceOf[AnyRef].getClass.getName), + cis) + case Timeout(_) => + EvalResponse(`Timeout Exceded`, None, None, Map.empty) + case UnresolvedDependency(msg) => + EvalResponse( + `Unresolved Dependency` + " : " + msg, + None, + None, + Map.empty) + case EvalRuntimeError(cis, _) => + EvalResponse(`Runtime Error`, None, None, cis) + case CompilationError(cis) => + EvalResponse(`Compilation Error`, None, None, cis) + case GeneralError(err) => + EvalResponse(`Unforeseen Exception`, None, None, Map.empty) + } + Ok(response.asJson) + } + } + }) + +} + +object EvaluatorServer extends App { + + import services._ + + private[this] val logger = getLogger + + val ip = Option(System.getenv("EVALUATOR_SERVER_IP")).getOrElse("0.0.0.0") + + val port = (Option(System.getenv("EVALUATOR_SERVER_PORT")) orElse + Option(System.getProperty("http.port"))).map(_.toInt).getOrElse(8080) + + logger.info(s"Initializing Evaluator at $ip:$port") + + BlazeBuilder + .bindHttp(port, ip) + .mountService(evalService) + .start + .run + .awaitShutdown() + +} diff --git a/src/test/scala/EvalEndpointSpec.scala b/server/src/test/scala/EvalEndpointSpec.scala similarity index 53% rename from src/test/scala/EvalEndpointSpec.scala rename to server/src/test/scala/EvalEndpointSpec.scala index 144bf248..c9e27978 100644 --- a/src/test/scala/EvalEndpointSpec.scala +++ b/server/src/test/scala/EvalEndpointSpec.scala @@ -28,20 +28,26 @@ class EvalEndpointSpec extends FunSpec with Matchers { val sonatypeReleases = "https://oss.sonatype.org/content/repositories/releases/" :: Nil - val validToken = Jwt.encode("""{"user": "scala-exercises"}""", auth.secretKey, JwtAlgorithm.HS256) - - val invalidToken = java.util.UUID.randomUUID.toString - - def serve(evalRequest: EvalRequest, authHeader : Header) = - evalService.run(Request( - POST, - Uri(path = "/eval"), - body = emit( - ByteVector.view( - evalRequest.asJson.noSpaces.getBytes(StandardCharsets.UTF_8) - ) - ) - ).putHeaders(authHeader)).run + val validToken = Jwt.encode( + """{"user": "scala-exercises"}""", + auth.secretKey, + JwtAlgorithm.HS256) + + val invalidToken = java.util.UUID.randomUUID.toString + + def serve(evalRequest: EvalRequest, authHeader: Header) = + evalService + .run( + Request( + POST, + Uri(path = "/eval"), + body = emit( + ByteVector.view( + evalRequest.asJson.noSpaces.getBytes(StandardCharsets.UTF_8) + ) + ) + ).putHeaders(authHeader)) + .run def verifyEvalResponse( response: Response, @@ -59,7 +65,9 @@ class EvalEndpointSpec extends FunSpec with Matchers { describe("evaluation") { it("can evaluate simple expressions") { verifyEvalResponse( - response = serve(EvalRequest(code = "{ 41 + 1 }"), `X-Scala-Eval-Api-Token`(validToken)), + response = serve( + EvalRequest(code = "{ 41 + 1 }"), + `X-Scala-Eval-Api-Token`(validToken)), expectedStatus = HttpStatus.Ok, expectedValue = Some("42"), expectedMessage = `ok` @@ -68,7 +76,9 @@ class EvalEndpointSpec extends FunSpec with Matchers { it("fails with a timeout when takes longer than the configured timeout") { verifyEvalResponse( - response = serve(EvalRequest(code = "{ while(true) {}; 123 }"), `X-Scala-Eval-Api-Token`(validToken)), + response = serve( + EvalRequest(code = "{ while(true) {}; 123 }"), + `X-Scala-Eval-Api-Token`(validToken)), expectedStatus = HttpStatus.Ok, expectedValue = None, expectedMessage = `Timeout Exceded` @@ -77,11 +87,13 @@ class EvalEndpointSpec extends FunSpec with Matchers { it("can load dependencies for an evaluation") { verifyEvalResponse( - response = serve(EvalRequest( - code = "{import cats._; Eval.now(42).value}", - resolvers = sonatypeReleases, - dependencies = Dependency("org.typelevel", "cats_2.11", "0.6.0") :: Nil - ), `X-Scala-Eval-Api-Token`(validToken)), + response = serve( + EvalRequest( + code = "{import cats._; Eval.now(42).value}", + resolvers = sonatypeReleases, + dependencies = Dependency("org.typelevel", "cats_2.11", "0.6.0") :: Nil + ), + `X-Scala-Eval-Api-Token`(validToken)), expectedStatus = HttpStatus.Ok, expectedValue = Some("42"), expectedMessage = `ok` @@ -89,16 +101,22 @@ class EvalEndpointSpec extends FunSpec with Matchers { } it("can load different versions of a dependency across evaluations") { - val code = "{import cats._; Eval.now(42).value}" + val code = "{import cats._; Eval.now(42).value}" val resolvers = sonatypeReleases List("0.6.0", "0.4.1") foreach { version => verifyEvalResponse( - response = serve(EvalRequest( - code = code, - resolvers = resolvers, - dependencies = Dependency("org.typelevel", "cats_2.11", version) :: Nil - ), `X-Scala-Eval-Api-Token`(validToken)), + response = + serve( + EvalRequest( + code = code, + resolvers = resolvers, + dependencies = Dependency( + "org.typelevel", + "cats_2.11", + version) :: Nil + ), + `X-Scala-Eval-Api-Token`(validToken)), expectedStatus = HttpStatus.Ok, expectedValue = Some("42"), expectedMessage = `ok` @@ -109,11 +127,16 @@ class EvalEndpointSpec extends FunSpec with Matchers { it("can run code from the exercises content") { verifyEvalResponse( - response = serve(EvalRequest( - code = "{import stdlib._; Asserts.scalaTestAsserts(true)}", - resolvers = sonatypeReleases, - dependencies = Dependency("org.scala-exercises", "exercises-stdlib_2.11", "0.2.0") :: Nil - ), `X-Scala-Eval-Api-Token`(validToken)), + response = serve( + EvalRequest( + code = "{import stdlib._; Asserts.scalaTestAsserts(true)}", + resolvers = sonatypeReleases, + dependencies = Dependency( + "org.scala-exercises", + "exercises-stdlib_2.11", + "0.2.0") :: Nil + ), + `X-Scala-Eval-Api-Token`(validToken)), expectedStatus = HttpStatus.Ok, expectedValue = Some("()"), expectedMessage = `ok` @@ -122,11 +145,16 @@ class EvalEndpointSpec extends FunSpec with Matchers { it("captures exceptions when running the exercises content") { verifyEvalResponse( - response = serve(EvalRequest( - code = "{import stdlib._; Asserts.scalaTestAsserts(false)}", - resolvers = sonatypeReleases, - dependencies = Dependency("org.scala-exercises", "exercises-stdlib_2.11", "0.2.0") :: Nil - ), `X-Scala-Eval-Api-Token`(validToken)), + response = serve( + EvalRequest( + code = "{import stdlib._; Asserts.scalaTestAsserts(false)}", + resolvers = sonatypeReleases, + dependencies = Dependency( + "org.scala-exercises", + "exercises-stdlib_2.11", + "0.2.0") :: Nil + ), + `X-Scala-Eval-Api-Token`(validToken)), expectedStatus = HttpStatus.Ok, expectedValue = None, expectedMessage = `Runtime Error` @@ -134,21 +162,25 @@ class EvalEndpointSpec extends FunSpec with Matchers { } it("rejects requests with invalid tokens") { - serve(EvalRequest( + serve( + EvalRequest( code = "1", resolvers = Nil, - dependencies = Nil - ), `X-Scala-Eval-Api-Token`(invalidToken)).status should be (HttpStatus.Unauthorized) + dependencies = Nil + ), + `X-Scala-Eval-Api-Token`(invalidToken)).status should be( + HttpStatus.Unauthorized) } it("rejects requests with missing tokens") { - serve(EvalRequest( + serve( + EvalRequest( code = "1", resolvers = Nil, - dependencies = Nil - ), `Accept-Ranges`(Nil)).status should be (HttpStatus.Unauthorized) + dependencies = Nil + ), + `Accept-Ranges`(Nil)).status should be(HttpStatus.Unauthorized) } } } - diff --git a/src/test/scala/EvaluatorSpec.scala b/server/src/test/scala/EvaluatorSpec.scala similarity index 59% rename from src/test/scala/EvaluatorSpec.scala rename to server/src/test/scala/EvaluatorSpec.scala index 15008e93..33b5a820 100644 --- a/src/test/scala/EvaluatorSpec.scala +++ b/server/src/test/scala/EvaluatorSpec.scala @@ -11,7 +11,7 @@ import org.scalatest._ class EvaluatorSpec extends FunSpec with Matchers { implicit val scheduler: Scheduler = Scheduler.io("exercises-spec") - val evaluator = new Evaluator(20 seconds) + val evaluator = new Evaluator(20 seconds) describe("evaluation") { it("can evaluate simple expressions") { @@ -23,7 +23,8 @@ class EvaluatorSpec extends FunSpec with Matchers { } it("fails with a timeout when takes longer than the configured timeout") { - val result: EvalResult[Int] = evaluator.eval("{ while(true) {}; 123 }").run + val result: EvalResult[Int] = + evaluator.eval("{ while(true) {}; 123 }").run result should matchPattern { case Timeout(_) ⇒ @@ -36,16 +37,19 @@ import cats._ Eval.now(42).value """ - val remotes = List("https://oss.sonatype.org/content/repositories/releases/") + val remotes = + List("https://oss.sonatype.org/content/repositories/releases/") val dependencies = List( Dependency("org.typelevel", "cats_2.11", "0.6.0") ) - val result: EvalResult[Int] = evaluator.eval( - code, - remotes = remotes, - dependencies = dependencies - ).run + val result: EvalResult[Int] = evaluator + .eval( + code, + remotes = remotes, + dependencies = dependencies + ) + .run result should matchPattern { case EvalSuccess(_, 42, _) => @@ -57,7 +61,8 @@ Eval.now(42).value import cats._ Eval.now(42).value """ - val remotes = List("https://oss.sonatype.org/content/repositories/releases/") + val remotes = + List("https://oss.sonatype.org/content/repositories/releases/") val dependencies1 = List( Dependency("org.typelevel", "cats_2.11", "0.4.1") ) @@ -65,16 +70,20 @@ Eval.now(42).value Dependency("org.typelevel", "cats_2.11", "0.6.0") ) - val result1: EvalResult[Int] = evaluator.eval( - code, - remotes = remotes, - dependencies = dependencies1 - ).run - val result2: EvalResult[Int] = evaluator.eval( - code, - remotes = remotes, - dependencies = dependencies2 - ).run + val result1: EvalResult[Int] = evaluator + .eval( + code, + remotes = remotes, + dependencies = dependencies1 + ) + .run + val result2: EvalResult[Int] = evaluator + .eval( + code, + remotes = remotes, + dependencies = dependencies2 + ) + .run result1 should matchPattern { case EvalSuccess(_, 42, _) => @@ -89,16 +98,19 @@ Eval.now(42).value import stdlib._ Asserts.scalaTestAsserts(true) """ - val remotes = List("https://oss.sonatype.org/content/repositories/releases/") + val remotes = + List("https://oss.sonatype.org/content/repositories/releases/") val dependencies = List( Dependency("org.scala-exercises", "exercises-stdlib_2.11", "0.2.0") ) - val result: EvalResult[Unit] = evaluator.eval( - code, - remotes = remotes, - dependencies = dependencies - ).run + val result: EvalResult[Unit] = evaluator + .eval( + code, + remotes = remotes, + dependencies = dependencies + ) + .run result should matchPattern { case EvalSuccess(_, (), _) => @@ -110,19 +122,24 @@ Asserts.scalaTestAsserts(true) import stdlib._ Asserts.scalaTestAsserts(false) """ - val remotes = List("https://oss.sonatype.org/content/repositories/releases/") + val remotes = + List("https://oss.sonatype.org/content/repositories/releases/") val dependencies = List( Dependency("org.scala-exercises", "exercises-stdlib_2.11", "0.2.0") ) - val result: EvalResult[Unit] = evaluator.eval( - code, - remotes = remotes, - dependencies = dependencies - ).run + val result: EvalResult[Unit] = evaluator + .eval( + code, + remotes = remotes, + dependencies = dependencies + ) + .run result should matchPattern { - case EvalRuntimeError(_, Some(RuntimeError(err: TestFailedException, _))) => + case EvalRuntimeError( + _, + Some(RuntimeError(err: TestFailedException, _))) => } } } diff --git a/shared/src/main/scala/org/scalaexercises/evaluator/types.scala b/shared/src/main/scala/org/scalaexercises/evaluator/types.scala new file mode 100644 index 00000000..fd97cac3 --- /dev/null +++ b/shared/src/main/scala/org/scalaexercises/evaluator/types.scala @@ -0,0 +1,64 @@ +package org.scalaexercises.evaluator + +import scala.concurrent.duration._ + +final case class RangePosition(start: Int, point: Int, end: Int) + +final case class CompilationInfo(message: String, pos: Option[RangePosition]) + +final case class RuntimeError(error: Throwable, position: Option[Int]) + +sealed trait EvalResult[+A] + +object EvalResult { + type CI = Map[String, List[CompilationInfo]] +} + +import org.scalaexercises.evaluator.EvalResult._ + +final case class EvalSuccess[A](compilationInfos: CI, + result: A, + consoleOutput: String) + extends EvalResult[A] + +final case class Timeout[A](duration: FiniteDuration) extends EvalResult[A] + +final case class UnresolvedDependency[A](explanation: String) + extends EvalResult[A] + +final case class EvalRuntimeError[A](compilationInfos: CI, + runtimeError: Option[RuntimeError]) + extends EvalResult[A] + +final case class CompilationError[A](compilationInfos: CI) + extends EvalResult[A] + +final case class GeneralError[A](stack: Throwable) extends EvalResult[A] + +final case class Dependency(groupId: String, + artifactId: String, + version: String) + +final case class EvalRequest(resolvers: List[String] = Nil, + dependencies: List[Dependency] = Nil, + code: String) + +final case class EvalResponse(msg: String, + value: Option[String] = None, + valueType: Option[String] = None, + compilationInfos: CI = Map.empty) + +object EvalResponse { + + object messages { + + val `ok` = "Ok" + val `Timeout Exceded` = "Timeout" + val `Unresolved Dependency` = "Unresolved Dependency" + val `Runtime Error` = "Runtime Error" + val `Compilation Error` = "Compilation Error" + val `Unforeseen Exception` = "Unforeseen Exception" + + } + +} diff --git a/src/main/scala/codecs.scala b/src/main/scala/codecs.scala deleted file mode 100644 index 982ffb2d..00000000 --- a/src/main/scala/codecs.scala +++ /dev/null @@ -1,32 +0,0 @@ -package org.scalaexercises.evaluator - -import org.http4s._, org.http4s.dsl._ -import io.circe.{Encoder, Decoder, Json, Printer} -import org.http4s.headers.`Content-Type` -import io.circe.jawn.CirceSupportParser.facade - -/** 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 jsonEncoderOf[A](implicit encoder: Encoder[A]): EntityEncoder[A] = - jsonEntityEncoder.contramap[A](encoder.apply) - -} - -object codecs extends Http4sCodecInstances diff --git a/src/main/scala/services.scala b/src/main/scala/services.scala deleted file mode 100644 index 776b15c9..00000000 --- a/src/main/scala/services.scala +++ /dev/null @@ -1,74 +0,0 @@ -package org.scalaexercises.evaluator - -import org.http4s._, org.http4s.dsl._, org.http4s.server._ -import org.http4s.server.blaze._ -import org.log4s.getLogger - -import monix.execution.Scheduler - -import scala.concurrent.duration._ - -import scalaz.concurrent.Task -import scalaz._ - - -object services { - - import codecs._ - import io.circe.generic.auto._ - import EvalResponse.messages._ - - private val logger = getLogger - - implicit val scheduler: Scheduler = Scheduler.io("scala-evaluator") - - val evaluator = new Evaluator(20 seconds) - - 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 => - val response = result match { - case EvalSuccess(cis, result, out) => - EvalResponse(`ok`, Option(result.toString), Option(result.asInstanceOf[AnyRef].getClass.getName), cis) - case Timeout(_) => EvalResponse(`Timeout Exceded`, None, None, Map.empty) - case UnresolvedDependency(msg) => EvalResponse(`Unresolved Dependency` + " : " + msg, None, None, Map.empty) - case EvalRuntimeError(cis, _) => EvalResponse(`Runtime Error`, None, None, cis) - case CompilationError(cis) => EvalResponse(`Compilation Error`, None, None, cis) - case GeneralError(err) => EvalResponse(`Unforeseen Exception`, None, None, Map.empty) - } - Ok(response.asJson) - } - } - }) - -} - -object EvaluatorServer extends App { - - import services._ - - private[this] val logger = getLogger - - val ip = Option(System.getenv("EVALUATOR_SERVER_IP")).getOrElse("0.0.0.0") - - val port = (Option(System.getenv("EVALUATOR_SERVER_PORT")) orElse - Option(System.getProperty("http.port"))) - .map(_.toInt) - .getOrElse(8080) - - logger.info(s"Initializing Evaluator at $ip:$port") - - BlazeBuilder - .bindHttp(port, ip) - .mountService(evalService) - .start - .run - .awaitShutdown() - -} diff --git a/version.sbt b/version.sbt deleted file mode 100644 index 19138f19..00000000 --- a/version.sbt +++ /dev/null @@ -1 +0,0 @@ -version in ThisBuild := "0.1-SNAPSHOT" \ No newline at end of file