File Coverage

blib/lib/Rose/Planter.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Rose::Planter;
2              
3 1     1   9546 use warnings;
  1         3  
  1         37  
4 1     1   5 use strict;
  1         1  
  1         66  
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.36';
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 1     1   450 use Rose::DB::Object::Loader;
  0            
  0            
71             use Rose::Planter::ConventionManager;
72             use Rose::Planter::Gardener;
73             use List::MoreUtils qw/mesh/;
74             use File::Path qw/mkpath/;
75             use File::Slurp qw/slurp/;
76             use Module::Find;
77             use strict;
78             use warnings;
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             return unless $ENV{ROSE_PLANTER_DEBUG};
89             my $msg = shift;
90             unless ($_logfp) {
91             $_logfp = IO::File->new(">>/tmp/rose.log");
92             }
93             print $_logfp "$msg\n";
94             }
95             }
96              
97             sub import {
98             my ($class, %p) = @_;
99             return unless %p && keys %p;
100             my $from = caller;
101             return $class->_read_classes(%p, seed => $from) || $class->_write_classes(%p, seed => $from) || $class->_setup_classes(%p);
102             }
103              
104             sub _class2path {
105             my $cl = shift;
106             $cl =~ s[::][/]g;
107             return $cl;
108             }
109              
110             sub _read_classes {
111             my ($class, %params) = @_;
112             my $seed = $params{seed};
113             return 0 if $seed && $are_planting{$seed};
114             my $seed_dir = _class2path($seed).'.pm';
115             my $inc_dir = $INC{$seed_dir} or return 0; # e.g. testing
116             my ($abs_seed_dir) = $inc_dir=~ m{^(.*)/$seed_dir$};
117             my $prefix = $params{loader_params}{class_prefix} ;
118             my $autolib = $seed. '::autolib';
119             my $autodir = join '/', $abs_seed_dir, _class2path($autolib);
120             _trace "Looking for $autolib in $autodir";
121             unshift @INC, $autodir;
122             local $SIG{__WARN__} = sub {
123             return if $_[0] =~ /^subroutine.*redefined/i;
124             warn @_;
125             };
126             setmoduledirs($abs_seed_dir);
127             my @used = useall $autolib;
128             _trace "used $_" for @used;
129             shift @INC;
130             unless (@used) {
131             warn "# No autolib found ($autolib), try :\n";
132             warn "# Rose::Planter->plant(q[$seed] => q[$autodir])\n";
133             return 0;
134             };
135             do { s/${autolib}:://; } for @used;
136             $class->_setup_classes(made => \@used, %params);
137             return 1;
138             }
139              
140             sub _sow {
141             my $class = shift;
142             my $seed = shift;
143             my $dir = shift;
144             $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             my $class = shift;
165             my $seed = shift;
166             my $dir = shift;
167             $class->_sow($seed => $dir);
168             if ($INC{_class2path($seed).'.pm'}) {
169             die "Cannot plant $seed since it has already been loaded.";
170             }
171             eval "use $seed";
172             die "plant failed : $@" if $@;
173             }
174              
175             sub _add_postamble {
176             my ($db_class, $met,$manager) = @_;
177             my $want = ($manager || $met->class);
178             my $file = $want;
179             $file =~ s[::][/]g;
180             $file .= ".pm";
181             my ($found) = map "$_/$file", grep { -e "$_/$file" } @INC;
182             my $setdb = $db_class && !$manager ? "\n sub init_db { $db_class->new() };\n" : "";
183             if ($found) {
184             _trace "# adding functions from $found";
185             return join "", $setdb, "# EXTRAS LOADED FROM $found : \n", slurp $found;
186             }
187             return "$setdb\n# NOTHING LOADED FOR $want";
188             }
189              
190             sub _write_classes {
191             my $class = shift;
192             my %params = @_;
193             my $dir;
194             my $seed = $params{seed} or die "no seed";
195             return 0 unless $dir = $are_planting{$seed};
196             mkpath $dir;
197             warn "# writing classes to $dir\n";
198             my $db_class = $params{loader_params}{db_class};
199             $params{loader_params}{module_dir} = $dir;
200             $params{loader_params}{module_postamble} = sub { _add_postamble($db_class, @_) };
201             $class->_setup_classes(%params, make_modules => 1);
202             return 1;
203             }
204              
205             sub _setup_classes {
206             my $class = shift;
207             my %params = @_;
208              
209             my %loader_params = %{ $params{loader_params} || {} };
210              
211             unless ($loader_params{base_class} || $loader_params{base_classes}) {
212             $loader_params{base_class} = "Rose::Planter::Soil";
213             }
214              
215             unless ($loader_params{manager_base_class} || $loader_params{manager_base_classes}) {
216             $loader_params{manager_base_class} = "Rose::Planter::Gardener";
217             }
218              
219             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             my $method = $params{make_modules} ? "make_modules" : "make_classes";
225             my @made = $params{made} ? @{ $params{made} } : $loader->$method; # include_tables => ...
226             die "did not make any classes" unless @made > 0;
227             # Keep track of what we made
228             for my $made (@made) {
229             if ( $made->can("meta") ) {
230             _trace "Made object class $made";
231             my $table = $made->meta->table;
232             warn "replacing $table ($table2Class{$table}) with $made"
233             if $table2Class{$table} && $table2Class{$table} ne $made;
234             $table2Class{$table} = $made;
235             if ( $table =~ /^(.*)_def$/ ) {
236             warn "replacing $1 ($table2Class{$1}) with $made"
237             if $table2Class{$1} && $table2Class{$1} ne $made;
238             $deftable2Class{$1} = $made;
239             }
240             }
241             if ( $made->can("get_objects") ) {
242             _trace "Made manager class $made";
243             my $object_class = $made->object_class;
244             my $table = $object_class->meta->table;
245             $table =~ s/_def//;
246             my $plural = Rose::Planter::ConventionManager->new()->singular_to_plural($table);
247             $plural2Class{$plural} = $made;
248             }
249             # Load any extra functions, too.
250             unless ($method eq 'make_modules') {
251             eval "use $made";
252             die "Errors using $made : $@" if $@ && $@ !~ /Can't locate/;
253             }
254             }
255              
256             my %nested_tables = %{ $params{nested_tables} || {} };
257             for my $t (keys %nested_tables) {
258             my $found = $class->find_class($t) or die "could not find class for base table $t";
259             $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             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             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             my $re = join '|', reverse sort $self->tables;
285             return qr[$re];
286             }
287              
288             =head2 plurals
289              
290             Return a list of all plurals.
291              
292             =cut
293              
294             sub plurals {
295             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             my $self = shift;
306             my $re = join '|', reverse sort $self->plurals;
307             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             my $class = shift;
333             my $table = shift;
334             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             my $package = shift;
347             my $table = shift;
348             my @keys = @_;
349              
350             my $object_class = Rose::Planter->find_class($table) or die "could not find class for $table";
351             return unless $object_class->can("meta");
352              
353             foreach my $keycols ([$object_class->meta->primary_key_column_names],
354             $object_class->meta->unique_keys_column_names) {
355             next unless @keys == @$keycols;
356             my $object = $object_class->new( mesh @$keycols, @keys );
357             return $object if $object->load(speculative => 1,
358             with => $object_class->nested_tables);
359             }
360              
361             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;