Wiki
Clone wikishelf_bind / dump
Shelf RequestHandlers have one of the following signatures
Response handler(Request request)
or
Future<Response> handler(Request request)
In a nutshell, what Shelf Bind allows you to do is to use functions with a signature more in tune with your domain and adapt it to look like a proper Shelf RequestHandler.
For example your function may look like
Future<Account> createAccount(String accountId, Person person)
where Person looks like
class Person { final String name; Person.build({this.name}); }
To create a Shelf RequestHandler for createAccount you can simply do
var handler = bind(createAccount);
This is just a normal shelf handler so you can do the usual stuff.
For example you can set up a Shelf Route router
var router = route.router() ..put('/account/{accountId}{?name}', handler);
Which you can then serve as follows
io.serve(router.handler, 'localhost', 8080).then((server) { print('Serving at http://${server.address.host}:${server.port}'); });
Now you can hit this route with
curl -X PUT http://localhost:8080/account/1234?name=fred
This will bind 1234 to the accountId parameter and fred to the name property of the Person class. Additionally it will call toJson on the returned account and set that as the body of the Response.
In effect Shelf Bind created a handler that acts as the equivalent of
Future<Response> handler(Request request) { var accountId = getPathParameter(request, 'accountId'); var name = getPathParameter(request, 'name'); var person = new Person(name); Future<Account> accountFuture = createAccount(accountId, person); return accountFuture.then((account) { return new Response.ok(account.toJson()); }); }
The above code is included in the Examples and produces and output like
{"accountId":"1234","person":{"name":"fred"}}
Binding Inference
The previous example worked because Shelf Bind inferred the bindings. The inferrence works as follows:
For each parameter in the handler function:
- if the parameter is of type Request then the shelf Request will be passed unchanged
- if the parameter is a simple type then it will be passed the value of a Shelf Route path parameter of the same name. Note: currently no type conversions are performed. This will be added in the future. Only strings supported at the moment.
- otherwise it will be treated as a complex type binding which will either:
- bind request parameters to named parameters (with the same name) of a constructor called build if present OR
- bind to property setters of the same name
In the example above the route (/account/{accountId}{?name}) has two parameters:
- accountId
- name
The createAccount function has two parameters:
- accountId
- person
From this Shelf Bind inferred the following:
- bind createAccount's accountId parameter to the path parameter accountId
- bind createAccount's person parameter to a new instance of Person and bind the path parameter name to the constructor parameter of Person.build called name
Overriding Default Bindings
Shelf Bind allows you to override all the default (inferred) bindings to give you full control over the process.
Binding to Shelf Route Path Variables
Binding via Constructor
Binding a simple path variable
final routeHandler = route.router() ..get('/person/{name}', bind.bind(Person, {'name': #firstname}, _handlePerson)) .handler
The constructor looks like
Person.build({String firstname}) : this(firstname);
The handlers for Shelf Bind differ from the standard Shelf handlers as folows:
- They take the bound object as the first argument
- They optionally take the request as a second argument
- They can return either a Response / Future<Response> as normal or else any object that has a toJson method. For example Person / Future<Person>
The handler in this example looks like
Person _handlePerson(Person person) { return person; }
Note build is the default name for the constructor. It can be overriden by passing the constructor parameter to bind
final routeHandler = route.router() ..get('/person/{name}', bind.bind(Person, {'name': #firstname}, _handlePerson, constructor: #foo)) .handler
Query Params
To bind to a query parameter
final routeHandler = route.router() ..get('/person{?name}', bind.bind(Person, {'name': #firstname}, _handlePerson)) .handler
'/person{?name}'
Binding via Properties
If you prefer, you can bind via properties (fields or setters) instead of via the constructor.
To do that you specify the bindMode named argument as BindMode.PROPERTY (default is BindMode.CONSTRUCTOR) like
final routeHandler = route.router() ..get('/person/{name}', bind.bind(Person, {'name': #firstname}, _handlePerson, bindMode: BindMode.PROPERTY)) .handler
plus you must have a default (unnamed) constructor that takes no mandatory arguments. For example a constructor like
class Person { String name; Person(); }
Binding to a Json Body
Binding to a Json body is very similar. Instead you call the bindJsonBody function.
final routeHandler = route.router() ..post('/person', bind.bindJsonBody(Person, _handlePerson))) .handler
As this is binding to a Json map it does not take the binding map as above.
Details: Handler Functions
Handler functions in Shelf Bind can take the following forms:
1/ A handler with an additional argument
Response handler(someType boundObject, Request request); OR Future<Response> handler(someType boundObject, Request request);
2/ A handler without the Request argument
Response handler(someType boundObject); OR Future<Response> handler(someType boundObject);
3/ A handler that returns some other type (that has a toJson method)
Note the return type can be any type, not necessarily the same as the boundObject
someType handler(someType boundObject, Request request); OR Future<someType> handler(someType boundObject, Request request); OR someType handler(someType boundObject); OR Future<someType> handler(someType boundObject);
Example
Full source at example/binding_example.dart
The domain objects
class Person { final String name; Person(this.name); Person.build({String name}) : this(name); Person.fromJson(Map json) : this(json['name']); Map toJson() => { 'name': name }; String toString() => 'Person[name: $name]'; }
A simple handler that just echos back the bound person
Person _handlePerson(Person person) { print(person); return person; }
The routes and bindings
final pathBindHandler = bind.bind(Person, {'name': #name}, _handlePerson); var router = (route.router() ..get('/person/{name}', pathBindHandler) ..get('/person{?name}', pathBindHandler) ..post('/person', bind.bindJsonBody(Person, _handlePerson))) .handler; var handler = const shelf.Stack() .addMiddleware(shelf.logRequests()) .addHandler(router);
Serve it up
io.serve(handler, 'localhost', 8080).then((server) { print('Serving at http://${server.address.host}:${server.port}'); }); }
Try it out
First route
curl http://localhost:8080/person/fred
The output should look like
GET http://localhost:8080/person/fred => {name: fred}
Second route
curl http://localhost:8080/person?name=fred
Third route (json POST)
curl -d '{"name": "fred"}' http://localhost:8080/person
TODO
See open issues.
Contributing
Contributions are welcome. Please:
- fork the repo and implement your changes with good unit test coverage of your changes
- create a pull request and include enough detail in the descriptio
Updated