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