diff --git a/app/jvm/src/main/scala/io/youi/app/OfflineGenerator.scala b/app/jvm/src/main/scala/io/youi/app/OfflineGenerator.scala index 30b6ba71c..493f0a711 100644 --- a/app/jvm/src/main/scala/io/youi/app/OfflineGenerator.scala +++ b/app/jvm/src/main/scala/io/youi/app/OfflineGenerator.scala @@ -25,8 +25,8 @@ class OfflineGenerator(application: ServerApplication, file.getParentFile.mkdirs() scribe.info(s"Writing $path to ${file.getAbsolutePath}..") content match { - case c: StringContent => IO.stream(c.value, file) - case c: URLContent => IO.stream(c.url, file) + case c: StringContent => Stream.apply(c.value, file) + case c: URLContent => Stream.apply(c.url, file) case _ => throw new RuntimeException(s"Unsupported Content-Type: $content") } } diff --git a/app/jvm/src/main/scala/io/youi/app/ServerApplication.scala b/app/jvm/src/main/scala/io/youi/app/ServerApplication.scala index 17e54bf00..bbc7c3231 100644 --- a/app/jvm/src/main/scala/io/youi/app/ServerApplication.scala +++ b/app/jvm/src/main/scala/io/youi/app/ServerApplication.scala @@ -254,7 +254,7 @@ trait ServerApplication extends YouIApplication with Server { val directory = cacheDirectory() val file = new File(directory, path) file.getParentFile.mkdirs() - IO.stream(new java.net.URL(url.toString), file) + Stream.apply(new java.net.URL(url.toString), file) val content = Content.file(file) handler.matcher(http.path.exact(path)).resource(content) path diff --git a/app/jvm/src/main/scala/io/youi/upload/UploadManager.scala b/app/jvm/src/main/scala/io/youi/upload/UploadManager.scala index ca022c146..a6a56af77 100644 --- a/app/jvm/src/main/scala/io/youi/upload/UploadManager.scala +++ b/app/jvm/src/main/scala/io/youi/upload/UploadManager.scala @@ -6,7 +6,7 @@ import io.youi.http.content.{Content, FormDataContent} import io.youi.http.{HttpConnection, HttpStatus} import io.youi.net._ import io.youi.server.handler.HttpHandler -import io.youi.stream.IO +import io.youi.stream.Stream import reactify.Channel import scala.concurrent.Future @@ -58,7 +58,7 @@ case class UploadManager(path: Path = path"/upload", val slices = content.string("slices").value.split(',').toList val sources = slices.map(fn => new File(directory, fn)) val destination = File.createTempFile(fileName, s".$ext", directory) - IO.merge(sources, destination) + Stream.merge(sources, destination) received @= UploadedFile(destination, fileName) destination.getName } diff --git a/build.sbt b/build.sbt index 013bcbe17..7333fd558 100644 --- a/build.sbt +++ b/build.sbt @@ -6,15 +6,11 @@ ThisBuild / organization := "io.youi" ThisBuild / version := "0.15.0-SNAPSHOT" ThisBuild / scalaVersion := "2.13.8" ThisBuild / crossScalaVersions := List("2.13.8", "2.12.16") -ThisBuild / resolvers ++= Seq( - Resolver.sonatypeRepo("releases"), - Resolver.sonatypeRepo("snapshots") -) ThisBuild / scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature") ThisBuild / publishTo := sonatypePublishToBundle.value ThisBuild / sonatypeProfileName := "io.youi" -ThisBuild / publishMavenStyle := true +//ThisBuild / publishMavenStyle := true ThisBuild / licenses := Seq("MIT" -> url("https://github.com/outr/youi/blob/master/LICENSE")) ThisBuild / sonatypeProjectHosting := Some(xerial.sbt.Sonatype.GitHubHosting("outr", "youi", "matt@outr.com")) ThisBuild / homepage := Some(url("https://github.com/outr/youi")) @@ -70,6 +66,8 @@ val fs2Version: String = "3.2.12" val scalaTestVersion: String = "3.2.13" +val catsEffectTestVersion: String = "1.4.0" + ThisBuild / evictionErrorLevel := Level.Info lazy val root = project.in(file(".")) @@ -93,7 +91,8 @@ lazy val core = crossProject(JSPlatform, JVMPlatform).in(file("core")) "com.outr" %%% "reactify" % reactifyVersion, "org.typelevel" %%% "cats-effect" % catsVersion, "co.fs2" %% "fs2-core" % fs2Version, - "org.scalatest" %%% "scalatest" % scalaTestVersion % "test" + "org.scalatest" %%% "scalatest" % scalaTestVersion % Test, + "org.typelevel" %%% "cats-effect-testing-scalatest" % catsEffectTestVersion % Test ) ) .jsSettings( @@ -109,7 +108,8 @@ lazy val client = crossProject(JSPlatform, JVMPlatform).in(file("client")) .settings( name := "youi-client", libraryDependencies ++= Seq( - "org.scalatest" %%% "scalatest" % scalaTestVersion % "test" + "org.scalatest" %%% "scalatest" % scalaTestVersion % Test, + "org.typelevel" %%% "cats-effect-testing-scalatest" % catsEffectTestVersion % Test ) ) .jvmSettings( @@ -126,7 +126,8 @@ lazy val spatial = crossProject(JSPlatform, JVMPlatform).in(file("spatial")) .settings( name := "youi-spatial", libraryDependencies ++= Seq( - "org.scalatest" %%% "scalatest" % scalaTestVersion % "test" + "org.scalatest" %%% "scalatest" % scalaTestVersion % Test, + "org.typelevel" %%% "cats-effect-testing-scalatest" % catsEffectTestVersion % Test ) ) .dependsOn(core) @@ -138,7 +139,8 @@ lazy val stream = project.in(file("stream")) .settings( name := "youi-stream", libraryDependencies ++= Seq( - "org.scalatest" %%% "scalatest" % scalaTestVersion % "test" + "org.scalatest" %%% "scalatest" % scalaTestVersion % Test, + "org.typelevel" %%% "cats-effect-testing-scalatest" % catsEffectTestVersion % Test ) ) .dependsOn(coreJVM) @@ -149,7 +151,8 @@ lazy val dom = project.in(file("dom")) name := "youi-dom", libraryDependencies ++= Seq( "com.outr" %%% "profig" % profigVersion, - "org.scalatest" %%% "scalatest" % scalaTestVersion % "test" + "org.scalatest" %%% "scalatest" % scalaTestVersion % Test, + "org.typelevel" %%% "cats-effect-testing-scalatest" % catsEffectTestVersion % Test ), test := {}, // TODO: figure out why this no longer works jsEnv := new org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv() @@ -164,7 +167,8 @@ lazy val communication = crossProject(JSPlatform, JVMPlatform) name := "youi-communication", libraryDependencies ++= Seq( "org.scala-lang" % "scala-reflect" % scalaVersion.value, - "org.scalatest" %%% "scalatest" % scalaTestVersion % "test" + "org.scalatest" %%% "scalatest" % scalaTestVersion % Test, + "org.typelevel" %%% "cats-effect-testing-scalatest" % catsEffectTestVersion % Test ) ) .dependsOn(core) @@ -177,7 +181,9 @@ lazy val server = project.in(file("server")) name := "youi-server", libraryDependencies ++= Seq( "net.sf.uadetector" % "uadetector-resources" % uaDetectorVersion, - "org.scalatest" %%% "scalatest" % scalaTestVersion % "test" + "org.typelevel" %% "cats-effect" % catsVersion, + "org.scalatest" %% "scalatest" % scalaTestVersion % Test, + "org.typelevel" %% "cats-effect-testing-scalatest" % catsEffectTestVersion % Test ) ) .dependsOn(communicationJVM, stream) @@ -188,7 +194,8 @@ lazy val serverUndertow = project.in(file("serverUndertow")) fork := true, libraryDependencies ++= Seq( "io.undertow" % "undertow-core" % undertowVersion, - "org.scalatest" %%% "scalatest" % scalaTestVersion % "test" + "org.scalatest" %%% "scalatest" % scalaTestVersion % Test, + "org.typelevel" %%% "cats-effect-testing-scalatest" % catsEffectTestVersion % Test ) ) .dependsOn(server, clientJVM % "test->test") @@ -228,7 +235,8 @@ lazy val app = crossProject(JSPlatform, JVMPlatform).in(file("app")) .settings( name := "youi-app", libraryDependencies ++= Seq( - "org.scalatest" %%% "scalatest" % scalaTestVersion % "test" + "org.scalatest" %%% "scalatest" % scalaTestVersion % Test, + "org.typelevel" %%% "cats-effect-testing-scalatest" % catsEffectTestVersion % Test ) ) .dependsOn(core, communication) @@ -244,7 +252,6 @@ lazy val example = crossApplication.in(file("example")) jsEnv := new org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv() ) .jvmSettings( - scalaJSUseMainModuleInitializer := true, libraryDependencies ++= Seq( "org.scala-lang" % "scala-reflect" % scalaVersion.value, "org.scala-lang.modules" %% "scala-xml" % scalaXMLVersion diff --git a/client/js/src/main/scala/io/youi/client/JSHttpClientImplementation.scala b/client/js/src/main/scala/io/youi/client/JSHttpClientImplementation.scala index 0fdcc7101..c44304ee9 100644 --- a/client/js/src/main/scala/io/youi/client/JSHttpClientImplementation.scala +++ b/client/js/src/main/scala/io/youi/client/JSHttpClientImplementation.scala @@ -1,17 +1,17 @@ package io.youi.client +import cats.effect.IO import io.youi.ajax.{AjaxAction, AjaxRequest} import io.youi.http.content._ import io.youi.http.{Headers, HttpRequest, HttpResponse, HttpStatus} import io.youi.net.ContentType -import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success, Try} class JSHttpClientImplementation(config: HttpClientConfig) extends HttpClientImplementation(config) { private val HeaderRegex = """(.+)[:](.+)""".r - override def send(request: HttpRequest, executionContext: ExecutionContext): Future[HttpResponse] = { - implicit val implicitContext: ExecutionContext = executionContext + override def send(request: HttpRequest): IO[Try[HttpResponse]] = { val manager = config.connectionPool.asInstanceOf[JSConnectionPool].manager val ajaxRequest = new AjaxRequest( url = request.url, @@ -22,25 +22,27 @@ class JSHttpClientImplementation(config: HttpClientConfig) extends HttpClientImp responseType = "" ) val action = new AjaxAction(ajaxRequest) - manager.enqueue(action).map { xmlHttpRequest => - val headers: Map[String, List[String]] = xmlHttpRequest.getAllResponseHeaders().split('\n').map(_.trim).map { - case HeaderRegex(key, value) => key.trim -> value.trim - case s => throw new RuntimeException(s"Invalid Header: [$s]") - }.groupBy(_._1).map { - case (key, array) => key -> array.toList.map(_._2) - } - val content = xmlHttpRequest.responseType match { - case null => None - case _ => { - val `type` = if (xmlHttpRequest.responseType == "") ContentType.`text/plain` else ContentType.parse(xmlHttpRequest.responseType) - Some(Content.string(xmlHttpRequest.responseText, `type`)) + manager.enqueue(action).map { + case Failure(err) => Failure(err) + case Success(xmlHttpRequest) => + val headers: Map[String, List[String]] = xmlHttpRequest.getAllResponseHeaders().split('\n').map(_.trim).map { + case HeaderRegex(key, value) => key.trim -> value.trim + case s => throw new RuntimeException(s"Invalid Header: [$s]") + }.groupBy(_._1).map { + case (key, array) => key -> array.toList.map(_._2) } - } - HttpResponse( - status = HttpStatus(xmlHttpRequest.status, xmlHttpRequest.statusText), - headers = Headers(headers), - content = content - ) + val content = xmlHttpRequest.responseType match { + case null => None + case _ => { + val `type` = if (xmlHttpRequest.responseType == "") ContentType.`text/plain` else ContentType.parse(xmlHttpRequest.responseType) + Some(Content.string(xmlHttpRequest.responseText, `type`)) + } + } + Success(HttpResponse( + status = HttpStatus(xmlHttpRequest.status, xmlHttpRequest.statusText), + headers = Headers(headers), + content = content + )) } } diff --git a/client/jvm/src/main/scala/io/youi/client/JVMHttpClientImplementation.scala b/client/jvm/src/main/scala/io/youi/client/JVMHttpClientImplementation.scala index f449677f7..0748ec87e 100644 --- a/client/jvm/src/main/scala/io/youi/client/JVMHttpClientImplementation.scala +++ b/client/jvm/src/main/scala/io/youi/client/JVMHttpClientImplementation.scala @@ -1,5 +1,7 @@ package io.youi.client +import cats.effect.{Deferred, IO} + import java.io.{File, IOException} import java.net.{InetAddress, Socket} import java.security.SecureRandom @@ -7,17 +9,18 @@ import java.security.cert.X509Certificate import java.util import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicLong - import io.youi.http._ import io.youi.http.content._ import io.youi.net.ContentType +import io.youi.stream import io.youi.stream._ + import javax.net.ssl._ import okhttp3.Dns -import scala.concurrent.{ExecutionContext, Future, Promise} +import scala.collection.mutable import scala.jdk.CollectionConverters._ -import scala.util.{Failure, Success} +import scala.util.{Failure, Success, Try} /** * Asynchronous HttpClient for simple request response support. @@ -118,19 +121,17 @@ class JVMHttpClientImplementation(config: HttpClientConfig) extends HttpClientIm HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid) }*/ - override def send(request: HttpRequest, - executionContext: ExecutionContext): Future[HttpResponse] = { + override def send(request: HttpRequest): IO[Try[HttpResponse]] = Deferred[IO, Try[HttpResponse]].flatMap { deferred => val req = requestToOk(request) - val promise = Promise[HttpResponse]() client.newCall(req).enqueue(new okhttp3.Callback { override def onResponse(call: okhttp3.Call, res: okhttp3.Response): Unit = { val response = responseFromOk(res) - promise.success(response) + deferred.complete(Success(response)) } - override def onFailure(call: okhttp3.Call, exc: IOException): Unit = promise.failure(exc) + override def onFailure(call: okhttp3.Call, exc: IOException): Unit = deferred.complete(Failure(exc)) }) - JVMHttpClientImplementation.process(promise.future)(executionContext) + JVMHttpClientImplementation.process(deferred.get) } private def requestToOk(request: HttpRequest): okhttp3.Request = { @@ -198,7 +199,7 @@ class JVMHttpClientImplementation(config: HttpClientConfig) extends HttpClientIm } else { val suffix = contentType.extension.getOrElse("client") val file = File.createTempFile("youi", s".$suffix", new File(config.saveDirectory)) - IO.stream(responseBody.byteStream(), file) + stream.Stream.apply(responseBody.byteStream(), file) Content.file(file, contentType) } } @@ -218,7 +219,7 @@ class JVMHttpClientImplementation(config: HttpClientConfig) extends HttpClientIm override def content2String(content: Content): String = content match { case c: StringContent => c.value case c: BytesContent => String.valueOf(c.value) - case c: FileContent => IO.stream(c.file, new StringBuilder).toString + case c: FileContent => stream.Stream.apply(c.file, new mutable.StringBuilder).toString case _ => throw new RuntimeException(s"$content not supported") } @@ -229,7 +230,7 @@ class JVMHttpClientImplementation(config: HttpClientConfig) extends HttpClientIm protected def content2Bytes(content: Content): Array[Byte] = content match { case c: StringContent => c.value.getBytes("UTF-8") case c: BytesContent => c.value - case c: FileContent => IO.stream(c.file, new StringBuilder).toString.getBytes("UTF-*") + case c: FileContent => stream.Stream.apply(c.file, new mutable.StringBuilder).toString.getBytes("UTF-*") case _ => throw new RuntimeException(s"$content not supported") } @@ -245,20 +246,21 @@ object JVMHttpClientImplementation { private[client] val _successful = new AtomicLong(0L) private[client] val _failure = new AtomicLong(0L) - private[client] def process(future: Future[HttpResponse])(implicit executionContext: ExecutionContext): Future[HttpResponse] = { + private[client] def process(io: IO[Try[HttpResponse]]): IO[Try[HttpResponse]] = { _total.incrementAndGet() _active.incrementAndGet() - future.onComplete { - case Success(_) => { - _successful.incrementAndGet() - _active.decrementAndGet() - } - case Failure(_) => { - _failure.incrementAndGet() - _active.decrementAndGet() - } + io.flatMap { t => + IO { + t match { + case Success(_) => + _successful.incrementAndGet() + _active.decrementAndGet() + case Failure(_) => + _failure.incrementAndGet() + _active.decrementAndGet() + } + }.map(_ => t) } - future } def total: Long = _total.get() diff --git a/client/jvm/src/test/scala/spec/HttpClientSpec.scala b/client/jvm/src/test/scala/spec/HttpClientSpec.scala index 4ece8e890..beb89f343 100644 --- a/client/jvm/src/test/scala/spec/HttpClientSpec.scala +++ b/client/jvm/src/test/scala/spec/HttpClientSpec.scala @@ -1,22 +1,22 @@ package spec -import testy._ +import cats.effect.IO +import cats.effect.testing.scalatest.AsyncIOSpec import io.youi.client.HttpClient import io.youi.client.intercept.Interceptor import io.youi.http.HttpStatus import io.youi.http.content.StringContent import io.youi.net._ -import scala.concurrent.Future import scala.concurrent.duration._ -import scribe.Execution.global - import fabric.rw._ +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AsyncWordSpec -class HttpClientSpec extends Spec { +class HttpClientSpec extends AsyncWordSpec with AsyncIOSpec with Matchers { "HttpClient" should { "GET the user-agent" in { - HttpClient.url(url"https://httpbin.org/user-agent").get.send().map { response => + HttpClient.url(url"https://httpbin.org/user-agent").get.send().map(_.get).map { response => response.status should be(HttpStatus.OK) val content = response.content.get.asInstanceOf[StringContent] content.value.contains("user-agent") should be(true) @@ -25,14 +25,14 @@ class HttpClientSpec extends Spec { "call a URL multiple times with a rate limiter" in { var calls = 0 val limiter = Interceptor.rateLimited(1.seconds) - def callMultiple(counter: Int): Future[Unit] = { - HttpClient.interceptor(limiter).url(url"https://httpbin.org/user-agent").get.send().flatMap { response => + def callMultiple(counter: Int): IO[Unit] = { + HttpClient.interceptor(limiter).url(url"https://httpbin.org/user-agent").get.send().map(_.get).flatMap { response => response.status should be(HttpStatus.OK) calls += 1 if (counter > 0) { callMultiple(counter - 1) } else { - Future.successful(()) + IO.unit } } } @@ -45,7 +45,7 @@ class HttpClientSpec extends Spec { } } "call a URL and get a case class back" in { - HttpClient.url(url"https://jsonplaceholder.typicode.com/todos/1").get.call[Placeholder].map { p => + HttpClient.url(url"https://jsonplaceholder.typicode.com/todos/1").get.call[Placeholder].map(_.get).map { p => p.userId should be(1) p.id should be(1) p.title should be("delectus aut autem") @@ -55,7 +55,7 @@ class HttpClientSpec extends Spec { "restful call to a URL" in { HttpClient .url(url"https://jsonplaceholder.typicode.com/posts") - .restful[Placeholder, Placeholder](Placeholder(123, 456, "Test YouI", completed = false)).map { p => + .restful[Placeholder, Placeholder](Placeholder(123, 456, "Test YouI", completed = false)).map(_.get).map { p => p.userId should be(123) p.id should be(101) p.title should be("Test YouI") @@ -64,7 +64,7 @@ class HttpClientSpec extends Spec { } "call a URL and get an image back" in { val url = URL("https://s.yimg.com/ny/api/res/1.2/8Qe5c2B.moDrzo4jn7T5VQ--~A/YXBwaWQ9aGlnaGxhbmRlcjt3PTU2MzI7aD0zNzU1O3NtPTE7aWw9cGxhbmU-/https://media-mbst-pub-ue1.s3.amazonaws.com/creatr-images/2020-04/81f62d40-7ff9-11ea-bfdd-25ac22907561.cf.jpg") - HttpClient.url(url).send().map { response => + HttpClient.url(url).send().map(_.get).map { response => response.status should be(HttpStatus.OK) response.content.map(_.contentType) should be(Some(ContentType.`image/jpeg`)) } diff --git a/client/shared/src/main/scala/io/youi/client/HttpClient.scala b/client/shared/src/main/scala/io/youi/client/HttpClient.scala index 077dbe4bc..afa73e9e1 100644 --- a/client/shared/src/main/scala/io/youi/client/HttpClient.scala +++ b/client/shared/src/main/scala/io/youi/client/HttpClient.scala @@ -1,7 +1,9 @@ package io.youi.client +import cats.effect.IO import fabric.Json import fabric.parse.JsonParser + import fabric.rw._ import io.youi.client.intercept.Interceptor import io.youi.http._ @@ -12,6 +14,7 @@ import io.youi.util.Time import scala.concurrent.duration.FiniteDuration import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success, Try} case class HttpClient(request: HttpRequest, implementation: HttpClientImplementation, @@ -105,7 +108,7 @@ case class HttpClient(request: HttpRequest, * * @return Future[HttpResponse] */ - final def send(retries: Int = this.retries)(implicit executionContext: ExecutionContext): Future[HttpResponse] = { + final def send(retries: Int = this.retries): IO[Try[HttpResponse]] = { val updatedHeaders = sessionManager match { case Some(sm) => { val cookieHeaders = sm.session.cookies.map { cookie => @@ -115,25 +118,27 @@ case class HttpClient(request: HttpRequest, } case None => request.headers } - val future = for { + val io = for { updatedRequest <- interceptor.before(request.copy(headers = updatedHeaders)) - response <- implementation.send(updatedRequest, executionContext) - updatedResponse <- interceptor.after(updatedRequest, response) + responseTry <- implementation.send(updatedRequest) + updatedResponse <- interceptor.after(updatedRequest, responseTry) } yield { updatedResponse } - future.recoverWith { - case t: Throwable if retries > 0 => { - scribe.warn(s"Request to ${request.url} failed (${t.getMessage}). Retrying after $retryDelay...") - Time.delay(retryDelay).flatMap(_ => send(retries - 1)) - } - }.map { response => - sessionManager.foreach { sm => - val cookies = response.cookies - sm(cookies) - } + io.flatMap { + case Success(response) => + sessionManager.foreach { sm => + val cookies = response.cookies + sm(cookies) + } - response + IO.pure(Success(response)) + case Failure(t) if retries > 0 => + scribe.warn(s"Request to ${request.url} failed (${t.getMessage}). Retrying after $retryDelay...") + IO.sleep(retryDelay).flatMap { _ => + send(retries - 1) + } + case Failure(t) => IO(throw t) } } @@ -144,17 +149,20 @@ case class HttpClient(request: HttpRequest, * @tparam Response the response type * @return Future[Response] */ - def call[Response: Writer](implicit executionContext: ExecutionContext): Future[Response] = send().map { response => - try { - val responseJson = response.content.map(implementation.content2String).getOrElse("") - if (!failOnHttpStatus || response.status.isSuccess) { - if (responseJson.isEmpty) throw new ClientException(s"No content received in response for ${request.url}.", request, response, None) - JsonParser.parse(responseJson).as[Response] - } else { - throw new ClientException("HttpStatus was not successful", request, response, None) + + def call[Response: Writer]: IO[Try[Response]] = send().flatMap { responseTry => + IO { + responseTry match { + case Success(response) => + val responseJson = response.content.map(implementation.content2String).getOrElse("") + if (!failOnHttpStatus || response.status.isSuccess) { + if (responseJson.isEmpty) throw new ClientException(s"No content received in response for ${request.url}.", request, response, None) + Success(Json.parse(responseJson).as[Response]) + } else { + throw new ClientException("HttpStatus was not successful", request, response, None) + } + case Failure(exception) => throw exception } - } catch { - case t: Throwable => throw new ClientException("Response processing error", request, response, Some(t)) } } @@ -167,8 +175,8 @@ case class HttpClient(request: HttpRequest, * @tparam Response the response type * @return Future[Response] */ - def restful[Request: Reader, Response: Writer](request: Request) - (implicit executionContext: ExecutionContext): Future[Response] = { + + def restful[Request: Reader, Response: Writer](request: Request): IO[Try[Response]] = { val requestJson = request.json method(if (method == HttpMethod.Get) HttpMethod.Post else method).json(requestJson).call[Response] } @@ -182,11 +190,10 @@ case class HttpClient(request: HttpRequest, * @tparam Failure the failure (non-OK response) response type * @return either Failure or Success */ - def restfulEither[Request: Reader, Success: Writer, Failure: Writer](request: Request) - (implicit executionContext: ExecutionContext): Future[Either[Failure, Success]] = { - val requestJson = request.json - method(if (method == HttpMethod.Get) HttpMethod.Post else method).json(requestJson).send().map { response => - try { + def restfulEither[Request: Reader, Success: Writer, Failure: Writer](request: Request): IO[Either[Failure, Success]] = { + val requestJson = request.toValue + method(if (method == HttpMethod.Get) HttpMethod.Post else method).json(requestJson).send().flatMap { + case Success(response) => IO { val responseJson = response.content.map(implementation.content2String).getOrElse("") if (responseJson.isEmpty) throw new ClientException(s"No content received in response for ${this.request.url}.", this.request, response, None) if (response.status.isSuccess) { @@ -194,9 +201,8 @@ case class HttpClient(request: HttpRequest, } else { Left(JsonParser.parse(responseJson).as[Failure]) } - } catch { - case t: Throwable => throw new ClientException("Response processing error", this.request, response, Some(t)) } + case Failure(exception) => throw exception } } } diff --git a/client/shared/src/main/scala/io/youi/client/HttpClientImplementation.scala b/client/shared/src/main/scala/io/youi/client/HttpClientImplementation.scala index 4c86d908a..29c80bb1e 100644 --- a/client/shared/src/main/scala/io/youi/client/HttpClientImplementation.scala +++ b/client/shared/src/main/scala/io/youi/client/HttpClientImplementation.scala @@ -1,12 +1,14 @@ package io.youi.client +import cats.effect.IO import io.youi.http.content.Content import io.youi.http.{HttpRequest, HttpResponse} import scala.concurrent.{ExecutionContext, Future} +import scala.util.Try abstract class HttpClientImplementation(val config: HttpClientConfig) { - def send(request: HttpRequest, executionContext: ExecutionContext): Future[HttpResponse] + def send(request: HttpRequest): IO[Try[HttpResponse]] def content2String(content: Content): String } \ No newline at end of file diff --git a/client/shared/src/main/scala/io/youi/client/intercept/Interceptor.scala b/client/shared/src/main/scala/io/youi/client/intercept/Interceptor.scala index 077cdf46a..242af173e 100644 --- a/client/shared/src/main/scala/io/youi/client/intercept/Interceptor.scala +++ b/client/shared/src/main/scala/io/youi/client/intercept/Interceptor.scala @@ -1,14 +1,16 @@ package io.youi.client.intercept +import cats.effect.IO import io.youi.http.{HttpRequest, HttpResponse} import scala.concurrent.Future import scala.concurrent.duration.FiniteDuration +import scala.util.Try trait Interceptor { - def before(request: HttpRequest): Future[HttpRequest] + def before(request: HttpRequest): IO[HttpRequest] - def after(request: HttpRequest, response: HttpResponse): Future[HttpResponse] + def after(request: HttpRequest, result: Try[HttpResponse]): IO[Try[HttpResponse]] } object Interceptor { diff --git a/client/shared/src/main/scala/io/youi/client/intercept/InterceptorAdapter.scala b/client/shared/src/main/scala/io/youi/client/intercept/InterceptorAdapter.scala index 476710082..a5d032481 100644 --- a/client/shared/src/main/scala/io/youi/client/intercept/InterceptorAdapter.scala +++ b/client/shared/src/main/scala/io/youi/client/intercept/InterceptorAdapter.scala @@ -1,11 +1,13 @@ package io.youi.client.intercept +import cats.effect.IO import io.youi.http.{HttpRequest, HttpResponse} import scala.concurrent.Future +import scala.util.Try abstract class InterceptorAdapter extends Interceptor { - override def before(request: HttpRequest): Future[HttpRequest] = Future.successful(request) + override def before(request: HttpRequest): IO[HttpRequest] = IO.pure(request) - override def after(request: HttpRequest, response: HttpResponse): Future[HttpResponse] = Future.successful(response) + override def after(request: HttpRequest, result: Try[HttpResponse]): IO[Try[HttpResponse]] = IO.pure(result) } \ No newline at end of file diff --git a/client/shared/src/main/scala/io/youi/client/intercept/MultiInterceptor.scala b/client/shared/src/main/scala/io/youi/client/intercept/MultiInterceptor.scala index 713a51dc3..c5806b556 100644 --- a/client/shared/src/main/scala/io/youi/client/intercept/MultiInterceptor.scala +++ b/client/shared/src/main/scala/io/youi/client/intercept/MultiInterceptor.scala @@ -1,15 +1,17 @@ package io.youi.client.intercept +import cats.effect.IO import io.youi.http.{HttpRequest, HttpResponse} import scribe.Execution.global import scala.concurrent.Future +import scala.util.Try case class MultiInterceptor(interceptors: List[Interceptor]) extends Interceptor { - override def before(request: HttpRequest): Future[HttpRequest] = beforeRecursive(request, interceptors) + override def before(request: HttpRequest): IO[HttpRequest] = beforeRecursive(request, interceptors) - private def beforeRecursive(request: HttpRequest, list: List[Interceptor]): Future[HttpRequest] = if (list.isEmpty) { - Future.successful(request) + private def beforeRecursive(request: HttpRequest, list: List[Interceptor]): IO[HttpRequest] = if (list.isEmpty) { + IO.pure(request) } else { val interceptor = list.head interceptor.before(request).flatMap { updated => @@ -17,17 +19,17 @@ case class MultiInterceptor(interceptors: List[Interceptor]) extends Interceptor } } - override def after(request: HttpRequest, response: HttpResponse): Future[HttpResponse] = { - afterRecursive(request, response, interceptors) + override def after(request: HttpRequest, result: Try[HttpResponse]): IO[Try[HttpResponse]] = { + afterRecursive(request, result, interceptors) } private def afterRecursive(request: HttpRequest, - response: HttpResponse, - list: List[Interceptor]): Future[HttpResponse] = if (list.isEmpty) { - Future.successful(response) + result: Try[HttpResponse], + list: List[Interceptor]): IO[Try[HttpResponse]] = if (list.isEmpty) { + IO.pure(result) } else { val interceptor = list.head - interceptor.after(request, response).flatMap { updated => + interceptor.after(request, result).flatMap { updated => afterRecursive(request, updated, list.tail) } } diff --git a/client/shared/src/main/scala/io/youi/client/intercept/RateLimiter.scala b/client/shared/src/main/scala/io/youi/client/intercept/RateLimiter.scala index 22740c72a..59a20b90d 100644 --- a/client/shared/src/main/scala/io/youi/client/intercept/RateLimiter.scala +++ b/client/shared/src/main/scala/io/youi/client/intercept/RateLimiter.scala @@ -1,42 +1,24 @@ package io.youi.client.intercept -import io.youi.http.{HttpRequest, HttpResponse} -import io.youi.util.Time -import scribe.Execution.global +import cats.effect.IO +import io.youi.http.HttpRequest import scala.concurrent.duration._ -import scala.concurrent.{Future, Promise} -import scala.util.{Failure, Success} -case class RateLimiter(perRequestDelay: FiniteDuration) extends InterceptorAdapter { +case class RateLimiter(perRequestDelay: FiniteDuration) extends InterceptorAdapter { self => private val maxDelay = perRequestDelay.toMillis - @volatile private var _lastTime: Long = 0L + @volatile private var lastTime: Long = 0L - private var future: Future[_] = Future.successful(()) - - override def before(request: HttpRequest): Future[HttpRequest] = synchronized { - val p = Promise[HttpRequest]() - future.onComplete { _ => + override def before(request: HttpRequest): IO[HttpRequest] = IO.unit.flatMap { _ => + self.synchronized { val now = System.currentTimeMillis() - val elapsed = now - _lastTime - val delay = maxDelay - elapsed + val delay = lastTime + maxDelay if (delay > 0L) { - Time.delay(delay.millis).map(_ => request).onComplete { - case Success(v) => p.success(v) - case Failure(exception) => p.failure(exception) - } + lastTime = now + IO.sleep(delay.millis).map(_ => request) } else { - p.success(request) + IO.pure(request) } } - val f = p.future - future = f - f - } - - override def after(request: HttpRequest, response: HttpResponse): Future[HttpResponse] = { - _lastTime = System.currentTimeMillis() - - super.after(request, response) } } \ No newline at end of file diff --git a/core/js/src/main/scala/io/youi/ajax/AjaxAction.scala b/core/js/src/main/scala/io/youi/ajax/AjaxAction.scala index 7fa6f0e62..7e5853220 100644 --- a/core/js/src/main/scala/io/youi/ajax/AjaxAction.scala +++ b/core/js/src/main/scala/io/youi/ajax/AjaxAction.scala @@ -3,11 +3,12 @@ package io.youi.ajax import cats.effect.IO import org.scalajs.dom.XMLHttpRequest import reactify._ - import cats.effect.unsafe.implicits.global +import scala.util.Try + class AjaxAction(request: AjaxRequest) { - lazy val io: IO[Either[Throwable, XMLHttpRequest]] = request.deferred.get + lazy val io: IO[Try[XMLHttpRequest]] = request.deferred.get private[ajax] val _state = Var[ActionState](ActionState.New) def state: Val[ActionState] = _state def loaded: Val[Double] = request.loaded diff --git a/core/js/src/main/scala/io/youi/ajax/AjaxManager.scala b/core/js/src/main/scala/io/youi/ajax/AjaxManager.scala index 545787204..f420c08ea 100644 --- a/core/js/src/main/scala/io/youi/ajax/AjaxManager.scala +++ b/core/js/src/main/scala/io/youi/ajax/AjaxManager.scala @@ -8,6 +8,7 @@ import org.scalajs.dom.XMLHttpRequest import scribe.Logging import scala.collection.immutable.Queue +import scala.util.Try class AjaxManager(val maxConcurrent: Int) extends Logging { private var _queue = Queue.empty[AjaxAction] @@ -29,7 +30,7 @@ class AjaxManager(val maxConcurrent: Int) extends Logging { action } - def enqueue(action: AjaxAction): IO[Either[Throwable, XMLHttpRequest]] = { + def enqueue(action: AjaxAction): IO[Try[XMLHttpRequest]] = { _queue = _queue.enqueue(action) action._state @= ActionState.Enqueued checkQueue() diff --git a/core/js/src/main/scala/io/youi/ajax/AjaxRequest.scala b/core/js/src/main/scala/io/youi/ajax/AjaxRequest.scala index f1e215819..db2044878 100644 --- a/core/js/src/main/scala/io/youi/ajax/AjaxRequest.scala +++ b/core/js/src/main/scala/io/youi/ajax/AjaxRequest.scala @@ -9,6 +9,7 @@ import reactify._ import scala.scalajs.js import scala.scalajs.js.| +import scala.util.{Failure, Success, Try} class AjaxRequest(url: URL, method: HttpMethod = HttpMethod.Post, @@ -18,7 +19,7 @@ class AjaxRequest(url: URL, withCredentials: Boolean = true, responseType: String = "") { val req = new dom.XMLHttpRequest() - val deferred: Deferred[IO, Either[Throwable, XMLHttpRequest]] = Deferred.unsafe[IO, Either[Throwable, XMLHttpRequest]] + val deferred: Deferred[IO, Try[XMLHttpRequest]] = Deferred.unsafe[IO, Try[XMLHttpRequest]] val loaded: Val[Double] = Var(0.0) val total: Val[Double] = Var(0.0) val percentage: Val[Int] = Var(0) @@ -27,9 +28,9 @@ class AjaxRequest(url: URL, req.onreadystatechange = { _: dom.Event => if (req.readyState == 4) { if ((req.status >= 200 && req.status < 300) || req.status == 304) { - deferred.complete(Right(req)) + deferred.complete(Success(req)) } else { - deferred.complete(Left(new RuntimeException(s"AjaxRequest failed: ${req.readyState}"))) + deferred.complete(Failure(new RuntimeException(s"AjaxRequest failed: ${req.readyState}"))) } } } @@ -45,7 +46,7 @@ class AjaxRequest(url: URL, req.withCredentials = withCredentials headers.foreach(x => req.setRequestHeader(x._1, x._2)) - def send(): IO[Either[Throwable, XMLHttpRequest]] = { + def send(): IO[Try[XMLHttpRequest]] = { data match { case Some(formData) => req.send(formData.asInstanceOf[js.Any]) case None => req.send() diff --git a/core/js/src/main/scala/io/youi/stream/StreamURL.scala b/core/js/src/main/scala/io/youi/stream/StreamURL.scala index e63997dae..1a09f51fb 100644 --- a/core/js/src/main/scala/io/youi/stream/StreamURL.scala +++ b/core/js/src/main/scala/io/youi/stream/StreamURL.scala @@ -6,6 +6,8 @@ import io.youi.http.HttpMethod import io.youi.net.URL import org.scalajs.dom.FormData +import scala.util.{Failure, Success} + object StreamURL { def stream(url: URL, method: HttpMethod = HttpMethod.Post, @@ -16,8 +18,8 @@ object StreamURL { responseType: String = ""): IO[String] = { val request = new AjaxRequest(url, method, data, timeout, headers + ("streaming" -> "true"), withCredentials, responseType) request.send().map { - case Left(throwable) => throw throwable - case Right(req) => req.responseText + case Failure(throwable) => throw throwable + case Success(req) => req.responseText } } } \ No newline at end of file diff --git a/core/jvm/src/main/scala/io/youi/http/StreamZipContent.scala b/core/jvm/src/main/scala/io/youi/http/StreamZipContent.scala index c366e41a6..69ed61d9c 100644 --- a/core/jvm/src/main/scala/io/youi/http/StreamZipContent.scala +++ b/core/jvm/src/main/scala/io/youi/http/StreamZipContent.scala @@ -24,7 +24,7 @@ class StreamZipContent(entries: List[ZipFileEntry], entries.foreach { e => val entry = new ZipEntry(e.path) zos.putNextEntry(entry) - IO.stream(e.file, zos, closeOnComplete = false) + Stream.apply(e.file, zos, closeOnComplete = false) zos.closeEntry() } zos.flush() diff --git a/core/jvm/src/main/scala/io/youi/http/content/FileContent.scala b/core/jvm/src/main/scala/io/youi/http/content/FileContent.scala index dd07237ed..aec97aca2 100644 --- a/core/jvm/src/main/scala/io/youi/http/content/FileContent.scala +++ b/core/jvm/src/main/scala/io/youi/http/content/FileContent.scala @@ -3,7 +3,7 @@ package io.youi.http.content import java.io.File import io.youi.net.ContentType -import io.youi.stream.IO +import io.youi.stream.Stream case class FileContent(file: File, contentType: ContentType, lastModifiedOverride: Option[Long] = None) extends Content { assert(file.isFile, s"Cannot send back ${file.getAbsolutePath} as it is a directory or does not exist!") @@ -17,5 +17,5 @@ case class FileContent(file: File, contentType: ContentType, lastModifiedOverrid override def toString: String = s"FileContent(file: ${file.getAbsolutePath}, contentType: $contentType)" - override def asString: String = IO.stream(file, new StringBuilder).toString + override def asString: String = Stream.apply(file, new StringBuilder).toString } \ No newline at end of file diff --git a/core/jvm/src/main/scala/io/youi/http/content/URLContent.scala b/core/jvm/src/main/scala/io/youi/http/content/URLContent.scala index 28e65c25d..ba182e338 100644 --- a/core/jvm/src/main/scala/io/youi/http/content/URLContent.scala +++ b/core/jvm/src/main/scala/io/youi/http/content/URLContent.scala @@ -3,7 +3,7 @@ package io.youi.http.content import java.net.{HttpURLConnection, JarURLConnection, URL} import io.youi.net.ContentType -import io.youi.stream.IO +import io.youi.stream.Stream import sun.net.www.protocol.file.FileURLConnection case class URLContent(url: URL, contentType: ContentType, lastModifiedOverride: Option[Long] = None) extends Content { @@ -37,5 +37,5 @@ case class URLContent(url: URL, contentType: ContentType, lastModifiedOverride: override def toString: String = s"URLContent(url: $url, contentType: $contentType)" - override def asString: String = IO.stream(url, new StringBuilder).toString + override def asString: String = Stream.apply(url, new StringBuilder).toString } \ No newline at end of file diff --git a/core/jvm/src/main/scala/io/youi/processor/Processor.scala b/core/jvm/src/main/scala/io/youi/processor/Processor.scala index 80eb4882c..c3dc4b98f 100644 --- a/core/jvm/src/main/scala/io/youi/processor/Processor.scala +++ b/core/jvm/src/main/scala/io/youi/processor/Processor.scala @@ -3,9 +3,7 @@ package io.youi.processor import cats.effect.IO import cats.implicits._ -import java.util.concurrent.atomic.AtomicLong -import java.util.concurrent.{ConcurrentLinkedQueue, Executors} -import scala.concurrent.duration.DurationInt +import java.util.concurrent.ConcurrentLinkedQueue object Processor { lazy val DefaultThreads: Int = Runtime.getRuntime.availableProcessors() * 2 diff --git a/core/jvm/src/main/scala/io/youi/stream/IO.scala b/core/jvm/src/main/scala/io/youi/stream/Stream.scala similarity index 83% rename from core/jvm/src/main/scala/io/youi/stream/IO.scala rename to core/jvm/src/main/scala/io/youi/stream/Stream.scala index 9d579ed47..1e31c210d 100644 --- a/core/jvm/src/main/scala/io/youi/stream/IO.scala +++ b/core/jvm/src/main/scala/io/youi/stream/Stream.scala @@ -4,13 +4,13 @@ import java.io.{File, FileOutputStream, IOException} import scala.annotation.tailrec import scala.concurrent.duration.{DurationInt, FiniteDuration} -object IO { - final def stream(reader: Reader, - writer: Writer, - monitor: Monitor = Monitor.Ignore, - monitorDelay: FiniteDuration = 15.millis, - buffer: Array[Byte] = new Array[Byte](1024), - closeOnComplete: Boolean = true): Writer = { +object Stream { + final def apply(reader: Reader, + writer: Writer, + monitor: Monitor = Monitor.Ignore, + monitorDelay: FiniteDuration = 15.millis, + buffer: Array[Byte] = new Array[Byte](1024), + closeOnComplete: Boolean = true): Writer = { val length = reader.length monitor.open(length) var total = 0L @@ -71,16 +71,16 @@ object IO { } } else if (source.isFile) { if (destination.isDirectory) { - stream(source, new File(destination, source.getName)) + apply(source, new File(destination, source.getName)) } else { - stream(source, destination) + apply(source, destination) } } def merge(sources: List[File], destination: File): Unit = { val outputStream = new FileOutputStream(destination) sources.foreach { source => - stream(source, outputStream, closeOnComplete = false) + apply(source, outputStream, closeOnComplete = false) } outputStream.flush() outputStream.close() diff --git a/optimizer/src/main/scala/io/youi/optimizer/HTMLOptimizer.scala b/optimizer/src/main/scala/io/youi/optimizer/HTMLOptimizer.scala index ea2ab5670..c16331865 100644 --- a/optimizer/src/main/scala/io/youi/optimizer/HTMLOptimizer.scala +++ b/optimizer/src/main/scala/io/youi/optimizer/HTMLOptimizer.scala @@ -36,12 +36,12 @@ object HTMLOptimizer { if (src.startsWith("http://") || src.startsWith("https://") || src.startsWith("//")) { // Remote script val file = createTempFile("remote", "js") val url = new URL(src) - IO.stream(url, file) + Stream.apply(url, file) val map = if (minified) { try { val minifiedFile = createTempFile("remote", "js.map") val minifiedURL = new URL(s"$src.map") - IO.stream(minifiedURL, minifiedFile) + Stream.apply(minifiedURL, minifiedFile) Some(minifiedFile) } catch { case exc: FileNotFoundException => { @@ -75,7 +75,7 @@ object HTMLOptimizer { val script = content.substring(start, start + end).trim val file = createTempFile("inline", "js") file.deleteOnExit() - IO.stream(script, file) + Stream.apply(script, file) ScriptFile(file, None) } } @@ -85,7 +85,7 @@ object HTMLOptimizer { )) val output = createTempFile("stage1", "html") - IO.stream(result, output) + Stream.apply(result, output) Optimized(output, scripts.toList, input.length()) } @@ -123,7 +123,7 @@ object HTMLOptimizer { val content = stream.stream(List( Delta.InsertLastChild(ByTag("body"), s"""""") )) - IO.stream(content, htmlFileOut) + Stream.apply(content, htmlFileOut) } /** diff --git a/server/src/main/scala/io/youi/server/DefaultErrorHandler.scala b/server/src/main/scala/io/youi/server/DefaultErrorHandler.scala index 079cabcb9..6b064766e 100644 --- a/server/src/main/scala/io/youi/server/DefaultErrorHandler.scala +++ b/server/src/main/scala/io/youi/server/DefaultErrorHandler.scala @@ -1,13 +1,12 @@ package io.youi.server +import cats.effect.IO import io.youi.http.content.Content import io.youi.http.{CacheControl, HttpConnection, HttpStatus} import io.youi.net.ContentType import io.youi.server.dsl._ import perfolation._ -import scala.concurrent.Future - object DefaultErrorHandler extends ErrorHandler { lazy val lastModified: Long = System.currentTimeMillis() @@ -20,7 +19,7 @@ object DefaultErrorHandler extends ErrorHandler { """.withContentType(ContentType.`text/html`).withLastModified(lastModified) - override def handle(connection: HttpConnection, t: Option[Throwable]): Future[HttpConnection] = Future.successful { + override def handle(connection: HttpConnection, t: Option[Throwable]): IO[HttpConnection] = IO { connection.modify { response => val status = if (response.status.isError) { response.status diff --git a/server/src/main/scala/io/youi/server/ErrorHandler.scala b/server/src/main/scala/io/youi/server/ErrorHandler.scala index 0fc144e1c..c0662b724 100644 --- a/server/src/main/scala/io/youi/server/ErrorHandler.scala +++ b/server/src/main/scala/io/youi/server/ErrorHandler.scala @@ -1,9 +1,8 @@ package io.youi.server +import cats.effect.IO import io.youi.http.HttpConnection -import scala.concurrent.Future - trait ErrorHandler { - def handle(connection: HttpConnection, t: Option[Throwable]): Future[HttpConnection] + def handle(connection: HttpConnection, t: Option[Throwable]): IO[HttpConnection] } diff --git a/server/src/main/scala/io/youi/server/Server.scala b/server/src/main/scala/io/youi/server/Server.scala index b0ee121e4..bd908097d 100644 --- a/server/src/main/scala/io/youi/server/Server.scala +++ b/server/src/main/scala/io/youi/server/Server.scala @@ -1,19 +1,17 @@ package io.youi.server +import cats.effect.{ExitCode, IO, IOApp} + import java.util.concurrent.atomic.AtomicBoolean import io.youi.http.{HttpConnection, HttpStatus, ProxyHandler} import io.youi.server.handler.{HttpHandler, HttpHandlerBuilder} -import io.youi.util.Time import io.youi.{ErrorSupport, ItemContainer} import moduload.Moduload import profig._ import reactify._ -import scribe.Execution.global - -import scala.concurrent.{Await, Future} import scala.concurrent.duration._ -trait Server extends HttpHandler with ErrorSupport { +trait Server extends HttpHandler with ErrorSupport with IOApp { private val initialized = new AtomicBoolean(false) val config = new ServerConfig(this) @@ -47,21 +45,21 @@ trait Server extends HttpHandler with ErrorSupport { def isInitialized: Boolean = initialized.get() def isRunning: Boolean = isInitialized && implementation.isRunning - def initialize(): Future[Unit] = { + def initialize(): IO[Unit] = { val shouldInit = initialized.compareAndSet(false, true) if (shouldInit) { init() } else { - Future.successful(()) + IO.unit } } /** * Init is called on start(), but only the first time. If the server is restarted it is not invoked again. */ - protected def init(): Future[Unit] = Future.successful(()) + protected def init(): IO[Unit] = IO.unit - def start(): Future[Unit] = initialize().map { _ => + def start(): IO[Unit] = initialize().map { _ => implementation.start() scribe.info(s"Server started on ${config.enabledListeners.mkString(", ")}") } @@ -75,22 +73,20 @@ trait Server extends HttpHandler with ErrorSupport { start() } - def whileRunning(delay: FiniteDuration = 1.second): Future[Unit] = if (isRunning) { - Time.delay(delay).flatMap(_ => whileRunning(delay)) + def whileRunning(delay: FiniteDuration = 1.second): IO[Unit] = if (isRunning) { + IO.sleep(delay).flatMap(_ => whileRunning(delay)) } else { - Future.successful(()) + IO.unit } def dispose(): Unit = stop() - override final def handle(connection: HttpConnection): Future[HttpConnection] = handleInternal(connection).recoverWith { - case t => { - error(t) - errorHandler.get.handle(connection, Some(t)) - } + override final def handle(connection: HttpConnection): IO[HttpConnection] = handleInternal(connection).handleErrorWith { throwable => + error(throwable) + errorHandler.get.handle(connection, Some(throwable)) } - protected def handleInternal(connection: HttpConnection): Future[HttpConnection] = { + protected def handleInternal(connection: HttpConnection): IO[HttpConnection] = { handleRecursive(connection, handlers()).flatMap { updated => // NotFound handling if (updated.response.content.isEmpty && updated.response.status == HttpStatus.OK) { @@ -99,14 +95,14 @@ trait Server extends HttpHandler with ErrorSupport { } errorHandler.get.handle(notFound, None) } else { - Future.successful(updated) + IO.pure(updated) } } } - private def handleRecursive(connection: HttpConnection, handlers: List[HttpHandler]): Future[HttpConnection] = { + private def handleRecursive(connection: HttpConnection, handlers: List[HttpHandler]): IO[HttpConnection] = { if (connection.finished || handlers.isEmpty) { - Future.successful(connection) // Finished + IO.pure(connection) // Finished } else { val handler = handlers.head handler.handle(connection).flatMap { updated => @@ -115,18 +111,21 @@ trait Server extends HttpHandler with ErrorSupport { } } - def main(args: Array[String]): Unit = { + override def run(args: List[String]): IO[ExitCode] = IO.unit.flatMap { _ => Profig.initConfiguration() Profig.merge(args.toSeq) - val future = start() - future.failed.map { throwable => - scribe.error("Error during application startup", throwable) - dispose() + for { + _ <- start().handleError { throwable => + scribe.error("Error during application startup", throwable) + dispose() + sys.exit(1) + } + _ <- whileRunning() + } yield { + scribe.info("Server terminated successfully.") + ExitCode.Success } - - Await.result(future, Duration.Inf) - Await.result(whileRunning(), Duration.Inf) } } diff --git a/server/src/main/scala/io/youi/server/ServerConfig.scala b/server/src/main/scala/io/youi/server/ServerConfig.scala index 9466f5c77..ec90e46c4 100644 --- a/server/src/main/scala/io/youi/server/ServerConfig.scala +++ b/server/src/main/scala/io/youi/server/ServerConfig.scala @@ -12,7 +12,7 @@ class ServerConfig(server: Server) { /** * The Server name set in the HTTP header */ - val name: Var[String] = Var(Profig("server.name").as[String]("youi")) + val name: Var[String] = Var(Profig("server.name").asOr[String]("youi")) object session { private val config = Profig("session").as[SessionConfig] @@ -66,8 +66,8 @@ class ServerConfig(server: Server) { * To easily enable HTTPS just pass "-listeners.https.enabled=true". */ lazy val listeners: Var[List[ServerSocketListener]] = prop(List( - Profig("listeners.http").as[HttpServerListener](HttpServerListener()), - Profig("listeners.https").as[HttpsServerListener](HttpsServerListener()) + Profig("listeners.http").asOr[HttpServerListener](HttpServerListener()), + Profig("listeners.https").asOr[HttpsServerListener](HttpsServerListener()) )) def enabledListeners: List[ServerSocketListener] = listeners().filter(_.enabled) diff --git a/server/src/main/scala/io/youi/server/WebSocketListener.scala b/server/src/main/scala/io/youi/server/WebSocketListener.scala index 8931b8163..20573081d 100644 --- a/server/src/main/scala/io/youi/server/WebSocketListener.scala +++ b/server/src/main/scala/io/youi/server/WebSocketListener.scala @@ -3,8 +3,6 @@ package io.youi.server import cats.effect.IO import io.youi.http.{ConnectionStatus, HttpConnection, WebSocket} -import scala.concurrent.Future - class WebSocketListener(val httpConnection: HttpConnection) extends WebSocket { override def connect(): IO[ConnectionStatus] = { _status @= ConnectionStatus.Open diff --git a/server/src/main/scala/io/youi/server/dsl/ActionFilter.scala b/server/src/main/scala/io/youi/server/dsl/ActionFilter.scala index fbe166206..fb973c963 100644 --- a/server/src/main/scala/io/youi/server/dsl/ActionFilter.scala +++ b/server/src/main/scala/io/youi/server/dsl/ActionFilter.scala @@ -1,16 +1,14 @@ package io.youi.server.dsl +import cats.effect.IO import io.youi.http.HttpConnection -import scribe.Execution.global -import scala.concurrent.Future - -class ActionFilter(f: HttpConnection => Future[HttpConnection]) extends ConnectionFilter { - override def filter(connection: HttpConnection): Future[FilterResponse] = { +class ActionFilter(f: HttpConnection => IO[HttpConnection]) extends ConnectionFilter { + override def filter(connection: HttpConnection): IO[FilterResponse] = { f(connection).map(FilterResponse.Continue) } } object ActionFilter { - def apply(f: HttpConnection => Future[HttpConnection]): ActionFilter = new ActionFilter(f) + def apply(f: HttpConnection => IO[HttpConnection]): ActionFilter = new ActionFilter(f) } \ No newline at end of file diff --git a/server/src/main/scala/io/youi/server/dsl/CombinedConnectionFilter.scala b/server/src/main/scala/io/youi/server/dsl/CombinedConnectionFilter.scala index 09bcf108b..79a380f8f 100644 --- a/server/src/main/scala/io/youi/server/dsl/CombinedConnectionFilter.scala +++ b/server/src/main/scala/io/youi/server/dsl/CombinedConnectionFilter.scala @@ -1,15 +1,13 @@ package io.youi.server.dsl +import cats.effect.IO import io.youi.http.HttpConnection -import scribe.Execution.global - -import scala.concurrent.Future case class CombinedConnectionFilter(first: ConnectionFilter, second: ConnectionFilter) extends ConnectionFilter { - override def filter(connection: HttpConnection): Future[FilterResponse] = { + override def filter(connection: HttpConnection): IO[FilterResponse] = { first.filter(connection).flatMap { case FilterResponse.Continue(c) => second.filter(c) - case FilterResponse.Stop(c) => Future.successful(FilterResponse.Stop(c)) + case FilterResponse.Stop(c) => IO.pure(FilterResponse.Stop(c)) } } } \ No newline at end of file diff --git a/server/src/main/scala/io/youi/server/dsl/ConditionalFilter.scala b/server/src/main/scala/io/youi/server/dsl/ConditionalFilter.scala index 53b48e597..61cf733e4 100644 --- a/server/src/main/scala/io/youi/server/dsl/ConditionalFilter.scala +++ b/server/src/main/scala/io/youi/server/dsl/ConditionalFilter.scala @@ -1,11 +1,10 @@ package io.youi.server.dsl +import cats.effect.IO import io.youi.http.HttpConnection -import scala.concurrent.Future - class ConditionalFilter(f: HttpConnection => Boolean) extends ConnectionFilter { - override def filter(connection: HttpConnection): Future[FilterResponse] = Future.successful { + override def filter(connection: HttpConnection): IO[FilterResponse] = IO { if (f(connection)) { FilterResponse.Continue(connection) } else { diff --git a/server/src/main/scala/io/youi/server/dsl/ConnectionFilter.scala b/server/src/main/scala/io/youi/server/dsl/ConnectionFilter.scala index 319231572..612e6a63b 100644 --- a/server/src/main/scala/io/youi/server/dsl/ConnectionFilter.scala +++ b/server/src/main/scala/io/youi/server/dsl/ConnectionFilter.scala @@ -1,13 +1,11 @@ package io.youi.server.dsl +import cats.effect.IO import io.youi.http.HttpConnection import io.youi.server.handler.HttpHandler -import scribe.Execution.global - -import scala.concurrent.Future trait ConnectionFilter extends HttpHandler { - def filter(connection: HttpConnection): Future[FilterResponse] + def filter(connection: HttpConnection): IO[FilterResponse] protected def continue(connection: HttpConnection): FilterResponse = FilterResponse.Continue(connection) protected def stop(connection: HttpConnection): FilterResponse = FilterResponse.Stop(connection) @@ -27,13 +25,13 @@ trait ConnectionFilter extends HttpHandler { connection.store(ConnectionFilter.LastKey) = current ::: filters.toList } - override def handle(connection: HttpConnection): Future[HttpConnection] = { + override def handle(connection: HttpConnection): IO[HttpConnection] = { filter(connection).flatMap { case FilterResponse.Continue(c) => { val last = c.store.getOrElse[List[ConnectionFilter]](ConnectionFilter.LastKey, Nil) ConnectionFilter.recurse(c, last).map(_.connection) } - case FilterResponse.Stop(c) => Future.successful(c) + case FilterResponse.Stop(c) => IO.pure(c) } } } @@ -41,12 +39,12 @@ trait ConnectionFilter extends HttpHandler { object ConnectionFilter { private val LastKey: String = "ConnectionFilterLast" - def recurse(connection: HttpConnection, filters: List[ConnectionFilter]): Future[FilterResponse] = if (filters.isEmpty) { - Future.successful(FilterResponse.Continue(connection)) + def recurse(connection: HttpConnection, filters: List[ConnectionFilter]): IO[FilterResponse] = if (filters.isEmpty) { + IO.pure(FilterResponse.Continue(connection)) } else { val filter = filters.head filter.filter(connection).flatMap { - case stop: FilterResponse.Stop => Future.successful(stop) + case stop: FilterResponse.Stop => IO.pure(stop) case FilterResponse.Continue(c) => recurse(c, filters.tail) } } diff --git a/server/src/main/scala/io/youi/server/dsl/IPAddressFilter.scala b/server/src/main/scala/io/youi/server/dsl/IPAddressFilter.scala index 3c2747f11..c31bbb867 100644 --- a/server/src/main/scala/io/youi/server/dsl/IPAddressFilter.scala +++ b/server/src/main/scala/io/youi/server/dsl/IPAddressFilter.scala @@ -1,12 +1,11 @@ package io.youi.server.dsl +import cats.effect.IO import io.youi.http.HttpConnection import io.youi.net.IP -import scala.concurrent.Future - case class IPAddressFilter(allow: List[IP] = Nil, deny: List[IP] = Nil) extends ConnectionFilter { - override def filter(connection: HttpConnection): Future[FilterResponse] = Future.successful { + override def filter(connection: HttpConnection): IO[FilterResponse] = IO { val ip = connection.request.originalSource val allowed = if (allow.isEmpty) true else allow.contains(ip) val denied = if (deny.isEmpty) false else deny.contains(ip) diff --git a/server/src/main/scala/io/youi/server/dsl/LastConnectionFilter.scala b/server/src/main/scala/io/youi/server/dsl/LastConnectionFilter.scala index 55fb99bfc..29bf7b836 100644 --- a/server/src/main/scala/io/youi/server/dsl/LastConnectionFilter.scala +++ b/server/src/main/scala/io/youi/server/dsl/LastConnectionFilter.scala @@ -1,10 +1,10 @@ package io.youi.server.dsl -import io.youi.http.HttpConnection -import scala.concurrent.Future +import cats.effect.IO +import io.youi.http.HttpConnection case class LastConnectionFilter(filters: ConnectionFilter*) extends ConnectionFilter { - override def filter(connection: HttpConnection): Future[FilterResponse] = Future.successful { + override def filter(connection: HttpConnection): IO[FilterResponse] = IO { last(connection, filters: _*) FilterResponse.Continue(connection) } diff --git a/server/src/main/scala/io/youi/server/dsl/ListConnectionFilter.scala b/server/src/main/scala/io/youi/server/dsl/ListConnectionFilter.scala index 9995208c1..f10e70bde 100644 --- a/server/src/main/scala/io/youi/server/dsl/ListConnectionFilter.scala +++ b/server/src/main/scala/io/youi/server/dsl/ListConnectionFilter.scala @@ -1,20 +1,18 @@ package io.youi.server.dsl +import cats.effect.IO import io.youi.http.HttpConnection -import scribe.Execution.global - -import scala.concurrent.Future case class ListConnectionFilter(filters: List[ConnectionFilter]) extends ConnectionFilter { - override def filter(connection: HttpConnection): Future[FilterResponse] = firstPath(connection, filters) + override def filter(connection: HttpConnection): IO[FilterResponse] = firstPath(connection, filters) private def firstPath(connection: HttpConnection, - filters: List[ConnectionFilter]): Future[FilterResponse] = if (filters.isEmpty) { - Future.successful(FilterResponse.Stop(connection)) + filters: List[ConnectionFilter]): IO[FilterResponse] = if (filters.isEmpty) { + IO.pure(FilterResponse.Stop(connection)) } else { val filter = filters.head filter.filter(connection).flatMap { - case r: FilterResponse.Continue => Future.successful(r) + case r: FilterResponse.Continue => IO.pure(r) case r: FilterResponse.Stop => firstPath(r.connection, filters.tail) } } diff --git a/server/src/main/scala/io/youi/server/dsl/PathFilter.scala b/server/src/main/scala/io/youi/server/dsl/PathFilter.scala index 73e3e88b9..b9a0d988c 100644 --- a/server/src/main/scala/io/youi/server/dsl/PathFilter.scala +++ b/server/src/main/scala/io/youi/server/dsl/PathFilter.scala @@ -1,12 +1,11 @@ package io.youi.server.dsl +import cats.effect.IO import io.youi.http.HttpConnection import io.youi.net.Path -import scala.concurrent.Future - case class PathFilter(path: Path) extends ConnectionFilter { - override def filter(connection: HttpConnection): Future[FilterResponse] = Future.successful { + override def filter(connection: HttpConnection): IO[FilterResponse] = IO { if (path == connection.request.url.path) { val args = path.extractArguments(connection.request.url.path) if (args.nonEmpty) { diff --git a/server/src/main/scala/io/youi/server/dsl/PathRegexFilter.scala b/server/src/main/scala/io/youi/server/dsl/PathRegexFilter.scala index 7377aa387..e51df83b3 100644 --- a/server/src/main/scala/io/youi/server/dsl/PathRegexFilter.scala +++ b/server/src/main/scala/io/youi/server/dsl/PathRegexFilter.scala @@ -1,12 +1,12 @@ package io.youi.server.dsl +import cats.effect.IO import io.youi.http.HttpConnection -import scala.concurrent.Future import scala.util.matching.Regex case class PathRegexFilter(regex: Regex) extends ConnectionFilter { - override def filter(connection: HttpConnection): Future[FilterResponse] = Future.successful { + override def filter(connection: HttpConnection): IO[FilterResponse] = IO { val path = connection.request.url.path.decoded if (path.matches(regex.regex)) { FilterResponse.Continue(connection) diff --git a/server/src/main/scala/io/youi/server/dsl/package.scala b/server/src/main/scala/io/youi/server/dsl/package.scala index 8aae68aa6..23db8ba5b 100644 --- a/server/src/main/scala/io/youi/server/dsl/package.scala +++ b/server/src/main/scala/io/youi/server/dsl/package.scala @@ -1,7 +1,8 @@ package io.youi.server -import java.io.File +import cats.effect.IO +import java.io.File import io.youi.http.content.Content import io.youi.http.{HttpConnection, HttpMethod, HttpStatus} import io.youi.net.{ContentType, IP, Path, URLMatcher} @@ -9,10 +10,8 @@ import io.youi.server.handler._ import io.youi.server.rest.Restful import io.youi.server.validation.{ValidationResult, Validator} import io.youi.stream.delta.Delta -import scribe.Execution.global import fabric.rw._ -import scala.concurrent.Future import scala.language.implicitConversions package object dsl { @@ -21,7 +20,7 @@ package object dsl { implicit class ValidatorFilter(val validator: Validator) extends ConnectionFilter { private lazy val list = List(validator) - override def filter(connection: HttpConnection): Future[FilterResponse] = { + override def filter(connection: HttpConnection): IO[FilterResponse] = { ValidatorHttpHandler.validate(connection, list).map { case ValidationResult.Continue(c) => FilterResponse.Continue(c) case vr => FilterResponse.Stop(vr.connection) @@ -35,24 +34,24 @@ package object dsl { if (PathPart.fulfilled(connection)) { handler.handle(connection) } else { - Future.successful(connection) + IO.pure(connection) } } implicit class CachingManagerFilter(val caching: CachingManager) extends LastConnectionFilter(handler2Filter(caching)) - implicit class DeltasFilter(val deltas: List[Delta]) extends ActionFilter(connection => Future.successful { + implicit class DeltasFilter(val deltas: List[Delta]) extends ActionFilter(connection => IO { connection.deltas ++= deltas connection }) - implicit class DeltaFilter(delta: Delta) extends ActionFilter(connection => Future.successful { + implicit class DeltaFilter(delta: Delta) extends ActionFilter(connection => IO { connection.deltas += delta connection }) implicit class StringFilter(val s: String) extends ConnectionFilter { - override def filter(connection: HttpConnection): Future[FilterResponse] = Future.successful { + override def filter(connection: HttpConnection): IO[FilterResponse] = IO { PathPart.take(connection, s) match { case Some(c) => FilterResponse.Continue(c) case None => FilterResponse.Stop(connection) @@ -69,7 +68,7 @@ package object dsl { directory } - override def filter(connection: HttpConnection): Future[FilterResponse] = { + override def filter(connection: HttpConnection): IO[FilterResponse] = { val path = pathTransform(connection.request.url.path.decoded) val resourcePath = s"$dir$path" match { case s if s.startsWith("/") => s.substring(1) @@ -80,7 +79,7 @@ package object dsl { .filter(_.isFile) .map { file => SenderHandler(Content.file(file)).handle(connection) - }.map(_.map(FilterResponse.Continue)).getOrElse(Future.successful(FilterResponse.Stop(connection))) + }.map(_.map(FilterResponse.Continue)).getOrElse(IO.pure(FilterResponse.Stop(connection))) } } @@ -106,7 +105,7 @@ package object dsl { } def redirect(path: Path): ConnectionFilter = new ConnectionFilter { - override def filter(connection: HttpConnection): Future[FilterResponse] = Future.successful { + override def filter(connection: HttpConnection): IO[FilterResponse] = IO.pure { FilterResponse.Continue(HttpHandler.redirect(connection, path.encoded)) } } diff --git a/server/src/main/scala/io/youi/server/handler/ContentHandler.scala b/server/src/main/scala/io/youi/server/handler/ContentHandler.scala index 5af031ebe..97c2932e2 100644 --- a/server/src/main/scala/io/youi/server/handler/ContentHandler.scala +++ b/server/src/main/scala/io/youi/server/handler/ContentHandler.scala @@ -1,12 +1,11 @@ package io.youi.server.handler +import cats.effect.IO import io.youi.http.content.Content import io.youi.http.{HttpConnection, HttpStatus} -import scala.concurrent.Future - case class ContentHandler(content: Content, status: HttpStatus) extends HttpHandler { - override def handle(connection: HttpConnection): Future[HttpConnection] = Future.successful { + override def handle(connection: HttpConnection): IO[HttpConnection] = IO { connection.modify { response => response.copy(status = status, content = Some(content)) } diff --git a/server/src/main/scala/io/youi/server/handler/HttpHandler.scala b/server/src/main/scala/io/youi/server/handler/HttpHandler.scala index de0cae2f8..f4ede32df 100644 --- a/server/src/main/scala/io/youi/server/handler/HttpHandler.scala +++ b/server/src/main/scala/io/youi/server/handler/HttpHandler.scala @@ -1,15 +1,14 @@ package io.youi.server.handler +import cats.effect.IO import io.youi.http.content.Content import io.youi.http.{HttpConnection, HttpStatus, StringHeaderKey} import scribe.Priority -import scala.concurrent.Future - trait HttpHandler extends Ordered[HttpHandler] { def priority: Priority = Priority.Normal - def handle(connection: HttpConnection): Future[HttpConnection] + def handle(connection: HttpConnection): IO[HttpConnection] override def compare(that: HttpHandler): Int = Priority.PriorityOrdering.compare(this.priority, that.priority) } diff --git a/server/src/main/scala/io/youi/server/handler/HttpHandlerBuilder.scala b/server/src/main/scala/io/youi/server/handler/HttpHandlerBuilder.scala index 906aab30c..d32bba540 100644 --- a/server/src/main/scala/io/youi/server/handler/HttpHandlerBuilder.scala +++ b/server/src/main/scala/io/youi/server/handler/HttpHandlerBuilder.scala @@ -1,5 +1,6 @@ package io.youi.server.handler +import cats.effect.IO import fabric._ import fabric.parse.JsonParser @@ -11,11 +12,9 @@ import io.youi.server.Server import io.youi.server.validation.{ValidationResult, Validator} import io.youi.stream.delta.Delta import io.youi.stream.{HTMLParser, Selector} -import scribe.Execution.global import scribe.Priority import fabric.rw._ -import scala.concurrent.Future import scala.util.Try case class HttpHandlerBuilder(server: Server, @@ -42,7 +41,7 @@ case class HttpHandlerBuilder(server: Server, handle { connection => f(connection.request.url).map { content => SenderHandler(content, caching = cachingManager).handle(connection) - }.getOrElse(Future.successful(connection)) + }.getOrElse(IO.pure(connection)) } } @@ -53,7 +52,7 @@ case class HttpHandlerBuilder(server: Server, if (file.isFile) { SenderHandler(Content.file(file), caching = cachingManager).handle(connection) } else { - Future.successful(connection) + IO.pure(connection) } } } @@ -75,9 +74,9 @@ case class HttpHandlerBuilder(server: Server, if (!file.isDirectory) { SenderHandler(Content.classPath(url), caching = cachingManager).handle(connection) } else { - Future.successful(connection) + IO.pure(connection) } - }.getOrElse(Future.successful(connection)) + }.getOrElse(IO.pure(connection)) } } @@ -96,29 +95,29 @@ case class HttpHandlerBuilder(server: Server, val handler = SenderHandler(content, caching = cachingManager) handler.handle(connection) } else { - Future.successful(connection) + IO.pure(connection) } } else { - Future.successful(connection) + IO.pure(connection) } } - def handle(f: HttpConnection => Future[HttpConnection]): HttpHandler = wrap(new HttpHandler { - override def handle(connection: HttpConnection): Future[HttpConnection] = f(connection) + def handle(f: HttpConnection => IO[HttpConnection]): HttpHandler = wrap(new HttpHandler { + override def handle(connection: HttpConnection): IO[HttpConnection] = f(connection) }) - def validation(validator: HttpConnection => Future[ValidationResult]): HttpHandler = validation(new Validator { - override def validate(connection: HttpConnection): Future[ValidationResult] = validator(connection) + def validation(validator: HttpConnection => IO[ValidationResult]): HttpHandler = validation(new Validator { + override def validate(connection: HttpConnection): IO[ValidationResult] = validator(connection) }) def validation(validators: Validator*): HttpHandler = wrap(new ValidatorHttpHandler(validators.toList)) def redirect(path: Path): HttpHandler = handle { connection => - Future.successful(HttpHandler.redirect(connection, path.encoded)) + IO.pure(HttpHandler.redirect(connection, path.encoded)) } def content(content: => Content): HttpHandler = handle { connection => - Future.successful { + IO { connection.modify { response => response.withContent(content) } @@ -145,7 +144,7 @@ case class HttpHandlerBuilder(server: Server, case None => None // Ignore calls to this end-point that have no content } } - Future.successful { + IO { jsonOption.map { json => val request: Request = json.as[Request] val response: Response = handler(request) @@ -165,14 +164,14 @@ case class HttpHandlerBuilder(server: Server, val wrapper = new HttpHandler { override def priority: Priority = p - override def handle(connection: HttpConnection): Future[HttpConnection] = { + override def handle(connection: HttpConnection): IO[HttpConnection] = { if (urlMatcher.forall(_.matches(connection.request.url)) && requestMatchers.forall(_(connection.request))) { ValidatorHttpHandler.validate(connection, validators).flatMap { case ValidationResult.Continue(c) => handler.handle(c) - case vr => Future.successful(vr.connection) // Validation failed, handled by ValidatorHttpHandler + case vr => IO.pure(vr.connection) // Validation failed, handled by ValidatorHttpHandler } } else { - Future.successful(connection) + IO.pure(connection) } } } diff --git a/server/src/main/scala/io/youi/server/handler/HttpProcessor.scala b/server/src/main/scala/io/youi/server/handler/HttpProcessor.scala index 05519efac..69699a0f9 100644 --- a/server/src/main/scala/io/youi/server/handler/HttpProcessor.scala +++ b/server/src/main/scala/io/youi/server/handler/HttpProcessor.scala @@ -1,10 +1,8 @@ package io.youi.server.handler +import cats.effect.IO import io.youi.http.HttpConnection import io.youi.server.validation.{ValidationResult, Validator} -import scribe.Execution.global - -import scala.concurrent.Future /** * HttpProcessor extends HttpHandler to provide a clean and efficient mechanism to manage proper @@ -17,14 +15,14 @@ trait HttpProcessor[T] extends HttpHandler { protected def matches(connection: HttpConnection): Option[T] - protected def process(connection: HttpConnection, value: T): Future[HttpConnection] + protected def process(connection: HttpConnection, value: T): IO[HttpConnection] - override def handle(connection: HttpConnection): Future[HttpConnection] = { + override def handle(connection: HttpConnection): IO[HttpConnection] = { matches(connection).map { value => ValidatorHttpHandler.validate(connection, validators(connection)).flatMap { case ValidationResult.Continue(c) => process(c, value) - case vr => Future.successful(vr.connection) + case vr => IO.pure(vr.connection) } - }.getOrElse(Future.successful(connection)) + }.getOrElse(IO.pure(connection)) } } \ No newline at end of file diff --git a/server/src/main/scala/io/youi/server/handler/LanguageSupport.scala b/server/src/main/scala/io/youi/server/handler/LanguageSupport.scala index c04eeefce..0611a700f 100644 --- a/server/src/main/scala/io/youi/server/handler/LanguageSupport.scala +++ b/server/src/main/scala/io/youi/server/handler/LanguageSupport.scala @@ -1,5 +1,6 @@ package io.youi.server.handler +import cats.effect.IO import fabric.MergeType import fabric.parse.JsonParser @@ -16,7 +17,6 @@ import scribe.Execution.global import scribe.Priority import scala.annotation.tailrec -import scala.concurrent.Future /** * LanguageSupport adds simple multi-lingual support to HTML files @@ -35,8 +35,8 @@ class LanguageSupport(val default: Locale = Locale.ENGLISH, languagePath: Path = private val languageConfig = new ConcurrentHashMap[String, Profig] private val cookieName = "lang" - override def handle(connection: HttpConnection): Future[HttpConnection] = if (connection.request.url.path == languagePath) { - Future { + override def handle(connection: HttpConnection): IO[HttpConnection] = if (connection.request.url.path == languagePath) { + IO { val (c, l) = locales(connection) val config = firstConfig(l) c.modify { response => @@ -51,7 +51,7 @@ class LanguageSupport(val default: Locale = Locale.ENGLISH, languagePath: Path = .map(_.asString) .map(translate(connection, _)) .getOrElse(connection) - Future.successful(updated) + IO.pure(updated) } def locales(connection: HttpConnection): (HttpConnection, List[String]) = { diff --git a/server/src/main/scala/io/youi/server/handler/ProxyCache.scala b/server/src/main/scala/io/youi/server/handler/ProxyCache.scala index 4b2b26002..fc738dab7 100644 --- a/server/src/main/scala/io/youi/server/handler/ProxyCache.scala +++ b/server/src/main/scala/io/youi/server/handler/ProxyCache.scala @@ -1,8 +1,9 @@ package io.youi.server.handler +import cats.effect.IO + import java.net.{URL, URLEncoder} import java.nio.file.{Path, Paths} - import io.youi.http.content.Content import io.youi.http.{HttpConnection, HttpStatus} import io.youi.net.ContentType @@ -10,10 +11,8 @@ import io.youi.stream.Selector._ import io.youi.stream._ import io.youi.stream.delta.Delta -import scala.concurrent.Future - case class ProxyCache(directory: Path = Paths.get(System.getProperty("java.io.tmpdir"))) extends HttpHandler { - override def handle(connection: HttpConnection): Future[HttpConnection] = connection.request.url.param("url") match { + override def handle(connection: HttpConnection): IO[HttpConnection] = connection.request.url.param("url") match { case Some(cacheURL) => { val url = new URL(cacheURL) val fileName = URLEncoder.encode(cacheURL, "UTF-8") @@ -21,8 +20,8 @@ case class ProxyCache(directory: Path = Paths.get(System.getProperty("java.io.tm if (!file.exists()) { scribe.debug(s"Downloading $cacheURL...") val tmp = directory.resolve(s"$fileName.tmp").toAbsolutePath.toFile - IO.stream(url, tmp) - IO.stream(tmp, file) + Stream.apply(url, tmp) + Stream.apply(tmp, file) if (!tmp.delete()) { tmp.deleteOnExit() } @@ -30,9 +29,9 @@ case class ProxyCache(directory: Path = Paths.get(System.getProperty("java.io.tm scribe.debug(s"$cacheURL already exists") } - Future.successful(connection.modify(_.withContent(Content.file(file)).withStatus(HttpStatus.OK))) + IO(connection.modify(_.withContent(Content.file(file)).withStatus(HttpStatus.OK))) } - case None => Future.successful { + case None => IO { connection.modify(_.withStatus(HttpStatus.BadRequest).withContent(Content.string("Must include `url` as GET parameter", ContentType.`text/plain`))) } } diff --git a/server/src/main/scala/io/youi/server/handler/RestfulHookup.scala b/server/src/main/scala/io/youi/server/handler/RestfulHookup.scala deleted file mode 100644 index 70f766329..000000000 --- a/server/src/main/scala/io/youi/server/handler/RestfulHookup.scala +++ /dev/null @@ -1,56 +0,0 @@ -//package io.youi.server.handler -// -//import com.outr.hookup.{Hookup, HookupServer} -//import io.circe.Json -//import io.circe.parser._ -//import io.youi.http.content.Content -//import io.youi.http.{HttpConnection, HttpRequest} -//import io.youi.net.{ContentType, Path} -// -//import scala.concurrent.Future -//import scribe.Execution.global -// TODO: IMPLEMENT SUPPORT FOR RESTful communication endpoints -// -//class RestfulHookup[H <: Hookup](hookup: HookupServer[H], prefixes: (Class[_], String)*) extends HttpHandler { -// private val prefixMap = prefixes.map { -// case (c, p) => c.getName -> p -// }.toMap -// private val instance = hookup("restful") -// private val endpoints = instance.callables.values.flatMap { h => -// val prefix = prefixMap.get(h.interfaceName).map(_.split('.').toList).getOrElse(Nil) -// h.callables.values.map { callable => -// val pathList = prefix ::: List(callable.name) -// val path = Path.parse(pathList.mkString("/", "/", "/")) -// path -> callable -// } -// }.toMap -// -// override def handle(connection: HttpConnection): Future[HttpConnection] = { -// endpoints.get(connection.request.url.path) match { -// case Some(callable) => { -// val request = request2JSON(connection.request) -// callable.call(request).map { response => -// connection.modify(_.withContent(Content.json(response, pretty = true))) -// } -// } -// case None => Future.successful(connection) -// } -// } -// -// private def request2JSON(request: HttpRequest): Json = { -// val params = Json.obj(request.url.parameters.map.toList.map { -// case (key, param) => key -> (param.values match { -// case entry :: Nil => Json.fromString(entry) -// case entries => Json.arr(entries.map(Json.fromString): _*) -// }) -// }: _*) -// val body = request.content.map { -// case content if content.contentType == ContentType.`application/json` => parse(content.asString) match { -// case Left(pf) => throw pf -// case Right(json) => json -// } -// case content => throw new RuntimeException(s"Unsupported content-type: ${content.contentType}") -// }.getOrElse(Json.obj()) -// body.deepMerge(params) -// } -//} diff --git a/server/src/main/scala/io/youi/server/handler/SenderHandler.scala b/server/src/main/scala/io/youi/server/handler/SenderHandler.scala index 7482cb1e0..c74439e73 100644 --- a/server/src/main/scala/io/youi/server/handler/SenderHandler.scala +++ b/server/src/main/scala/io/youi/server/handler/SenderHandler.scala @@ -1,12 +1,11 @@ package io.youi.server.handler +import cats.effect.IO import io.youi.http.content.Content import io.youi.http._ -import scala.concurrent.Future - class SenderHandler private(content: Content, length: Option[Long], caching: CachingManager) extends HttpHandler { - override def handle(connection: HttpConnection): Future[HttpConnection] = { + override def handle(connection: HttpConnection): IO[HttpConnection] = { SenderHandler.handle(connection, content, length, caching) } } @@ -20,7 +19,7 @@ object SenderHandler { content: Content, length: Option[Long] = None, caching: CachingManager = CachingManager.Default, - replace: Boolean = false): Future[HttpConnection] = { + replace: Boolean = false): IO[HttpConnection] = { if (connection.response.content.nonEmpty && !replace) { throw new RuntimeException(s"Content already set (${connection.response.content.get}) for HttpResponse in ${connection.request.url} when attempting to set $content.") } @@ -34,17 +33,17 @@ sealed trait CachingManager extends HttpHandler object CachingManager { case object Default extends CachingManager { - override def handle(connection: HttpConnection): Future[HttpConnection] = Future.successful(connection) + override def handle(connection: HttpConnection): IO[HttpConnection] = IO.pure(connection) } case object NotCached extends CachingManager { - override def handle(connection: HttpConnection): Future[HttpConnection] = Future.successful { + override def handle(connection: HttpConnection): IO[HttpConnection] = IO { connection.modify(_.withHeader(Headers.`Cache-Control`(CacheControl.NoCache, CacheControl.NoStore))) } } case class LastModified(publicCache: Boolean = true) extends CachingManager { val visibility: CacheControlEntry = if (publicCache) CacheControl.Public else CacheControl.Private - override def handle(connection: HttpConnection): Future[HttpConnection] = Future.successful { + override def handle(connection: HttpConnection): IO[HttpConnection] = IO { connection.modify { response => response.content match { case Some(content) => { @@ -63,7 +62,7 @@ object CachingManager { } } case class MaxAge(seconds: Long) extends CachingManager { - override def handle(connection: HttpConnection): Future[HttpConnection] = Future.successful { + override def handle(connection: HttpConnection): IO[HttpConnection] = IO { connection.modify(_.withHeader(Headers.`Cache-Control`(CacheControl.MaxAge(seconds)))) } } diff --git a/server/src/main/scala/io/youi/server/handler/ValidatorHttpHandler.scala b/server/src/main/scala/io/youi/server/handler/ValidatorHttpHandler.scala index d90711c85..e2e11d7b6 100644 --- a/server/src/main/scala/io/youi/server/handler/ValidatorHttpHandler.scala +++ b/server/src/main/scala/io/youi/server/handler/ValidatorHttpHandler.scala @@ -1,33 +1,31 @@ package io.youi.server.handler +import cats.effect.IO import io.youi.http.content.Content import io.youi.http.{HttpConnection, HttpStatus} import io.youi.server.validation.ValidationResult.{Continue, Error, Redirect} import io.youi.server.validation.{ValidationResult, Validator} -import scribe.Execution.global - -import scala.concurrent.Future class ValidatorHttpHandler(validators: List[Validator]) extends HttpHandler { - override def handle(connection: HttpConnection): Future[HttpConnection] = { + override def handle(connection: HttpConnection): IO[HttpConnection] = { ValidatorHttpHandler.validate(connection, validators).map(_.connection) } } object ValidatorHttpHandler { def validate(connection: HttpConnection, - validators: List[Validator]): Future[ValidationResult] = if (validators.isEmpty) { - Future.successful(ValidationResult.Continue(connection)) + validators: List[Validator]): IO[ValidationResult] = if (validators.isEmpty) { + IO.pure(ValidationResult.Continue(connection)) } else { val validator = validators.head validator.validate(connection).flatMap { case Continue(c) => validate(c, validators.tail) - case v: Redirect => Future.successful(v.copy(HttpHandler.redirect(v.connection, v.location))) + case v: Redirect => IO(v.copy(HttpHandler.redirect(v.connection, v.location))) case v: Error => { val modified = connection .modify(_.withStatus(HttpStatus(v.status, v.message)).withContent(Content.empty)) .finish() - Future.successful(v.copy(connection = modified)) + IO(v.copy(connection = modified)) } } } diff --git a/server/src/main/scala/io/youi/server/rest/Restful.scala b/server/src/main/scala/io/youi/server/rest/Restful.scala index c5f41d03a..bc77d10d8 100644 --- a/server/src/main/scala/io/youi/server/rest/Restful.scala +++ b/server/src/main/scala/io/youi/server/rest/Restful.scala @@ -1,16 +1,16 @@ package io.youi.server.rest +import cats.effect.IO import io.youi.ValidationError import io.youi.http.{HttpConnection, HttpStatus} import io.youi.server.handler.HttpHandler import fabric.rw._ -import scala.concurrent.Future import scala.concurrent.duration.Duration import scala.language.experimental.macros trait Restful[Request, Response] { - def apply(connection: HttpConnection, request: Request): Future[RestfulResponse[Response]] + def apply(connection: HttpConnection, request: Request): IO[RestfulResponse[Response]] def validations: List[RestfulValidation[Request]] = Nil diff --git a/server/src/main/scala/io/youi/server/rest/RestfulHandler.scala b/server/src/main/scala/io/youi/server/rest/RestfulHandler.scala index ba42985ec..8eee68424 100644 --- a/server/src/main/scala/io/youi/server/rest/RestfulHandler.scala +++ b/server/src/main/scala/io/youi/server/rest/RestfulHandler.scala @@ -1,5 +1,6 @@ package io.youi.server.rest +import cats.effect.IO import fabric._ import fabric.parse.JsonParser import io.youi.ValidationError @@ -12,15 +13,14 @@ import fabric.rw._ import scribe.Execution.global import scala.collection.mutable.ListBuffer -import scala.concurrent.Future class RestfulHandler[Request, Response](restful: Restful[Request, Response]) (implicit writer: Writer[Request], reader: Reader[Response]) extends HttpHandler { - override def handle(connection: HttpConnection): Future[HttpConnection] = { + override def handle(connection: HttpConnection): IO[HttpConnection] = { // Build JSON - val future: Future[RestfulResponse[Response]] = RestfulHandler.jsonFromConnection(connection) match { + val io: IO[RestfulResponse[Response]] = RestfulHandler.jsonFromConnection(connection) match { case Left(err) => { - Future.successful(restful.error(List(err), err.status)) + IO.pure(restful.error(List(err), err.status)) } case Right(json) => { // Decode request @@ -29,21 +29,21 @@ class RestfulHandler[Request, Response](restful: Restful[Request, Response]) RestfulHandler.validate(req, restful.validations) match { case Left(errors) => { val status = errors.map(_.status).max - Future.successful(restful.error(errors, status)) + IO.pure(restful.error(errors, status)) } case Right(request) => try { restful(connection, request) } catch { case t: Throwable => { val err = ValidationError(s"Error while calling restful: ${t.getMessage}", ValidationError.Internal) - Future.successful(restful.error(List(err), err.status)) + IO.pure(restful.error(List(err), err.status)) } } } } } - future.map { result => + io.map { result => // Encode response val responseJsonString = JsonParser.format(result.response.json) diff --git a/server/src/main/scala/io/youi/server/session/InMemorySessionManager.scala b/server/src/main/scala/io/youi/server/session/InMemorySessionManager.scala index 1c25529d3..0dad62012 100644 --- a/server/src/main/scala/io/youi/server/session/InMemorySessionManager.scala +++ b/server/src/main/scala/io/youi/server/session/InMemorySessionManager.scala @@ -1,9 +1,9 @@ package io.youi.server.session + +import cats.effect.IO import io.youi.MapStore import io.youi.http.HttpConnection -import scala.concurrent.Future - /** * In-Memory session that only lives as long as the server is running * @@ -13,15 +13,15 @@ trait InMemorySessionManager[Session] extends SessionManager[Session] { private def store = InMemorySessionManager.store override protected def loadBySessionId(sessionId: String, - connection: HttpConnection): Future[Option[SessionTransaction[Session]]] = { - Future.successful { + connection: HttpConnection): IO[Option[SessionTransaction[Session]]] = { + IO { store.get[Session](sessionId).map { session => SessionTransaction(sessionId, session, connection) } } } - override protected def save(transaction: SessionTransaction[Session]): Future[SessionTransaction[Session]] = Future.successful { + override protected def save(transaction: SessionTransaction[Session]): IO[SessionTransaction[Session]] = IO { store(transaction.id) = transaction.session transaction } diff --git a/server/src/main/scala/io/youi/server/session/SessionManager.scala b/server/src/main/scala/io/youi/server/session/SessionManager.scala index 77a2ea8c2..f50a14700 100644 --- a/server/src/main/scala/io/youi/server/session/SessionManager.scala +++ b/server/src/main/scala/io/youi/server/session/SessionManager.scala @@ -1,14 +1,12 @@ package io.youi.server.session +import cats.effect.IO import io.youi.Unique import io.youi.communication.Connection import io.youi.http.cookie.ResponseCookie import io.youi.http.{Headers, HttpConnection} import io.youi.net.Protocol import io.youi.server.WebSocketListener -import scribe.Execution.global - -import scala.concurrent.Future /** * SessionManager must be implemented in order to have support for sessions @@ -26,16 +24,16 @@ trait SessionManager[Session] { * * @param listener the WebSocketListener * @param f the functionality to work with and potentially modify a session instance - * @return Future[Unit] since Connection cannot modify the state of HttpConnection + * @return IO[Unit] since Connection cannot modify the state of HttpConnection */ def withWebSocketListener(listener: WebSocketListener) - (f: SessionTransaction[Session] => Future[SessionTransaction[Session]] = t => Future.successful(t)): Future[Session] = { + (f: SessionTransaction[Session] => IO[SessionTransaction[Session]] = t => IO.pure(t)): IO[Session] = { val httpConnection = listener.httpConnection session(httpConnection, f, requestModifiable = false).map(_.session) } def withConnection(connection: Connection) - (f: SessionTransaction[Session] => Future[SessionTransaction[Session]] = t => Future.successful(t)): Future[Session] = { + (f: SessionTransaction[Session] => IO[SessionTransaction[Session]] = t => IO.pure(t)): IO[Session] = { val listener = connection.webSocket.getOrElse(throw new RuntimeException("No active connection")).asInstanceOf[WebSocketListener] withWebSocketListener(listener)(f) } @@ -48,7 +46,7 @@ trait SessionManager[Session] { * @return potentially modified HttpConnection */ def withHttpConnection(connection: HttpConnection) - (f: SessionTransaction[Session] => Future[SessionTransaction[Session]] = t => Future.successful(t)): Future[SessionTransaction[Session]] = { + (f: SessionTransaction[Session] => IO[SessionTransaction[Session]] = t => IO.pure(t)): IO[SessionTransaction[Session]] = { session(connection, f, requestModifiable = true).map(_.copy(sessionModifiable = false)) } @@ -61,15 +59,16 @@ trait SessionManager[Session] { * @return the final, modified HttpConnection */ protected def session(connection: HttpConnection, - f: SessionTransaction[Session] => Future[SessionTransaction[Session]], - requestModifiable: Boolean): Future[SessionTransaction[Session]] = for { + f: SessionTransaction[Session] => IO[SessionTransaction[Session]], + requestModifiable: Boolean): IO[SessionTransaction[Session]] = for { // Load or create the session id - (modifiedConnection, sessionId) <- Future.successful(getOrCreateSessionId(connection)) + tuple <- IO(getOrCreateSessionId(connection)) + (modifiedConnection, sessionId) = tuple // Get the existing session if it exists originalTransaction <- loadBySessionId(sessionId, modifiedConnection) // Update the existing transaction or create a new one if none exists transaction <- originalTransaction match { - case Some(t) => Future.successful(t.copy(requestModifiable = requestModifiable)) + case Some(t) => IO(t.copy(requestModifiable = requestModifiable)) case None => createBySessionId(sessionId, modifiedConnection).map(_.copy(requestModifiable = requestModifiable)) } // Apply the transaction handler @@ -80,7 +79,7 @@ trait SessionManager[Session] { saved <- if (modified) { save(updatedTransaction) } else { - Future.successful(updatedTransaction) + IO.pure(updatedTransaction) } } yield { saved @@ -91,20 +90,20 @@ trait SessionManager[Session] { * * @param sessionId the session id to load from * @param connection the HttpConnection to work with - * @return a future SessionTransaction[Session] if one is persisted for this manager + * @return a IO SessionTransaction[Session] if one is persisted for this manager */ protected def loadBySessionId(sessionId: String, - connection: HttpConnection): Future[Option[SessionTransaction[Session]]] + connection: HttpConnection): IO[Option[SessionTransaction[Session]]] /** * Creates a new session by session id * * @param sessionId the session id to create from * @param connection the HttpConnection to work with - * @return a future SessionTransaction[Session] + * @return a IO SessionTransaction[Session] */ protected def createBySessionId(sessionId: String, - connection: HttpConnection): Future[SessionTransaction[Session]] = { + connection: HttpConnection): IO[SessionTransaction[Session]] = { create(sessionId).map { session => SessionTransaction[Session](sessionId, session, connection) } @@ -115,7 +114,7 @@ trait SessionManager[Session] { * * @param sessionId the session id to create a Session for */ - protected def create(sessionId: String): Future[Session] + protected def create(sessionId: String): IO[Session] /** * Saves a potentially modified Session to this manager @@ -123,7 +122,7 @@ trait SessionManager[Session] { * @param transaction the transaction to persist from * @return a potentially modified HttpConnection */ - protected def save(transaction: SessionTransaction[Session]): Future[SessionTransaction[Session]] + protected def save(transaction: SessionTransaction[Session]): IO[SessionTransaction[Session]] /** * Gets a session id if it already exists or creates a new one (and applies it on the HttpConnection) if it doesn't diff --git a/server/src/main/scala/io/youi/server/validation/IPAddressValidator.scala b/server/src/main/scala/io/youi/server/validation/IPAddressValidator.scala index 14b457ff2..2129ac70a 100644 --- a/server/src/main/scala/io/youi/server/validation/IPAddressValidator.scala +++ b/server/src/main/scala/io/youi/server/validation/IPAddressValidator.scala @@ -1,12 +1,11 @@ package io.youi.server.validation +import cats.effect.IO import io.youi.http.{HttpConnection, HttpStatus} import io.youi.net.IP -import scala.concurrent.Future - class IPAddressValidator(allow: Set[IP], reject: Set[IP], defaultAllow: Boolean) extends Validator { - override def validate(connection: HttpConnection): Future[ValidationResult] = Future.successful { + override def validate(connection: HttpConnection): IO[ValidationResult] = IO { val ip = connection.request.originalSource if ((allow.contains(ip) || defaultAllow) && !reject.contains(ip)) { ValidationResult.Continue(connection) diff --git a/server/src/main/scala/io/youi/server/validation/Validator.scala b/server/src/main/scala/io/youi/server/validation/Validator.scala index 4ae417fc6..4ff92e2bf 100644 --- a/server/src/main/scala/io/youi/server/validation/Validator.scala +++ b/server/src/main/scala/io/youi/server/validation/Validator.scala @@ -1,9 +1,8 @@ package io.youi.server.validation +import cats.effect.IO import io.youi.http.HttpConnection -import scala.concurrent.Future - trait Validator { - def validate(connection: HttpConnection): Future[ValidationResult] + def validate(connection: HttpConnection): IO[ValidationResult] } \ No newline at end of file diff --git a/server/src/test/scala/spec/ServerDSLSpec.scala b/server/src/test/scala/spec/ServerDSLSpec.scala index 3465b5e59..14b488888 100644 --- a/server/src/test/scala/spec/ServerDSLSpec.scala +++ b/server/src/test/scala/spec/ServerDSLSpec.scala @@ -1,14 +1,14 @@ package spec -import testy._ +import cats.effect.testing.scalatest.AsyncIOSpec import io.youi.http.{HttpConnection, HttpMethod, HttpRequest, HttpStatus} import io.youi.net._ import io.youi.server.dsl._ import io.youi.server.{DefaultErrorHandler, Server} +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AsyncWordSpec -import scribe.Execution.global - -class ServerDSLSpec extends Spec { +class ServerDSLSpec extends AsyncWordSpec with AsyncIOSpec with Matchers { private lazy val text = "Hello, World!".withContentType(ContentType.`text/plain`) private lazy val html = """ diff --git a/server/src/test/scala/spec/ServerSpec.scala b/server/src/test/scala/spec/ServerSpec.scala index bcd4bd507..fe6076ea5 100644 --- a/server/src/test/scala/spec/ServerSpec.scala +++ b/server/src/test/scala/spec/ServerSpec.scala @@ -1,6 +1,7 @@ package spec -import testy._ +import cats.effect.IO +import cats.effect.testing.scalatest.AsyncIOSpec import fabric.parse.JsonParser import io.youi.ValidationError import io.youi.http._ @@ -11,11 +12,10 @@ import io.youi.server.dsl._ import io.youi.server.handler.HttpHandler import io.youi.server.rest.{Restful, RestfulResponse} import fabric.rw._ +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AsyncWordSpec -import scala.concurrent.Future -import scribe.Execution.global - -class ServerSpec extends Spec { +class ServerSpec extends AsyncWordSpec with AsyncIOSpec with Matchers { Server.config("implementation").store("io.youi.server.test.TestServerImplementation") object server extends Server @@ -23,7 +23,7 @@ class ServerSpec extends Spec { "TestHttpApplication" should { "configure the TestServer" in { server.handler.matcher(path.exact("/test.html")).wrap(new HttpHandler { - override def handle(connection: HttpConnection): Future[HttpConnection] = Future.successful { + override def handle(connection: HttpConnection): IO[HttpConnection] = IO { connection.modify { response => response.withContent(Content.string("test!", ContentType.`text/plain`)) } @@ -59,7 +59,6 @@ class ServerSpec extends Spec { content = Some(content) ))).map { connection => connection.response.status should be(HttpStatus.OK) - connection.response.content should be(Some("")) val jsonString = connection.response.content.get.asInstanceOf[StringContent].value val response = JsonParser.parse(jsonString).as[ReverseResponse] response.errors should be(Nil) @@ -72,7 +71,6 @@ class ServerSpec extends Spec { url = URL("http://localhost/test/reverse?value=Testing") ))).map { connection => connection.response.status should be(HttpStatus.OK) - connection.response.content should be(Some("")) val jsonString = connection.response.content.get.asInstanceOf[StringContent].value val response = JsonParser.parse(jsonString).as[ReverseResponse] response.errors should be(Nil) @@ -85,7 +83,6 @@ class ServerSpec extends Spec { url = URL("http://localhost/test/reverse/Testing") ))).map { connection => connection.response.status should be(HttpStatus.OK) - connection.response.content should be(Some("")) val jsonString = connection.response.content.get.asInstanceOf[StringContent].value val response = JsonParser.parse(jsonString).as[ReverseResponse] response.errors should be(Nil) @@ -99,7 +96,6 @@ class ServerSpec extends Spec { url = URL("http://localhost/test/time") ))).map { connection => connection.response.status should be(HttpStatus.OK) - connection.response.content should be(Some("")) val jsonString = connection.response.content.get.asInstanceOf[StringContent].value val response = JsonParser.parse(jsonString).as[Long] response should be >= begin @@ -120,8 +116,8 @@ class ServerSpec extends Spec { } object ReverseService extends Restful[ReverseRequest, ReverseResponse] { - override def apply(connection: HttpConnection, request: ReverseRequest): Future[RestfulResponse[ReverseResponse]] = { - Future.successful(RestfulResponse(ReverseResponse(Some(request.value.reverse), Nil), HttpStatus.OK)) + override def apply(connection: HttpConnection, request: ReverseRequest): IO[RestfulResponse[ReverseResponse]] = { + IO.pure(RestfulResponse(ReverseResponse(Some(request.value.reverse), Nil), HttpStatus.OK)) } override def error(errors: List[ValidationError], status: HttpStatus): RestfulResponse[ReverseResponse] = { @@ -130,8 +126,8 @@ class ServerSpec extends Spec { } object ServerTimeService extends Restful[Unit, Long] { - override def apply(connection: HttpConnection, request: Unit): Future[RestfulResponse[Long]] = { - Future.successful(RestfulResponse(System.currentTimeMillis(), HttpStatus.OK)) + override def apply(connection: HttpConnection, request: Unit): IO[RestfulResponse[Long]] = { + IO.pure(RestfulResponse(System.currentTimeMillis(), HttpStatus.OK)) } override def error(errors: List[ValidationError], status: HttpStatus): RestfulResponse[Long] = { diff --git a/serverUndertow/src/main/scala/io/youi/client/WebSocketClient.scala b/serverUndertow/src/main/scala/io/youi/client/WebSocketClient.scala index 1147a6cc9..41492cadb 100644 --- a/serverUndertow/src/main/scala/io/youi/client/WebSocketClient.scala +++ b/serverUndertow/src/main/scala/io/youi/client/WebSocketClient.scala @@ -1,9 +1,10 @@ package io.youi.client +import cats.effect.IO + import java.io.IOException import java.net.URI import java.nio.ByteBuffer - import io.undertow.protocols.ssl.UndertowXnioSsl import io.undertow.server.DefaultByteBufferPool import io.undertow.util.Headers @@ -72,7 +73,7 @@ class WebSocketClient(url: URL, private var backlog = List.empty[AnyRef] - def connect(): Future[ConnectionStatus] = if (_channel.get.isEmpty) { + def connect(): IO[ConnectionStatus] = if (_channel.get.isEmpty) { val promise = Promise[ConnectionStatus]() connectionBuilder.connect().addNotifier(new IoFuture.HandlingNotifier[WebSocketChannel, Any] { override def handleDone(data: WebSocketChannel, attachment: Any): Unit = { @@ -115,7 +116,7 @@ class WebSocketClient(url: URL, _channel @= None if (autoReconnect) { scribe.warn(s"Connection closed or unable to connect to $url (${exception.getMessage}). Trying again in ${reconnectDelay.toSeconds} seconds...") - Time.delay(reconnectDelay).foreach(_ => connect()) + IO.sleep(reconnectDelay).map(_ => connect()) } else { scribe.warn("Connection closed or unable to connect.") } diff --git a/serverUndertow/src/main/scala/io/youi/server/UndertowServerImplementation.scala b/serverUndertow/src/main/scala/io/youi/server/UndertowServerImplementation.scala index b5d34a0ca..12319e3cc 100644 --- a/serverUndertow/src/main/scala/io/youi/server/UndertowServerImplementation.scala +++ b/serverUndertow/src/main/scala/io/youi/server/UndertowServerImplementation.scala @@ -31,8 +31,8 @@ import scala.jdk.CollectionConverters._ class UndertowServerImplementation(val server: Server) extends ServerImplementation with UndertowHttpHandler { val enableHTTP2: Boolean = Server.config("enableHTTP2").opt[Boolean].getOrElse(true) - val persistentConnections: Boolean = Server.config("persistentConnections").as[Boolean](true) - val webSocketCompression: Boolean = Server.config("webSocketCompression").opt[Boolean].getOrElse(true) + val persistentConnections: Boolean = Server.config("persistentConnections").asOr[Boolean](true) + val webSocketCompression: Boolean = Server.config("webSocketCompression").asOr[Boolean](true) private var instance: Option[Undertow] = None @@ -123,7 +123,7 @@ class UndertowServerImplementation(val server: Server) extends ServerImplementat UndertowServerImplementation.processRequest(exchange, url) { request => try { val connection: HttpConnection = HttpConnection(server, request) - server.handle(connection).foreach { c => + server.handle(connection).map { c => UndertowServerImplementation.response(this, c, exchange) } } catch { @@ -184,7 +184,7 @@ object UndertowServerImplementation extends Moduload with ServerImplementationCr val runnable = new Runnable { override def run(): Unit = { val cis = new ChannelInputStream(exchange.getRequestChannel) - val data = IO.stream(cis, new StringBuilder).toString + val data = Stream.apply(cis, new StringBuilder).toString handle(Some(StringContent(data, ct))) } } diff --git a/stream/src/main/scala/io/youi/stream/HTMLParser.scala b/stream/src/main/scala/io/youi/stream/HTMLParser.scala index 253bf4f84..8becb9ca5 100644 --- a/stream/src/main/scala/io/youi/stream/HTMLParser.scala +++ b/stream/src/main/scala/io/youi/stream/HTMLParser.scala @@ -40,14 +40,14 @@ object HTMLParser { def cache(url: URL): StreamableHTML = { val file = File.createTempFile("htmlparser", "cache") file.deleteOnExit() - IO.stream(url, file) + Stream.apply(url, file) cache(file) } def cache(html: String): StreamableHTML = { val file = File.createTempFile("htmlparser", "cache") file.deleteOnExit() - IO.stream(html, file) + Stream.apply(html, file) cache(file) }