diff --git a/client/src/main/scala/org/scalaexercises/evaluator/Decoders.scala b/client/src/main/scala/org/scalaexercises/evaluator/Decoders.scala deleted file mode 100644 index b43093f0..00000000 --- a/client/src/main/scala/org/scalaexercises/evaluator/Decoders.scala +++ /dev/null @@ -1,23 +0,0 @@ -/* - * - * scala-exercises - evaluator-client - * Copyright (C) 2015-2019 47 Degrees, LLC. - * - */ - -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.forProduct5("msg", "value", "valueType", "consoleOutput", "compilationInfos")( - EvalResponse.apply) -} diff --git a/client/src/main/scala/org/scalaexercises/evaluator/EvaluatorAPI.scala b/client/src/main/scala/org/scalaexercises/evaluator/EvaluatorAPI.scala deleted file mode 100644 index 4707e950..00000000 --- a/client/src/main/scala/org/scalaexercises/evaluator/EvaluatorAPI.scala +++ /dev/null @@ -1,21 +0,0 @@ -/* - * - * scala-exercises - evaluator-client - * Copyright (C) 2015-2019 47 Degrees, LLC. - * - */ - -package org.scalaexercises.evaluator - -import cats.free.Free -import org.scalaexercises.evaluator.EvaluatorResponses.EvaluationResponse -import org.scalaexercises.evaluator.free.algebra.EvaluatorOps - -class EvaluatorAPI[F[_]](url: String, authKey: String)(implicit O: EvaluatorOps[F]) { - - def evaluates( - resolvers: List[String] = Nil, - dependencies: List[Dependency] = Nil, - code: String): Free[F, EvaluationResponse[EvalResponse]] = - O.evaluates(url, authKey, resolvers, dependencies, code) -} diff --git a/client/src/main/scala/org/scalaexercises/evaluator/EvaluatorClient.scala b/client/src/main/scala/org/scalaexercises/evaluator/EvaluatorClient.scala index 69433b40..3b0ec3a7 100644 --- a/client/src/main/scala/org/scalaexercises/evaluator/EvaluatorClient.scala +++ b/client/src/main/scala/org/scalaexercises/evaluator/EvaluatorClient.scala @@ -7,38 +7,19 @@ package org.scalaexercises.evaluator -import cats.data.EitherT -import cats.~> -import cats.implicits._ -import org.scalaexercises.evaluator.EvaluatorResponses.{ - EvalIO, - EvaluationException, - EvaluationResponse, - EvaluationResult -} -import org.scalaexercises.evaluator.free.algebra.EvaluatorOp - -import scala.concurrent.Future -import scala.concurrent.ExecutionContext.Implicits.global +import cats.effect.{ConcurrentEffect, Resource} +import org.http4s.client.Client +import org.http4s.client.blaze.BlazeClientBuilder +import org.scalaexercises.evaluator.service.{HttpClientHandler, HttpClientService} -class EvaluatorClient(url: String, authKey: String) { - - lazy val api: EvaluatorAPI[EvaluatorOp] = new EvaluatorAPI(url, authKey) - -} +import scala.concurrent.ExecutionContext object EvaluatorClient { - def apply(url: String, authKey: String) = - new EvaluatorClient(url, authKey) - - implicit class EvaluationIOSyntaxEither[A](evalIO: EvalIO[EvaluationResponse[A]]) { - - def exec(implicit I: (EvaluatorOp ~> Future)): Future[EvaluationResponse[A]] = - evalIO foldMap I + private def clientResource[F[_]: ConcurrentEffect]: Resource[F, Client[F]] = + BlazeClientBuilder[F](ExecutionContext.global).resource - def liftEvaluator: EitherT[EvalIO, EvaluationException, EvaluationResult[A]] = - EitherT[EvalIO, EvaluationException, EvaluationResult[A]](evalIO) + def apply[F[_]: ConcurrentEffect](url: String, authKey: String): HttpClientService[F] = + HttpClientHandler[F](url, authKey, clientResource[F]) - } } diff --git a/client/src/main/scala/org/scalaexercises/evaluator/EvaluatorResponses.scala b/client/src/main/scala/org/scalaexercises/evaluator/EvaluatorResponses.scala deleted file mode 100644 index c4dfe52d..00000000 --- a/client/src/main/scala/org/scalaexercises/evaluator/EvaluatorResponses.scala +++ /dev/null @@ -1,62 +0,0 @@ -/* - * - * scala-exercises - evaluator-client - * Copyright (C) 2015-2019 47 Degrees, LLC. - * - */ - -package org.scalaexercises.evaluator - -import cats.free.Free -import cats.implicits._ -import io.circe.Decoder -import io.circe.parser._ -import org.scalaexercises.evaluator.free.algebra.EvaluatorOp - -import scala.concurrent.Future -import scala.concurrent.ExecutionContext.Implicits.global -import fr.hmil.roshttp.response.SimpleHttpResponse -import fr.hmil.roshttp.util.HeaderMap - -object EvaluatorResponses { - - type EvalIO[A] = Free[EvaluatorOp, A] - - type EvaluationResponse[A] = Either[EvaluationException, EvaluationResult[A]] - - case class EvaluationResult[A](result: A, statusCode: Int, headers: Map[String, String]) - - sealed abstract class EvaluationException(msg: String, cause: Option[Throwable] = None) - extends Throwable(msg) { - cause foreach initCause - } - - case class JsonParsingException(msg: String, json: String) extends EvaluationException(msg) - - case class UnexpectedException(msg: String) extends EvaluationException(msg) - - def toEntity[A](futureResponse: Future[SimpleHttpResponse])( - implicit D: Decoder[A]): Future[EvaluationResponse[A]] = - futureResponse map { - case r if isSuccess(r.statusCode) ⇒ - decode[A](r.body) match { - case Left(e) => - Either.left(JsonParsingException(e.getMessage, r.body)) - case Right(result) => - Either.right(EvaluationResult(result, r.statusCode, r.headers.toLowerCase)) - } - case r ⇒ - Either.left( - UnexpectedException( - s"Failed i(nvoking get with status : ${r.statusCode}, body : \n ${r.body}")) - } - - private[this] def isSuccess(statusCode: Int) = - statusCode >= 200 && statusCode <= 299 - - implicit class HeadersLowerCase[A >: String](headers: HeaderMap[A]) { - - def toLowerCase: Map[String, A] = - headers.iterator.map(t => (t._1.toLowerCase, t._2)).toList.toMap - } -} diff --git a/client/src/main/scala/org/scalaexercises/evaluator/api/Evaluator.scala b/client/src/main/scala/org/scalaexercises/evaluator/api/Evaluator.scala deleted file mode 100644 index 3d0c4bc8..00000000 --- a/client/src/main/scala/org/scalaexercises/evaluator/api/Evaluator.scala +++ /dev/null @@ -1,35 +0,0 @@ -/* - * - * scala-exercises - evaluator-client - * Copyright (C) 2015-2019 47 Degrees, LLC. - * - */ - -package org.scalaexercises.evaluator.api - -import io.circe.generic.auto._ -import io.circe.syntax._ -import org.scalaexercises.evaluator.EvaluatorResponses.EvaluationResponse -import org.scalaexercises.evaluator.http.HttpClient -import org.scalaexercises.evaluator.{Decoders, Dependency, EvalRequest, EvalResponse} - -import scala.concurrent.Future - -class Evaluator { - - import Decoders._ - - private val httpClient = new HttpClient - - def eval( - url: String, - authKey: String, - resolvers: List[String] = Nil, - dependencies: List[Dependency] = Nil, - code: String): Future[EvaluationResponse[EvalResponse]] = - httpClient.post[EvalResponse]( - url = url, - secretKey = authKey, - data = EvalRequest(resolvers, dependencies, code).asJson.noSpaces) - -} diff --git a/client/src/main/scala/org/scalaexercises/evaluator/free/algebra/EvaluatorOps.scala b/client/src/main/scala/org/scalaexercises/evaluator/free/algebra/EvaluatorOps.scala deleted file mode 100644 index c18022af..00000000 --- a/client/src/main/scala/org/scalaexercises/evaluator/free/algebra/EvaluatorOps.scala +++ /dev/null @@ -1,41 +0,0 @@ -/* - * - * scala-exercises - evaluator-client - * Copyright (C) 2015-2019 47 Degrees, LLC. - * - */ - -package org.scalaexercises.evaluator.free.algebra - -import cats.free.{Free, Inject} -import org.scalaexercises.evaluator.EvaluatorResponses.EvaluationResponse -import org.scalaexercises.evaluator.{Dependency, EvalResponse} - -sealed trait EvaluatorOp[A] -final case class Evaluates( - url: String, - authKey: String, - resolvers: List[String] = Nil, - dependencies: List[Dependency] = Nil, - code: String) - extends EvaluatorOp[EvaluationResponse[EvalResponse]] - -class EvaluatorOps[F[_]](implicit I: Inject[EvaluatorOp, F]) { - - def evaluates( - url: String, - authKey: String, - resolvers: List[String] = Nil, - dependencies: List[Dependency] = Nil, - code: String - ): Free[F, EvaluationResponse[EvalResponse]] = - Free.inject[EvaluatorOp, F](Evaluates(url, authKey, resolvers, dependencies, code)) - -} - -object EvaluatorOps { - - implicit def instance[F[_]](implicit I: Inject[EvaluatorOp, F]): EvaluatorOps[F] = - new EvaluatorOps[F] - -} diff --git a/client/src/main/scala/org/scalaexercises/evaluator/free/interpreters/Interpreter.scala b/client/src/main/scala/org/scalaexercises/evaluator/free/interpreters/Interpreter.scala deleted file mode 100644 index a8928cfc..00000000 --- a/client/src/main/scala/org/scalaexercises/evaluator/free/interpreters/Interpreter.scala +++ /dev/null @@ -1,32 +0,0 @@ -/* - * - * scala-exercises - evaluator-client - * Copyright (C) 2015-2019 47 Degrees, LLC. - * - */ - -package org.scalaexercises.evaluator.free.interpreters - -import cats.~> -import org.scalaexercises.evaluator.api.Evaluator -import org.scalaexercises.evaluator.free.algebra.{Evaluates, EvaluatorOp} - -import scala.concurrent.Future - -trait Interpreter { - - /** - * Lifts Evaluator Ops to an effect capturing Monad such as Task via natural transformations - */ - implicit def evaluatorOpsInterpreter: EvaluatorOp ~> Future = - new (EvaluatorOp ~> Future) { - - val evaluator = new Evaluator() - - def apply[A](fa: EvaluatorOp[A]): Future[A] = fa match { - case Evaluates(url, authKey, resolvers, dependencies, code) ⇒ - evaluator.eval(url, authKey, resolvers, dependencies, code) - } - - } -} diff --git a/client/src/main/scala/org/scalaexercises/evaluator/http/HttpClient.scala b/client/src/main/scala/org/scalaexercises/evaluator/http/HttpClient.scala deleted file mode 100644 index 89d73905..00000000 --- a/client/src/main/scala/org/scalaexercises/evaluator/http/HttpClient.scala +++ /dev/null @@ -1,38 +0,0 @@ -/* - * - * scala-exercises - evaluator-client - * Copyright (C) 2015-2019 47 Degrees, LLC. - * - */ - -package org.scalaexercises.evaluator.http - -import io.circe.Decoder -import org.scalaexercises.evaluator.EvaluatorResponses -import org.scalaexercises.evaluator.EvaluatorResponses.EvaluationResponse - -import scala.concurrent.Future - -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]): Future[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 deleted file mode 100644 index 69ac6a49..00000000 --- a/client/src/main/scala/org/scalaexercises/evaluator/http/HttpRequestBuilder.scala +++ /dev/null @@ -1,46 +0,0 @@ -/* - * - * scala-exercises - evaluator-client - * Copyright (C) 2015-2019 47 Degrees, LLC. - * - */ - -package org.scalaexercises.evaluator.http - -import org.scalaexercises.evaluator.http.HttpClient._ - -import scala.concurrent.Future -import fr.hmil.roshttp.{HttpRequest, Method} -import fr.hmil.roshttp.body.BulkBodyPart -import fr.hmil.roshttp.response.SimpleHttpResponse -import java.nio.ByteBuffer - -import monix.execution.Scheduler.Implicits.global - -case class HttpRequestBuilder( - url: String, - httpVerb: String, - headers: Headers = Map.empty[String, String], - body: String = "" -) { - - case class CirceJSONBody(value: String) extends BulkBodyPart { - override def contentType: String = s"application/json; charset=utf-8" - override def contentData: ByteBuffer = - ByteBuffer.wrap(value.getBytes("utf-8")) - } - - def withHeaders(headers: Headers) = copy(headers = headers) - - def withBody(body: String) = copy(body = body) - - def run: Future[SimpleHttpResponse] = { - - val request = HttpRequest(url) - .withMethod(Method(httpVerb)) - .withHeader("content-type", "application/json") - .withHeaders(headers.toList: _*) - - request.send(CirceJSONBody(body)) - } -} diff --git a/client/src/main/scala/org/scalaexercises/evaluator/implicits.scala b/client/src/main/scala/org/scalaexercises/evaluator/implicits.scala deleted file mode 100644 index 3fa354e9..00000000 --- a/client/src/main/scala/org/scalaexercises/evaluator/implicits.scala +++ /dev/null @@ -1,12 +0,0 @@ -/* - * - * scala-exercises - evaluator-client - * Copyright (C) 2015-2019 47 Degrees, LLC. - * - */ - -package org.scalaexercises.evaluator - -import org.scalaexercises.evaluator.free.interpreters.Interpreter - -object implicits extends Interpreter diff --git a/client/src/main/scala/org/scalaexercises/evaluator/service/HttpClientHandler.scala b/client/src/main/scala/org/scalaexercises/evaluator/service/HttpClientHandler.scala new file mode 100644 index 00000000..2f0c1bbb --- /dev/null +++ b/client/src/main/scala/org/scalaexercises/evaluator/service/HttpClientHandler.scala @@ -0,0 +1,35 @@ +/* + * + * scala-exercises - evaluator-client + * Copyright (C) 2015-2019 47 Degrees, LLC. + * + */ + +package org.scalaexercises.evaluator.service +import cats.effect.{Resource, Sync} +import cats.implicits._ +import org.http4s.client.Client +import org.http4s.{Header, Method, Request, Uri} +import org.scalaexercises.evaluator._ +import org.scalaexercises.evaluator.util.Codecs._ + +object HttpClientHandler { + + private def headerToken(value: String) = Header("x-scala-eval-api-token", value) + + private val headerContentType = Header("content-type", "application/json") + + def apply[F[_]](uri: String, authString: String, resource: Resource[F, Client[F]])( + implicit F: Sync[F]): HttpClientService[F] = + new HttpClientService[F] { + override def evaluates(evalRequest: EvalRequest): F[EvalResponse] = + for { + uri <- F.fromEither(Uri.fromString(uri)) + request = Request[F](Method.POST, uri) + .withEntity(evalRequest) + .withHeaders(headerToken(authString), headerContentType) + result <- resource.use(_.expect[EvalResponse](request)) + } yield result + } + +} diff --git a/client/src/main/scala/org/scalaexercises/evaluator/service/HttpClientService.scala b/client/src/main/scala/org/scalaexercises/evaluator/service/HttpClientService.scala new file mode 100644 index 00000000..562e71bb --- /dev/null +++ b/client/src/main/scala/org/scalaexercises/evaluator/service/HttpClientService.scala @@ -0,0 +1,16 @@ +/* + * + * scala-exercises - evaluator-client + * Copyright (C) 2015-2019 47 Degrees, LLC. + * + */ + +package org.scalaexercises.evaluator.service + +import org.scalaexercises.evaluator.{EvalRequest, EvalResponse} + +trait HttpClientService[F[_]] { + + def evaluates(evalRequest: EvalRequest): F[EvalResponse] + +} diff --git a/client/src/main/scala/org/scalaexercises/evaluator/util/Codecs.scala b/client/src/main/scala/org/scalaexercises/evaluator/util/Codecs.scala new file mode 100644 index 00000000..0a14a99f --- /dev/null +++ b/client/src/main/scala/org/scalaexercises/evaluator/util/Codecs.scala @@ -0,0 +1,50 @@ +/* + * + * scala-exercises - evaluator-client + * Copyright (C) 2015-2019 47 Degrees, LLC. + * + */ + +package org.scalaexercises.evaluator.util + +import cats.effect.Sync +import io.circe.generic.semiauto +import io.circe.generic.semiauto.deriveDecoder +import io.circe.syntax.EncoderOps +import io.circe.{Decoder, Encoder, Json} +import org.http4s.circe._ +import org.http4s.{EntityDecoder, EntityEncoder} +import org.scalaexercises.evaluator._ + +object Codecs { + + implicit val decodeRangePosition: Decoder[RangePosition] = deriveDecoder[RangePosition] + + implicit val decodeCompilationInfo: Decoder[CompilationInfo] = deriveDecoder[CompilationInfo] + + implicit val decodeEvalResponse: Decoder[EvalResponse] = deriveDecoder[EvalResponse] + + implicit val encodeEvalRequest: Encoder[EvalRequest] = Encoder.instance( + req => + Json.obj( + ("resolvers", Json.arr(req.resolvers.map(Json.fromString): _*)), + ("dependencies", Json.arr(req.dependencies.map(_.asJson): _*)), + ("code", Json.fromString(req.code)) + )) + + implicit val encodeDependency: Encoder[Dependency] = Encoder.instance( + dep => + Json.obj( + ("groupId", Json.fromString(dep.groupId)), + ("artifactId", Json.fromString(dep.artifactId)), + ("version", Json.fromString(dep.version)), + ("exclusions", dep.exclusions.asJson) + )) + + implicit val encodeExclusion: Encoder[Exclusion] = semiauto.deriveEncoder[Exclusion] + + implicit def decoder[F[_]: Sync, A: Decoder]: EntityDecoder[F, A] = jsonOf[F, A] + + implicit def encoder[F[_]: Sync, A: Encoder]: EntityEncoder[F, A] = jsonEncoderOf[F, A] + +}