diff --git a/core/src/main/scala/io/finch/Endpoint.scala b/core/src/main/scala/io/finch/Endpoint.scala
index 4be9c8435..d41df6f0d 100644
--- a/core/src/main/scala/io/finch/Endpoint.scala
+++ b/core/src/main/scala/io/finch/Endpoint.scala
@@ -2,7 +2,7 @@ package io.finch
import cats.{Alternative, Applicative, ApplicativeError, Id, Monad, MonadError}
import cats.data.NonEmptyList
-import cats.effect.{Effect, Sync}
+import cats.effect.{ContextShift, Effect, Resource, Sync}
import cats.syntax.all._
import com.twitter.concurrent.AsyncStream
import com.twitter.finagle.Service
@@ -17,6 +17,7 @@ import com.twitter.io.{Buf, Reader}
import io.finch.endpoint._
import io.finch.internal._
import io.finch.items.RequestItem
+import java.io.{File, FileInputStream, InputStream}
import scala.reflect.ClassTag
import shapeless._
import shapeless.ops.adjoin.Adjoin
@@ -575,6 +576,96 @@ object Endpoint {
EndpointResult.Matched(input, Trace.empty, F.suspend(foa))
}
+ /**
+ * Creates an [[Endpoint]] from a given [[InputStream]]. Uses [[Resource]] for safer resource
+ * management and [[ContextShift]] for offloading blocking work from a worker pool.
+ *
+ * @see [[fromFile]]
+ */
+ def fromInputStream[F[_]](stream: Resource[F, InputStream])(
+ implicit F: Effect[F], S: ContextShift[F]
+ ): Endpoint[F, Buf] = new FromInputStream[F](stream)
+
+ /**
+ * Creates an [[Endpoint]] from a given [[File]]. Uses [[Resource]] for safer resource
+ * management and [[ContextShift]] for offloading blocking work from a worker pool.
+ *
+ * @see [[fromInputStream]]
+ */
+ def fromFile[F[_]](file: File)(
+ implicit F: Effect[F], S: ContextShift[F]
+ ): Endpoint[F, Buf] =
+ fromInputStream[F](
+ Resource.fromAutoCloseable(F.delay(new FileInputStream(file)))
+ )
+
+ /**
+ * Creates an [[Endpoint]] that serves an asset (static content) from a Java classpath resource,
+ * located at `path`, as a static content. The returned endpoint will only match `GET` requests
+ * with path identical to asset's.
+ *
+ * This could be especially useful in local development, when throughput and latency matter less
+ * than quick iterations. These means, however, are not recommended for production usage. Web
+ * servers (Nginx, Apache) will do much better job serving static files.
+ *
+ * Example project structure:
+ *
+ * {{{
+ * ├── scala
+ * │ └── Main.scala
+ * └── resources
+ * ├── index.html
+ * └── script.js
+ * }}}
+ *
+ * Example bootstrap:
+ *
+ * {{{
+ * Bootstrap
+ * ...
+ * .serve[Text.Html](Endpoint[IO].classpathAsset("/index.html"))
+ * .serve[Application.Javascript](Endpoint[IO].classpathAsset("/script.js"))
+ * ...
+ * }}}
+ *
+ * @see https://docs.oracle.com/javase/8/docs/technotes/guides/lang/resources.html
+ */
+ def classpathAsset[F[_]](path: String)(implicit
+ F: Effect[F],
+ S: ContextShift[F]
+ ): Endpoint[F, Buf] = {
+ val asset = new Asset[F](path)
+ val stream =
+ fromInputStream[F](Resource.fromAutoCloseable(F.delay(getClass.getResourceAsStream(path))))
+
+ asset :: stream
+ }
+
+ /**
+ * Creates an [[Endpoint]] that serves an asset (static content) from a filesystem, located at
+ * `path`, as a static content. The returned endpoint will only match `GET` requests with path
+ * identical to asset's.
+ *
+ * Example bootstrap:
+ *
+ * {{{
+ * Bootstrap
+ * ...
+ * .serve[Text.Html](Endpoint[IO].filesystemAsset("index.html"))
+ * .serve[Application.Javascript](Endpoint[IO].filesystemAsset("script.js"))
+ * ...
+ * }}}
+ */
+ def filesystemAsset[F[_]](path: String)(implicit
+ F: Effect[F],
+ S: ContextShift[F]
+ ): Endpoint[F, Buf] = {
+ val asset = new Asset[F](path)
+ val file = fromFile[F](new File(path))
+
+ asset :: file
+ }
+
/**
* A root [[Endpoint]] that always matches and extracts the current request.
*/
@@ -592,7 +683,11 @@ object Endpoint {
def pathAny[F[_]](implicit F: Applicative[F]): Endpoint[F, HNil] =
new Endpoint[F, HNil] {
final def apply(input: Input): Result[F, HNil] =
- EndpointResult.Matched(input.withRoute(Nil), Trace.empty, F.pure(Output.HNil))
+ EndpointResult.Matched(
+ input.withRoute(Nil),
+ Trace.fromRoute(input.route),
+ F.pure(Output.HNil)
+ )
final override def toString: String = "*"
}
diff --git a/core/src/main/scala/io/finch/EndpointModule.scala b/core/src/main/scala/io/finch/EndpointModule.scala
index 93618075a..2f1e57774 100644
--- a/core/src/main/scala/io/finch/EndpointModule.scala
+++ b/core/src/main/scala/io/finch/EndpointModule.scala
@@ -2,11 +2,12 @@ package io.finch
import cats.Applicative
import cats.data.NonEmptyList
-import cats.effect.{Effect, Sync}
+import cats.effect.{ContextShift, Effect, Resource, Sync}
import com.twitter.concurrent.AsyncStream
import com.twitter.finagle.http.{Cookie, Request}
import com.twitter.finagle.http.exp.Multipart
import com.twitter.io.Buf
+import java.io.{File, InputStream}
import scala.reflect.ClassTag
import shapeless.HNil
@@ -95,6 +96,34 @@ trait EndpointModule[F[_]] {
def liftOutputAsync[A](foa: => F[Output[A]])(implicit F: Sync[F]): Endpoint[F, A] =
Endpoint.liftOutputAsync[F, A](foa)
+ /**
+ * An alias for [[Endpoint.fromInputStream]].
+ */
+ def fromInputStream(stream: Resource[F, InputStream])(
+ implicit F: Effect[F], S: ContextShift[F]
+ ): Endpoint[F, Buf] =
+ Endpoint.fromInputStream[F](stream)
+
+ /**
+ * An alias for [[Endpoint.fromFile]].
+ */
+ def fromFile(file: File)(
+ implicit F: Effect[F], S: ContextShift[F]
+ ): Endpoint[F, Buf] =
+ Endpoint.fromFile[F](file)
+
+ /**
+ * An alias for [[Endpoint.classpathAsset]].
+ */
+ def classpathAsset(path: String)(implicit F: Effect[F], S: ContextShift[F]): Endpoint[F, Buf] =
+ Endpoint.classpathAsset[F](path)
+
+ /**
+ * An alias for [[Endpoint.classpathAsset]].
+ */
+ def filesystemAsset(path: String)(implicit F: Effect[F], S: ContextShift[F]): Endpoint[F, Buf] =
+ Endpoint.filesystemAsset[F](path)
+
/**
* An alias for [[Endpoint.root]].
*/
diff --git a/core/src/main/scala/io/finch/Trace.scala b/core/src/main/scala/io/finch/Trace.scala
index fbf2110c5..214261742 100644
--- a/core/src/main/scala/io/finch/Trace.scala
+++ b/core/src/main/scala/io/finch/Trace.scala
@@ -63,6 +63,29 @@ object Trace {
def empty: Trace = Empty
def segment(s: String): Trace = Segment(s, empty)
+ def fromRoute(r: Seq[String]): Trace = {
+ var result = empty
+ var current: Segment = null
+
+ def prepend(segment: Segment): Unit = {
+ if (result == empty) {
+ result = segment
+ current = segment
+ } else {
+ current.next = segment
+ current = segment
+ }
+ }
+
+ var rs = r
+ while (rs.nonEmpty) {
+ prepend(Segment(rs.head, empty))
+ rs = rs.tail
+ }
+
+ result
+ }
+
/**
* Within a given context `fn`, capture the [[Trace]] instance under `Trace.captured` for each
* matched endpoint.
diff --git a/core/src/main/scala/io/finch/endpoint/endpoint.scala b/core/src/main/scala/io/finch/endpoint/endpoint.scala
new file mode 100644
index 000000000..d3998dc15
--- /dev/null
+++ b/core/src/main/scala/io/finch/endpoint/endpoint.scala
@@ -0,0 +1,47 @@
+package io.finch
+
+import cats.effect.{ContextShift, Effect, Resource}
+import cats.syntax.all._
+import com.twitter.finagle.http.{Method => FinagleMethod}
+import com.twitter.io.Buf
+import java.io.InputStream
+import shapeless.HNil
+
+package object endpoint {
+
+ private[finch] class FromInputStream[F[_]](stream: Resource[F, InputStream])(
+ implicit F: Effect[F], S: ContextShift[F]
+ ) extends Endpoint[F, Buf] {
+
+ private def readLoop(left: Buf, stream: InputStream): F[Buf] = F.suspend {
+ val buffer = new Array[Byte](1024)
+ val n = stream.read(buffer)
+ if (n == -1) F.pure(left)
+ else readLoop(left.concat(Buf.ByteArray.Owned(buffer, 0, n)), stream)
+ }
+
+ final def apply(input: Input): Endpoint.Result[F, Buf] =
+ EndpointResult.Matched(
+ input,
+ Trace.empty,
+ stream.use(s =>
+ S.shift.flatMap(_ => readLoop(Buf.Empty, s)).map(buf => Output.payload(buf))
+ )
+ )
+ }
+
+ private[finch] class Asset[F[_]](path: String)(implicit F: Effect[F]) extends Endpoint[F, HNil] {
+ final def apply(input: Input): Endpoint.Result[F, HNil] = {
+ val req = input.request
+ if (req.method != FinagleMethod.Get || req.path != path) EndpointResult.NotMatched[F]
+ else
+ EndpointResult.Matched(
+ input.withRoute(Nil),
+ Trace.fromRoute(input.route),
+ F.pure(Output.HNil)
+ )
+ }
+
+ final override def toString: String = s"GET /$path"
+ }
+}
diff --git a/core/src/test/resources/test.txt b/core/src/test/resources/test.txt
new file mode 100644
index 000000000..1aeaedbf4
--- /dev/null
+++ b/core/src/test/resources/test.txt
@@ -0,0 +1 @@
+foo bar baz
diff --git a/core/src/test/scala/io/finch/EndpointSpec.scala b/core/src/test/scala/io/finch/EndpointSpec.scala
index 6dec721d1..0a5ca03fc 100644
--- a/core/src/test/scala/io/finch/EndpointSpec.scala
+++ b/core/src/test/scala/io/finch/EndpointSpec.scala
@@ -4,11 +4,13 @@ import java.util.UUID
import java.util.concurrent.TimeUnit
import cats.data.NonEmptyList
-import cats.effect.IO
+import cats.effect.{IO, Resource}
import cats.laws.discipline.AlternativeTests
import cats.laws.discipline.SemigroupalTests.Isomorphisms
import com.twitter.finagle.http.{Cookie, Method, Request}
+import com.twitter.io.Buf
import io.finch.data.Foo
+import java.io.{ByteArrayInputStream, InputStream}
import scala.concurrent.duration.Duration
import shapeless._
@@ -363,4 +365,21 @@ class EndpointSpec extends FinchSpec {
endpoint(Input.get("/index", "testEndpoint" -> "a")).awaitValueUnsafe(Duration(10, TimeUnit.SECONDS))
)
}
+
+ it should "fromInputStream" in {
+ val bytes = Array[Byte](1, 2, 3, 4, 5)
+ val bis = Resource.fromAutoCloseable[IO, InputStream](IO.delay(new ByteArrayInputStream(bytes)))
+
+ val is = fromInputStream(bis)
+
+ is(Input.get("/")).awaitValueUnsafe() shouldBe Some(Buf.ByteArray.Owned(bytes))
+ }
+
+ it should "classpathAsset" in {
+ val r = classpathAsset("/test.txt")
+
+ r(Input.get("/foo")).awaitOutputUnsafe() shouldBe None
+ r(Input.post("/")).awaitOutputUnsafe() shouldBe None
+ r(Input.get("/test.txt")).awaitValueUnsafe() shouldBe Some(Buf.Utf8("foo bar baz\n"))
+ }
}
diff --git a/core/src/test/scala/io/finch/MissingInstances.scala b/core/src/test/scala/io/finch/MissingInstances.scala
index 4aedb980b..22a9bedb4 100644
--- a/core/src/test/scala/io/finch/MissingInstances.scala
+++ b/core/src/test/scala/io/finch/MissingInstances.scala
@@ -1,7 +1,9 @@
package io.finch
import cats.Eq
+import cats.effect.{ContextShift, IO}
import com.twitter.io.Buf
+import scala.concurrent.ExecutionContext
/**
* Type class instances for non-Finch types.
@@ -14,4 +16,6 @@ trait MissingInstances {
}
implicit def eqBuf: Eq[Buf] = Eq.fromUniversalEquals
+
+ implicit val shift: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
}
diff --git a/core/src/test/scala/io/finch/TraceSpec.scala b/core/src/test/scala/io/finch/TraceSpec.scala
index 6f8b44782..31fc5f95f 100644
--- a/core/src/test/scala/io/finch/TraceSpec.scala
+++ b/core/src/test/scala/io/finch/TraceSpec.scala
@@ -16,4 +16,10 @@ class TraceSpec extends FinchSpec {
a.concat(b).toList === (a.toList ++ b.toList)
}
}
+
+ it should "create fromRoute" in {
+ check { l: List[String] =>
+ Trace.fromRoute(l).toList === l
+ }
+ }
}
diff --git a/scalastyle-config.xml b/scalastyle-config.xml
index a15ea5cab..6a632a4ff 100644
--- a/scalastyle-config.xml
+++ b/scalastyle-config.xml
@@ -9,7 +9,7 @@
-
+