Skip to content

Commit e75cd74

Browse files
authored
Merge pull request #8 from scala-exercises/rr-circe-integration
Evaluation Endpoint
2 parents 4213ab2 + 192e73d commit e75cd74

File tree

7 files changed

+283
-38
lines changed

7 files changed

+283
-38
lines changed

build.sbt

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
lazy val http4sVersion = "0.15.0-SNAPSHOT"
1+
val http4sVersion = "0.14.1"
2+
3+
val circeVersion = "0.4.1"
24

35
lazy val evaluator = (project in file("."))
46
.settings(
@@ -11,12 +13,20 @@ lazy val evaluator = (project in file("."))
1113
"org.http4s" %% "http4s-dsl" % http4sVersion,
1214
"org.http4s" %% "http4s-blaze-server" % http4sVersion,
1315
"org.http4s" %% "http4s-blaze-client" % http4sVersion,
16+
"org.http4s" %% "http4s-circe" % http4sVersion,
17+
"io.circe" %% "circe-core" % circeVersion,
18+
"io.circe" %% "circe-generic" % circeVersion,
19+
"io.circe" %% "circe-parser" % circeVersion,
1420
"org.log4s" %% "log4s" % "1.3.0",
1521
"org.slf4j" % "slf4j-simple" % "1.7.21",
1622
"io.get-coursier" %% "coursier" % "1.0.0-M12",
1723
"io.get-coursier" %% "coursier-cache" % "1.0.0-M12",
1824
"org.scalatest" %% "scalatest" % "2.2.4" % "test"
1925
)
20-
)
26+
)
2127

2228
enablePlugins(JavaAppPackaging)
29+
30+
addCompilerPlugin(
31+
"org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full
32+
)

src/main/scala/codecs.scala

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.scalaexercises.evaluator
2+
3+
import org.http4s._, org.http4s.dsl._
4+
import io.circe.{Encoder, Decoder, Json, Printer}
5+
import org.http4s.headers.`Content-Type`
6+
import io.circe.jawn.CirceSupportParser.facade
7+
8+
/** Provides Json serialization codecs for the http4s services */
9+
trait Http4sCodecInstances {
10+
11+
implicit val jsonDecoder: EntityDecoder[Json] = jawn.jawnDecoder(facade)
12+
13+
implicit def jsonDecoderOf[A](implicit decoder: Decoder[A]): EntityDecoder[A] =
14+
jsonDecoder.flatMapR { json =>
15+
decoder.decodeJson(json).fold(
16+
failure =>
17+
DecodeResult.failure(InvalidMessageBodyFailure(s"Could not decode JSON: $json", Some(failure))),
18+
DecodeResult.success(_)
19+
)
20+
}
21+
22+
implicit val jsonEntityEncoder: EntityEncoder[Json] =
23+
EntityEncoder[String].contramap[Json] { json =>
24+
Printer.noSpaces.pretty(json)
25+
}.withContentType(`Content-Type`(MediaType.`application/json`))
26+
27+
implicit def jsonEncoderOf[A](implicit encoder: Encoder[A]): EntityEncoder[A] =
28+
jsonEntityEncoder.contramap[A](encoder.apply)
29+
30+
}
31+
32+
object codecs extends Http4sCodecInstances

src/main/scala/evaluation.scala

+4-10
Original file line numberDiff line numberDiff line change
@@ -38,25 +38,19 @@ import org.scalaexercises.evaluator._
3838
class Evaluator(timeout: FiniteDuration = 20.seconds)(
3939
implicit S: Scheduler
4040
) {
41-
type Dependency = (String, String, String)
4241
type Remote = String
4342

44-
private[this] def convert(errors: (Position, String, String)): (Severity, List[CompilationInfo]) = {
43+
private[this] def convert(errors: (Position, String, String)): (String, List[CompilationInfo]) = {
4544
val (pos, msg, severity) = errors
46-
val sev = severity match {
47-
case "ERROR" Error
48-
case "WARNING" Warning
49-
case _ Informational
50-
}
51-
(sev, CompilationInfo(msg, Some(RangePosition(pos.start, pos.point, pos.end))) :: Nil)
45+
(severity, CompilationInfo(msg, Some(RangePosition(pos.start, pos.point, pos.end))) :: Nil)
5246
}
5347

5448
def remoteToRepository(remote: Remote): Repository =
5549
MavenRepository(remote)
5650

5751
def dependencyToModule(dependency: Dependency): coursier.Dependency =
5852
coursier.Dependency(
59-
Module(dependency._1, dependency._2), dependency._3
53+
Module(dependency.groupId, dependency.artifactId), dependency.version
6054
)
6155

6256
def resolveArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): Task[Resolution] = {
@@ -75,7 +69,7 @@ class Evaluator(timeout: FiniteDuration = 20.seconds)(
7569

7670
def createEval(jars: Seq[File]) = {
7771
new Eval(jars = jars.toList) {
78-
@volatile var errors: Map[Severity, List[CompilationInfo]] = Map.empty
72+
@volatile var errors: Map[String, List[CompilationInfo]] = Map.empty
7973

8074
override lazy val compilerSettings: Settings = new EvalSettings(None){
8175
if (!jars.isEmpty) {

src/main/scala/services.scala

+38-6
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,47 @@ import org.http4s._, org.http4s.dsl._, org.http4s.server._
44
import org.http4s.server.blaze._
55
import org.log4s.getLogger
66

7+
import monix.execution.Scheduler
8+
9+
import scala.concurrent.duration._
10+
711
import scalaz.concurrent.Task
12+
import scalaz._
13+
814

915
object services {
1016

17+
import codecs._
18+
import io.circe.generic.auto._
19+
import EvalResponse.messages._
20+
1121
private val logger = getLogger
1222

13-
val service = HttpService {
14-
case GET -> Root / "eval" =>
15-
Ok(s"Hello, evaluator!.")
23+
implicit val scheduler: Scheduler = Scheduler.io("scala-evaluator")
24+
25+
val evaluator = new Evaluator(20 seconds)
26+
27+
def service = HttpService {
28+
case req @ POST -> Root / "eval" =>
29+
import io.circe.syntax._
30+
req.decode[EvalRequest] { evalRequest =>
31+
evaluator.eval[Any](
32+
code = evalRequest.code,
33+
remotes = evalRequest.resolvers,
34+
dependencies = evalRequest.dependencies
35+
) flatMap { result =>
36+
val response = result match {
37+
case EvalSuccess(cis, result, out) =>
38+
EvalResponse(`ok`, Option(result.toString), Option(result.asInstanceOf[AnyRef].getClass.getName), cis)
39+
case Timeout(_) => EvalResponse(`Timeout Exceded`, None, None, Map.empty)
40+
case UnresolvedDependency(msg) => EvalResponse(`Unresolved Dependency` + " : " + msg, None, None, Map.empty)
41+
case EvalRuntimeError(cis, _) => EvalResponse(`Runtime Error`, None, None, cis)
42+
case CompilationError(cis) => EvalResponse(`Compilation Error`, None, None, cis)
43+
case GeneralError(err) => EvalResponse(`Unforeseen Exception`, None, None, Map.empty)
44+
}
45+
Ok(response.asJson)
46+
}
47+
}
1648
}
1749

1850
}
@@ -26,9 +58,9 @@ object EvaluatorServer extends App {
2658
val ip = Option(System.getenv("EVALUATOR_SERVER_IP")).getOrElse("0.0.0.0")
2759

2860
val port = (Option(System.getenv("EVALUATOR_SERVER_PORT")) orElse
29-
Option(System.getProperty("http.port")))
30-
.map(_.toInt)
31-
.getOrElse(8080)
61+
Option(System.getProperty("http.port")))
62+
.map(_.toInt)
63+
.getOrElse(8080)
3264

3365
logger.info(s"Initializing Evaluator at $ip:$port")
3466

src/main/scala/types.scala

+63-15
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,74 @@
11
package org.scalaexercises.evaluator
22

33
import scala.concurrent.duration._
4+
import io.circe._, io.circe.generic.auto._
45

5-
sealed trait Severity
6-
final case object Informational extends Severity
7-
final case object Warning extends Severity
8-
final case object Error extends Severity
6+
final case class RangePosition(
7+
start: Int,
8+
point: Int,
9+
end: Int)
910

10-
case class RangePosition(start: Int, point: Int, end: Int)
11-
case class CompilationInfo(message: String, pos: Option[RangePosition])
12-
case class RuntimeError(val error: Throwable, position: Option[Int])
11+
final case class CompilationInfo(
12+
message: String,
13+
pos: Option[RangePosition])
1314

14-
sealed trait EvalResult[+T]
15+
final case class RuntimeError(
16+
val error: Throwable,
17+
position: Option[Int])
18+
19+
sealed trait EvalResult[+A]
1520

1621
object EvalResult {
17-
type CI = Map[Severity, List[CompilationInfo]]
22+
type CI = Map[String, List[CompilationInfo]]
1823
}
24+
1925
import EvalResult._
2026

21-
case class EvalSuccess[T](complilationInfos: CI, result: T, consoleOutput: String) extends EvalResult[T]
22-
case class Timeout[T](duration: FiniteDuration) extends EvalResult[T]
23-
case class UnresolvedDependency[T](explanation: String) extends EvalResult[T]
24-
case class EvalRuntimeError[T](complilationInfos: CI, runtimeError: Option[RuntimeError]) extends EvalResult[T]
25-
case class CompilationError[T](complilationInfos: CI) extends EvalResult[T]
26-
case class GeneralError[T](stack: Throwable) extends EvalResult[T]
27+
final case class EvalSuccess[A](
28+
complilationInfos: CI,
29+
result: A,
30+
consoleOutput: String) extends EvalResult[A]
31+
32+
final case class Timeout[A](duration: FiniteDuration) extends EvalResult[A]
33+
34+
final case class UnresolvedDependency[A](explanation: String) extends EvalResult[A]
35+
36+
final case class EvalRuntimeError[A](
37+
complilationInfos: CI,
38+
runtimeError: Option[RuntimeError]) extends EvalResult[A]
39+
40+
final case class CompilationError[A](complilationInfos: CI) extends EvalResult[A]
41+
42+
final case class GeneralError[A](stack: Throwable) extends EvalResult[A]
43+
44+
final case class Dependency(
45+
groupId: String,
46+
artifactId: String,
47+
version: String)
48+
49+
final case class EvalRequest(
50+
resolvers: List[String] = Nil,
51+
dependencies: List[Dependency] = Nil,
52+
code: String)
53+
54+
final case class EvalResponse(
55+
msg: String,
56+
value: Option[String] = None,
57+
valueType: Option[String] = None,
58+
compilationInfos: CI = Map.empty)
59+
60+
object EvalResponse {
61+
62+
object messages {
63+
64+
val `ok` = "Ok"
65+
val `Timeout Exceded` = "Timeout"
66+
val `Unresolved Dependency` = "Unresolved Dependency"
67+
val `Runtime Error` = "Runtime Error"
68+
val `Compilation Error` = "Compilation Error"
69+
val `Unforeseen Exception` = "Unforeseen Exception"
70+
71+
}
72+
73+
}
74+

src/test/scala/EvalEndpointSpec.scala

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* scala-exercises-evaluator
3+
* Copyright (C) 2015-2016 47 Degrees, LLC. <http://www.47deg.com>
4+
*/
5+
package org.scalaexercises.evaluator
6+
7+
import org.scalatest._
8+
import org.http4s._, org.http4s.dsl._, org.http4s.server._
9+
10+
import io.circe.syntax._
11+
import io.circe.generic.auto._
12+
import scalaz.stream.Process.emit
13+
import java.nio.charset.StandardCharsets
14+
import scodec.bits.ByteVector
15+
16+
import org.http4s.{Status => HttpStatus}
17+
18+
class EvalEndpointSpec extends FunSpec with Matchers {
19+
20+
import services._
21+
import codecs._
22+
import EvalResponse.messages._
23+
24+
val sonatypeReleases = "https://oss.sonatype.org/content/repositories/releases/" :: Nil
25+
26+
def serve(evalRequest: EvalRequest) =
27+
service.run(Request(
28+
POST,
29+
Uri(path = "/eval"),
30+
body = emit(
31+
ByteVector.view(
32+
evalRequest.asJson.noSpaces.getBytes(StandardCharsets.UTF_8)
33+
)
34+
)
35+
)).run
36+
37+
def verifyEvalResponse(
38+
response: Response,
39+
expectedStatus: HttpStatus,
40+
expectedValue: Option[String] = None,
41+
expectedMessage: String
42+
) = {
43+
44+
response.status should be(expectedStatus)
45+
val evalResponse = response.as[EvalResponse].run
46+
evalResponse.value should be(expectedValue)
47+
evalResponse.msg should be(expectedMessage)
48+
}
49+
50+
describe("evaluation") {
51+
it("can evaluate simple expressions") {
52+
verifyEvalResponse(
53+
response = serve(EvalRequest(code = "{ 41 + 1 }")),
54+
expectedStatus = HttpStatus.Ok,
55+
expectedValue = Some("42"),
56+
expectedMessage = `ok`
57+
)
58+
}
59+
60+
it("fails with a timeout when takes longer than the configured timeout") {
61+
verifyEvalResponse(
62+
response = serve(EvalRequest(code = "{ while(true) {}; 123 }")),
63+
expectedStatus = HttpStatus.Ok,
64+
expectedValue = None,
65+
expectedMessage = `Timeout Exceded`
66+
)
67+
}
68+
69+
it("can load dependencies for an evaluation") {
70+
verifyEvalResponse(
71+
response = serve(EvalRequest(
72+
code = "{import cats._; Eval.now(42).value}",
73+
resolvers = sonatypeReleases,
74+
dependencies = Dependency("org.typelevel", "cats_2.11", "0.6.0") :: Nil
75+
)),
76+
expectedStatus = HttpStatus.Ok,
77+
expectedValue = Some("42"),
78+
expectedMessage = `ok`
79+
)
80+
}
81+
82+
it("can load different versions of a dependency across evaluations") {
83+
val code = "{import cats._; Eval.now(42).value}"
84+
val resolvers = sonatypeReleases
85+
86+
List("0.6.0", "0.4.1") foreach { version =>
87+
verifyEvalResponse(
88+
response = serve(EvalRequest(
89+
code = code,
90+
resolvers = resolvers,
91+
dependencies = Dependency("org.typelevel", "cats_2.11", version) :: Nil
92+
)),
93+
expectedStatus = HttpStatus.Ok,
94+
expectedValue = Some("42"),
95+
expectedMessage = `ok`
96+
)
97+
}
98+
99+
}
100+
101+
it("can run code from the exercises content") {
102+
verifyEvalResponse(
103+
response = serve(EvalRequest(
104+
code = "{import stdlib._; Asserts.scalaTestAsserts(true)}",
105+
resolvers = sonatypeReleases,
106+
dependencies = Dependency("org.scala-exercises", "exercises-stdlib_2.11", "0.2.0") :: Nil
107+
)),
108+
expectedStatus = HttpStatus.Ok,
109+
expectedValue = Some("()"),
110+
expectedMessage = `ok`
111+
)
112+
}
113+
114+
it("captures exceptions when running the exercises content") {
115+
verifyEvalResponse(
116+
response = serve(EvalRequest(
117+
code = "{import stdlib._; Asserts.scalaTestAsserts(false)}",
118+
resolvers = sonatypeReleases,
119+
dependencies = Dependency("org.scala-exercises", "exercises-stdlib_2.11", "0.2.0") :: Nil
120+
)),
121+
expectedStatus = HttpStatus.Ok,
122+
expectedValue = None,
123+
expectedMessage = `Runtime Error`
124+
)
125+
}
126+
127+
}
128+
}
129+

0 commit comments

Comments
 (0)