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