File Coverage

blib/lib/WebNano.pm
Criterion Covered Total %
statement 41 42 97.6
branch 6 8 75.0
condition 1 3 33.3
subroutine 11 11 100.0
pod 3 3 100.0
total 62 67 92.5


line stmt bran cond sub pod time code
1 4     4   185536 use strict;
  4         8  
  4         97  
2 4     4   18 use warnings;
  4         7  
  4         145  
3              
4             package WebNano;
5              
6 4     4   1951 use WebNano::FindController 'find_nested';
  4         9  
  4         266  
7              
8             our $VERSION = '0.001';
9 4     4   2755 use Plack::Response;
  4         41505  
  4         142  
10 4     4   31 use Scalar::Util qw(blessed);
  4         31  
  4         372  
11 4     4   4153 use Object::Tiny::RW 'renderer';
  4         1298  
  4         25  
12 4     4   4148 use Encode;
  4         44897  
  4         1717  
13              
14             sub psgi_callback {
15 3     3 1 205 my $self = shift;
16              
17             sub {
18 36     36   137207 $self->handle( shift );
19 3         40 };
20             }
21              
22 37     37 1 156 sub controller_search_path { [ ref(shift) ] };
23              
24             sub handle {
25 36     36 1 68 my( $self, $env ) = @_;
26 36         61 my $path = $env->{PATH_INFO};
27 36         144 my $c_class = find_nested( '', $self->controller_search_path );
28 36         158 $path =~ s{^/}{};
29 36 50       95 die 'Cannot find root controller' if !$c_class;
30 36         145 my $out = $c_class->handle(
31             path => $path,
32             app => $self,
33             env => $env,
34             self_url => '/',
35             );
36 35 100 33     416 if( not defined $out ){
    50          
    100          
37 4         26 my $res = Plack::Response->new(404);
38 4         71 $res->content_type('text/plain');
39 4         97 $res->body( 'No such page' );
40 4         29 return $res->finalize;
41             }
42             elsif( blessed $out and $out->isa( 'Plack::Response' ) ){
43 0         0 return $out->finalize;
44             }
45             elsif( ref $out eq 'CODE' ){
46 2         16 return $out;
47             }
48             else{
49 29         173 my $res = Plack::Response->new(200);
50 29         503 $res->content_type('text/html');
51 29         742 $res->body( encode( 'utf8', $out ) );
52 29         979 return $res->finalize;
53             }
54             }
55              
56             1;
57              
58              
59              
60             =pod
61              
62             =head1 NAME
63              
64             WebNano - A minimalistic PSGI based web framework.
65              
66             =head1 VERSION
67              
68             version 0.001
69              
70             =head1 SYNOPSIS
71              
72             A minimal WebNano application can be an
73             app.psgi file like this:
74              
75             {
76             package MyApp;
77             use base 'WebNano';
78             }
79            
80             {
81             package MyApp::Controller;
82             use base 'WebNano::Controller';
83            
84             sub index_action {
85             my $self = shift;
86             return 'This is my home';
87             }
88             }
89            
90             my $app = MyApp->new();
91             $app->psgi_callback;
92              
93             You can then run it with C<plackup> (see L<http://search.cpan.org/dist/Plack/scripts/plackup>).
94             A more practical approach is to split this into three different files.
95              
96             =head1 DESCRIPTION
97              
98             Every WebNano application has at least three parts - the application
99             class, at least one controller class and the
100             L<app.psgi|http://search.cpan.org/~miyagawa/Plack/scripts/plackup> file (or
101             something else that uses L<http://search.cpan.org/dist/Plack/lib/Plack/Runner.pm>
102             run the app).
103              
104             The application object is instantiated only once and is used to hold all the
105             other constand data objects - like connection to the database, a template
106             renderer object (if it is too heavy to be created per request) and generally
107             stuff that is too heavy to be rebuild with each request. In contrast the
108             controller objects are recreated for each request a new.
109              
110             The dispatching implemented by WebNano is simple mapping of HTTP request paths into method
111             calls as in the following examples:
112              
113             '/page' -> 'MyApp::Controller->page_action()'
114             '/Some/Very/long/path' -> 'MyApp::Controller::Some::Very->long_action( 'path' )
115              
116             The first type of dispatching is done by the plain L<WebNano::Controller> - to get actions
117             dispatched to controllers in subdirs you need to subclass L<WebNano::DirController>
118             (which is also a subclass of C<WebNano::Controller>).
119             So your root controllers should usually start with C<use base 'WebNano::DirController'>.
120             Other controllers also can subclass C<WebNano::DirController> - but if they do
121             their own dispatching to sub-controllers then they need to subclass plain C<WebNano::Controller>,
122             otherwise this automatic dispatching, sidestepping the custom-made code could become
123             a security risk.
124              
125             Additionally if the last part of the path is empty then C<index> is added to it - so C</> is
126             mapped to C<index_action> and C</SomeController/> is mapped to
127             C<MyApp::SomeController-E<gt>index_action>.
128              
129             If someone does not like the C<_action> postfixes then he can use the
130             C<url_map> controller attribute which works like the C<run_modes> attribute in
131             C<CGI::Application> - that is provides a map for method dispatching:
132              
133             $self->url_map( { 'mapped url' => 'mapped_url' } );
134              
135             or a list of approved methods to be dispached by name:
136              
137             $self->url_map( [ 'safe_method' ] );
138              
139             More advanced dispatching is done by overriding the C<local_dispatch> method in
140             the Controller class:
141              
142             around 'local_dispatch' => sub {
143             my( $orig, $self, $path) = @_;
144             my( $id, $method, @args ) = split qr{/}, $path;
145             $method ||= 'view';
146             if( $id && $id =~ /^\d+$/ && $self->is_record_method( $method ) ){
147             my $rs = $self->app->schema->resultset( 'Dvd' );
148             my $record = $rs->find( $id );
149             if( ! $record ) {
150             my $res = $self->req->new_response(404);
151             $res->content_type('text/plain');
152             $res->body( 'No record with id: ' . $id );
153             return $res;
154             }
155             return $self->$method( $record, @args );
156             }
157             return $self->$orig( $path );
158             };
159              
160             This one checks if the first part of the path is a number - if it is it uses
161             it to look for a Dvd object by primary key. If it cannot find such a Dvd then
162             it returns a 404. If it finds that dvd it then redispatches by the next path
163             part and passes that dvd object as the first parameter to that method call.
164             Note the need to check if the called method is an allowed one.
165             If the first part of the url is not a number - then the request is dispatched in
166             the normal way.
167              
168             The design goal numer one here is to provide basic functionality that should cover most
169             of use cases and a easy way to override it and extend. In general it is easy
170             to write your own dispatcher that work for your limited use case - and here
171             you just need to do that, you can override the dispatching only for a
172             particular controller and you don't need to warry about the general cases.
173              
174             The example in F<extensions/WebNano-Controller-DSL/> shows how to create a DSL
175             for dispatching (ala Dancer):
176              
177             get '/some_address' => sub { 'This is some_address in web_dispatch table' };
178              
179             =head2 Controller object live in the request scope (new controller per request)
180              
181             If you need to build a heavy
182             structure used in the controller you can always build it as the
183             application attribute and use it in the controller as it has access to
184             the application object, but since all the work of controllers is done
185             in the request scope (i.e. creating the request) - then it makes sense
186             that the whole object lives in that scope. This is the same as
187             Tatsumaki handlers (and controllers in Rails, Django and probably
188             other frameworks) - but different from Catalyst.
189              
190             =head2 Things that you can do with WebNano even though it does not actively support them
191              
192             There is a tendency in other frameworks to add interfaces to any other CPAN
193             library. With WebNano I want to keep it small, both in code and in it's
194             interface, and avoid adding new WebNano interfaces to things that can be used
195             directly, but instead I try to make that direct usage as simple as possible.
196              
197             In particular a WebNano script is a PSGI application and you can use all the Plack
198             tools with it.
199             For example to use sessions you can add following line to your app.psgi file:
200              
201             enable 'session'
202              
203             Read
204             L<http://search.cpan.org/dist/Plack-Middleware-Session/lib/Plack/Middleware/Session.pm>
205             about the additional options that you can enable here. See also
206             L<http://search.cpan.org/dist/Plack/lib/Plack/Builder.pm>
207             to read about the sweetened syntax you can use in your app.psgi file
208             and L<http://search.cpan.org/search?query=Plack+Middleware&mode=all>
209             to find out what other Plack::Middleware packages are available.
210              
211             The same goes for MVC. WebNano does not have any methods or attributes for
212             models, not because I don't structure my web application using the 'web MVC'
213             pattern - but rather because I don't see any universal attribute or method of
214             the possible models. Users are free to add their own methods. For example most
215             of my code uses L<http://search.cpan.org/dist/DBIx-Class/lib/DBIx/Class.pm>
216             - and I add these lines to my application:
217              
218             has schema => ( is => 'ro', isa => 'DBIx::Class::Schema', lazy_build => 1 );
219            
220             sub _build_schema {
221             my $self = shift;
222             my $config = $self->config->{schema};
223             return DvdDatabase::DBSchema->connect( $config->{dbi_dsn},
224             $config->{user}, $config->{pass}, $config->{dbi_params} );
225             }
226              
227             then I use it with C<$self-E<gt>app-E<gt>schema> in the controller objects.
228              
229             As to Views - I've added some support for two templating engines for WebNano,
230             but this is only because I wanted to experiment with 'template inheritance'. If
231             you don't want to use 'template inheritance' you can use Template::Tookit
232             directly in your controller actions or you can use directly any templating
233             engine in your controller actions - like
234             C<$self-E<gt>app-E<gt>my_templating-E<gt>process('template_name' )>
235             or even C<$self-E<gt>my_templating-E<gt>process( ... )> as long as it
236             returns a string.
237              
238             =head3 Streamming
239              
240             You can use the original L<http://search.cpan.org/dist/PSGI/PSGI.pod#Delayed_Reponse_and_Streaming_Body>
241             The streaming_action method in F<t/lib/MyApp/Controller.pm> can be used as an example.
242              
243             =head3 Authentication
244              
245             Example code in the application class:
246              
247             around handle => sub {
248             my $orig = shift;
249             my $self = shift;
250             my $env = shift;
251             if( $env->{'psgix.session'}{user_id} ){
252             $env->{user} = $self->schema->resultset( 'User' )->find( $env->{'psgix.session'}{user_id} );
253             }
254             else{
255             my $req = Plack::Request->new( $env );
256             if( $req->param( 'username' ) && $req->param( 'password' ) ){
257             my $user = $self->schema->resultset( 'User' )->search( { username => $req->param( 'username' ) } )->first;
258             if( $user->check_password( $req->param( 'password' ) ) ){
259             $env->{user} = $user;
260             $env->{'psgix.session'}{user_id} = $user->id;
261             }
262             }
263             }
264             $self->$orig( $env, @_ );
265             };
266              
267             =head3 Authorization
268              
269             Example:
270              
271             around 'local_dispatch' => sub {
272             my $orig = shift;
273             my $self = shift;
274             if( !$self->env->{user} ){
275             return $self->render( template => 'login_required.tt' );
276             }
277             $self->$orig( @_ );
278             };
279              
280             C<local_dispatch> is called before the controll is passed to child controllers,
281             so if you put that into the C<MyApp::Controller::Admin> controller - then both
282             all local actions and actions in child controllers (for example
283             C<MyApp::Controller::Admin::User>) would be guarded agains unauthorized usage.
284              
285             =head1 ATTRIBUTES and METHODS
286              
287             =head2 psgi_callback
288              
289             This is a method which returns a subroutine reference suitable for PSGI.
290             The returned subrourine ref is a closure over the application object.
291              
292             =head2 controller_search_path
293              
294             Experimental.
295              
296             =head2 handle
297              
298             Application method that acts as the PSGI callback - takes environment
299             as input and returns the response.
300              
301             =head2 renderer
302              
303             Nearly every web application uses some templating engine - this is the
304             attribute to keep the templating engine object. It is not mandatory that you
305             follow this rule.
306              
307             =head1 DIAGNOSTICS
308              
309             =for author to fill in:
310              
311             =over
312              
313             =back
314              
315             =head1 SEE ALSO
316              
317             L<WebNano::Renderer::TT> - Template Toolkit renderer with template inheritance
318              
319             L<WebNano::Controller::CRUD> (experimental),
320              
321             L<http://github.com/zby/Nblog> - example blog engine using WebNano
322              
323             =head1 DEPENDENCIES
324              
325             See Makefile.PL
326              
327             =head1 INCOMPATIBILITIES
328              
329             None reported.
330              
331             =head1 BUGS AND LIMITATIONS
332              
333             No bugs have been reported.
334              
335             Please report any bugs or feature requests to
336             C<bug-webnano@rt.cpan.org>, or through the web interface at
337             L<http://rt.cpan.org>.
338              
339             =head1 AUTHOR
340              
341             Zbigniew Lukasiak <zby@cpan.org>
342              
343             =head1 COPYRIGHT AND LICENSE
344              
345             This software is copyright (c) 2010 by Zbigniew Lukasiak <zby@cpan.org>.
346              
347             This is free software; you can redistribute it and/or modify it under
348             the same terms as the Perl 5 programming language system itself.
349              
350             =cut
351              
352              
353             __END__
354              
355             # ABSTRACT: A minimalistic PSGI based web framework.
356