shelf_bind 0.8.7

  • README.md
  • CHANGELOG.md
  • Installing
  • Versions
  • 56

Binding Handler for Dart Shelf

Build Status Pub Version

Introduction

Provides Shelf middleware that lets you use ordinary Dart functions as Shelf Handlers.

Shelf Bind frees you to:

  • use your own functions without worrying about the Shelf boilerplate
  • focus on writing the business logic with your own classes and let Shelf 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:

  • binding to simple types
    • including type conversion
  • binding to your own domain objects
    • via property setters
    • via constructors
  • injecting your own custom arguments like http clients
  • seamless integration with Shelf Route
  • automatic parameter validation with Constrain

It can be used as a standalone Shelf component or as part of framework that integrates it with other components.

Using

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);

Response

Response Body

By default the return value of your function is encoded as JSON by calling JSON.encode.

So for example you can return a map

bind(() => { "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 };
}

bind(() => new SayHello()..greeting = "Hello World")

Response Status

You can override the default status code as described in the section on Annotations.

Shelf Response

If you want full control over the response you can simply return a Shelf Reponse directly

bind(() => new Response.ok("Hello World"))

Error Response

Shelf Bind doesn't do any specific formatting for errors. Instead it leaves it to upstream middleware to handle, such as shelf_exception_response.

This allows all your error handling to be kept in one location.

bind(() => throw new BadRequestException())

Sprinkling in some shelf_exception_response middleware

var handler = const Pipeline()
    .addMiddleware(exceptionResponse())
    .addHandler(bind(() => throw new BadRequestException()));

we get a handler that will return a 400 response.

Response Validation

Similar to handler function parameter validation (see Validation in the Path Parameters section below), 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

bind((String name) => new Person(name))

A HttpException (from shelf_exception_response package) with a 500 status will be thrown if validation fails.

See the Validation part of the Path Parameters section for more detailed explanation of validation.

Path Parameters

Any parameters you add to your function will match to path parameters of the same name.

bind((String name) => "Hello $name")

Shelf Bind supports binding to any path parameters including:

  • path segments like /greeting/fred
  • query parameters like /greeting?name=fred

It 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.

Simple Types

You can also bind to simple types like int

bind((String name, int age) => "Hello $name of age $age"))

Supports:

  • num
  • int
  • double
  • bool
  • DateTime
  • Uri

Please file a feature request (or pull request) if you want a new type supported

Domain Objects

You can bind path variables to properties of your classes too.

class Person {
  String name;
}

bind((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 Request

If you want the Request object passed in you can have that too ;)

bind((String name, Request request) => "Hello $name ${request.method}")

Validation

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 shelf_exception_response package) will be thrown containing the detailed constraint violations.

If you've configured shelf_exception_response 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
}

Injecting Custom Parameters

In addition to domain objects whose properties are populated from data in the request, Shelf Bind also supports injecting arbitrary objects.

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 bind to inject your own factories for these objects

bind((String name, PersonLookupClient client) => client.lookup(name),
    customObjects: customObjects);

The customObjects parameter is a just a map from the type to the factory. The factory takes a Request argument.

var customObjects = {
    // note we may need to get authentication info from the request
    // and based on that lookup a user from the db before creating an
    // authenticated client. Simulating here by returning a future
    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.

The handlerAdapter function (see below on Shelf Route integration) also takes a customObjects parameter, which means you can set this up once and use in any route you create with that router.

This feature is particularly powerful for framework authors as they can inject a whole set of useful services to handler functions.

Tweaking with Annotations

Binding to Request Body

Use the RequestBody annotation to bind a handler parameter to the body of the request instead of path parameters. Note, only one handler parameter can be mapped to the body.

Currently JSON and Form url encoded bodies are supported.

JSON Encoded Body

JSON is the default formatting (although this will soon change - see below)

bind(@RequestBody() Person person) => "Hello ${person.name}")

which will map from a request body like

{"name":"fred"}

Form Url Encoded Body

bind(@RequestBody(format: ContentType.FORM) Person person) => "Hello ${person.name}")

Note: format currently defaults to JSON. Soon that will change to being inferred from the content-type of the request, with a fallback to JSON. You will then only need to supply the format parameter if you want to override it

Response Headers

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

More to come

The bind function provides named parameters that allow full control over the bindings. Over time these will all be supported via new annotations.

Using With Shelf Route

One of the main uses of Shelf Bind is with a router like Shelf Route.

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");

Example

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:shelf_exception_response/exception_response.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 shelf.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, shelf.Request request) => "Hello $name ${request.method}");

  var handler = const shelf.Pipeline()
      .addMiddleware(shelf.logRequests())
      .addMiddleware(exceptionResponse())
      .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

More Information

See the wiki for more details on all the options

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

0.8.1

  • unittest replaced with test
  • explicit matcher dependency added, ported to 0.12.0 version
  • expose more info to support shelf_rest style extensions

0.8.0

  • Now uses http_exception package rather than shelf_exception_response

0.7.0+3

  • increased upper bound on constrain dependency

0.7.0+2

  • bumped self_exception_response dependency version

0.7.0+1

  • increased upper bound on shelf_route dependency

0.7.0

  • added support for mergeable handler adapters. This allows things like customObjects to be extended by child routes

  • removed deprecated methods

0.6.1+4

  • widened shelf_route version range

0.6.1+3

  • widened shelf_route version range

0.6.1+2

  • minor doco changes

0.6.1+1

  • Improved handling of some errors

0.6.1

  • Improved integration with constrain package
    • Support for constraints directly on parameters and returns
    • Constraint violations for all parameters included

0.6.0+1

  • Fixed use of Deprecated

0.6.0

  • New verson of shelf_route that fixed spelling mistake in HandlerAdapter

0.5.1

  • Support for form encoded body

0.5.0

  • breaking change: handlerAdapter is now a function
  • Support for Custom parameter injection
  • Integration with the constrain package for automatic validation of
    • handler function parameters
    • handler function returns

0.4.2

  • Added idField to ResponseHeaders annotation

0.4.1

  • Added type converters for bool and DateTime

0.4.0

Major release with lots of changes including:

  • Extensive binding support:
    • binding multiple function parameters,
    • simple type parameters,
    • custom classes (both to path parameters and body)
  • Seamless integration with Shelf Route
  • New annotations to customise bindings
  • Simple type conversions
  • Improved documentation
  • Support for Shelf Path to remove direct dependency to Shelf Route

0.3.0

  • Upgraded shelf and shelf route

0.2.2

  • Made second argument (the request) to handler optional.

  • Support for returning an object and automatically transforming to json

0.2.1

  • Added support for binding to properties instead of to constructor args

0.2.0

  • Upgrade shelf to 0.5.0 and shelf route to 0.4.0

0.1.0

  • First version

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  shelf_bind: "^0.8.7"

2. Install it

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.

3. Import it

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 Go to the documentation of shelf_bind 0.9.4 Download shelf_bind 0.9.4 archive
0.9.3 Apr 10, 2016 Go to the documentation of shelf_bind 0.9.3 Download shelf_bind 0.9.3 archive
0.9.2 Jan 6, 2016 Go to the documentation of shelf_bind 0.9.2 Download shelf_bind 0.9.2 archive
0.9.1 Aug 12, 2015 Go to the documentation of shelf_bind 0.9.1 Download shelf_bind 0.9.1 archive
0.9.0 Jul 12, 2015 Go to the documentation of shelf_bind 0.9.0 Download shelf_bind 0.9.0 archive
0.8.7 Jul 4, 2015 Go to the documentation of shelf_bind 0.8.7 Download shelf_bind 0.8.7 archive
0.8.6 Jul 1, 2015 Go to the documentation of shelf_bind 0.8.6 Download shelf_bind 0.8.6 archive
0.8.5 Jun 26, 2015 Go to the documentation of shelf_bind 0.8.5 Download shelf_bind 0.8.5 archive
0.8.4 Jun 25, 2015 Go to the documentation of shelf_bind 0.8.4 Download shelf_bind 0.8.4 archive
0.8.3 Jun 25, 2015 Go to the documentation of shelf_bind 0.8.3 Download shelf_bind 0.8.3 archive

All 37 versions...

Analysis

This feature is new.
We welcome feedback.

We analyzed this package, and provided a score, details, and suggestions below.

  • tool failures on Dec 7, 2017
  • Dart: 2.0.0-dev.8.0
  • pana: 0.7.3+1

Scores

Popularity:
Describes how popular the package is relative to other packages. [more]
68
Health:
Code health derived from static analysis. [more]
73
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
0
Overall score:
Weighted score of the above. [more]
56

Platforms

Detected platforms:

Error(s) prevent platform classification.

Suggestions

  • Fix lib/src/binding/handler_bindings.dart.

    Strong-mode analysis of lib/src/binding/handler_bindings.dart failed with the following error:

    line: 77 col: 3
    Invalid override. The type of 'BoundHandlerAdapter.merge' ('(BoundHandlerAdapter) → BoundHandlerAdapter') isn't a subtype of 'MergeableHandlerAdapter.merge' ('(MergeableHandlerAdapter) → (Function) → (Request) → dynamic').

  • Fix lib/src/util.dart.

    Strong-mode analysis of lib/src/util.dart failed with the following error:

    line: 4 col: 5
    The return type 'List' isn't a 'Iterable<Map>', as defined by the method 'iterableToJson'.

Dependencies

Package Constraint Resolved Available
Direct dependencies
constrain ^0.2.4 0.2.8
http_exception ^0.1.0 0.1.0
matcher ^0.12.0 0.12.1+4
shelf ^0.6.0 0.6.8 0.7.1
shelf_path ^0.1.6 0.1.7 0.1.8
shelf_route ^0.13.4 0.13.5 0.14.3
Transitive dependencies
async 1.13.3 2.0.1
charcode 1.1.1
collection 1.14.3
concepts 0.2.0
either 0.1.8
http_parser 3.1.1
option 1.2.0
path 1.5.1
quiver 0.21.4 0.26.2
source_span 1.4.0
stack_trace 1.9.1
stream_channel 1.6.2
string_scanner 1.0.2
typed_data 1.1.5
uri 0.11.1
utf 0.9.0+3
Dev dependencies
mock ^0.12.0
shelf_exception_handler ^0.1.0
test ^0.12.0