Provides shelf middleware that lets you use ordinary Dart functions as Shelf
Handlers.
shelf_bind
frees you to:
shelf
boilerplateshelf_bind
deal with fitting it in to shelf
shelf_bind
favours convention over configuration so that you can write the minimal code necessary but still be able to override defaults as needed.
shelf_bind
is a powerful binding framework that supports:
path
, query
, body
and header
fieldssnake_case
and camelCase
for query params and between kebab-case
and camelCase
for headersIt can be used as a standalone shelf component or as part of framework that integrates it with other components.
The simplest way to use it with shelf_route is to use mojito or shelf_rest, as their routers have already wired in shelf_bind
.
If you are starting out then I recommend looking at mojito first and use this README as more detailed information about handler binding.
You can skip this standalone usage section if you are using shelf_bind
with mojito or shelf_rest.
The bind
function creates a shelf
Handler from a normal dart function.
var handler = bind(() => "Hello World");
This creates a shelf
Handler equivalent of
var handler = (Request request) => new Response.ok("Hello World");
If the function returns a Future
this will be mapped to a Future<Response>
bind(() => new Future.value("Hello World"))
Now you can set up a shelf-io server to bring your much needed greeting the world (awthanks)
io.serve(bind(() => "Hello World"), 'localhost', 8080);
Any simple type parameters you add to your function will match to path parameters of the same name.
Names will automatically be converted between snake_case
and camelCase
(String name) => "Hello $name"
shelf_bind
supports binding to any path parameters including:
path segments
like /greeting/fredquery parameters
like /greeting?name=fredIt accesses the path parameters using shelf_path which means it will work with any middleware (such as shelf_route) that uses shelf_path to store path parameters in the Request context property.
This also means it is not tied to any particular format for representing paths. For example it doesn't matter if the paths are defined like /greeting/:name
or /greeting/{name}
or /person{?name}
or whatever.
You can also bind to simple types like int
(String name, int age) => "Hello $name of age $age"
Supports:
Please file a feature request (or pull request) if you want a new type supported
You can use optional named parameters with defaults too.
(String name, {int age: 20}) => "Hello $name of age $age"
If a named parameter is not provided (or is null) in the context, then the default value will be used.
You can bind multiple path parameters into your own classes. This is described in the advanced section.
Handler parameters that are not simple types are by default assumed to come from the body.
This includes:
Map
List
custom objects
).For example, the handler parameters in the following will all be assumed to come from the request body.
(Map myMap) => ...
(List myList) => ...
(Person myMap) => ...
shelf_bind
currently supports both JSON
and FORM
encoded bodies.
By default, shelf_bind
attempts to determine the encoding the request content-type
as follows:
JSON
content-type
is set and is FORM
or JSON
then it will be handled as that typecontent-type
then a 400
response will be returnedYou can override this behaviour by using the @RequestBody
annotation. If the @RequestBody
annotation is present then the content will be treated as the type provided in the annotation.
For example the following will be treated as FORM
encoded regardless of the request content-type
(@RequestBody(format: ContentType.FORM) Map myMap) => ...
You can access the shelf
Request
object simply by adding that as a parameter to your function.
Note: since you can access all the parts of the request directly, including headers, you rarely need to do this.
(String name, Request request) => "Hello $name ${request.method}"
By default the return value of your function is encoded as JSON
by calling JSON.encode
.
So for example you can return a map
() => { "greeting" : "Hello World" }
This will work for anything that can be encoded as JSON
including any of your custom classes
class SayHello {
String greeting;
Map toJson() => { 'greeting': greeting };
}
SayHello myGreeter() => new SayHello()..greeting = "Hello World"
You can override the default status code as described in the section on Annotations.
If you want full control over the response you can simply return a Shelf Reponse directly
() => new Response.ok("Hello World")
shelf_bind
doesn't do any specific formatting for errors. Instead it leaves it to upstream middleware to handle, such as shelf_exception_handler.
This allows all your error handling to be kept in one location.
import 'package:http_exception/http_exception.dart';
() => throw new BadRequestException()
Sprinkling in some shelf_exception_handler middleware
var handler = const Pipeline()
.addMiddleware(exceptionHandler())
.addHandler(bind(() => throw new BadRequestException()));
we get a handler that will return a 400 response.
To tweak how the binding from the request path parameters is performed, use the @PathParam
annotation.
You can change the default mapping for the path name. For example if you have a handler parameter called argOne
, by default this would map to a request path parameter called arg_one
If you instead want this to map to arg1
you can specify that as follows
(@PathParam(pathName: 'arg1') String argOne) => ...
To tweak how the binding from the request body is performed, use the @RequestBody
annotation.
Note, only one handler parameter can be mapped to the body.
####JSON
To force the body to always be interpreted as JSON
set the format
as follows
bind(@RequestBody(format: ContentType.JSON) Person person) => "Hello ${person.name}")
####Form
bind(@RequestBody(format: ContentType.FORM) Person person) => "Hello ${person.name}")
You can override the default status (200) that is set on a successful return of the handler method using the ResponseHeaders
annotation. You can also have the location
header set to the incoming request url.
@ResponseHeaders.created()
String _create(String name) => "Hello $name";
final handler = bind(_create);
You can set the status to anything you like
@ResponseHeaders(successStatus: 204)
String _whatever(String name) => "Hello $name";
When setting the location
field on a POST
, the primary key field on the return object is used for the last segment of the path.
By default the primary key field is id
, but this can be overridden by specifying the idField
parameter.
@ResponseHeaders.created(idField: #name)
Person _create(@RequestBody() Person person) => person;
The name
field is now used for the last segment. For example if a POST
is made to http://localhost/person
and the name is fred
, the location will be set as
location: http://localhost/person/fred
One of the main uses of shelf_bind
is with a router like shelf_route.
The easiest way to do that is simply to use mojito or shelf_rest as they provide this out of the box
As bind
returns a Handler
you can simply pass that handler into the shelf_route's Router
methods
var myRouter = router()
..get('/', bind(() => "Hello World"));
Couldn't be much easier. However, having to wrap all your handler's in the bind
adds a bit of noise. To avoid that we can install a HandlerAdapter
into the router first. shelf_bind
provides one out of the box.
var myRouter = router(handlerAdapter: handlerAdapter())
..get('/', () => "Hello World");
The following is shows all the example handlers from above using shelf_route as the router
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_route/shelf_route.dart' as route;
import 'package:shelf_bind/shelf_bind.dart';
import 'package:http_exception/http_exception.dart';
import 'package:shelf_exception_handler/shelf_exception_handler.dart';
import 'dart:async';
void main() {
var router = route.router(handlerAdapter: handlerAdapter())
..get('/', () => "Hello World")
..get('/later', () => new Future.value("Hello World"))
..get('/map', () => {"greeting": "Hello World"})
..get('/object', () => new SayHello()..greeting = "Hello World")
..get('/ohnoes', () => throw new BadRequestException())
..get('/response', () => new Response.ok("Hello World"))
..get('/greeting/{name}', (String name) => "Hello $name")
..get('/greeting2/{name}{?age}',
(String name, int age) => "Hello $name of age $age")
..get('/greeting3/{name}', (Person person) => "Hello ${person.name}")
..get(
'/greeting5/{name}',
(String name, Request request) => "Hello $name ${request.method}")
..post('/greeting6', (Person person) => "Hello ${person.name}")
..get('/greeting8{?name}',
(@PathParams() Person person) => "Hello ${person.name}");
var handler = const shelf.Pipeline()
.addMiddleware(shelf.logRequests())
.addMiddleware(exceptionHandler())
.addHandler(router.handler);
route.printRoutes(router);
io.serve(handler, 'localhost', 8080).then((server) {
print('Serving at http://${server.address.host}:${server.port}');
});
}
class SayHello {
String greeting;
Map toJson() => { 'greeting': greeting };
}
class Person {
final String name;
Person.build({this.name});
Person.fromJson(Map json) : this.name = json['name'];
Map toJson() => { 'name': name };
}
See more detailed example in the project at example/binding_example.dart
You can bind path variables to properties of your classes by using the @PathParams
annotation.
class Person {
String name;
}
bind((@PathParams() Person person) => "Hello ${person.name}")
If you prefer immutable classes then you can bind to a constructor
class Person {
final String name;
Person.build({this.name});
}
The constructor must use named arguments for all the properties and the names must match the request path parameter names.
By default the constructor must be called build
. This will be overridable with annotations in the future.
shelf_bind
integrates with the powerful Constrain package to support automatic validation of your handler function parameters.
Enable validation via the validateParameters
property to the bind
function
bind((Person person) => "Hello ${person.name}", validateParameters: true)
Or when using with Shelf Route you can set it on handlerAdapter to apply to all routes (see section on Shelf Route integration below)
handlerAdapter: handlerAdapter(validateParameters: true)
Now lets spice up the Person class with a few (contrived) constraints.
class Person {
@NotNull()
@Ensure(nameIsAtLeast3Chars, description: 'name must be at least 3 characters')
final String name;
@NotNull()
@Ensure(isNotEmpty)
@Ensure(allStreetsStartWith15, description: "All streets must start with 15")
List<Address> addresses;
Person.build({this.name});
Person.fromJson(Map json) :
this.name = json['name'],
this.addresses = _addressesFromJson(json['addresses']);
static List<Address> _addressesFromJson(json) {
if (json == null || json is! List) {
return null;
}
return json.map((a) => new Address.fromJson(a)).toList(growable: false);
}
Map toJson() => { 'name': name, 'addresses': addresses };
String toString() => 'Person[name: $name]';
}
class Address {
@Ensure(streetIsAtLeast10Characters)
String street;
Address.fromJson(Map json) : this.street = json['street'];
Map toJson() => { 'street': street };
String toString() => 'Address[street: $street]';
}
// The constraint functions
Matcher nameIsAtLeast3Chars() => hasLength(greaterThan(3));
bool allStreetsStartWith15(List<Address> addresses) =>
addresses.every((a) => a.street == null || a.street.startsWith("15"));
Matcher streetIsAtLeast10Characters() => hasLength(greaterThanOrEqualTo(10));
Now whenever the handler is invoked, the Person object will be validated before it is passed to your Dart function. If it fails validation a BadRequestException
(from the http_exception package) will be thrown containing the detailed constraint violations.
If you've configured shelf_exception_handler correctly you will get responses like
HTTP/1.1 400 Bad Request
content-type: application/json
{
"errors": [
{
"constraint": {
"description": "all streets must start with 15",
"group": "DefaultGroup",
"type": "Ensure"
},
"details": null,
"invalidValue": {
"type": "List",
"value": [
"Address[street: blah blah st]"
]
},
"leafObject": {
"type": "Person",
"value": "Person[name: fred]"
},
"message": "Constraint violated at path addresses\nall streets must start with 15\n",
"propertyPath": "addresses",
"reason": null,
"rootObject": {
"type": "Person",
"value": "Person[name: fred]"
}
}
],
"message": "Bad Request",
"status": 400
}
Similar to handler function parameter validation, you can enable validation of your responses using the constrain
package. This is to ensure you never send out invalid data.
Enable response validation via the validateReturn
property to the bind
function
(String name) => new Person(name)
A HttpException
(from http_exception package) with a 500
status will be thrown if validation fails as this means you have messed up your code ;-).
See the Validation part of the Path Parameters section for more detailed explanation of validation.
In addition to the normal request related data like path parameters, body and headers, shelf_bind
also supports injecting arbitrary objects to handler functions. These are referred to as Custom Objects
.
Typically these objects are instantiated from data relating to the request but this is not a requirement.
A common usage is to inject clients to remote services such HTTP clients and database clients. These services may need to be invoked as the authenticated user.
Use the customObjects
parameter to handlerAdapter
or bind
to inject your own factories for these objects
bind((String name, PersonLookupClient client) => client.lookup(name),
customObjects: customObjects);
var adapter = handlerAdapter(customObjects: customObjects);
The customObjects
parameter is a just a map from the type to the factory. The factory takes a Request
argument.
var customObjects = {
PersonLookupClient: (req) => new Future.value(new PersonLookupClient())
};
class PersonLookupClient {
Future<Person> lookup(String name) =>
new Future.value(new Person.build(name: name));
}
Factories may return Future
's in which case the future will be resolved before passing the resolved object to the handler method.
Packages like mojito and shelf_rest inject their own custom objects
See the wiki for more details on all the options
See open issues.
Contributions are welcome. Please:
Warning: There are several things in this release that are potentially BREAKING
Whether it is breaking for your case depends on how you use it. For most users I expect little to no impact (for example there was no impact in these changes for backlog.io).
This is a major rewrite of the internals. In addition to many new features, the default behaviour has changed to minimise the need for annotations. The most common cases should require no annotations.
The changes are:
@PathParam
, @PathParams
, @BodyRequest
etc).
The old way of passing in your own bindings into the bind
function is no longer supported.
You can alternatively use the new bindManually
function but you must provide all the bindings in this case. No inference will be done if you call this function.
* Additionally the binding classes has changed substantially in this releaseMap
, List
and any of your classes (that are not registered as custom objects
).
* If no annotation is added to the parameter then shelf_bind
will first look at the request content-type
* if none is present it defaults to JSON
* if the content-type
is set and is FORM
or JSON
then it will be handled as that type
* if it is any other content-type
then a 400
response will be returned@PathParams
annotationaccount_id
) in query parameters to camel case (e.g accountId
) handler parameters@HeaderParam
annotationcontent-type
) in header fields to camel case (e.g. contentType
) handler parametersadded support for mergeable handler adapters. This allows things like customObjects to be extended by child routes
removed deprecated methods
Major release with lots of changes including:
Made second argument (the request) to handler optional.
Support for returning an object and automatically transforming to json
Add this to your package's pubspec.yaml file:
dependencies:
shelf_bind: "^0.9.0"
You can install packages from the command line:
with pub:
$ pub get
Alternatively, your editor might support pub get
.
Check the docs for your editor to learn more.
Now in your Dart code, you can use:
import 'package:shelf_bind/shelf_bind.dart';
Version | Uploaded | Documentation | Archive |
---|---|---|---|
0.9.4 | Oct 21, 2016 |
|
|
0.9.3 | Apr 10, 2016 |
|
|
0.9.2 | Jan 6, 2016 |
|
|
0.9.1 | Aug 12, 2015 |
|
|
0.9.0 | Jul 12, 2015 |
|
|
0.8.7 | Jul 4, 2015 |
|
|
0.8.6 | Jul 1, 2015 |
|
|
0.8.5 | Jun 26, 2015 |
|
|
0.8.4 | Jun 25, 2015 |
|
|
0.8.3 | Jun 25, 2015 |
|
|
This package version is not analyzed, because it is more than two years old. Check the latest stable version for its analysis.