File Coverage

blib/lib/Catalyst/Helper/Controller/DBIC/API/REST.pm
Criterion Covered Total %
statement 107 113 94.6
branch 11 14 78.5
condition 4 9 44.4
subroutine 14 14 100.0
pod 1 1 100.0
total 137 151 90.7


line stmt bran cond sub pod time code
1             package Catalyst::Helper::Controller::DBIC::API::REST;
2              
3 1     1   5728 use Class::Load;
  1         1  
  1         56  
4 1     1   4 use namespace::autoclean;
  1         1  
  1         10  
5              
6 1     1   67 use strict;
  1         1  
  1         18  
7 1     1   4 use warnings;
  1         1  
  1         22  
8              
9 1     1   3 use FindBin;
  1         2  
  1         33  
10 1     1   4 use File::Spec;
  1         1  
  1         23  
11 1     1   786 use Path::Tiny;
  1         8839  
  1         57  
12 1     1   7 use List::Util;
  1         1  
  1         61  
13              
14 1     1   4 use lib "$FindBin::Bin/../lib";
  1         2  
  1         8  
15              
16             BEGIN {
17              
18 1     1   830 our $VERSION = '0.09'; # VERSION
19             }
20              
21             =head1 NAME
22              
23             Catalyst::Helper::Controller::DBIC::API::REST
24              
25             =encoding UTF-8
26              
27             =head1 SYNOPSIS
28              
29             $ catalyst.pl MyApp
30             $ cd MyApp
31             $ script/myapp_create.pl controller API::REST DBIC::API::REST \
32             MyApp::Schema MyApp::Model::DB
33              
34              
35             ...
36              
37             package MyApp::Controller::API::REST::Producer;
38              
39             use strict;
40             use warnings;
41             use base qw/MyApp::ControllerBase::REST/;
42             use JSON::XS;
43              
44             __PACKAGE__->config(
45             action => { setup => {
46             PathPart => 'producer',
47             Chained => '/api/rest/rest_base' }
48             }, # define parent chain action and partpath
49             class => 'DB::Producer', # DBIC result class
50             create_requires => [qw/name/], # columns required
51             # to create
52             create_allows => [qw//], # additional non-required
53             # columns that
54             # create allows
55             update_allows => [qw/name/], # columns that
56             # update allows
57             list_returns => [qw/producerid name/], # columns that
58             # list returns
59              
60             list_prefetch_allows => [ # every possible prefetch param allowed
61             [qw/cd_to_producer/], { 'cd_to_producer' => [qw//] },
62             [qw/tags/], { 'tags' => [qw//] },
63             [qw/tracks/], { 'tracks' => [qw//] },
64             ],
65              
66             list_ordered_by => [ qw/producerid/ ],
67             # order of generated list
68             list_search_exposes => [ qw/producerid name/ ],
69             # columns that can be searched on via list
70             );
71              
72             =head1 DESCRIPTION
73              
74             This creates REST controllers according to the specifications at
75             L<Catalyst::Controller::DBIC::API> and L<Catalyst::Controller::DBIC::API::REST>
76             for all the classes in your Catalyst app.
77              
78             It creates the following files:
79              
80             MyApp/lib/MyApp/Controller/API.pm
81             MyApp/lib/MyApp/Controller/API/REST.pm
82             MyApp/lib/MyApp/Controller/API/REST/*
83             MyApp/lib/MyApp/ControllerBase/REST.pm
84              
85             Individual class controllers are under MyApp/lib/MyApp/Controller/API/REST/*.
86              
87             =head2 CONFIGURATION
88              
89             The idea is to make configuration as painless and as automatic as possible, so most
90             of the work has been done for you.
91              
92             There are 8 __PACKAGE__->config(...) options for L<Catalyst::Controller::DBIC::API/CONFIGURATION>.
93             Here are the defaults.
94              
95             =head2 create_requires
96              
97             All non-nullable columns that are (1) not autoincrementing,
98             (2) don't have a default value, are neither (3) nextvals,
99             (4) sequences, nor (5) timestamps.
100              
101             =head2 create_allows
102              
103             All nullable columns that are (1) not autoincrementing,
104             (2) don't have a default value, are neither (3) nextvals,
105             (4) sequences, nor (5) timestamps.
106              
107             =head2 update_allows
108              
109             The union of create_requires and create_allows.
110              
111             =head2 list_returns
112              
113             Every column in the class.
114              
115             =head2 list_prefetch
116              
117             Nothing is prefetched by default.
118              
119             =head2 list_prefetch_allows
120              
121             (1) An arrayref consisting of the name of each of the class's
122             has_many relationships, accompanied by (2) a hashref keyed on
123             the name of that relationship, whose values are the names of
124             its has_many's, e.g., in the "Producer" controller above, a
125             Producer has many cd_to_producers, many tags, and many tracks.
126             None of those classes have any has_many's:
127              
128             list_prefetch_allows => [
129             [qw/cd_to_producer/], { 'cd_to_producer' => [qw//] },
130             [qw/tags/], { 'tags' => [qw//] },
131             [qw/tracks/], { 'tracks' => [qw//] },
132             ],
133              
134             =head2 list_ordered_by
135              
136             The primary key.
137              
138             =head2 list_search_exposes
139              
140             (1) An arrayref consisting of the name of each column in the class,
141             and (2) a hashref keyed on the name of each of the class's has many
142             relationships, the values of which are all the columns in the
143             corresponding class, e.g.,
144              
145             list_search_exposes => [
146             qw/cdid artist title year/,
147             { 'cd_to_producer' => [qw/cd producer/] },
148             { 'tags' => [qw/tagid cd tag/] },
149             { 'tracks' => [qw/trackid cd position title last_updated_on/] },
150             ], # columns that can be searched on via list
151              
152             =head1 CONTROLLERBASE
153              
154             Following the advice in L<Catalyst::Controller::DBIC::API/EXTENDING>, this
155             module creates an intermediate class between your controllers and
156             L<Catalyst::Controller::DBIC::API::REST>. It contains one method, create,
157             which serializes object information and stores it in the stash, which is
158             not the default behavior.
159              
160             =head1 METHODS
161              
162             =head2 mk_compclass
163              
164             This is the meat of the helper. It writes the directory structure if it is
165             not in place, API.pm, REST.pm, the controllerbase, and the result class
166             controllers. It replaces $helper->{} values as it goes through, rendering
167             the files for each.
168              
169             =over
170              
171             =back
172              
173             =head1 AUTHOR
174              
175             Amiri Barksdale E<lt>amiri@roosterpirates.comE<gt>
176              
177             =head1 CONTRIBUTORS
178              
179             Franck Cuny (lumberjaph) <franck@lumberjaph.net>
180              
181             Pablo Rodríguez González <pablo.rodriguez.gonzalez@gmail.com>
182              
183             Chris Weyl (RsrchBoy) <cweyl@alumni.drew.edu>
184              
185             =head1 SEE ALSO
186              
187             L<Catalyst::Controller::DBIC::API>
188             L<Catalyst::Controller::DBIC::API::REST>
189             L<Catalyst::Controller::DBIC::API::RPC>
190              
191             =head1 LICENSE
192              
193             This library is free software; you can redistribute it and/or modify
194             it under the same terms as Perl itself.
195              
196             =cut
197              
198             sub mk_compclass {
199 1     1 1 14 my ( $self, $helper, $schema_class, $model, @extra_options ) = @_;
200 1         2 my %extra_options = map {split /=/} @extra_options;
  0         0  
201              
202 1   33     8 $schema_class ||= $helper->{app} . '::Schema';
203 1   33     5 $model ||= $helper->{app} . '::Model::DB';
204              
205 1         6 ( my $model_base = $model ) =~ s/^.*::Model:://;
206              
207 1 50       5 $helper->{script} = File::Spec->catdir( $helper->{dir}, 'script' ) if $helper->{dir};
208 1         6 $helper->{appprefix} = Catalyst::Utils::appprefix( $helper->{name} );
209 1         12 my @path_to_name = split( /::/, $helper->{name} );
210              
211             ## Connect to schema for class info
212 1         4 Class::Load::load_class($schema_class);
213 1         135755 my $schema = $schema_class->connect;
214              
215             my $path_app = File::Spec->catdir( $FindBin::Bin, "..", "lib",
216 1         63977 split( /::/, $helper->{app} ) );
217              
218             ## Make api base
219             my $api_file
220 1         15 = File::Spec->catfile( $path_app, $helper->{type}, "API.pm" );
221              
222 1         17 ( my $api_path = $api_file ) =~ s/\.pm$//;
223 1         7 $helper->mk_dir($api_path);
224 1         329 $helper->render_file( 'apibase', $api_file );
225 1         45310 $helper->{test} = $helper->next_test('API');
226 1         324 $helper->_mk_comptest;
227              
228             ## Make rest base
229             my $rest_file
230 1         5822 = File::Spec->catfile( $path_app, $helper->{type}, "API", "REST.pm" );
231              
232 1         8 ( my $rest_path = $rest_file ) =~ s/\.pm$//;
233 1         5 $helper->mk_dir($rest_path);
234 1         323 $helper->render_file( 'restbase', $rest_file );
235 1         1843 $helper->{test} = $helper->next_test('API_REST');
236 1         336 $helper->_mk_comptest;
237              
238             ## Make controller base
239 1         5487 my $base_file
240             = File::Spec->catfile( $path_app, "ControllerBase", "REST.pm" );
241              
242 1         9 $helper->mk_dir( File::Spec->catdir( $path_app, "ControllerBase" ) );
243 1         285 $helper->render_file( 'controllerbase', $base_file );
244 1         1780 $helper->{test} = $helper->next_test('controller_base');
245 1         267 $helper->_mk_comptest;
246              
247 1         5598 $helper->mk_dir( File::Spec->catdir( $path_app, $helper->{type}, @path_to_name ));
248              
249             ## Make result class controllers
250 1         150 for my $source ( $schema->sources ) {
251 6         28863 my ( $class, $result_class );
252             my $file
253 6         86 = File::Spec->catfile( $path_app, $helper->{type}, @path_to_name,
254             split(/::/, $source . ".pm") );
255 6         28 Path::Tiny::path($file)->parent()->mkpath;
256             $class
257             = $helper->{app} . "::"
258             . $helper->{type}
259 6         916 . "::" . join("::", @path_to_name) ."::"
260             . $source;
261              
262             #$result_class = $helper->{app} . "::Model::DB::" . $source;
263 6         13 $result_class = $model_base . '::' . $source;
264              
265             ### Declare config vars
266 6         10 my @create_requires;
267             my @create_allows;
268 0         0 my @update_allows;
269 0         0 my @list_prefetch;
270 6         34 my @list_search_exposes = my @list_returns
271             = $schema->source($source)->columns;
272              
273             ### HAIRY RELATIONSHIPS STUFF
274 6         387 my @sub_list_search_exposes = my @list_prefetch_allows
275             = _return_has_many_list(
276             $schema->source($source)->_relationships );
277             @list_prefetch_allows = map {
278 6         13 my $ref = $_;
  4         6  
279             qq|[qw/$ref->[0]/], { |
280             . qq| '$ref->[0]' => [qw/|
281             . join(
282             ' ',
283 4         32 map { $_->[0] } _return_has_many_list(
  3         10  
284             $schema->source( $ref->[1] )->_relationships
285             )
286             ) . qq|/] },\n\t\t|;
287             } @list_prefetch_allows;
288              
289             @sub_list_search_exposes = map {
290 6         12 my $ref = $_;
  4         121  
291 4         12 qq|{ '$ref->[0]' => [qw/|
292             . join( ' ', $schema->source( $ref->[1] )->columns )
293             . qq|/] },\n\t\t|;
294             } @sub_list_search_exposes;
295             ### END HAIRY RELATIONSHIP STUFF
296              
297 6         135 my @list_ordered_by = $schema->source($source)->primary_columns;
298              
299             ### Prepare hash of column info for this class, so we can extract config
300             my %source_col_info
301 6         230 = map { $_, $schema->source($source)->column_info($_) }
  18         643  
302             $schema->source($source)->columns;
303 6         237 for my $k ( sort keys %source_col_info ) {
304 1     1   6 no warnings qw/uninitialized/;
  1         1  
  1         591  
305 18 100 66     104 if (( !$source_col_info{$k}->{'is_auto_increment'} )
306             && !(
307             $source_col_info{$k}->{'default_value'}
308             =~ /(nextval|sequence|timestamp)/
309             )
310             )
311             {
312              
313             ### Extract create required
314             push @create_requires, $k
315 13 100       27 if !$source_col_info{$k}->{'is_nullable'};
316              
317             ### Extract create_allowed
318             push @create_allows, $k
319 13 100       30 if $source_col_info{$k}->{'is_nullable'};
320             }
321             else { }
322 18         31 @update_allows = ( @create_requires, @create_allows );
323             }
324              
325 6         14 $helper->{package} = $class;
326 6         17 $helper->{class} = $model_base . '::' . $source;
327 6 50       16 if (defined $extra_options{'result_class'}) {
328 0         0 $helper->{result_class} = $extra_options{'result_class'};
329             }
330 6         148 $helper->{path_class_name} = join("/", (map {lc} @path_to_name[ 2 .. (scalar @path_to_name - 1)]), split(/::|\./, $schema->source_registrations->{$source}->name));
  0         0  
331             $helper->{class_name}
332 6     6   224 = List::Util::first {1;} reverse split(/::/, $schema->source_registrations->{$source}->name);
  6         87  
333 6         20 $helper->{file} = $file;
334 6         17 $helper->{create_requires} = join( ' ', @create_requires );
335 6         12 $helper->{create_allows} = join( ' ', @create_allows );
336 6         15 $helper->{list_returns} = join( ' ', @list_returns );
337 6         13 $helper->{list_search_exposes} = join( ' ', @list_search_exposes );
338             $helper->{sub_list_search_exposes}
339 6         11 = join( '', @sub_list_search_exposes );
340 6         12 $helper->{update_allows} = join( ' ', @update_allows );
341 6 100       20 $helper->{list_prefetch_allows} = join( '', @list_prefetch_allows )
342             if scalar @list_prefetch_allows > 0;
343             $helper->{list_prefetch}
344 6 50       13 = join( ', ', map {qq|'$_->[0]'|} @list_prefetch )
  0         0  
345             if scalar @list_prefetch > 0;
346 6         16 $helper->{list_ordered_by} = join( ' ', @list_ordered_by );
347 6         19 $helper->render_file( 'compclass', $file );
348 6         56375 $helper->{test} = $helper->next_test($source);
349 6         1844 $helper->_mk_comptest;
350             }
351             }
352              
353             sub _return_has_many_list {
354 10     10   448 my ($relationships) = @_;
355             return
356 17         76 grep { $relationships->{ $_->[0] }->{attrs}->{accessor} =~ /multi/ }
357 10         47 map { [ $_, $relationships->{$_}->{source} ] }
  17         51  
358             sort keys %$relationships;
359             }
360              
361             1;
362              
363             __DATA__
364              
365             =begin pod_to_ignore
366              
367             __apibase__
368             package [% app %]::Controller::API;
369              
370             use strict;
371             use warnings;
372              
373             use parent qw/Catalyst::Controller/;
374              
375             sub api_base : Chained('/') PathPart('api') CaptureArgs(0) {
376             my ( $self, $c ) = @_;
377             }
378              
379             1;
380              
381             __restbase__
382             package [% app %]::Controller::API::REST;
383              
384             use strict;
385             use warnings;
386              
387             use parent qw/Catalyst::Controller/;
388              
389             sub rest_base : Chained('/api/api_base') PathPart('rest') CaptureArgs(0) {
390             my ($self, $c) = @_;
391             }
392              
393             1;
394             __controllerbase__
395             package [% app %]::ControllerBase::REST;
396              
397             use strict;
398             use warnings;
399              
400             use parent qw/Catalyst::Controller::DBIC::API::REST/;
401              
402             sub create :Private {
403             my ($self, $c) = @_;
404             $self->next::method($c);
405             if ($c->stash->{created_object}) {
406             %{$c->stash->{response}->{new_object}} = $c->stash->{created_object}->get_columns;
407             }
408             }
409              
410             1;
411             __compclass__
412             package [% package %];
413              
414             use strict;
415             use warnings;
416             use JSON::XS;
417              
418             use parent qw/[% app %]::ControllerBase::REST/;
419              
420             __PACKAGE__->config(
421             # Define parent chain action and partpath
422             action => { setup => { PathPart => '[% path_class_name %]', Chained => '/api/rest/rest_base' } },
423             # DBIC result class
424             class => '[% class %]',
425             [% IF result_class %]
426             result_class => '[% result_class %]',
427             [% END %]
428             # Columns required to create
429             create_requires => [qw/[% create_requires %]/],
430             # Additional non-required columns that create allows
431             create_allows => [qw/[% create_allows %]/],
432             # Columns that update allows
433             update_allows => [qw/[% update_allows %]/],
434             # Columns that list returns
435             list_returns => [qw/[% list_returns %]/],
436             [% IF list_prefetch %]
437             # relationships prefetched by default
438             list_prefetch => [[% list_prefetch %]],
439             [% END %]
440             [% IF list_prefetch_allows %]
441             # Every possible prefetch param allowed
442             list_prefetch_allows => [
443             [% list_prefetch_allows %]
444             ],
445             [% END %]
446             # Order of generated list
447             list_ordered_by => [qw/[% list_ordered_by %]/],
448             # columns that can be searched on via list
449             list_search_exposes => [
450             qw/[% list_search_exposes %]/,
451             [% sub_list_search_exposes %]
452             ],);
453              
454             =head1 NAME
455              
456             [% PACKAGE %] - REST Controller for [% schema_class %]
457              
458             =head1 DESCRIPTION
459              
460             REST Methods to access the DBIC Result Class [% class_name %]
461              
462             =head1 AUTHOR
463              
464             [% author %]
465              
466             =head1 SEE ALSO
467              
468             L<Catalyst::Controller::DBIC::API>
469             L<Catalyst::Controller::DBIC::API::REST>
470             L<Catalyst::Controller::DBIC::API::RPC>
471              
472             =head1 LICENSE
473              
474             [% license %]
475              
476             =cut
477              
478             1;