File Coverage

blib/lib/Object/Annotate.pm
Criterion Covered Total %
statement 105 116 90.5
branch 23 36 63.8
condition 18 45 40.0
subroutine 18 21 85.7
pod 11 11 100.0
total 175 229 76.4


line stmt bran cond sub pod time code
1 2     2   848264 use warnings;
  2         9  
  2         114  
2 2     2   38 use strict;
  2         5  
  2         125  
3             package Object::Annotate;
4             # ABSTRACT: mix in logging-to-database to objects (deprecated)
5             $Object::Annotate::VERSION = '0.025';
6 2     2   24 use Carp ();
  2         6  
  2         62  
7 2     2   2247 use UNIVERSAL::moniker 0.01;
  2         104  
  2         266  
8              
9             #pod =head1 SYNOPSIS
10             #pod
11             #pod B This library was an experiment. It failed. Consider using
12             #pod L instead.
13             #pod
14             #pod package Your::Class;
15             #pod use Object::Annotate annotate => { dsn => '...', table => 'notes' };
16             #pod
17             #pod ...
18             #pod
19             #pod my $object = Your::Class->new( ... );
20             #pod $object->annotate({ event => "created", comment => "(as example)" });
21             #pod
22             #pod =head1 DESCRIPTION
23             #pod
24             #pod Object::Annotate is a mixin that provides any class with method for storing
25             #pod and retrieving notes about its objects. It can also produce objects which
26             #pod exist only to store annotations about abstract (uninstantiated) objects,
27             #pod procedures, or concepts.
28             #pod
29             #pod =head1 USAGE
30             #pod
31             #pod To mix Object::Annotate into a class, just C it. To create a classless
32             #pod annotator object, use Object::Annotate's C method. Both of these usages
33             #pod accept the same arguments:
34             #pod
35             #pod db - options for the database in which notes are stored; a hashref:
36             #pod
37             #pod dsn - the DSN to pass to Class::DBI to create a connection
38             #pod user - the username to use in connecting to the database
39             #pod pass - the password to use in connecting to the database
40             #pod table - the table in which annotations are stored
41             #pod sequence - if given, the Class::DBI table's primary key values comes from
42             #pod this sequence; see L for more information
43             #pod
44             #pod columns - columns for the annotation table
45             #pod obj_class - the class name to use for annotations for this class
46             #pod (defaults to Class->moniker, see UNIVERSAL::moniker)
47             #pod id_attr - the object attribute to use for "id"; called as a method
48             #pod if it's a scalar ref, it's de-ref'd and used as a constant string
49             #pod
50             #pod =cut
51              
52             # We'll store the constructed Class::DBI subclasses here.
53             # $class_for->{ $dsn }->{ $table } = $class
54             my $class_for = {};
55              
56             # We'll keep a counter, here, to use to form unique class names.
57             my $current_suffix = 0;
58              
59             # The "id" column isn't here because we want it first, always.
60             my %note_columns = (
61             mandatory => [ qw(class object_id created) ],
62             # I plan to use these values in the future. -- rjbs, 2006-01-13
63             # default => [ qw(event attr old_val new_val via comment expire_time) ],
64             default => [ qw(event attr old_val new_val via comment expire_time) ],
65             );
66              
67 2         25 use Sub::Exporter 0.92 -setup => {
68             groups => { annotator => \&setup_class },
69 2     2   32752 };
  2         32534  
70              
71             #pod =head2 new
72             #pod
73             #pod You can use the C method to create a singularity -- an object that can
74             #pod annotate as if it was of a class that used Object::Annotate, but is of its own
75             #pod unique class.
76             #pod
77             #pod my $notepad = Object::Annotate->new({ db => { ... } });
78             #pod
79             #pod =cut
80              
81             sub new {
82 0     0 1 0 my ($self, $arg) = @_;
83 0 0       0 my $class = (ref $self) ? ref $self : $self;
84              
85 0         0 my $target
86             = sprintf '%s::Singularity::0x%08x', $class, ++$current_suffix;
87              
88 0         0 $self->setup_class($target, $arg);
89              
90 0         0 my $singularity = \do { undef };
  0         0  
91 0         0 bless $singularity => $target;
92             }
93              
94             #pod =head1 METHODS
95             #pod
96             #pod These methods are not provided by Object::Annotate, but are installed into
97             #pod classes that use Object::Annotate.
98             #pod
99             #pod =head2 annotations_class
100             #pod
101             #pod my $annotations_class = Your::Class->annotations_class;
102             #pod
103             #pod This method returns the name of the automatically constructed class that
104             #pod handles annotations for the class or object on which it is installed.
105             #pod
106             #pod =head2 annotate
107             #pod
108             #pod $object->annotate({
109             #pod event => 'update',
110             #pod attr => 'priority',
111             #pod old_val => 1,
112             #pod new_val => 3,
113             #pod });
114             #pod
115             #pod This method creates an annotation for the object on which it is called.
116             #pod
117             #pod =head2 search_annotations
118             #pod
119             #pod # search all annotations for this class
120             #pod my @notes = Class->search_annotations({ event => 'explosion' });
121             #pod
122             #pod # searches only annotations for this object
123             #pod my @notes = $object->search_annotations({ event => 'explosion' });
124             #pod
125             #pod This method searches through the annotations for a class or an object, using
126             #pod the Class::DBI C method.
127             #pod
128             #pod =cut
129              
130             #pod =head1 INTERNALS
131             #pod
132             #pod =head2 setup_class
133             #pod
134             #pod Object::Annotate->setup_class('annotator', \%arg, \%col);
135             #pod
136             #pod This method does the heavy lifting needed to turn the class named by C<$target>
137             #pod into one that does annotation. It is a group generator as described in
138             #pod L.
139             #pod
140             #pod =cut
141              
142             sub setup_class {
143 3     3 1 1396 my ($self, $name, $arg, $col) = @_;
144              
145 3   33     14 $arg->{db}{dsn} ||= $self->default_dsn;
146 3   33     11 $arg->{db}{table} ||= $self->default_table;
147              
148 3   33     17 $arg->{db}{user} ||= $self->default_user;
149 3   33     15 $arg->{db}{pass} ||= $self->default_pass;
150              
151 3   33     14 $arg->{db}{sequence} ||= $self->_default_sequence;
152              
153 3 50 25     27 if ($arg->{noun} xor $arg->{verb}) {
    50 33        
154 0         0 Carp::croak 'you must supply either both or neither "noun" and "verb"';
155             } elsif (not ($arg->{noun} or $arg->{verb})) {
156 3         8 @$arg{qw(noun verb)} = qw(annotations annotate);
157             }
158              
159 3         9 my $class = $self->class_for($arg);
160              
161 3         6 my $obj_class = $arg->{obj_class};
162              
163             my %build_option = (
164             obj_class => $obj_class,
165             id_attr => $arg->{id_attr} || 'id',
166              
167             noun => $arg->{noun},
168             verb => $arg->{verb},
169 3   100     24 );
170              
171             my $annotator = $self->build_annotator({
172             %build_option,
173             columns => $arg->{columns},
174 3   50     47 set_time => ($arg->{db}{dsn} && (scalar $arg->{db}{dsn} =~ /SQLite/)),
175             });
176              
177             my $return = {
178 25     25   13961 "$arg->{noun}_class" => sub { $class },
179 3         27 $arg->{verb} => $annotator,
180             "search_$arg->{noun}" => $self->build_searcher(\%build_option),
181             };
182             }
183              
184             #pod =head2 class_for
185             #pod
186             #pod my $class = Object::Annotate->class_for(\%arg);
187             #pod
188             #pod This method returns the class to use for the described database and table,
189             #pod constructing it (see C>) if needed.
190             #pod
191             #pod Valid arguments are (for all, see the L section): dsn, table, db_user,
192             #pod db_pass, sequence
193             #pod
194             #pod See the L section, above, for information on these arguments, which
195             #pod typically are passed along by the import routine.
196             #pod
197             #pod =cut
198              
199             sub class_for {
200 3     3 1 6 my ($self, $arg) = @_;
201              
202 3         7 my $dsn = $arg->{db}{dsn};
203 3         5 my $table = $arg->{db}{table};
204              
205 3         6 my $user = $arg->{db}{user};
206 3         5 my $pass = $arg->{db}{pass};
207              
208             # Try to find an already-constructed class.
209             my $class = ! $arg->{extra_setup}
210             && exists $class_for->{ $dsn }
211             && exists $class_for->{ $dsn }->{ $table }
212 3   33     23 && $class_for->{ $dsn }->{ $table };
213              
214 3 100       14 return $class if $class;
215              
216             # If we have no class built for this combination, build it.
217             $class = $self->construct_cdbi_class({
218             dsn => $dsn,
219             user => $user,
220             pass => $pass,
221             table => $table,
222             columns => $arg->{columns},
223             sequence => $arg->{db}{sequence},
224             base_class => $arg->{base_class},
225 1         6 });
226              
227 1 50       7 $arg->{extra_setup}->($class) if $arg->{extra_setup};
228              
229 1         5 return $class;
230             }
231              
232             #pod =head2 default_dsn
233             #pod
234             #pod =head2 default_table
235             #pod
236             #pod =head2 default_user
237             #pod
238             #pod =head2 default_pass
239             #pod
240             #pod These methods return the default database settings to use if none is specified
241             #pod when importing Object::Annotate. The built-in behavior is to return the
242             #pod OBJ_ANNOTATE_DSN, OBJ_ANNOTATE_TABLE, etc. environment variables.
243             #pod
244             #pod =head2 default_base_class
245             #pod
246             #pod This method returns the class from which the annotator subclass will inherit.
247             #pod It defaults to Class::DBI.
248             #pod
249             #pod =cut
250              
251 0     0 1 0 sub default_dsn { $ENV{OBJ_ANNOTATE_DSN}; }
252 0     0 1 0 sub default_table { $ENV{OBJ_ANNOTATE_TABLE}; }
253 3     3 1 20 sub default_user { $ENV{OBJ_ANNOTATE_USER}; }
254 3     3 1 12 sub default_pass { $ENV{OBJ_ANNOTATE_PASS}; }
255 1     1 1 4 sub default_base_class { 'Class::DBI' }
256              
257       3     sub _default_sequence { }
258              
259             #pod =head2 construct_cdbi_class
260             #pod
261             #pod my $new_class = Object::Annotate->construct_cdbi_class(\%arg);
262             #pod
263             #pod This method sets up a new Class::DBI subclass that will store in the database
264             #pod described by the arguments.
265             #pod
266             #pod Valid arguments are:
267             #pod
268             #pod dsn - the dsn for the database in which to store
269             #pod user - the database user as whom to connect
270             #pod pass - the database password
271             #pod table - the table in which to store annotations
272             #pod columns - the extra columns for the table
273             #pod base_class - class from which the new class inherits (default: Class::DBI)
274             #pod
275             #pod =cut
276              
277             sub construct_cdbi_class {
278 1     1 1 2 my ($class, $arg) = @_;
279              
280 1         9 my $new_class
281             = sprintf '%s::Construct::0x%08x', __PACKAGE__, ++$current_suffix;
282              
283 1   33     8 $arg->{base_class} ||= $class->default_base_class;
284              
285 1 50       81 eval "require $arg->{base_class};" or die $@;
286 1         75972 do {
287 2     2   2538 no strict 'refs';
  2         5  
  2         1505  
288 1         5 @{$new_class . '::ISA'} = $arg->{base_class};
  1         24  
289             };
290              
291 1 50       5 if ($arg->{dsn}) {
292 1         11 $new_class->connection($arg->{dsn}, $arg->{user}, $arg->{pass});
293             }
294              
295 1         242 $new_class->table($arg->{table});
296              
297 1         49 my @columns = @{ $note_columns{mandatory} };
  1         5  
298 1 50       3 my @extra_columns = @{ $arg->{columns} || $note_columns{default} };
  1         9  
299 1         4 push @columns, @extra_columns;
300              
301 1         7 $new_class->columns(All => ('id', @columns));
302              
303 1 50       2275 $new_class->sequence($arg->{sequence}) if $arg->{sequence};
304              
305 1         6 $new_class->db_Main->{ AutoCommit } = 1;
306              
307 1   50     3461 return $class_for->{ $arg->{dsn} || '' }->{ $arg->{table} } = $new_class;
308             }
309              
310             #pod =head2 build_annotator
311             #pod
312             #pod my $code = Object::Annotate->build_annotator(\%arg);
313             #pod
314             #pod This builds the routine that will be installed as "annotate" in the importing
315             #pod class. It returns a coderef.
316             #pod
317             #pod It takes the following arguments:
318             #pod
319             #pod obj_class - the class name to use for this class's log entries
320             #pod id_attr - the method to use to get object ids; if a scalar ref,
321             #pod the dereferenced string is used as a constant
322             #pod set_time - if true, the created value will be created as the current time
323             #pod
324             #pod =cut
325              
326             sub build_annotator {
327 3     3 1 6 my ($self, $arg) = @_;
328              
329 3         7 my $obj_class = $arg->{obj_class};
330 3         5 my $id_attr = $arg->{id_attr};
331 3         7 my $set_time = $arg->{set_time};
332              
333             my @columns
334 3 50       26 = $arg->{columns} ? @{ $arg->{columns} } : @{ $note_columns{default} };
  0         0  
  3         20  
335              
336 3         5 my $noun = $arg->{noun};
337              
338             my $annotator = sub {
339             # This $arg purposefully shadows the previous; I don't want to enclose
340             # those args. -- rjbs, 2006-01-05
341 9     9   6640 my ($self, $arg) = @_;
342 9   33     127 my $obj_class = $arg->{obj_class} || $self->moniker;
343              
344 9         250 my $id;
345 9 100       44 if (ref $id_attr) {
346 5         15 $id = $$id_attr;
347             } else {
348 4         31 $id = $self->$id_attr;
349 4 50       44 Carp::croak "couldn't get id for $self via $id_attr" unless $id;
350             }
351              
352             # build up only those attributes we declared
353 9         28 my %attr;
354 9         43 for (@columns) {
355 63 100       264 next unless exists $arg->{$_};
356 17         71 $attr{$_} = $arg->{$_};
357             }
358              
359 9 50       65 $attr{created} = time if $set_time;
360              
361 9         34 my $class_name_method = "$noun\_class";
362 9         47 my $request = $self->$class_name_method->create({
363             class => $obj_class,
364             object_id => $id,
365             %attr,
366             });
367              
368 9         151527 return $request;
369 3         23 };
370              
371 3         9 return $annotator;
372             }
373              
374             #pod =head2 build_searcher
375             #pod
376             #pod my $code = Object::Annotate->build_searcher(\%arg);
377             #pod
378             #pod This builds the routine that will be installed as "search_annotations" in the
379             #pod importing class. It returns a coderef.
380             #pod
381             #pod It takes the following arguments:
382             #pod
383             #pod obj_class - the class name to use for this class's log entries
384             #pod id_attr - the method to use to get object ids; if a scalar ref,
385             #pod the dereferenced string is used as a constant
386             #pod
387             #pod =cut
388              
389             sub build_searcher {
390 3     3 1 7 my ($self, $arg) = @_;
391              
392 3         7 my $obj_class = $arg->{obj_class};
393 3         5 my $id_attr = $arg->{id_attr};
394              
395 3         7 my $noun = $arg->{noun};
396              
397             my $searcher = sub {
398 10     10   13130 my ($self, $arg) = @_;
399 10   33     83 my $obj_class = $arg->{obj_class} || $self->moniker;
400 10   50     190 $arg ||= {};
401              
402 10         16 my $id;
403 10 100       45 if (ref $id_attr) {
    100          
404 3         8 $id = $$id_attr;
405             } elsif (ref $self) {
406 5         37 $id = $self->$id_attr;
407 5 50       38 Carp::croak "couldn't get id for $self via $id_attr" unless $id;
408             }
409              
410 10         33 $arg->{class} = $obj_class;
411 10 100 66     68 $arg->{object_id} = $id if defined $id and not exists $arg->{object_id};
412              
413 10         32 my $class_name_method = "$noun\_class";
414 10         50 $self->$class_name_method->search(%$arg);
415             }
416 3         31 }
417              
418             '2. see footnote #1';
419              
420             __END__