Pod doc follows - always see Status302 for the latest and most up to date documentation.
Quick Start
Create an AppModule
Contribute to Routes and other services
Create some page / request handlers
Start your web app...
using afBedSheet
using afIoc
class AppModule {
@Contribute
static Void contributeRoutes(OrderedConfig conf) {
conf.add(Route(`/hello/**`, HelloPage#hello))
}
}
class HelloPage {
Text hello(Str name, Int iq := 666) {
return Text.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 configures afIoc services. Here we told the Routes service to send all request URIs that start with /hello to the HelloPage#hello method. Route converts extra URI path segments into method arguments, or in our case, to 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 plain Text response.
BedSheet may be started from the command line with:
$ fan afBedSheet <fully-qualified-app-module-name> <port-number>
For example:
$ fan afBedSheet myWebApp::AppModule 80
Every BedSheet application has an AppModule class that defines and configures your afIoc services. It is an afIoc concept that allows you centralise your application 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 convenient to create your own BedSheet Main wrapper.
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.
HTTP Pipeline
When a http request is received it is passed through a pipeline of filters. The terminating filter performs the standard BedSheet routing (see Request Routing). Filters can be used to address cross cutting concerns such as authorisation.
Standard BedSheet filters are:
HttpCleanupFilter: Ensures the HttpOutStream is closed and cleans up all request / thread scoped data.
HttpErrFilter: Catches and processes Errs, see Error Processing.
IeAjaxCacheBustingFilter: Adds cache headers to the response to prevent IE from caching ajax responses.
It is important that the first two filters are always first in the pipeline. As such, when contributing your own filters you are encouraged to add the ordering constraint of "after: BedSheetFilters", example:
BedSheet is bundled with the following Route objs:
Route: Matches against the request URI, converting deeper path segments into method arguments.
Routes are not limited to matching against uris, you can create your own routes that match against anything, such as http headers or time of day! Just create a RouteMatcher and contribute it to RouteMatchers.
Using Draft Routes
If you prefer the draft style of routing, that's no problem, you can use draft Routes in BedSheet!
Just add afBedSheetDraft and draft as dependencies in your build.fan (no config required!). Now you can contribute draft routes to BedSheet:
using draft::Route as DraftRoute
@Contribute { serviceType=Routes# }
static Void contributeRoutes(OrderedConfig conf) {
conf.add(DraftRoute("/", "GET", PageHandler#index))
conf.add(DraftRoute("/echo/{name}/{age}", "GET", PageHandler#print))
}
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 http response out stream, instead they should return a response object. Example, the above HelloPage handler returns a Text obj.
You usually write the handlers yourself, but BedSheet also bundles with the following request handlers:
FileHandler: Maps request URIs to files on file system.
PodHandler : Maps request URIs to pod file resources.
CorsHandler : A pass through filter for handling Cross Origin Resource Sharing requests.
Request handlers should always return an object to the processed and sent to the client.
Response Processing
ResponseProcessors process the return values from request handlers (sys::File, Text, 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 Text obj.
By default, BedSheet handles the following response objects:
Void / null / false : Processing should fall through to the next Route match.
true : No further processing is required.
Text : The text (be it plain, json, xml, etc...) is sent to the client with the given sys::MimeType.
RedirectResponseProcessor : Sends a 3xx redirect responses to the client.
InStreamResponseProcessor : Pipes an InStream to the client and closes the stream.
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, Text). 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:
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. If none are found, a default catch all HttpStatusProcessor sets the http status code and sends a mini html page to the client.
By default, it is this page you see when you receive a 404 Not Found error.
404 Not Found
To set your own 404 Not Found page, contribute to the HttpStatusProcessors service:
BedSheet bundles with a error handler for 500 Internal Server Error status codes. This handler renders a very verbose error page with shed loads of debugging information. Great for development! Not so great for production.
To set your own 500 Internal Server Error page, override the BedSheet default:
@Contribute { serviceType=HttpStatusProcessors# }
static Void contributeHttpStatusProcessors(MappedConfig conf) {
// override because BedSheet supplies one by default
conf.setOverride(500, "MyErrPage", conf.autobuild(Page500#))
}
Inject 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. (Consider config values to be immutable once the app has started).
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.
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
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.
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 sys::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!
Min 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.
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.
BedSheet ships with a means of testing your web app without starting the wisp server. This forgoes the overhead of starting a real web server, opening real ports and making real http requests. Essentially you're testing the entire app, just cutting out the middle man.
Use BedServer to start an instance of BedSheet, and then the re-usable BedClient to serve (fake) http requests. The benifits (besides the obvious performance ones) include:
override real services with test servies / configuration
inject your afIoc services direct into your test class
using afIoc
using afBedSheet
class TestWebApp : Test {
BedServer? server
@Inject UserDAO? userDAO // inject your services
override Void setup() {
server = BedServer(AppModule#)
server.addModule(TestOverrides#) // override services and config with test values
server.startup
server.injectIntoFields(this) // inject your services
}
override Void teardown() {
server.shutdown
}
Void testIndexPage() {
// given
client := server.makeClient
// when
res := client.get(`/index`)
// then
verify(res.asStr.contains("Welcome!"))
}
}
Tips
All request handlers and processors are built by afIoc so feel free to @Inject DAOs and other services.
Just check in your code (with Git) and Heroku will build your web app from source and deploy it to a live environment! See heroku-fantom-buildpack for more details.
Have fun!
:)
andySun 4 Aug 2013
Cool stuff Steve!
LightDyeWed 7 Aug 2013
Hi Steve
I would have expected a null argument instead of an empty string in the route below
In this example I can't get it to use the default value of the name parameter because it gets an empty string instead of null:
class HelloPage {
Text hello(Str name := "foo") {
return Text.fromPlain("Hello! I'm $name")
}
}
I know I can check if the parameter name equals "" then use a default value but that isn't ideal. I don't have a real need for this at the moment but was just playing around with it. You may have had a real reason to pass an empty string, but I don't get it yet.
SlimerDudeWed 7 Aug 2013
Hi LightDye,
I think you're right.
When matching routes to URIs, the empty strings are used to distinguish between an empty match and a no-match. But then, when applying the argument to a method, the empty strings should probably be converted to nulls. (Empty strings don't seem useful nor semantically correct in this case.)
I guess the matching should go further and inspect the method parameters - for if the method param isn't nullable and doesn't have have a default value, then I think a no-match is preferred over a NullErr.
SlimerDude Sat 3 Aug 2013
BedSheet 1.0.4 Released
BedSheet is a Fantom framework for delivering web applications.
Built on top of afIoc and Wisp, BedSheet's main concern is proving a rich mechanism for the routing and delivery of content over HTTP.
BedSheet is inspired by Java's Tapestry5, Ruby's Sinatra and Fantom's Draft.
fanr install -r http://repo.status302.com/fanr/ afBedSheet
Pod doc follows - always see Status302 for the latest and most up to date documentation.
Quick Start
AppModule
Routes
and other servicesFrom the command line:
Wow! That's awesome! But what just happened!?
Every application has an
AppModule
that configures afIoc services. Here we told the Routes service to send all request URIs that start with/hello
to theHelloPage#hello
method. Route converts extra URI path segments into method arguments, or in our case, toStr name
and an optionalInt iq
.Request handlers are typically what we, the app developers, write. They perform logic processing and render responses. Our
HelloPage
handler simply renders a plain Text response.A default ResponseProcessor then sends the Text response to the client.
Starting BedSheet
BedSheet may be started from the command line with:
For example:
Every BedSheet application has an
AppModule
class that defines and configures your afIoc services. It is an afIoc concept that allows you centralise your application configuration in one place.You may find it convenient to create your own BedSheet Main wrapper.
<fully-qualified-app-module-name>
may be replaced with<pod-name>
as long as your pod'sbuild.fan
defines the following meta:Note that
AppModule
is named so out of convention but may be called anything you like.HTTP Pipeline
When a http request is received it is passed through a pipeline of filters. The terminating filter performs the standard BedSheet routing (see Request Routing). Filters can be used to address cross cutting concerns such as authorisation.
Standard BedSheet filters are:
HttpCleanupFilter
: Ensures theHttpOutStream
is closed and cleans up all request / thread scoped data.HttpErrFilter
: Catches and processes Errs, see Error Processing.It is important that the first two filters are always first in the pipeline. As such, when contributing your own filters you are encouraged to add the ordering constraint of
"after: BedSheetFilters"
, example:Request Routing
When BedSheet receives a web request, it is matched to a handler for processing. This is configured through the Routes service.
BedSheet is bundled with the following Route objs:
Routes are not limited to matching against uris, you can create your own routes that match against anything, such as http headers or time of day! Just create a RouteMatcher and contribute it to RouteMatchers.
Using Draft Routes
If you prefer the draft style of routing, that's no problem, you can use draft Routes in BedSheet!
Just add afBedSheetDraft and draft as dependencies in your
build.fan
(no config required!). Now you can contribute draft routes to BedSheet: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 http response out stream, instead they should return a response object. Example, the above
HelloPage
handler returns a Text obj.You usually write the handlers yourself, but BedSheet also bundles with the following request handlers:
Request handlers should always return an object to the processed and sent to the client.
Response Processing
ResponseProcessors process the return values from request handlers (
sys::File
, Text, etc...) and send data to the client.ResponseProcessors
should returntrue
if no further processing should performed on the http request. Or they may return another response object for further processing, such as a Text obj.By default, BedSheet handles the following response objects:
Void
/null
/false
: Processing should fall through to the next Route match.true
: No further processing is required.sys::MimeType
.sys::File
: The file is sent to the client.sys::InStream
: TheInStream
is piped to the client. TheInStream
is guarenteed to be closed.For handling the above, BedSheet bundles with the following response processors:
TextResponseProcessor
: Sends text to the client.FileResponseProcessor
: Sends files to the client.HttpStatusProcessors
.RedirectResponseProcessor
: Sends a 3xx redirect responses to the client.InStreamResponseProcessor
: Pipes anInStream
to the client and closes the stream.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, Text). 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. If none are found, a default catch allHttpStatusProcessor
sets the http status code and sends a mini html page to the client.By default, it is this page you see when you receive a
404 Not Found
error.404 Not Found
To set your own
404 Not Found
page, contribute to theHttpStatusProcessors
service:500 Server Error
BedSheet bundles with a error handler for
500 Internal Server Error
status codes. This handler renders a very verbose error page with shed loads of debugging information. Great for development! Not so great for production.To set your own
500 Internal Server Error
page, override the BedSheet default:Inject 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. (Consider config values to be immutable once the app has started).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.
All BedSheet config keys are listed in ConfigIds meaning the above can be more safely rewriten as:
To inject config values in your services, use the
@Config
facet with conjunction with afIoc's@Inject
: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
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:The proxy sits on
<port>
, starts your real app on<port>+1
and forwards all requests to it.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.
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:
Disable per Response
Gzip can be disabled on a per request / response basis by calling the following:
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
:(Note: The GzipCompressible contrib type is actually
sys::MimeType
- afIoc kindly coerces theStr
toMimeType
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!
Min 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 bufferingHttpResponse.out
. When the stream is closed, it writes theContent-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.Testing
BedSheet ships with a means of testing your web app without starting the wisp server. This forgoes the overhead of starting a real web server, opening real ports and making real http requests. Essentially you're testing the entire app, just cutting out the middle man.
Use BedServer to start an instance of BedSheet, and then the re-usable BedClient to serve (fake) http requests. The benifits (besides the obvious performance ones) include:
web::WebSession
See below for a test example:
Tips
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.
Even if your request handlers aren't services, if they're
const
classes, they're cached by BedSheet and reused on every request.Go Live with Heroku
In a hurry to go live? Use Heroku!
Heroku and the heroku-fantom-buildpack makes it ridiculously to deploy your web app to a live server.
Just check in your code (with Git) and Heroku will build your web app from source and deploy it to a live environment! See heroku-fantom-buildpack for more details.
Have fun!
:)
andy Sun 4 Aug 2013
Cool stuff Steve!
LightDye Wed 7 Aug 2013
Hi Steve
I would have expected a null argument instead of an empty string in the route below
In this example I can't get it to use the default value of the name parameter because it gets an empty string instead of null:
I know I can check if the parameter name equals "" then use a default value but that isn't ideal. I don't have a real need for this at the moment but was just playing around with it. You may have had a real reason to pass an empty string, but I don't get it yet.
SlimerDude Wed 7 Aug 2013
Hi LightDye,
I think you're right.
When matching routes to URIs, the empty strings are used to distinguish between an empty match and a no-match. But then, when applying the argument to a method, the empty strings should probably be converted to nulls. (Empty strings don't seem useful nor semantically correct in this case.)
I guess the matching should go further and inspect the method parameters - for if the method param isn't nullable and doesn't have have a default value, then I think a no-match is preferred over a NullErr.