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.

2 Comments

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

Leave a comment

About Konstantin Uvarin

user-pic A humble Perl developer