Skip to content

Commit

Permalink
Merge pull request #1017 from finagle/vk/resource
Browse files Browse the repository at this point in the history
Introduce asset endpoints
  • Loading branch information
vkostyukov authored Nov 14, 2018
2 parents 6f5006f + 15eb10b commit fdf8987
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 5 deletions.
99 changes: 97 additions & 2 deletions core/src/main/scala/io/finch/Endpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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 = "*"
}
Expand Down
31 changes: 30 additions & 1 deletion core/src/main/scala/io/finch/EndpointModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]].
*/
Expand Down
23 changes: 23 additions & 0 deletions core/src/main/scala/io/finch/Trace.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
47 changes: 47 additions & 0 deletions core/src/main/scala/io/finch/endpoint/endpoint.scala
Original file line number Diff line number Diff line change
@@ -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"
}
}
1 change: 1 addition & 0 deletions core/src/test/resources/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo bar baz
21 changes: 20 additions & 1 deletion core/src/test/scala/io/finch/EndpointSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._

Expand Down Expand Up @@ -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"))
}
}
4 changes: 4 additions & 0 deletions core/src/test/scala/io/finch/MissingInstances.scala
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -14,4 +16,6 @@ trait MissingInstances {
}

implicit def eqBuf: Eq[Buf] = Eq.fromUniversalEquals

implicit val shift: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
}
6 changes: 6 additions & 0 deletions core/src/test/scala/io/finch/TraceSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
2 changes: 1 addition & 1 deletion scalastyle-config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<check level="error" class="org.scalastyle.file.FileTabChecker" enabled="true"></check>
<check level="error" class="org.scalastyle.file.FileLengthChecker" enabled="true">
<parameters>
<parameter name="maxFileLength"><![CDATA[1000]]></parameter>
<parameter name="maxFileLength"><![CDATA[1500]]></parameter>
</parameters>
</check>
<check level="error" class="org.scalastyle.file.FileLineLengthChecker" enabled="true">
Expand Down

0 comments on commit fdf8987

Please sign in to comment.