Wiki

Clone wiki

shelf_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
This binds the path variable called name as defined in the path /person/{name} to the named argument called firstname of the Person.build constructor and calling the _handlePerson function when matched.

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
Would look for a constructor Person.foo({String firstname}).

Query Params

To bind to a query parameter

final routeHandler = route.router()
      ..get('/person{?name}', bind.bind(Person, {'name': #firstname}, _handlePerson))
      .handler
The only change was that path passed to get is now '/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:

  1. fork the repo and implement your changes with good unit test coverage of your changes
  2. create a pull request and include enough detail in the descriptio

Updated