Fintrospect
Fintrospect is a Scala web-framework with an intelligent HTTP routing layer, based on the Finagle RPC framework from Twitter.
Request Free Consultationoverview
Fintrospect is a Scala web-framework with an intelligent HTTP routing layer, based on the Finagle RPC framework from Twitter. Via a shared contract, it provides a simple way to implement fast web service endpoints and HTTP clients which are:
-
Type-safe
: auto-marshalls all request parameters/bodies into the correct types (including primitives + JSON/XML etc...) -
Auto-validating
: the presence of required and optional request parameters and bodies are checked before entering service-layer code -
Auto-documenting
: runtime generation of endpoint documentation such as Swagger JSON or web sitemap XML. Generates JSON Schema for example object formats to be included in these API docs. -
Uniform
: reuse the same contract to define both incoming or outgoing Finagle HTTP services. This also allows extremely low effort fake servers to be created
Additionally, Fintrospect provides a number of mechanisms to leverage these routes:
- Easily build type-safe HTTP responses with a set of custom builders for a wide variety of message formats:
- JSON: Argo, Argonaut, Circe, GSON, Jackson, Json4S, Play JSON, Spray JSON
- Auto-marshaling of case classes instances to/from JSON (for
Argonaut
/Circe
/Json4S
/Play
). - Implement simple
PATCH
/PUT
endpoints of case class instances (Circe
only).
- Auto-marshaling of case classes instances to/from JSON (for
- Native implementations of XML, Plain Text, HTML, XHTML
- MsgPack binary format
- JSON: Argo, Argonaut, Circe, GSON, Jackson, Json4S, Play JSON, Spray JSON
- Serve static content from the classpath or a directory
- Template
View
support (with Hot-Reloading) for building responses with Mustache or Handlebars - Anonymising headers for dynamic-path based endpoints, removing all dynamic path elements. This allows, for example, calls to particular endpoints to be grouped for metric purposes. e.g.
/search/author/rowling
becomes/search/author/{name}
- Interacts seamlessly with other Finagle based libraries, such as Finagle OAuth2
- Utilities to help you unit-test endpoint services and write HTTP contract tests for remote dependencies
broad concepts
The main concepts in play:
-
RouteSpec
: defines the self-describing contract of an HTTP endpoint -
ServerRoute
: the binding of aRouteSpec
to a FinagleService
implementing some business-logic, to create an inbound HTTP endpoint -
RouteClient
: the binding of aRouteSpec
to a FinagleService
representing an HTTP client, to create a simple function for making outbound HTTP calls -
ParameterSpec
: defines the acceptable format for a request parameter (Path/Query/Header/Form-field). Provides the auto-marshaling mechanic for serializing and deserializing objects to and from HTTP messages -
BodySpec
: similar toParameterSpec
, but applied to the body of an HTTP message -
RouteModule
: defines a set ofServerRoute
instances which are grouped under a particular request path. These modules can be combined and then converted to a FinagleService
and attached to a Finagle HTTP server. Each module provides an endpoint under which it's own runtime-generated documentation can be served (eg. in Swagger format) -
ResponseSpec
: defines the characteristics of a possible response generated by aServerRoute
regarding finagle
Since Fintrospect is built on top of Finagle, it's worth acquainting yourself with its concepts, which can be found here.
&tldr; Finagle Primer
- Finagle provides protocol-agnostic RPC and is based on Netty
- It is mainly asynchronous and makes heavy usage of Twitter's version of Scala Futures
- It defines uniform
Service
andFilter
interfaces for both client and server APIs that are effectively a single method...Service: def apply(request : Request) : Future[Response] Filter: def apply(request : RequestIn, service : Service[RequestOut, ResponseIn]) : Future[ResponseOut]
-
Filters
can be chained together and then applied to aService
, which results in anotherService
. This is useful to apply layers of functionality such as caching headers, retry behavior, and timeouts.
a note on style
- The code in this guide has omitted imports that would have made it read more concisely. The sacrifices we make in the name of learning... :)
installation
Fintrospect is intentionally dependency-lite by design - other than Finagle, the core library itself only has a single non-org.scala
dependency.
To activate some optional features, additional dependencies may be required - these are shown in the subsequent sections depending on the use-case.
core
Add the following lines to build.sbt
- the lib is hosted in Maven Central and JCenter:
resolvers += "JCenter" at "https://jcenter.bintray.com" libraryDependencies += "io.fintrospect" %% "fintrospect-core" % "15.1.0"
message formats
For consuming request parameters in a type-safe way or using the relevant HTTP response builders add the following:
Library | Content-Type | Additional SBT deps |
Builder Object
|
Argo | application/json | - | Argo |
Argonaut | application/json | "io.fintrospect" %% "fintrospect-argonaut" % "15.1.0" | Argonaut |
Circe | application/json | "io.fintrospect" %% "fintrospect-circe" % "15.1.0" | Circe |
GSON | application/json | "io.fintrospect" %% "fintrospect-gson" % "15.1.0" | Gson |
HTML | text/html | - | Html |
Jackson | application/json | "io.fintrospect" %% "fintrospect-jackson" % "15.1.0" | Jackson |
Json4S Native | application/json | "io.fintrospect" %% "fintrospect-json4s" % "15.1.0" |
Json4s Json4sDoubleMode |
Json4S Jackson | application/json | "io.fintrospect" %% "fintrospect-json4s" % "15.1.0" |
Json4sJackson Json4sJacksonDoubleMode |
MsgPack | application/msgpack | "io.fintrospect" %% "fintrospect-msgpack" % "15.1.0" | MsgPack |
Plain Text | text/plain | - | PlainText |
Play | application/json | "io.fintrospect" %% "fintrospect-play" % "15.1.0" | Play |
Spray | application/json | "io.fintrospect" %% "fintrospect-spray" % "15.1.0" | Spray |
XHTML | application/xhtml+xml | - | XHtml |
XML | application/xml | - | Xml |
templating
For utilizing the templating engines listed below, add the following:
Templating Library | Template filename suffix | Additional SBT deps | Templates object |
Handlebars | .hbs | "io.fintrospect" %% "fintrospect-handlebars" % "15.1.0" | HandlebarsTemplates |
Mustache | .mustache | "io.fintrospect" %% "fintrospect-mustache" % "15.1.0" | MustacheTemplates |
request parameters & bodies
Fintrospect broadly abstracts the various parts of an HTTP Request Path
, Header
, Query
and Body
behind a common interface to which the main functions are:
- Retrieve a valid value from an incoming request in a type-safe way using
<--()
orfrom()
- Bind a specified value into an outgoing request in a type-safe way using
-->()
orof()
Parameters are created in a uniform way using the objects Path
, Header
, Query
, FormField
and Body
. The general form for definition is as follows, although since Path
and Body
parameters are always required, the middle step is omitted:
<parameter location class>.<required|optional>.<param type>("<name>")
Descriptions can be attached to these definitions for documentation purposes. Note the retrieved type for the optional param:
val anniversary = Header.required.localDate("anniversary", "the date you should not forget! format: yyyy-mm-dd") val myAnniversary: LocalDate = age <-- request val age = Header.optional.int("age", "your age") val age: Option[Integer] = age <-- request val ohDear = Body.xml("soapMessage", "This isn't Simple") val ohDearDear: Elem = anniversary <-- request
There are convenience methods for a standard set of "primitive" types, plus extensions for such as native Scala XML, Forms and JSON.
Additionally, there is another form for parameters which can appear multiple times in a request - simply insert the *()
method in the chain:
val kidsBirthdays = Query.required.*.localDate("birthdays", "the dates you should not forget! format: yyyy-mm-dd") val ourKidsBirthdays: Seq[LocalDate] = kidsBirthdays <-- request
forms
These represent a slightly special case: you first need to retrieve the form from the request, and then the fields from the form.
val name = FormField.required.string("name") val isMarried = FormField.optional.boolean("married") val form = Body.form(name, isMarried) val myForm = form <-- request val myName = name <-- myForm val iAmMarried = isMarried <-- myForm
working with custom parameter types
Custom parameter and body types can be implemented by defining a ParameterSpec
or BodySpec
and passing an instance instead of calling the <param type>
method in the form above. These Spec objects define:
- name and description of the entity type being handled
- for Parameters: the higher level
ParamType
of the on-the-wire representation. For custom formats, this isStringParamType
, althoughObjectParamType
should be used for JSON appearing in an request body - for Bodies: the higher level
ContentType
of the on-the-wire representation. - functions representing the serialization and deserialization from the String format that comes in on the request. Note that we are only concerned with the happy case on-the-wire values. These throw exceptions if unsuccessful - these are caught by the request validation mechanism and turned into a rejected
BadRequest
(400) response which is returned to the caller.
An example for a simple domain case class Birthday:
case class Birthday(value: LocalDate) val birthdayAsAQueryParam = Query.required(ParameterSpec.localDate().map(Birthday(_), (b:Birthday) => b.value), "DOB") val birthdayAsABody = Body(BodySpec.string(ContentTypes.TEXT_PLAIN) .map(s => Birthday(LocalDate.parse(s)), (b:Birthday) => b.value.toString), "DOB")
usage of JSON libraries
Fintrospect comes with binding support for several JSON libraries, each represented by static instances of JsonFormat
on a class named after the library in question. When defining a Body
or Parameter
, if required you can also specify the library format for it to use (else it will default to the bundled Argo JSON library) - and if this is done centrally then you can switch out JSON libraries with only a single line of code change.
val jsonFormat = Argonaut.JsonFormat val exampleObject = jsonFormat.obj("fieldName" -> json.string("hello")) val json = Body.json(Option("my lovely JSON object"), exampleObject, Argonaut) val body: Json = json <-- request
Notice that in the above we specified an example of the JSON message. This is not mandatory, but allows the generation of JSON Schema to be included in the auto-generated API documentation.
Additionally, in the case of some JSON libraries that provide auto marshaling and unmarshalling to case class instances, you can remove the JSON step altogether:
case class Email(address: String) val email = Body(Argonaut.bodySpec[Email](Option("an email address")), Email("jim@example.com")) val retrieved: Email = email <-- request
defining routes
A RouteSpec()
call starts to define the specification of the contract (in terms of the required parameters) and the API follows the immutable builder pattern. Apart from the path-building elements (which terminate the builder), all of the "builder-y" calls here are optional, as are the descriptive strings (used for the auto-documenting features). Here's the simplest possible REST-like example for getting all employees in a notional system:
RouteSpec().at(Method.Get) / "employee"
Notice that the request routing in that example was completely static? If we want an example of a dynamic endpoint, such as listing all users in a particular numerically-identified department, then we can introduce a Path
parameter:
RouteSpec("list all employees in a particular group").at(Method.Get) / "employee" / Path.integer("departmentId")
... and we can do the same for Header and Query parameters; both optional and mandatory parameters are supported, as are parameters that can appear multiple times.:
RouteSpec("list all employees in a particular group") .taking(Header.optional.boolean("listOnlyActive")) .taking(Query.required.*.localDate("datesTakenAsHoliday")) .at(Method.Get) / "employee" / Path.integer("departmentId")
Moving onto HTTP bodies - for example adding an employee via a HTTP Post and declaring the content types that we produce (although this is optional):
RouteSpec("add employee", "Insert a new employee, failing if it already exists") .producing(ContentTypes.TEXT_PLAIN) .body(Body.form(FormField.required.string("name"), FormField.required.localDate("dateOfBirth"))) .at(Method.Post) / "user" / Path.integer("departmentId")
... or via a form submission and declaring possible responses:
RouteSpec("add user", "Insert a new employee, failing if it already exists") .body(Body.form(FormField.required.string("name"), FormField.required.localDate("dateOfBirth"))) .returning(Created -> "Employee was created") .returning(Conflict -> "Employee already exists") .at(Method.Post) / "user" / Path.integer("departmentId")
using routes
As can be seen above, there are several stages to defining a route. Here is the complete construction lifecycle:
- Create a
RouteSpec
with a name and description - Add details of parameters, any body, media-types and possible responses
- Finalize the
RouteSpec
with a call toat()
. This creates anUnboundRoute
. - Continue to add static or dynamic
Path
parameters to the URL structure (creatingUnboundRoute<n>
instances).
Once the final UnboundRoute
has been created (with all of it's Path
parts declared), it represents an HTTP contract, which can then be bound to:
- an HTTP server
Service
if you wish to serve that contract to other systems. - an HTTP client
Service
if you wish to consume that contract from a remote system.
server routes & modules
A RouteSpec needs to be bound to a standard Finagle Service to receive requests, in order to create a ServerRoute
. Since Finagle services are very lightweight, we can create a new instance of the Service for every request, and bind the RouteSpec to a factory method which receives the dynamic Path
parameters and returns the Service. Other parameters can be retrieved directly in a type-safe manner from the HTTP request by using <--()
or from()
method on the parameter declaration.
validation
The presence and format validity of ALL parameters which are attached to a RouteSpec
is verified by Fintrospect before requests make it to this bound Service
, so no validation code is required. The response returned to the client is:
-
Not Found 404
: if there are anyPath
params which are missing or invalid (all are required) -
Bad Request 400
: if there are anyHeader
,Query
, orBody
params are missing (required only) or invalid
simple example
val holidays = Query.required.*.localDate("datesTakenAsHoliday") val includeManagement = Header.optional.boolean("includeManagement") def findEmployeesOnHoliday(departmentId: Integer) = Service.mk[Request, Response] { request => vale holidayDates: Seq[LocalDate] = holidays <-- request val includeManagementFlag: Option[Boolean] = includeManagement <-- request val response = Response(Ok) val baseMsg = s"Everyone from department $departmentId was at work on $holidayDates" response.contentString = baseMsg + (if (includeManagementFlag.getOrElse(false)) "" else ", even the management") Future(response) } val route = ServerRoute[Request, Response] = RouteSpec() .taking(holidays) .taking(includeManagement) .at(Method.Get) / "employee" / Path.integer("departmentId") bindTo findEmployeesOnHoliday
modules
A Module is a collection of ServerRoute
that share a common URL context, which is built up from the Root
object. Add the routes and then convert into a standard Finagle Service object which is then attached in the normal way to an HTTP server.
def listEmployees(): Service[Request, Response] = Service.mk(req => Future(Response())) Http.serve(":8080", RouteModule(Root / "employee") .withRoute(RouteSpec("lists all employees").at(Method.Get) bindTo listEmployees) .toService )
Modules with different root contexts can also be combined with one another and then converted to a Service
:
RouteModule(Root / "a").andThen(RouteModule(Root / "b")).toService
self-describing Module APIs
A big feature of the Fintrospect library is the ability to generate API documentation at runtime. This can be activated by passing in a ModuleRenderer implementation when creating the RouteModule and when this is done, a new endpoint is created at the root of the module context (this location is overridable) which serves this documentation.
Bundled with Fintrospect are:
- Swagger (1.1 and 2.0) JSON, including JSON Schema models
- A simple JSON format
- Sitemap XML format
Other implementations are pluggable by implementing the ModuleRenderer
trait - see the example code for a simple XML implementation.
val service = RouteModule(Root / "employee", Swagger2dot0Json(ApiInfo("an employee discovery API", "3.0"))).toService Http.serve(":8080", new HttpFilter(Cors.UnsafePermissivePolicy).andThen(service))
Note above the usage of the Finagle CorsPolicy
filter, which will allow the services to be called from a Swagger UI - without it, the server will reject any cross-domain requests initiated inside a browser.
security
Module routes can be secured by adding an implementation of the Security
trait - this essentially provides a filter through which all requests will be passed. An ApiKey
implementation is bundled with the library which returns an 401 Unauthorized
HTTP response code when a request does not pass authentication.
RouteModule(Root / "employee") .securedBy(ApiKey(Header.required.string("api_key"), (key: String) => Future(key == "extremelySecretThing")))
client routes
A RouteSpec
can also be bound to a standard Finagle HTTP client Service
and then called as a function, passing in the parameters which are bound to values by using the -->()
or of()
method. The client marshalls the passed parameters into an HTTP request and returns a Twitter Future
containing the response. Any required manipulation of the Request
(such as adding timeouts or caching headers) can be done in the standard way by chaining a Filter
to the client Service
. Note that Content-Type
headers for posted HTTP bodies is already handled by the bound Body
instance.:
val employeeId = Path.integer("employeeId") val name = Query.required.string("name") val client: RouteClient = RouteSpec() .taking(name) .at(Get) / "employee" / employeeId bindToClient Http.newService("localhost:10000") val response: Future[Response] = client(employeeId --> 1, name --> "")
super-cool feature time: reuse of HTTP contracts
Because the RouteSpec
objects can be used to bind to either a Server or a Client, we can be spectacularly smug and use them on both sides of an HTTP boundary to provide a type-safe remote contract, to either:
- Auto-generate fake HTTP server implementations for remote HTTP dependencies. In this case, defining the
RouteSpec
as part of an HTTP client contract and then simply reusing them as the server-side contract of a testing fake - see the/clients
example code - Use a shared library approach to define a contract and the data objects that go across it for reuse in multiple applications, each of which import the shared library. This obviously binary-couples the applications together to a certain degree, so utmost care should be taken, backed up with sufficient CDC-style testing to ensure that the version of the contract deployed is valid on both ends.
building http responses
It's all very well being able to extract pieces of data from HTTP requests, but that's only half the story - we also want to be able to easily build responses. Fintrospect comes bundled with a extensible set of HTTP Response Builders to do this. The very simplest way is by using a ResponseBuilder object directly...
ResponseBuilder.toFuture( ResponseBuilder.HttpResponse(ContentTypes.APPLICATION_JSON).withCode(Status.Ok).withContent("some text").build() )
However, this only handles Strings and Buffer types directly. Also bundled are a set of bindings which provide ResponseBuilders for handling content types like JSON or XML in a set of popular OSS libraries. These live in the io.fintrospect.formats
package. Currently supported formats are in the table below:
Library | Content-Type | Additional SBT deps |
Builder Object
|
Argo | application/json | - | Argo |
Argonaut | application/json | "io.fintrospect" %% "fintrospect-argonaut" % "15.1.0" | Argonaut |
Circe | application/json | "io.fintrospect" %% "fintrospect-circe" % "15.1.0" | Circe |
GSON | application/json | "io.fintrospect" %% "fintrospect-gson" % "15.1.0" | Gson |
HTML | text/html | - | Html |
Jackson | application/json | "io.fintrospect" %% "fintrospect-jackson" % "15.1.0" | Jackson |
Json4S Native | application/json | "io.fintrospect" %% "fintrospect-json4s" % "15.1.0" |
Json4s Json4sDoubleMode |
Json4S Jackson | application/json | "io.fintrospect" %% "fintrospect-json4s" % "15.1.0" |
Json4sJackson Json4sJacksonDoubleMode |
MsgPack | application/msgpack | "io.fintrospect" %% "fintrospect-msgpack" % "15.1.0" | MsgPack |
Plain Text | text/plain | - | PlainText |
Play | application/json | "io.fintrospect" %% "fintrospect-play" % "15.1.0" | Play |
Spray | application/json | "io.fintrospect" %% "fintrospect-spray" % "15.1.0" | Spray |
XHTML | application/xhtml+xml | - | XHtml |
XML | application/xml | - | Xml |
The simplest (least concise) way to invoke an auto-marshaling (ie. typesafe) ResponseBuilder is along the lines of:
Xml.ResponseBuilder.HttpResponse(Status.Ok).withContent(<xml>lashings and lashings of wonderful</xml>).toFuture
... although with tiny bit of implicit magic, we can use custom status methods on the builders and then convert the ResponseBuilder
to a Future[Response]
, you can reduce this to the rather more concise:
import io.fintrospect.formats.Xml.ResponseBuilder._ val responseViaImplicits: Future[Response] = Ok(<xml>lashings and lashings of wonderful</xml>)
These ResponseBuilders also support AsyncStream[T]
, so you can build services which can stream responses in a typesafe way.
taking advantage of auto-marshalling
controlled mode
Some of the JSON libraries (Circe
, Argonaut
, Json4S
, Play
) supported by Fintrospect support auto-marshaling of Scala Case class instances directly to JSON without any custom conversion code needing to be written. This is supported by encode()
and decode()
methods present on the relevant Fintrospect JsonFormat
format instance (e.g. io.fintrospect.formats.Circe.JsonFormat
). Generally, these are very simple to use:
case class EmailAddress(address: String) import io.circe.generic.auto._ import io.fintrospect.formats.Circe.ResponseBuilder._ Status.Ok(Circe.JsonFormat.encode(EmailAddress("dev@fintrospect.io")
The auto-marshaling functionality of these JSON libraries requires implicit parameters in order to make it work. This requirement is echoed in the signatures of the relevant Fintrospect encode()
and decode()
methods, but unless you're going to be providing custom encoder/decoder instances, you can get away with just importing the relevant implicit
params from the parent lib, as in the example above.
full-auto mode
Fintrospect also contains filters which allow you to abstract away the HTTP Request/Response entirely. In this example, the Circe.Filters.AutoInOut
filter converts the Service[Request, Response]
to a Service[EmailAddress, ReversedEmailAddress]
, auto-converting the case class objects in and out of the request/response. The returned status code in the Response
is 200, but this is overridable:
import io.circe.generic.auto._ import io.fintrospect.formats.Circe import io.fintrospect.formats.Circe.Auto._ case class ReversedEmailAddress(sserdda: String) val domainSvc = Service.mk[EmailAddress, ReversedEmailAddress] { email => Future(ReversedEmailAddress(email.address.reverse)) } val httpSvc: Service[Request, Response] = Circe.Auto.InOut(domainSvc)
cross-field validation
Further to the process of retrieving them from the request, there exists the problem of validating that the passed parameters are actually logically valid when passed together. For example, a date range is only valid when the start date is before the end date.
For this purpose, you can use an Extractor
- a trait which provides a single method <--?()
to return one of 2 states: Extracted(Option(value))
and ExtractionFailed(parameter)
for missing or invalid values. These constructs can be used inside a for comprehension to provide cross-field validation, and eventual creation of a target object. Below is a service that implements this logic - note the use of a predicate and a failure reason which provides the logic for the check:
case class DateRange(startDate: LocalDate, endDate: Option[LocalDate]) val range: Extractor[Request, DateRange] = Extractor.mk { (request: Request) => for { startDate <- Query.required.localDate("start") <--? request endDate <- Query.optional.localDate("end") <--?(request, "end date invalid", _.isAfter(startDate.get)) } yield DateRange(startDate.get, endDate) } val route = RouteSpec().at(Get) bindTo Service.mk { req: Request => range <--? req match { case Extracted(Some(dates)) => Ok(dates.startDate + " ->" + dates.endDate) case ExtractionFailed(sp) => BadRequest(sp.mkString(", ")) } }
(Although we are calling get()
on the Options in the above yield
- normally a VBT (Very Bad Thing), in this case we are safe to do so since any missing or invalid mandatory parameters short-circuit the comprehension.)
Extractable
is modular, so instances can be embedded inside each other for comprehensions to build object graphs from the incoming request.
The above example can be further simplified by use of the built-in RequestFilters.ExtractableRequest
filter to transform the input:
RequestFilters.ExtractableRequest(range).andThen(Service.mk[DateRange, Response] { dateRange => ... })
templating
Templates are applied by using a custom RenderView
filter to convert View
instances into standard Http Responses. Simply implement the View
trait and then put a matching template file onto the classpath, and chain the output of the model-creating Service
into the filter. You can do this for entire modules by making the RouteModule
itself generified on View
by using the templating Filter
as a Module-level filter:
case class ViewMessage(value: String) extends View val showMessage = Service.mk[Request, View] { _ => Future(ViewMessage("some value to be displayed")) } val renderer = if(devMode) MustacheTemplates.HotReload("src/main/resources") else MustacheTemplates.CachingClasspath(".") val webModule = RouteModule(Root / "web", new SiteMapModuleRenderer(new URL("https://root.com")), new RenderView(Html.ResponseBuilder, renderer)) .withRoute(RouteSpec().at(Get) / "message" bindTo showMessage)
redirects
After Form posts, it might be desirable to return an HTTP redirect instead of a View in the case of success. For this purpose, use an instance of the View.Redirect
class. The location and the status code (default 303) are configurable:
val redirect = Service.mk[Request, View] { _ => Future(View.Redirect("https://my.server/myRoute")) }
Available implementations of the TemplateRenderer
are (see the relevant implementation of Templates
):
- cached from the classpath
- cached from the filesystem
- hot-reloading from the filesystem
Similarly to how the ResponseBuilder
codecs work, no 3rd-party dependencies are bundled with Fintrospect - simply import the extra SBT dependencies as required:
Templating Library | Template filename suffix | Additional SBT deps | Templates object |
Handlebars | .hbs | "io.fintrospect" %% "fintrospect-handlebars" % "15.1.0" | HandlebarsTemplates |
Mustache | .mustache | "io.fintrospect" %% "fintrospect-mustache" % "15.1.0" | MustacheTemplates |
static content
Static files can easily be served from the either the Classpath or a Directory by using an instance of StaticModule
with an appropriately injected ResourceLoader
:
val cpModule = StaticModule(Root / "public", ResourceLoader.Classpath("package/path")) val dirModule = StaticModule(Root / "public", ResourceLoader.Directory("file/dir/path"))
Note that due to security concerns, ResourceLoader.Classpath
should be configured to use a package which does NOT overlap with class assets.
testing
routes
Provided trait Testing introspect Route
can be used to unit test your routes, as in the simple example below:
object EchoRoute { val route = RouteSpec().at(Method.Get) / Path.string("message") bindTo( (message: String) => Service.mk { req: Request => Future(PlainText.ResponseBuilder.OK(message)) }) } class EchoRouteTest extends FunSpec with Matchers with TestingFintrospectRoute { override val route = EchoRoute.route describe("Echo") { it("bounces back message") { responseFor(Request("hello")).contentString shouldBe "hello" } } }
test http server
The TestHttpServer
is convenient for attaching routes during development, or can be used as a scaffold to provide automatically generated fake servers for downstream dependencies - simply complete the stub implementation of the server-side and fire it up. This works especially well if you are utilizing custom serialization formats (such as one of the auto-marshaling JSON libraries), as there is absolutely no marshaling code required to send back objects over the wire from your stub.
val route = RouteSpec().at(Get) / "myRoute" bindTo(() => Service.mk {r => Future(Response(Status.Ok))}) new TestHttpServer(9999, route).start()
examples
The Fintrospect example code hosted here:
-
/circe
- using the Circe library for auto-marshaling of case classes to JSON -
/clients
- an example of using HTTP routes as a client and creating really easy fake remote servers for testing -
/custom formats
- creating a custom message format with it's own Response Builder: HipsterXML -
/custom parameters
- creating a custom serialization format for request parameters and bodies -
/extended
- the library application. Example used on the Github project README -
/formvalidation
- shows form validation, with feedback given to the user -
/oauth
- shows the usage of plugging in an OAuth2 library to change the input request type -
/msgpack
- using the MsgPack binary message format, including auto-marshaling of case classes in/out of responses -
/streaming
- shows streaming of responses in a typesafe way -
/strict contenttypes
- shows how to serve multiple content types from a single route -
/templating
- shows the usage of plugging in Mustache templating library to change the output response type -
/validation
- shows cross-field validation of inputs taken from the request
quickstart repo
If you want to dive in head-first, there is example source project of a Fintrospect application with most of the features hosted in this Github Repo
cookbook
This contains a set of cookbook recipes that may be useful to see how particular features are activated.
Eventually, these will be documented with explanations on the website, but for the moment, they hopefully are still useful