bloodless 0.1.0
bloodless: ^0.1.0 copied to clipboard
A microframework for Dart
bloodless #
A microframework for Dart, heavily inspired by Flask.
import 'package:bloodless/server.dart' as app;
@app.Route("/")
helloWorld() => "Hello, World!";
main() {
app.setupConsoleLog();
app.start();
}
$ dart hello.dart
INFO: 2014-02-24 13:16:19.086: Configured target for / : .helloWorld
INFO: 2014-02-24 13:16:19.102: Setting up VirtualDirectory for /home/user/project/web - index files: [index.html]
INFO: 2014-02-24 13:16:19.121: Running on 0.0.0.0:8080
Installation #
NOTE: It's recommended to use Dart 1.2 or above, which is currently available only at the dev channel (Dart 1.2 is stable now).
- Create a new Dart package (manually or through Dart Editor)
- Add bloodless as a dependency in
pubspec.yaml
file
name: my_app
dependencies:
bloodless: any
- Run
pub get
to update dependencies - Create a
bin
directory - Create a
server.dart
file under thebin
directory
import 'package:bloodless/server.dart' as app;
@app.Route("/")
helloWorld() => "Hello, World!";
main() {
app.setupConsoleLog();
app.start();
}
- To run the server, create a launch configuration in Dart Editor, or use the
dart
command:
$ dart bin/server.dart
INFO: 2014-02-24 13:16:19.086: Configured target for / : .helloWorld
INFO: 2014-02-24 13:16:19.102: Setting up VirtualDirectory for /home/user/project/web - index files: [index.html]
INFO: 2014-02-24 13:16:19.121: Running on 0.0.0.0:8080
- Now head over to http://127.0.0.1:8080/, and you should see your hello world greeting.
Routing #
Just use the Route
annotation to bind a method with a URL:
@app.Route("/")
helloWorld() => "Hello, World!";
The returned value will be serialized to the client according to its type. For example, if the value is a String, the client will receive a text/plain response.
Returned Value | Response type |
---|---|
String | text/plain |
Map or List | application/json |
File | (MimeType of the file) |
If a Future is returned, then the framework will wait for its completion.
@app.Route("/")
helloWorld() => new Future(() => "Hello, World!");
For other types, bloodless will convert the value to a String, and send it as text/plain.
Also, it's possible to override the content type of the response:
@app.Route("/", responseType: "text/xml")
getXml() => "<root><node>text</node></root>";
Retrieving path parameters #
You can easily bind URL parts to arguments:
@app.Route("/user/:username")
helloUser(String username) => "hello $username";
The argument doesn't need to be a String. If it's an int, for example, bloodless will try to convert the value for you (if the conversion fails, a 400 status code is sent to the client).
@app.Route("/user/:username/:addressId")
getAddress(String username, int addressId) {
...
};
The supported types are: int, double and bool
Retrieving query parameters #
Use the QueryParam
annotation to access a query parameter
@app.Route("/user")
getUser(@app.QueryParam("id") int userId) {
...
};
Like path parameters, the argument doesn't need to be a String.
HTTP Methods #
By default, a route only respond to GET requests. You can change that with the methods
arguments:
@app.Route("/user/:username", methods: const [app.GET, app.POST])
helloUser(String username) => "hello $username";
Retrieving request's body #
You can retrieve the requests's body as a form, json or text
@app.Route("/adduser", methods: const [app.POST])
addUser(@app.Body(app.JSON) Map json) {
...
};
@app.Route("/adduser", methods: const [app.POST])
addUser(@app.Body(app.FORM) Map form) {
...
};
The request object #
You can use the global request
object to access the request's information and content:
@app.Route("/user", methods: const [app.GET, app.POST])
user() {
if (app.request.method == app.GET) {
...
} else if (app.request.method == app.POST) {
if (app.request.bodyType == app.JSON) {
var json = app.request.body;
...
} else {
...
}
}
};
Actually, the request
object is a get method, that retrieves the request object from the current Zone. Since each request runs in its own zone, it's completely safe to use request
at any time, even in async callbacks.
Interceptors #
Each request is actually a chain, composed by 0 or more interceptors, and a target. A target is a method annotated with Route
, or a static file handled by a VirtualDirectory instance. An interceptor is a structure that allows you to apply a common behaviour to a group of targets. For example, you can use a interceptor to change the response of a group of targets, or to apply a security constraint.
@app.Interceptor(r'/.*')
handleResponseHeader() {
app.request.response.headers.add("Access-Control-Allow-Origin", "*");
app.chain.next();
}
@app.Interceptor(r'/admin/.*')
adminFilter() {
if (app.request.session["username"] != null) {
app.chain.next();
} else {
app.chain.interrupt(HttpStatus.UNAUTHORIZED);
//or app.redirect("/login.html");
}
}
When a request is received, the framework will execute all interceptors that matchs the URL, and then will look for a valid route. If a route is found, it will be executed, otherwise the request will be fowarded to the VirtualDirectory, which will look for a static file.
Each interceptor must execute the chain.next()
or chain.interrupt()
methods, otherwise, the request will be stucked. The chain.next()
method can receive a callback, that is executed when the target completes. All callbacks are executed in the reverse order they are created. If a callback returns a Future
, then the next callback will execute only when the future completes.
For example, consider this script:
@app.Route("/")
helloWorld() => "target\n";
@app.Interceptor(r'/.*', chainIdx: 0)
interceptor1() {
app.request.response.write("interceptor 1 - before target\n");
app.chain.next(() {
app.request.response.write("interceptor 1 - after target\n");
});
}
@app.Interceptor(r'/.*', chainIdx: 1)
interceptor2() {
app.request.response.write("interceptor 2 - before target\n");
app.chain.next(() {
app.request.response.write("interceptor 2 - after target\n");
});
}
main() {
app.setupConsoleLog();
app.start();
}
When you access http://127.0.0.1:8080/, the result is:
interceptor 1 - before target
interceptor 2 - before target
target
interceptor 2 - after target
interceptor 1 - after target
Like the request
object, the chain
object is also a get method, that returns the chain of the current zone.
NOTE: You can also call redirect()
or abort()
instead of chain.interrupt()
. The abort()
call will invoke the corresponding error handler.
Groups #
You can use classes to group routes and interceptors:
@Group("/user")
class UserService {
@app.Route("/find")
findUser(@app.QueryParam("n") String name,
@app.QueryParam("c") String city) {
...
}
@app.Route("/add", methods: const [app.POST])
addUser(@app.Body(app.JSON) Map json) {
...
}
}
The prefix defined with the Group
annotation, will be prepended in every route and interceptor inside the group.
NOTE: The class must provide a default constructor, with no required arguments.
Error handlers #
You can define error handlers with the ErrorHandler
annotation:
@app.ErrorHandler(HttpStatus.NOT_FOUND)
handleNotFoundError() => app.redirect("/error/not_found.html");
Server configuration #
If you invoke the start()
method with no arguments, the server will be configured with default values:
Argument | Default Value |
---|---|
host | "0.0.0.0" |
port | 8080 |
staticDir | "../web" |
indexFiles | ["index.html"] |
Logging #
Bloodless provides a helper method to set a simple log handler, that outputs the messages to the console:
app.setupConsoleLog();
By default, the log level is setted to INFO, which logs the startup process and errors. If you want to see all the log messages, you can set the level to ALL:
import 'package:logging/logging.dart';
main() {
app.setupConsoleLog(Level.ALL);
...
}
If you want to output the messages to a different place (for example, a file), you can define your own log handler:
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((LogRecord rec) {
...
});
Unit tests #
Bloodless provides a simple API that you can use to easily test your server.
For example, consider you have the following service at /lib/services.dart
library services;
import 'package:bloodless/server.dart' as app;
@app.Route("/user/:username")
helloUser(String username) => "hello, $username";
A simple script to test this service would be:
import 'package:unittest/unittest.dart';
import 'package:bloodless/server.dart' as app;
import 'package:bloodless/mocks.dart';
import 'package:your_package_name/services.dart'
main() {
//load the services in 'services' library
setUp(() => app.setUp([#services]);
test("hello service", () {
//create a mock request
var req = new MockRequest("/user/luiz");
//dispatch the request
return app.dispatch(req).then((resp) {
//verify the response
expect(resp.statusCode, equals(200));
expect(resp.mockContent, equals("hello, luiz"));
});
})
//remove all loaded services
tearDown(() => app.tearDown());
}
NOTE: To learn more abou unit tests in Dart, take a look at this article.
Deploying the app #
The easiest way to build your app is using the grinder library. Bloodless provides a simples task to properly copy the server's files to the build folder, which you can use in your build script. For example:
- Create a
build.dart
file inside thebin
folder
import 'package:grinder/grinder.dart';
import 'package:grinder/grinder_utils.dart';
import 'package:bloodless/tasks.dart';
main(List<String> args) {
defineTask('build', taskFunction: (GrinderContext ctx) => new PubTools().build(ctx));
defineTask('deploy_server', taskFunction: deployServer, depends: ['build']);
defineTask('all', depends: ['build', 'deploy_server']);
startGrinder(args);
}
-
Instead of running
pub build
directly, you can callbuild.dart
to properly build your app. -
To run
build.dart
through Dart Editor, you need to create a command-line launch configuration, with the following parameters:
Parameter | Value |
---|---|
Dart Script | bin/build.dart |
Working directory | (root path of your project) |
Script arguments | all |
- To run
build.dart
through command line, you need to set the DART_SDK environment variable:
$ export DART_SDK=(path to dart-sdk)
$ dart bin/build.dart all
NOTE: You can improve your build script according to your needs.