MVC::Neaf - Not Even A (Web Application) Framework
Hello everyone, today I'd like to present Neaf [ni:f], a web tool that tries hard to stay out of the way. Initially it was started for my own education. However, the result may be worth looking at even for users of serious stuff like Mojo, Dancer, and Kelp.
The main usage scenarios are perhaps sharing an existing module or script via the network, as well as supplementary tools and admin interfaces.
Usage
The following is a complete app, ready to run as a PSGI app, CGI script, or Apache2/mod_perl handler (see MVC::Neaf::Request::Apache2 if you want the latter):
use strict;
use warnings;
use MVC::Neaf qw(:sugar);
get '/hello' => sub { # the
my $req = shift; # one and only argument - the Request object
my $name = $req->param( name => qr/[\w\s]+/ );
# no way to get data without validation
$name ||= 'Stranger';
die 403 if $name eq 'root';
# error "403 Forbidden"
return {
-template => \'Hello, [% name %]!',
name => $name,
};
};
neaf->run;
The application is broken down into routes by request method & path combination, and each route gets one and only handler.
The handler gets a request object holding all information about the outside world.
The handler returns a hash containing data for rendering and possibly some dash-prefixed control keys.
The handler may also die. If the error message starts with a 3-digit number, it is assumed to be the returned HTTP status.
No assumptions are made about the underlying model.
This actually looks pretty similar to Mojolicious::Lite, which makes me think I was on the right track. However, unlike Mojo, we don't render anything inside the controller. Instead, just plain data is returned.
This "data in, data out" approach is one of the foundations of Neaf. A controller may still have side effects, of course, but what happened in the controller, stays in the controller. The same is true for the view section.
Features overview
Parameters and forms
There is no way to fetch parameters (and cookies and path_info
, too)
without some kind of validation (think perl -T
). This is against both
"not getting in the way" and "avoiding boilerplate" principles, but
promotes security. The most basic form is as follows:
my $foo = $request->param( foo => qr/.../ );
Note that fetching multi-value parameter requires special method:
my @bar = $request->multi_param( bar => qr/.../ );
All values must match the regexp, or an empty list is returned.
A more sophisticated way to get form parameters is using a form module:
use MVC::Neaf::X::Form;
my $validator = MVC::Neaf::X::Form->new({
id => qr/\d+/,
name => [ required => '.*' ],
# ....
});
# ...
my $form = $request->form( $validator );
$form->is_valid;
$form->data; # hash with validated keys
$form->error; # hash with errors
$form->raw; # entered data as is - can be used for resubmission
$form->as_url (replace => 'param', skip => '' );
# id=...&name=...&replace=param
Adding one's own errors is just fine (e.g. some value passed validation
but is not present in the database). This would reset is_valid
immediately:
$form->error( myvalue => "Not present in DB!" );
An even more sophisticated validation rules can be defined via
MVC::Neaf::X::Form::LIVR
based on
Validator::LIVR.
View
Even the most advanced data is probably of little use until it's displayed.
Neaf can use built-in view modules MVC::Neaf::View::TT
(the default),
MVC::Neaf::View::JS
, an object with render
method, or an anonymous function.
Such function (as well as the render
method) must return content as one
scalar and optionally content-type header as second value (that would be
overriden by -type return key, if present).
View is set up via 'view' (name, source, %arguments) command:
neaf view => TT => TT => INCLUDE_PATH => $path, ...;
neaf view => myview => sub { ... };
Later such view can be specified in the handler, or in the handler definition:
get '/somepath' => sub {
# ...
return { -view => 'myview', ... };
};
or equivalently (the returned value will override this default though):
get '/somepath' => sub {
# ...
return { ... };
}, -view => 'myview';
TT and JS views will be auto-loaded without parameters if not preloaded.
Sessions
A framework, even a toy one, is incomplete without session handling. Of course it can be done via cookies, but a built-in system is there to make life easier.
use MVC::Neaf qw(:sugar);
use MVC::Neaf::X::Session::Cookie;
neaf session => engine => MVC::Neaf::X::Session::Cookie->new (
key => 'very secret secret',
);
post '/login' => sub {
my $req = shift;
my $user = $req->param( user => '\w+' );
# check user here, show login form again
$req->session->{user} = $user;
$req->save_session;
# or just: $req->save_session( { user => $user } );
$req->redirect( $req->param( return => '/.*' ) || '/' );
};
get '/admin' => sub {
my $req = shift;
$req->session->{user} or die 403;
# .......
};
# ...
This engine will save session into cookies as cleartext JSON, signed with both the key and timestamp (so passing an old session will do nothing). So the session data may be read from outside, but not tampered with. The session will last a week and be renewed every day by default. This can be configured.
Note that currently no blessed data can be saved in the session.
Also there are sql-based and file-based engines, as well as a helper class
(MVC::Neaf::X::Session::Base
) to simplify building one's own.
Note that the most obvious in-memory storage is not supported, and deliberately so, as it would break under cgi or pre-fork execution. A key-value (possibly Redis) plugin is planned.
Hooks and defaults
Neaf has a fine grained, flexible hook system. For any (path prefix, request method, processing phase) combo, any number of anonymous functions can be added.
All such functions act on the request object, and their return value is discarded.
Also Neaf supports path-based default values to be merged into the hash returned by controller. For instance,
Set predefined return values for a group of requests - these would be merged into return hash, unless explicitly overridden in controller:
neaf default => {-view => 'JS', api_version => 1.1 }, path => '/js';
Only allow logged-in users:
neaf pre_logic => sub {
my $req = shift;
$req->session->{user_id} or die 403;
}, path => '/my', exclude_path => '/my/static';
Dump data into template for debugging:
use Data::Dumper;
neaf pre_render => sub {
my $req = shift;
if (defined $req->reply->{-template}) {
$req->reply->{raw_data} = Dumper( $req->reply );
};
};
Add custom header:
use Time::HiRes qw(time);
neaf pre_route => sub {
my $req = shift;
$req->stash->{t0} = time;
};
neaf pre_reply => sub {
my $req = shift;
$req->set_header( x_processing_time => time - $req->stash->{t0} );
};
Write down statistics after closing connection - user won't notice it:
neaf pre_cleanup => sub {
my $req = shift;
do_lengthly_db_write();
};
Debugging
A Neaf application that ends in a proper neaf->run();
instruction would
assume a CGI environment if run without parameters.
Use a server explicitly to run the app as a standalone server:
plackup myapp.pl --listen :31415
If any command line options were specified, the CLI module kicks in:
Get help:
perl myapp.pl --help
List available endpoints:
perl myapp.pl --list
Mangle request:
perl myapp.pl /submit --method POST --upload picture=file.jpg user_id=42
List data w/o processing template:
perl myapp.pl /foo?bar=42 --view JS
If that's not enough, even more magic can be used:
# a perl file
require 'myapp.pl';
my ($status, $head, $content) = neaf->run_test( '/life?answer=42' );
#examine status, head (a HTTP::Headers object), and content.
Effectively, this allows to build integration tests around the whole
application with Test::More
, though in the author's opinion the effort
is better spent on isolating and unit-testing the underlying model.
Other features
Neaf can serve static files:
neaf static '/img' => '/path/to/images';
neaf static '/favicon.ico' => '/path/to/logo.png';
Neaf can continue serving request after sending the headers and initial portion of content:
# ... (in the handler)
return {
-continue => sub {
my $req = shift;
# this basically follows the PSGI spec:
$req->write( ... );
$req->close;
},
# rest of data
};
Neaf supports custom error pages (not very flexible though):
neaf 403 => sub {
# create a link to login page here
};
More details can be found in the module's documentation.
Conclusion
Hope you enjoyed this reading. A sample application can be found at potracheno, as well as a number or examples in Neaf repository itself.
If you try out Neaf, please write a note here, at the perlmonks announcement, or straight in the github bug tracker.
Why are you using your own session handler when there are several very good Plack Middlewares available?
Generally, when working with micro/no-frameworks, I think it makes most sense to implement as little as possible on your own, and do the rest in Plack/PSGI...
Thanks, good point! Going along with the rest of Plack:: and getting all the good stuff for free is a good idea. Somehow I overlooked it.
I think I'll move in that direction somewhere after v.0.20 and only leave the core in this distro. E.g. form handling modules have nothing to do with Neaf's internals.