AF-BedSheetUser Guide

Overview

BedSheet is a Fantom framework for delivering web applications. Built on top of afIoc and Wisp, BedSheet aims to be: Powerful, Flexible and Simple.

BedSheet is inspired by Java's Tapestry5, Ruby's Sinatra and Fantom's Draft.

Quick Start

  1. Create an AppModule, this is where all your service configuration will go.
  2. Contribute to Routes and other services
  3. Create some page / request handlers
  4. Start the app...
using afBedSheet
using afIoc

class AppModule {
  @Contribute
  static Void contributeRoutes(OrderedConfig conf) {
    conf.add(Route(`/hello/**`, HelloPage#hello))
  }
}

class HelloPage {
  TextResponse hello(Str name, Int iq := 666) {
    return TextResponse.fromPlain("Hello! I'm $name and I have an IQ of $iq!")
  }
}

From the command line:

$ fan afBedSheet <mypod>::AppModule 8080
...
BedSheet v1.0 started up in 323ms

$ curl http://localhost:8080/hello/Traci/69
Hello! I'm Traci and I have an IQ of 69!

$ curl http://localhost:8080/hello/Luci
Hello! I'm Luci and I have an IQ of 666!

Wow! That's awesome! But what just happened!?

Every application has an AppModule that is used to contribute to afIoc services. In our AppModule we told the Routes service to route all request uris, that start with /hello, to our HelloPage class. Route converts any extra uri path segments into method arguments, or in our case, a Str name and an optional Int iq.

Request handlers are typically what we, the app developers, write. They perform logic processing and render responses. Our HelloPage handler simply renders a TextResponse.

A default ResponseProcessor then sends our TextResponse to the client.

Starting BedSheet

BedSheet may be started from the command line using:

$ fan afBedSheet <fully-qualified-app-module-name> <port-number>

Every BedSheet web application has an AppModule class that defines and configures your afIoc services. It is an afIoc concept that allows you centralise your app configuration in one place.

TIP: Should your AppModule grow too big, break logical chunks out into their own classes using the afIoc @SubModule facet.

You may find it more convenient to create your own BedSheet Main wrapper. (In fact, if using Heroku you have to.)

using util

class Main : AbstractMain {

  @Arg { help="The HTTP port to run the app on" }
  private Int port

  override Int run() {
    return afBedSheet::Main().main("<fully-qualified-app-module-name> $port".split)
  }
}

<fully-qualified-app-module-name> may be replaced with <pod-name> as long as your pod's build.fan defines the following meta:

meta  = [ ...
          ...
          "afIoc.module"  : "<fully-qualified-app-module-name>"
        ]

Note that AppModule is named so out of convention but may be called anything you like.

Request Routing

When BedSheet receives a web request, it is matched to a handler for processing. This is configured through the Routes service.

@Contribute { serviceType=Routes# }
static Void contributeRoutes(OrderedConfig conf) {

  conf.add(Route(`/index`, IndexPage#service))
}

BedSheet is bundled with the following Route objs:

  • Route: Matches against the request URI, converting deeper path segments into method arguments.

If the handler returns Void or null then processing is passed onto the next matching route. This makes it possible to create filter like handlers that pre-process a blanket wide set of uris. To do this, contribute a placeholder that all routes (ordered or unordered) naturally come after. Example:

@Contribute { serviceType=Routes# }
static Void contributeRoutes(OrderedConfig conf) {

  conf.addPlaceholder("routes")

  // routes
  conf.add(Route(`/index`, IndexPage#service))
  ...

  // filters - addOrdered to ensure they come before routes
  conf.addOrdered("AuthFilter", Route(`/admin/***`, AuthFilter#service), ["before: routes"])
}

BedSheet bundles with the following filters:

Routes are not limited to matching against uris, you can create your own routes that match against anything, such as http headers or the time of day! Just create a RouteMatcher and contribute it to RouteMatchers.

Routing lesson over.

(...you Aussies may stop giggling now.)

Request Handling

Request handlers is where logic is processed and responses are rendered. Handlers generally shouldn't pipe anything to Response.out. Instead they should return a response object. Example, the above HelloPage handler returns a TextResponse.

BedSheet bundles with the following request handlers:

  • FileHandler: Maps request URIs to files on file system.
  • PodHandler : Maps request URIs to pod file resources.

By default, BedSheet handles the following responses:

  • Void / null / false : Processing should fall through to the next Route match.
  • true : A response has been sent to the client and no further processing should performed on the http request.
  • TextResponse : The text (be it plain, json or xml), with the given sys::MimeType is sent to the client.
  • sys::File : The file is sent to the client.
  • HttpStatus : Sets the http response status and renders a mini html page with the status msg. (See Error Processing.)
  • Redirect : Sends a 3xx redirect response to the client.

Response Processing

ResponseProcessors process request handler responses (sys::File, TextResponse, etc...) and send data to the client. ResponseProcessors should return true if no further processing should performed on the http request. Or they may return another response object for further processing, such as a TextResponse.

BedSheet bundles with the following response processors:

  • TextResponseProcessor : Sends text to the client.
  • FileResponseProcessor : Sends files to the client.
  • RedirectResponseProcessor : Sends a 3xx redirect responses to the client.
  • HttpStatusProcessors : Routes HttpStatus to contributed HttpStatusProcessors.

Error Processing

When BedSheet catches an Err it scans through its list of contributed ErrProcessors to find the closest match. ErrProcessor's takes an Err and returns a response for further processing (example, TextResponse). Or it may return true if the error has been completely handled and no further processing is required.

BedSheet bundles with Err processors for the following Errs:

  • HttpStatusErr : Returns the wrapped HttpStatus.
  • Err : A general catch all processor that wraps (and returns) the Err in a HttpStatus 500.

HttpStatus responses are handled by HttpStatusProcessors which selects a contributed processor dependent on the http status code. The only specific HttpStatusProcessor that BedSheet bundles with is for a 500 Internal Server Error.

Example code to override the 500 Internal Server Error handler and set a 404 Not Found page:

@Contribute { serviceType=HttpStatusProcessors# }
static Void contributeHttpStatusProcessors(MappedConfig conf) {
  conf[404] = conf.autobuild(Page404#)

  // need to override the 500 page because BedSheet supplies one by default
  conf.setOverride(500, "MyErrPage", conf.autobuild(Page500#))
}

There is a default catch all HttpStatusProcessor which sets the http status and sends a mini html page to the client. The page is set via the Config mechanism so to set your own, contribute to ApplicationDefaults:

@Contribute { serviceType=ApplicationDefaults# }
static Void contributeApplicationDefaults(MappedConfig conf) {
  ...
  conf[ConfigIds.httpStatusDefaultPage] = MyStatusPage()
  ...
}

Config

BedSheet extends afIoc to give injectable @Config values. @Config values are essesntially a map of Str to immutable / constant values that may be set and overriden at application start up.

BedSheet sets the initial config values by contributing to the FactoryDefaults service. An application may then override these values by contibuting to the ApplicationDefaults service.

@Contribute { serviceType=ApplicationDefaults# }
static Void contributeApplicationDefaults(MappedConfig conf) {
  ...
  conf["afBedSheet.errPrinter.noOfStackFrames"] = 100
  ...
}

All BedSheet config keys are listed in ConfigIds meaning the above can be more safely rewriten as:

conf[ConfigIds.noOfStackFrames] = 100

To inject config values in your services, use the @Config facet with conjunction with afIoc's @Inject:

	@Inject	@Config { id="afBedSheet.errPrinter.noOfStackFrames" }
	Int noOfStackFrames

The config mechanism is not just for BedSheet, you can use it too when creating add-on libraries! Contributing to FactoryDefaults gives users of your library an easy way to override your values.

Development Proxy

You need never (manually) restart your app again!

Use the -proxy option to create a Development Proxy to auto re-start your app when any of your pods are updated:

$ fan afBedSheet -proxy <mypod> <port>

The proxy sits on <port>, starts your real app on <port>+1 and forwards all requests to it.

Client <--> Proxy (port) <--> Web App (port+1)

A problem other (Fantom) web development proxies suffer from, is that when the proxy dies your real web app is left hanging around; requiring you to manually kill it.

Client <-->   ????????   <--> Web App (port+1)

BedSheet goes a step further and, should it be started in proxy mode, it pings the proxy every second to stay alive. Should the proxy not respond, the web app kills itself.

See ConfigIds.proxyPingInterval for more details.

Gzip

By default, BedSheet compresses HTTP responses with gzip where it can.(1) But it doesn't do this willy nilly, oh no! There are many hurdles to overcome...

Disable All

Gzip, although enabled by default, can be disabled for the entire web app by setting the following config property:

config[ConfigIds.gzipDisabled] = true

Disable per Response

Gzip can be disabled on a per request / response basis by calling the following:

httpResponse.disableGzip()

Gzip'able Mime Types

Not everything should be gzipped. For example, text files gzip very well and yield high compression rates. JPG images on the other hand, because they're already compressed, don't gzip well and can end up bigger than the original! For this reason you must contribute to the GzipCompressible service to enable gzip for specified Mime Types:

config["text/funky"] = true

(Note: The GzipCompressible contrib type is actually MimeType - afIoc kindly coerces the Str to MimeType for us.)

By default BedSheet will compress plain text, css, html, javascript, xml, json and other text responses.

Gzip only when asked

Guaranteed that someone, somewhere is still using Internet Explorer 3.0 and they can't handle gzipped content. As such, and as per RFC 2616 HTTP1.1 Sec14.3, we only gzip the response if the client actually asked for it!

Minimum content threshold

Gzip is great when compressing large files, but if you've only got a few bytes to squash... the compressed version is going to be bigger, which kinda defeats the point of using gzip in the first place! For that reason the response data must reach a minimum size / threshold before it gets gzipped.

See GzipOutStream and ConfigIds.gzipThreshold for more details.

Phew! Made it!

If (and only if!) your request passed all the tests above, will it then be lovingly gzipped and sent to the client.

Buffered Response

By default, BedSheet attempts to set the Content-Length http response header.(2) It does this by buffering HttpResponse.out. When the stream is closed, it writes the Content-Length and pipes the buffer to the real http response.

Response buffering can be disabled on a per http response basis.

A threshold can be set, whereby if the buffer exeeds that value, all content is streamed directly to the client.

See BufferedOutStream and ConfigIds.responseBufferThreshold for more details.

More!

All request handlers and processors are built by afIoc so feel free to @Inject DAOs and other services. BedSheet itself is built with afIoc so look at the BedSheet source for afIoc examples.

TIP: const handler classes are cached by BedSheet and reused on every request.

Push To Live!

In a hurry to go live?

Check out Heroku and see how ridiculously easy it is to deploy your app to a live server with the heroku-fantom-buildpack.

Release Notes

Breaking Changes

v0.0.8

  • Request has been renamed to HttpRequest
  • Response has been renamed to HttpResponse

v0.0.6

  • Route URIs now need the suffix ** to match all remaiing method arguments
  • TextResult has been renamed to TextRepsonse
  • MoustacheSource has been renamed to MoustacheTemplates
  • All Services ending in Source have renamed to their plural. Example: ValueEncoderSource -> ValueEncoders