Skip to content

Commit 5f8e7f0

Browse files
Merge pull request #33 from scala-exercises/eval-32-compatibility-scalajs
Evaluator Client compatibility with ScalaJS
2 parents 5fa54cd + 758b7b3 commit 5f8e7f0

File tree

21 files changed

+276
-333
lines changed

21 files changed

+276
-333
lines changed

build.sbt

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,65 @@
1+
lazy val noPublishSettings = Seq(
2+
publish := (),
3+
publishLocal := (),
4+
publishArtifact := false
5+
)
6+
17
lazy val root = (project in file("."))
28
.settings(mainClass in Universal := Some("org.scalaexercises.evaluator.EvaluatorServer"))
39
.settings(stage <<= (stage in Universal in `evaluator-server`))
4-
.aggregate(`evaluator-server`, `evaluator-shared`, `evaluator-client`)
10+
.settings(noPublishSettings: _*)
11+
.aggregate(`evaluator-server`, `evaluator-shared-jvm`, `evaluator-shared-js`, `evaluator-client-jvm`, `evaluator-client-js`)
512

6-
lazy val `evaluator-shared` = (project in file("shared"))
13+
lazy val `evaluator-shared` = (crossProject in file("shared"))
714
.enablePlugins(AutomateHeaderPlugin)
815
.settings(name := "evaluator-shared")
916

10-
lazy val `evaluator-client` = (project in file("client"))
17+
lazy val `evaluator-shared-jvm` = `evaluator-shared`.jvm
18+
lazy val `evaluator-shared-js` = `evaluator-shared`.js
19+
20+
lazy val scalaJSSettings = Seq(
21+
requiresDOM := false,
22+
scalaJSUseRhino := false,
23+
jsEnv := NodeJSEnv().value,
24+
libraryDependencies ++= Seq(
25+
"fr.hmil" %%% "roshttp" % v('roshttp),
26+
"org.typelevel" %%% "cats-free" % v('cats),
27+
"io.circe" %%% "circe-core" % v('circe),
28+
"io.circe" %%% "circe-generic" % v('circe),
29+
"io.circe" %%% "circe-parser" % v('circe)
30+
)
31+
)
32+
33+
lazy val `evaluator-client` = (crossProject in file("client"))
1134
.dependsOn(`evaluator-shared`)
1235
.enablePlugins(AutomateHeaderPlugin)
1336
.settings(
1437
name := "evaluator-client",
15-
libraryDependencies <++= libraryVersions { v => Seq(
38+
libraryDependencies ++= Seq(
39+
"fr.hmil" %% "roshttp" % v('roshttp),
1640
"org.typelevel" %% "cats-free" % v('cats),
17-
"io.circe" %% "circe-core" % v('circe),
18-
"io.circe" %% "circe-generic" % v('circe),
19-
"io.circe" %% "circe-parser" % v('circe),
41+
"io.circe" %% "circe-core" % v('circe),
42+
"io.circe" %% "circe-generic" % v('circe),
43+
"io.circe" %% "circe-parser" % v('circe),
2044
"org.log4s" %% "log4s" % v('log4s),
21-
"org.scalaj" %% "scalaj-http" % v('scalajhttp),
2245
"org.slf4j" % "slf4j-simple" % v('slf4j),
2346
// Testing libraries
2447
"org.scalatest" %% "scalatest" % v('scalaTest) % "test"
2548
)
26-
}
27-
)
49+
)
50+
.jsSettings(scalaJSSettings: _*)
51+
52+
lazy val `evaluator-client-jvm` = `evaluator-client`.jvm
53+
lazy val `evaluator-client-js` = `evaluator-client`.js
2854

2955
lazy val `evaluator-server` = (project in file("server"))
30-
.dependsOn(`evaluator-shared`)
56+
.dependsOn(`evaluator-shared-jvm`)
3157
.enablePlugins(JavaAppPackaging)
3258
.enablePlugins(AutomateHeaderPlugin)
59+
.settings(noPublishSettings: _*)
3360
.settings(
3461
name := "evaluator-server",
35-
libraryDependencies <++= libraryVersions { v => Seq(
62+
libraryDependencies ++= Seq(
3663
"io.monix" %% "monix" % v('monix),
3764
"org.http4s" %% "http4s-dsl" % v('http4s),
3865
"org.http4s" %% "http4s-blaze-server" % v('http4s),
@@ -49,9 +76,8 @@ lazy val `evaluator-server` = (project in file("server"))
4976
"io.get-coursier" %% "coursier-cache" % v('coursier),
5077
"org.scalatest" %% "scalatest" % v('scalaTest) % "test"
5178
)
52-
}
5379
)
5480
.settings(compilerDependencySettings: _*)
5581

5682
onLoad in Global := (Command.process("project evaluator-server", _: State)) compose (onLoad in Global).value
57-
addCommandAlias("publishSignedAll", ";evaluator-shared/publishSigned;evaluator-client/publishSigned")
83+
addCommandAlias("publishSignedAll", ";evaluator-sharedJS/publishSigned;evaluator-sharedJVM/publishSigned;evaluator-clientJS/publishSigned;evaluator-clientJVM/publishSigned")
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* scala-exercises-evaluator-client
3+
* Copyright (C) 2015-2016 47 Degrees, LLC. <http://www.47deg.com>
4+
*/
5+
6+
package org.scalaexercises.evaluator
7+
8+
import cats.free.Free
9+
import org.scalaexercises.evaluator.EvaluatorResponses.EvaluationResponse
10+
import org.scalaexercises.evaluator.free.algebra.EvaluatorOps
11+
12+
class EvaluatorAPI[F[_]](url: String, authKey: String)(
13+
implicit O: EvaluatorOps[F]) {
14+
15+
def evaluates(resolvers: List[String] = Nil,
16+
dependencies: List[Dependency] = Nil,
17+
code: String): Free[F, EvaluationResponse[EvalResponse]] =
18+
O.evaluates(url, authKey, resolvers, dependencies, code)
19+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* scala-exercises-evaluator-client
3+
* Copyright (C) 2015-2016 47 Degrees, LLC. <http://www.47deg.com>
4+
*/
5+
6+
package org.scalaexercises.evaluator
7+
8+
import cats.data.XorT
9+
import cats.~>
10+
import cats._, cats.std.all._
11+
import org.scalaexercises.evaluator.EvaluatorResponses.{EvalIO, EvaluationException, EvaluationResponse, EvaluationResult}
12+
import org.scalaexercises.evaluator.free.algebra.EvaluatorOp
13+
14+
import scala.concurrent.Future
15+
import scala.concurrent.ExecutionContext.Implicits.global
16+
17+
class EvaluatorClient(url: String, authKey: String) {
18+
19+
lazy val api: EvaluatorAPI[EvaluatorOp] = new EvaluatorAPI(url, authKey)
20+
21+
}
22+
23+
object EvaluatorClient {
24+
25+
def apply(url: String, authKey: String) =
26+
new EvaluatorClient(url, authKey)
27+
28+
implicit class EvaluationIOSyntaxXOR[A](
29+
evalIO: EvalIO[EvaluationResponse[A]]) {
30+
31+
def exec(
32+
implicit I: (EvaluatorOp ~> Future)): Future[EvaluationResponse[A]] =
33+
evalIO foldMap I
34+
35+
def liftEvaluator: XorT[EvalIO, EvaluationException, EvaluationResult[A]] =
36+
XorT[EvalIO, EvaluationException, EvaluationResult[A]](evalIO)
37+
38+
}
39+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* scala-exercises-evaluator-client
3+
* Copyright (C) 2015-2016 47 Degrees, LLC. <http://www.47deg.com>
4+
*/
5+
6+
package org.scalaexercises.evaluator
7+
8+
import cats.data.Xor
9+
import cats.free.Free
10+
import cats.syntax.xor._
11+
import io.circe.Decoder
12+
import io.circe.parser._
13+
import io.circe.generic.auto._
14+
import org.scalaexercises.evaluator.free.algebra.EvaluatorOp
15+
16+
import scala.concurrent.Future
17+
import scala.concurrent.ExecutionContext.Implicits.global
18+
19+
import fr.hmil.roshttp.HttpResponse
20+
import fr.hmil.roshttp.HeaderMap
21+
22+
object EvaluatorResponses {
23+
24+
type EvalIO[A] = Free[EvaluatorOp, A]
25+
26+
type EvaluationResponse[A] = EvaluationException Xor EvaluationResult[A]
27+
28+
case class EvaluationResult[A](result: A,
29+
statusCode: Int,
30+
headers: Map[String, String])
31+
32+
sealed abstract class EvaluationException(msg: String,
33+
cause: Option[Throwable] = None)
34+
extends Throwable(msg) {
35+
cause foreach initCause
36+
}
37+
38+
case class JsonParsingException(msg: String, json: String)
39+
extends EvaluationException(msg)
40+
41+
case class UnexpectedException(msg: String) extends EvaluationException(msg)
42+
43+
def toEntity[A](futureResponse: Future[HttpResponse])(
44+
implicit D: Decoder[A]): Future[EvaluationResponse[A]] =
45+
futureResponse map {
46+
case r if isSuccess(r.statusCode)
47+
decode[A](r.body).fold(
48+
e
49+
JsonParsingException(e.getMessage, r.body)
50+
.left[EvaluationResult[A]],
51+
result
52+
Xor.Right(
53+
EvaluationResult(result, r.statusCode, r.headers.toLowerCase))
54+
)
55+
case r
56+
UnexpectedException(
57+
s"Failed invoking get with status : ${r.statusCode}, body : \n ${r.body}")
58+
.left[EvaluationResult[A]]
59+
}
60+
61+
private[this] def isSuccess(statusCode: Int) =
62+
statusCode >= 200 && statusCode <= 299
63+
64+
implicit class HeadersLowerCase[A >: String](headers: HeaderMap[A]) {
65+
66+
def toLowerCase: Map[String, A] =
67+
headers.iterator.map(t => (t._1.toLowerCase, t._2)).toList.toMap
68+
}
69+
}

client/src/main/scala/org/scalaexercises/evaluator/api/Evaluator.scala renamed to client/shared/src/main/scala/org/scalaexercises/evaluator/api/Evaluator.scala

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import org.scalaexercises.evaluator.http.HttpClient
1111
import io.circe.generic.auto._
1212
import io.circe.syntax._
1313

14-
import scala.concurrent.duration.Duration
14+
import scala.concurrent.Future
1515

1616
class Evaluator {
1717

@@ -21,16 +21,12 @@ class Evaluator {
2121

2222
def eval(url: String,
2323
authKey: String,
24-
connTimeout: Duration,
25-
readTimeout: Duration,
2624
resolvers: List[String] = Nil,
2725
dependencies: List[Dependency] = Nil,
28-
code: String): EvaluationResponse[EvalResponse] =
26+
code: String): Future[EvaluationResponse[EvalResponse]] =
2927
httpClient.post[EvalResponse](
3028
url = url,
3129
secretKey = authKey,
32-
connTimeout = connTimeout,
33-
readTimeout = readTimeout,
3430
data = EvalRequest(resolvers, dependencies, code).asJson.noSpaces)
3531

3632
}

client/src/main/scala/org/scalaexercises/evaluator/free/algebra/EvaluatorOps.scala renamed to client/shared/src/main/scala/org/scalaexercises/evaluator/free/algebra/EvaluatorOps.scala

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,9 @@ import cats.free.{Free, Inject}
99
import org.scalaexercises.evaluator.{Dependency, EvalResponse}
1010
import org.scalaexercises.evaluator.EvaluatorResponses.EvaluationResponse
1111

12-
import scala.concurrent.duration._
13-
import scala.concurrent.duration.Duration
14-
1512
sealed trait EvaluatorOp[A]
1613
final case class Evaluates(url: String,
1714
authKey: String,
18-
connTimeout: Duration,
19-
readTimeout: Duration,
2015
resolvers: List[String] = Nil,
2116
dependencies: List[Dependency] = Nil,
2217
code: String)
@@ -27,21 +22,12 @@ class EvaluatorOps[F[_]](implicit I: Inject[EvaluatorOp, F]) {
2722
def evaluates(
2823
url: String,
2924
authKey: String,
30-
connTimeout: Duration,
31-
readTimeout: Duration,
3225
resolvers: List[String] = Nil,
3326
dependencies: List[Dependency] = Nil,
3427
code: String
3528
): Free[F, EvaluationResponse[EvalResponse]] =
3629
Free.inject[EvaluatorOp, F](
37-
Evaluates(
38-
url,
39-
authKey,
40-
connTimeout,
41-
readTimeout,
42-
resolvers,
43-
dependencies,
44-
code))
30+
Evaluates(url, authKey, resolvers, dependencies, code))
4531

4632
}
4733

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* scala-exercises-evaluator-client
3+
* Copyright (C) 2015-2016 47 Degrees, LLC. <http://www.47deg.com>
4+
*/
5+
6+
package org.scalaexercises.evaluator.free.interpreters
7+
8+
import cats.~>
9+
import org.scalaexercises.evaluator.api.Evaluator
10+
import org.scalaexercises.evaluator.free.algebra.{Evaluates, EvaluatorOp}
11+
12+
import scala.concurrent.Future
13+
14+
trait Interpreter {
15+
16+
/**
17+
* Lifts Evaluator Ops to an effect capturing Monad such as Task via natural transformations
18+
*/
19+
implicit def evaluatorOpsInterpreter: EvaluatorOp ~> Future =
20+
new (EvaluatorOp ~> Future) {
21+
22+
val evaluator = new Evaluator()
23+
24+
def apply[A](fa: EvaluatorOp[A]): Future[A] = fa match {
25+
case Evaluates(url, authKey, resolvers, dependencies, code)
26+
evaluator.eval(url, authKey, resolvers, dependencies, code)
27+
}
28+
29+
}
30+
}

client/src/main/scala/org/scalaexercises/evaluator/http/HttpClient.scala renamed to client/shared/src/main/scala/org/scalaexercises/evaluator/http/HttpClient.scala

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import io.circe.Decoder
99
import org.scalaexercises.evaluator.EvaluatorResponses
1010
import org.scalaexercises.evaluator.EvaluatorResponses.EvaluationResponse
1111

12-
import scala.concurrent.duration._
13-
import scala.concurrent.duration.Duration
12+
import scala.concurrent.Future
1413

1514
object HttpClient {
1615

@@ -26,17 +25,11 @@ class HttpClient {
2625
url: String,
2726
secretKey: String,
2827
method: String = "post",
29-
connTimeout: Duration,
30-
readTimeout: Duration,
3128
headers: Headers = Map.empty,
3229
data: String
33-
)(implicit D: Decoder[A]): EvaluationResponse[A] =
30+
)(implicit D: Decoder[A]): Future[EvaluationResponse[A]] =
3431
EvaluatorResponses.toEntity(
35-
HttpRequestBuilder(
36-
url = url,
37-
httpVerb = method,
38-
connTimeout = connTimeout,
39-
readTimeout = readTimeout)
32+
HttpRequestBuilder(url = url, httpVerb = method)
4033
.withHeaders(headers + (authHeaderName -> secretKey))
4134
.withBody(data)
4235
.run)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* scala-exercises-evaluator-client
3+
* Copyright (C) 2015-2016 47 Degrees, LLC. <http://www.47deg.com>
4+
*/
5+
6+
package org.scalaexercises.evaluator.http
7+
8+
import org.scalaexercises.evaluator.http.HttpClient._
9+
10+
import scala.concurrent.Future
11+
12+
import fr.hmil.roshttp.{HttpRequest, Method, HttpResponse}
13+
import fr.hmil.roshttp.body.BodyPart
14+
15+
import java.nio.ByteBuffer
16+
17+
case class HttpRequestBuilder(
18+
url: String,
19+
httpVerb: String,
20+
headers: Headers = Map.empty[String, String],
21+
body: String = ""
22+
) {
23+
24+
case class CirceJSONBody(value: String) extends BodyPart {
25+
override def contentType: String = s"application/json; charset=utf-8"
26+
27+
override def content: ByteBuffer = ByteBuffer.wrap(value.getBytes("utf-8"))
28+
}
29+
30+
def withHeaders(headers: Headers) = copy(headers = headers)
31+
32+
def withBody(body: String) = copy(body = body)
33+
34+
def run: Future[HttpResponse] = {
35+
36+
val request = HttpRequest(url)
37+
.withMethod(Method(httpVerb))
38+
.withHeader("content-type", "application/json")
39+
.withHeaders(headers.toList: _*)
40+
41+
request.send(CirceJSONBody(body))
42+
}
43+
}

0 commit comments

Comments
 (0)