Wiki

Clone wiki

shelf_bind / Binding Inference

Shelf Bind infers the bindings between the request path parameters and the parameters of your functions.

This page explains in detail how the inferencing works and how you can override it.

Sample Code

The following code is used as examples in the explanations

Person Class

class Person {
  final String name;

  Person.build({this.name});
}
Account Class
class Account {
  String accountId;

  toJson() => { 'accountId': accountId };
}

Our Handler Function

Future<Account> createAccount(String accountId, Person person)

A Shelf Handler for our Handler Function

var handler = bind(createAccount);
A Shelf Route Router for our Handler Function

var router = route.router()
    ..put('/account/{accountId}{?name}', handler);
This has two path parameters:

  • accountId path segment
  • name query parameter

You can then serve this up as follows

io.serve(router.handler, 'localhost', 8080);

Basic Algorithm

The default behaviour is 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.
    • the value of the path parameter will first be converted to the type of the function parameter
  • 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 of type String
  • person of type 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);

Updated