File Coverage

blib/lib/Rose/Planter.pm
Criterion Covered Total %
statement 87 178 48.8
branch 21 64 32.8
condition 7 33 21.2
subroutine 21 32 65.6
pod 7 7 100.0
total 143 314 45.5


line stmt bran cond sub pod time code
1             package Rose::Planter;
2              
3 2     2   9366 use warnings;
  2         5  
  2         127  
4 2     2   13 use strict;
  2         6  
  2         136  
5              
6             =head1 NAME
7              
8             Rose::Planter - Keep track of classes created with Rose::DB::Object::Loader.
9              
10             =cut
11              
12             our $VERSION = '0.37';
13              
14             =head1 SYNOPSIS
15              
16             In My/Objects.pm :
17              
18             package My::Objects;
19              
20             use Rose::Planter
21             loader_params => {
22             class_prefix => "My::Object",
23             db_class => "My::DB",
24             },
25             nested_tables => {
26             foo => [ qw(params) ]
27             },
28             convention_manager_params => {};
29              
30             In plant.pl :
31              
32             #!/usr/bin/env perl
33              
34             Rose::Planter->plant("My::Objects" => "My/Objects/autolib");
35              
36             In another file :
37              
38             use My::Objects;
39              
40             my $class = Rose::Planter->find_class("my_table");
41             my $object = Rose::Planter->find_object("my_table","my_key1","my_key2");
42              
43              
44             =head1 DESCRIPTION
45              
46             This is a thin layer above L for keeping
47             track of and managing classes which are created based on a database
48             schema. It will transparently either query the database using
49             L or use an auto-generated class
50             hierarchy.
51              
52             This module works well with L and
53             L to create a simple RESTful service based on
54             a database schema. It can be used to provide a common base
55             class, conventions, and settings for a collection of services,
56             as well as describe which tables within a schema should be
57             coupled with other tables during CRUD operations.
58              
59             By default the loader is told that the base_class should be
60             L. You can send "base_classes" or
61             just "base_class" as loader_params to changes this.
62              
63             nested_tables will cause find_object to automatically join tables
64             connected to the primary table with a many-to-one relationship.
65              
66             =head1 FUNCTIONS
67              
68             =cut
69              
70 2     2   2792 use Rose::DB::Object::Loader;
  2         752628  
  2         72  
71 2     2   1217 use Rose::Planter::ConventionManager;
  2         6  
  2         65  
72 2     2   963 use Rose::Planter::Gardener;
  2         6  
  2         65  
73 2     2   13 use List::MoreUtils qw/mesh/;
  2         5  
  2         40  
74 2     2   1269 use File::Path qw/mkpath/;
  2         4  
  2         118  
75 2     2   2246 use Path::Tiny qw/path/;
  2         19099  
  2         128  
76 2     2   1485 use Module::Find;
  2         2509  
  2         135  
77 2     2   11 use strict;
  2         4  
  2         55  
78 2     2   11 use warnings;
  2         4  
  2         4008  
79              
80             our %table2Class; # mapping from table name to class name.
81             our %deftable2Class; # map for prefix of tables ending in _def to class name.
82             our %plural2Class; # map plurals of tables to manager classes
83             our %are_planting; # classes we are planting right now
84             {
85             my $_logfp;
86             sub _trace {
87             # Hook for tracing at compile time
88 4 50   4   14 return unless $ENV{ROSE_PLANTER_DEBUG};
89 0         0 my $msg = shift;
90 0 0       0 unless ($_logfp) {
91 0         0 $_logfp = IO::File->new(">>/tmp/rose.log");
92             }
93 0         0 print $_logfp "$msg\n";
94             }
95             }
96              
97             sub import {
98 1     1   13 my ($class, %p) = @_;
99 1 50 33     13 return unless %p && keys %p;
100 1         3 my $from = caller;
101 1   33     4 return $class->_read_classes(%p, seed => $from) || $class->_write_classes(%p, seed => $from) || $class->_setup_classes(%p);
102             }
103              
104             sub _class2path {
105 1     1   3 my $cl = shift;
106 1         2 $cl =~ s[::][/]g;
107 1         3 return $cl;
108             }
109              
110             sub _read_classes {
111 1     1   3 my ($class, %params) = @_;
112 1         2 my $seed = $params{seed};
113 1 50 33     6 return 0 if $seed && $are_planting{$seed};
114 1         3 my $seed_dir = _class2path($seed).'.pm';
115 1 50       10 my $inc_dir = $INC{$seed_dir} or return 0; # e.g. testing
116 0         0 my ($abs_seed_dir) = $inc_dir=~ m{^(.*)/$seed_dir$};
117 0         0 my $prefix = $params{loader_params}{class_prefix} ;
118 0         0 my $autolib = $seed. '::autolib';
119 0         0 my $autodir = join '/', $abs_seed_dir, _class2path($autolib);
120 0         0 _trace "Looking for $autolib in $autodir";
121 0         0 unshift @INC, $autodir;
122             local $SIG{__WARN__} = sub {
123 0 0   0   0 return if $_[0] =~ /^subroutine.*redefined/i;
124 0         0 warn @_;
125 0         0 };
126 0         0 setmoduledirs($abs_seed_dir);
127 0         0 my @used = useall $autolib;
128 0         0 _trace "used $_" for @used;
129 0         0 shift @INC;
130 0 0       0 unless (@used) {
131 0         0 warn "# No autolib found ($autolib), try :\n";
132 0         0 warn "# Rose::Planter->plant(q[$seed] => q[$autodir])\n";
133 0         0 return 0;
134             };
135 0         0 do { s/${autolib}:://; } for @used;
  0         0  
136 0         0 $class->_setup_classes(made => \@used, %params);
137 0         0 return 1;
138             }
139              
140             sub _sow {
141 0     0   0 my $class = shift;
142 0         0 my $seed = shift;
143 0         0 my $dir = shift;
144 0         0 $are_planting{$seed} = $dir;
145             }
146              
147             =head2 plant
148              
149             Rose::Planter->plant($class => $dir)
150              
151             Write a class hierarchy to disk. This will send the
152             make_modules parameter to L.
153             The directory used will default to My/Objects/autolib.
154             This directory is also searched when My::Objects uses
155             Rose::Planter.
156              
157             For each class, if the class already exists in @INC, the
158             source from that class will be included in the autogenerated
159             class.
160              
161             =cut
162              
163             sub plant {
164 0     0 1 0 my $class = shift;
165 0         0 my $seed = shift;
166 0         0 my $dir = shift;
167 0         0 $class->_sow($seed => $dir);
168 0 0       0 if ($INC{_class2path($seed).'.pm'}) {
169 0         0 die "Cannot plant $seed since it has already been loaded.";
170             }
171 0         0 eval "use $seed";
172 0 0       0 die "plant failed : $@" if $@;
173             }
174              
175             sub _add_postamble {
176 0     0   0 my ($db_class, $met,$manager) = @_;
177 0   0     0 my $want = ($manager || $met->class);
178 0         0 my $file = $want;
179 0         0 $file =~ s[::][/]g;
180 0         0 $file .= ".pm";
181 0         0 my ($found) = map "$_/$file", grep { -e "$_/$file" } @INC;
  0         0  
182 0 0 0     0 my $setdb = $db_class && !$manager ? "\n sub init_db { $db_class->new() };\n" : "";
183 0 0       0 if ($found) {
184 0         0 _trace "# adding functions from $found";
185 0         0 return join "", $setdb, "# EXTRAS LOADED FROM $found : \n", path($found)->slurp;
186             }
187 0         0 return "$setdb\n# NOTHING LOADED FOR $want";
188             }
189              
190             sub _write_classes {
191 1     1   1 my $class = shift;
192 1         3 my %params = @_;
193 1         2 my $dir;
194 1 50       6 my $seed = $params{seed} or die "no seed";
195 1 50       9 return 0 unless $dir = $are_planting{$seed};
196 0         0 mkpath $dir;
197 0         0 warn "# writing classes to $dir\n";
198 0         0 my $db_class = $params{loader_params}{db_class};
199 0         0 $params{loader_params}{module_dir} = $dir;
200 0     0   0 $params{loader_params}{module_postamble} = sub { _add_postamble($db_class, @_) };
  0         0  
201 0         0 $class->_setup_classes(%params, make_modules => 1);
202 0         0 return 1;
203             }
204              
205             sub _setup_classes {
206 1     1   2 my $class = shift;
207 1         2 my %params = @_;
208              
209 1 50       2 my %loader_params = %{ $params{loader_params} || {} };
  1         7  
210              
211 1 50 33     5 unless ($loader_params{base_class} || $loader_params{base_classes}) {
212 1         3 $loader_params{base_class} = "Rose::Planter::Soil";
213             }
214              
215 1 50 33     10 unless ($loader_params{manager_base_class} || $loader_params{manager_base_classes}) {
216 1         2 $loader_params{manager_base_class} = "Rose::Planter::Gardener";
217             }
218              
219 1         15 my $loader = Rose::DB::Object::Loader->new(
220             warn_on_missing_primary_key => 1,
221             convention_manager => "Rose::Planter::ConventionManager",
222             %loader_params
223             );
224 1 50       181 my $method = $params{make_modules} ? "make_modules" : "make_classes";
225 1 50       6 my @made = $params{made} ? @{ $params{made} } : $loader->$method; # include_tables => ...
  0         0  
226 1 50       24 die "did not make any classes" unless @made > 0;
227             # Keep track of what we made
228 1         3 for my $made (@made) {
229 4 100       40 if ( $made->can("meta") ) {
230 2         9 _trace "Made object class $made";
231 2         7 my $table = $made->meta->table;
232             warn "replacing $table ($table2Class{$table}) with $made"
233 2 50 33     35 if $table2Class{$table} && $table2Class{$table} ne $made;
234 2         5 $table2Class{$table} = $made;
235 2 50       8 if ( $table =~ /^(.*)_def$/ ) {
236             warn "replacing $1 ($table2Class{$1}) with $made"
237 0 0 0     0 if $table2Class{$1} && $table2Class{$1} ne $made;
238 0         0 $deftable2Class{$1} = $made;
239             }
240             }
241 4 100       40 if ( $made->can("get_objects") ) {
242 2         8 _trace "Made manager class $made";
243 2         56 my $object_class = $made->object_class;
244 2         16 my $table = $object_class->meta->table;
245 2         25 $table =~ s/_def//;
246 2         11 my $plural = Rose::Planter::ConventionManager->new()->singular_to_plural($table);
247 2         85 $plural2Class{$plural} = $made;
248             }
249             # Load any extra functions, too.
250 4 50       12 unless ($method eq 'make_modules') {
251 1     1   594 eval "use $made";
  0     1   0  
  0     1   0  
  1     1   494  
  0         0  
  0         0  
  1         500  
  0         0  
  0         0  
  1         483  
  0         0  
  0         0  
  4         230  
252 4 50 33     2600 die "Errors using $made : $@" if $@ && $@ !~ /Can't locate/;
253             }
254             }
255              
256 1 50       3 my %nested_tables = %{ $params{nested_tables} || {} };
  1         8  
257 1         23 for my $t (keys %nested_tables) {
258 0 0         my $found = $class->find_class($t) or die "could not find class for base table $t";
259 0           $found->nested_tables($nested_tables{$t});
260             }
261             }
262              
263             =head2 tables
264              
265             Return a list of all tables.
266              
267             =cut
268              
269             sub tables {
270 0     0 1   return (keys %table2Class, keys %deftable2Class);
271             }
272              
273             =head2 regex_for_tables
274              
275             Create a regex that matches all the tables.
276              
277             =cut
278              
279             sub regex_for_tables {
280 0     0 1   my $self = shift;
281             # the reverse sort is necessary so that tables which
282             # are prefixes to others match. e.g. app, appgroup
283             # see https://github.com/kraih/mojo/issues/183
284 0           my $re = join '|', reverse sort $self->tables;
285 0           return qr[$re];
286             }
287              
288             =head2 plurals
289              
290             Return a list of all plurals.
291              
292             =cut
293              
294             sub plurals {
295 0     0 1   return keys %plural2Class;
296             }
297              
298             =head2 regex_for_plurals
299              
300             Create a regex that matches all the plurals.
301              
302             =cut
303              
304             sub regex_for_plurals {
305 0     0 1   my $self = shift;
306 0           my $re = join '|', reverse sort $self->plurals;
307 0           return qr[$re];
308             }
309              
310             =head2 find_class
311              
312             Given the name of a database table, return the object class associated
313             with it. e.g.
314              
315             Rose::Planter->find_class("app");
316              
317             If the table name ends in _def, the prefix may be used, e.g
318             these are equivalent :
319              
320             Rose::Planter->find_class("esdt_def");
321             Rose::Planter->find_class("esdt");
322              
323             Also, given the plural of the name of a database table, return the
324             manager class associated with it.
325              
326             Rose::Planter->find_class("esdts");
327             Rose::Planter->find_class("apps");
328              
329             =cut
330              
331             sub find_class {
332 0     0 1   my $class = shift;
333 0           my $table = shift;
334 0   0       return $table2Class{$table} || $deftable2Class{$table} || $plural2Class{$table};
335             }
336              
337             =head2 find_object
338              
339             Given a table and a primary or other unique key(s), find a load an object.
340              
341             Return false if there is no object matching that key.
342              
343             =cut
344              
345             sub find_object {
346 0     0 1   my $package = shift;
347 0           my $table = shift;
348 0           my @keys = @_;
349              
350 0 0         my $object_class = Rose::Planter->find_class($table) or die "could not find class for $table";
351 0 0         return unless $object_class->can("meta");
352              
353 0           foreach my $keycols ([$object_class->meta->primary_key_column_names],
354             $object_class->meta->unique_keys_column_names) {
355 0 0         next unless @keys == @$keycols;
356 0           my $object = $object_class->new( mesh @$keycols, @keys );
357 0 0         return $object if $object->load(speculative => 1,
358             with => $object_class->nested_tables);
359             }
360              
361 0           return;
362             }
363              
364             =head1 NOTES
365              
366             This is a beta release. The API is subject to change without notice.
367              
368             =head1 AUTHORS
369              
370             Marty Brandon
371              
372             Brian Duggan
373              
374             Graham Ollis
375              
376             Curt Tilmes
377              
378             =head1 BUGS
379              
380             Currently only really used/tested against postgres.
381              
382             =cut
383              
384             1;