File Coverage

blib/lib/Mixin/ExtraFields.pm
Criterion Covered Total %
statement 63 63 100.0
branch 14 16 87.5
condition 7 9 77.7
subroutine 16 16 100.0
pod 9 9 100.0
total 109 113 96.4


line stmt bran cond sub pod time code
1 4     4   204625 use strict;
  4         40  
  4         124  
2 4     4   22 use warnings;
  4         8  
  4         155  
3              
4             package Mixin::ExtraFields 0.140003;
5             # ABSTRACT: add extra stashes of data to your objects
6              
7 4     4   21 use Carp ();
  4         9  
  4         62  
8 4     4   1730 use String::RewritePrefix;
  4         59432  
  4         28  
9              
10             #pod =head1 SYNOPSIS
11             #pod
12             #pod If you use the ExtraFields mixin in your class:
13             #pod
14             #pod package Corporate::WorkOrder;
15             #pod
16             #pod use Mixin::ExtraFields -fields => {
17             #pod id => 'workorder_id',
18             #pod moniker => 'note',
19             #pod driver => { HashGuts => { hash_key => '_notes' } }
20             #pod };
21             #pod
22             #pod ...your objects will then have methods for manipulating their extra fields:
23             #pod
24             #pod my $workorder = Corporate::WorkOrder->retrieve(1234);
25             #pod
26             #pod if ($workorder->note_exists('debug_next')) {
27             #pod warn $workorder->note_get('debug_next');
28             #pod $workorder->note_delete('debug_next');
29             #pod }
30             #pod
31             #pod if ($workorder->note_get('time_bomb')) {
32             #pod $workorder->note_delete_all;
33             #pod $workorder->note_set(
34             #pod last_explosion => time,
35             #pod explosion_cause => 'time bomb',
36             #pod );
37             #pod }
38             #pod
39             #pod =head1 DESCRIPTION
40             #pod
41             #pod Sometimes your well-defined object needs a way to tack on arbirary extra
42             #pod fields. This might be a set of session-specific ephemeral data, a stash of
43             #pod settings that need to be easy to grow over time, or any sort of name-and-value
44             #pod parameters. Adding more and more methods can be cumbersome, and may not be
45             #pod helpful if the names vary greatly. Accessing an object's guts directly is
46             #pod simple, but is difficult to control when subclassing, and can make altering
47             #pod your object's structure difficult.
48             #pod
49             #pod Mixin::ExtraFields provides a simple way to add an arbitrary number of stashes
50             #pod for named data. These data can be stored in the object, in a database, or
51             #pod anywhere else. The storage mechanism is abstracted away from the provided
52             #pod interface, so one storage mechanism can be easily swapped for another.
53             #pod Multiple ExtraFields stashes can be mixed into one class, using one or many
54             #pod storage mechanisms.
55             #pod
56             #pod =head1 MIXING IN
57             #pod
58             #pod To create a stash of extra fields, just C Mixin::ExtraFields and import
59             #pod the C group like this:
60             #pod
61             #pod use Mixin::ExtraFields -fields => { driver => 'SomeDriver' };
62             #pod
63             #pod The only argument required for the group is C, which names the driver
64             #pod (storage mechanism) to use. For more information, see L,
65             #pod below.
66             #pod
67             #pod Other valid arguments are:
68             #pod
69             #pod id - the name of the method to call on objects to get their unique identifier
70             #pod default: id; an explicit undef will use each object's reference addr
71             #pod
72             #pod moniker - the name to use in forming mixed-in method names
73             #pod default: extra
74             #pod
75             #pod =head2 Specifying a Driver
76             #pod
77             #pod The C argument can be given as either a driver identifier or a
78             #pod reference to a hash of options. If given as a hash reference, one of the
79             #pod entries in the hash must be C, giving the driver identifier for the
80             #pod driver.
81             #pod
82             #pod A driver identifier must be either:
83             #pod
84             #pod =over
85             #pod
86             #pod =item * an object of a class descended from the driver base class
87             #pod
88             #pod =item * a partial class name, to follow the driver base class name
89             #pod
90             #pod =item * a full class name, prepended with +
91             #pod
92             #pod =back
93             #pod
94             #pod The driver base class is provided by the C> method. In
95             #pod almost all cases, it will be C.
96             #pod
97             #pod =head1 GENERATED METHODS
98             #pod
99             #pod The default implementation of Mixin::ExtraFields provides a number of methods
100             #pod for accessing the extras.
101             #pod
102             #pod Wherever "extra" appears in the following method names, the C argument
103             #pod given to the C group will be used instead. For example, if the use
104             #pod statement looked like this:
105             #pod
106             #pod use Mixin::ExtraFields -fields => { moniker => 'info', driver => 'HashGuts' };
107             #pod
108             #pod ...then a method called C would be generated, rather than
109             #pod C. The C group also respects renaming options documented
110             #pod in L.
111             #pod
112             #pod =head2 exists_extra
113             #pod
114             #pod if ($obj->exists_extra($name)) { ... }
115             #pod
116             #pod This method returns true if there is an entry in the extras for the given name.
117             #pod
118             #pod =head2 get_extra
119             #pod
120             #pod =head2 get_detailed_extra
121             #pod
122             #pod my $value = $obj->get_extra($name);
123             #pod
124             #pod my $value_hash = $obj->get_detailed_extra($name);
125             #pod
126             #pod These methods return the entry for the given name. If none exists, the method
127             #pod returns undef. The detailed version of this method will return a hashref
128             #pod describing all information available about the entry. While this information
129             #pod is driver-specific, it is required to have an entry for the key C,
130             #pod providing the value that would have been returned by C.
131             #pod
132             #pod =head2 get_all_extra
133             #pod
134             #pod =head2 get_all_detailed_extra
135             #pod
136             #pod my %extra = $obj->get_all_extra;
137             #pod
138             #pod my %extra_hash = $obj->get_all_detailed_extra;
139             #pod
140             #pod These methods return a list of name/value pairs. The values are in the same
141             #pod form as those returned by the get-by-name methods, above.
142             #pod
143             #pod =head2 get_all_extra_names
144             #pod
145             #pod my @names = $obj->get_all_extra_names;
146             #pod
147             #pod This method returns the names of all existing extras.
148             #pod
149             #pod =head2 set_extra
150             #pod
151             #pod $obj->set_extra($name => $value);
152             #pod
153             #pod This method sets the given extra. If no entry existed before, one is created.
154             #pod If one existed for this name, it is replaced.
155             #pod
156             #pod =head2 delete_extra
157             #pod
158             #pod $obj->delete_extra($name);
159             #pod
160             #pod This method deletes the named entry. After deletion, no entry will exist for
161             #pod that name.
162             #pod
163             #pod =head2 delete_all_extra
164             #pod
165             #pod $obj->delete_all_extra;
166             #pod
167             #pod This method deletes all entries for the object.
168             #pod
169             #pod =cut
170              
171             #pod =head1 SUBCLASSING
172             #pod
173             #pod Mixin::ExtraFields can be subclassed to produce different methods, provide
174             #pod different names, or behave differently in other ways. Subclassing
175             #pod Mixin::ExtraFields can produce many distinct and powerful tools.
176             #pod
177             #pod None of the generated methods, above, are implemented in Mixin::ExtraFields.
178             #pod The methods below are its actual methods, which work together to build and
179             #pod export the methods that are mixed in. These are the methods you should
180             #pod override when subclassing Mixin::ExtraFields.
181             #pod
182             #pod For information on writing drivers, see L.
183             #pod
184             #pod =cut
185              
186             #pod =begin wishful_thinking
187             #pod
188             #pod Wouldn't that be super? Too bad that I can't defer the calling of this method
189             #pod until C is called.
190             #pod
191             #pod =head2 default_group_name
192             #pod
193             #pod my $name = Mixin::ExtraFields->default_group_name;
194             #pod
195             #pod This method returns the name to be used as the exported group. It defaults to
196             #pod "fields". By overriding this to return, for example, "stuff," your module
197             #pod could be used as follows:
198             #pod
199             #pod use Mixin::ExtraFields::Subclass -stuff => { moniker => "things" };
200             #pod
201             #pod =end wishful_thinking
202             #pod
203             #pod =cut
204              
205 4         28 use Sub::Exporter 0.972 -setup => {
206             groups => [ fields => \'gen_fields_group', ],
207 4     4   1064 };
  4         60  
208              
209             #pod =head2 default_moniker
210             #pod
211             #pod This method returns the default moniker. The default default moniker defaults
212             #pod to the default "extra".
213             #pod
214             #pod =cut
215              
216 3     3 1 10 sub default_moniker { 'extra' }
217              
218             #pod =head2 methods
219             #pod
220             #pod This method returns a list of base method names to construct and install.
221             #pod These method names will be transformed into the installed method names via
222             #pod C>.
223             #pod
224             #pod my @methods = Mixin::ExtraFields->methods;
225             #pod
226             #pod =cut
227              
228             sub methods {
229 7     7 1 26 qw(
230             exists
231             get_detailed get_all_detailed
232             get get_all
233             get_all_names
234             set
235             delete delete_all
236             )
237             }
238              
239             #pod =head2 method_name
240             #pod
241             #pod my $method_name = Mixin::ExtraFields->method_name($method_base, $moniker);
242             #pod
243             #pod This method returns the method name that will be installed into the importing
244             #pod class. Its default behavior is to join the method base (which comes from the
245             #pod C> method) and the moniker with an underscore, more or less.
246             #pod
247             #pod =cut
248              
249             sub method_name {
250 126     126 1 203 my ($self, $method, $moniker) = @_;
251              
252 126 100       233 return "get_all_$moniker\_names" if $method eq 'get_all_names';
253 112         260 return "$method\_$moniker";
254             }
255              
256             #pod =head2 driver_method_name
257             #pod
258             #pod This method returns the name of the driver method used to implement the given
259             #pod method name. This is primarily useful in the default implementation of
260             #pod MixinExtraFields, where there is a one-to-one correspondence between installed
261             #pod methods and driver methods.
262             #pod
263             #pod Changing this method could very easily cause incompatibility with standard
264             #pod driver classes, and should only be done by the wise, brave, or reckless.
265             #pod
266             #pod =cut
267              
268             sub driver_method_name {
269 63     63 1 99 my ($self, $method) = @_;
270 63         105 $self->method_name($method, 'extra');
271             }
272              
273             #pod =head2 gen_fields_group
274             #pod
275             #pod my $sub_href = Mixin::ExtraFields->gen_fields_group($name, \%arg, \%col);
276             #pod
277             #pod This method is a group generator, as used by L and described in
278             #pod its documentation. It is the method you are least likely to subclass.
279             #pod
280             #pod =cut
281              
282             sub gen_fields_group {
283 9     9 1 9666 my ($class, $name, $arg, $col) = @_;
284              
285 9   66     34 $arg->{driver} ||= $class->default_driver_arg;
286 8         26 my $driver = $class->build_driver($arg->{driver});
287              
288 7         23 my $id_method;
289 7 100 100     43 if (exists $arg->{id} and defined $arg->{id}) {
    100          
290 2         5 $id_method = $arg->{id};
291             } elsif (exists $arg->{id}) {
292 1         5 require Scalar::Util;
293 1         4 $id_method = \&Scalar::Util::refaddr;
294             } else {
295 4         8 $id_method = 'id';
296             }
297              
298 7   66     26 my $moniker = $arg->{moniker} || $class->default_moniker;
299              
300 7         15 my %method;
301 7         24 for my $method_name ($class->methods) {
302 63         127 my $install_method = $class->method_name($method_name, $moniker);
303              
304 63         175 $method{ $install_method } = $class->build_method(
305             $method_name,
306             {
307             id_method => \$id_method,
308             driver => \$driver,
309             moniker => \$moniker, # So that things can refer to one another
310             }
311             );
312             }
313              
314 7         30 return \%method;
315             }
316              
317             #pod =head2 build_method
318             #pod
319             #pod my $code = Mixin::ExtraFields->build_method($method_name, \%arg);
320             #pod
321             #pod This routine builds the requested method. It is passed a method name in the
322             #pod form returned by the C method and a hashref of the following data:
323             #pod
324             #pod id_method - the method to call on objects to get their unique id
325             #pod driver - the storage driver
326             #pod moniker - the moniker of the set of extras being built
327             #pod
328             #pod B The values for the above arguments are references to the values you'd
329             #pod expect. That is, if the id method is "foo" you will be given an reference to
330             #pod the string foo. (This reduces the copies of common values that will be enclosed
331             #pod into generated code.)
332             #pod
333             #pod =cut
334              
335             sub build_method {
336 63     63 1 103 my ($self, $method_name, $arg) = @_;
337              
338             # Remember that these are all passed in as references, to avoid unneeded
339             # copying. -- rjbs, 2006-12-07
340 63         107 my $id_method = $arg->{id_method};
341 63         85 my $driver = $arg->{driver};
342              
343 63         124 my $driver_method = $self->driver_method_name($method_name);
344              
345             return sub {
346 54     54   4224 my $object = shift;
347 54         177 my $id = $object->$$id_method;
348 54 100       497 Carp::confess "couldn't determine id for object" unless defined $id;
349 53         223 $$driver->$driver_method($object, $id, @_);
350 63         332 };
351             }
352              
353             #pod =head2 default_driver_arg
354             #pod
355             #pod my $arg = Mixin::ExtraFields->default_driver_arg;
356             #pod
357             #pod This method a default value for the C argument to the fields group
358             #pod generator. By default, this method will croak if called.
359             #pod
360             #pod =cut
361              
362             sub default_driver_arg {
363 1     1 1 3 my ($class) = shift;
364 1         208 Carp::croak "no driver supplied to $class";
365             }
366              
367             #pod =head2 build_driver
368             #pod
369             #pod my $driver = Mixin::ExtraFields->build_driver($arg);
370             #pod
371             #pod This method constructs and returns the driver object to be used by the
372             #pod generated methods. It is passed the C argument given in the importing
373             #pod code's C statement.
374             #pod
375             #pod =cut
376              
377             sub build_driver {
378 8     8 1 30 my ($self, $arg) = @_;
379              
380 8 50       19 return $arg if Params::Util::_INSTANCE($arg, $self->driver_base_class);
381              
382 8         22 my ($driver_class, $driver_args) = $self->_driver_class_and_args($arg);
383              
384 8 50       29 Carp::croak("invalid class name for driver: $driver_class")
385             unless Params::Util::_CLASS($driver_class);
386              
387 8 100       712 eval "require $driver_class; 1" or Carp::croak $@;
388              
389 7         44 my $driver = $driver_class->from_args($driver_args);
390             }
391              
392             sub _driver_class_and_args {
393 8     8   17 my ($self, $arg) = @_;
394              
395 8         14 my $class;
396 8 100       19 if (ref $arg) {
397 3         11 $class = delete $arg->{class};
398             } else {
399 5         8 $class = $arg;
400 5         10 $arg = {};
401             }
402              
403 8         21 $class = String::RewritePrefix->rewrite(
404             {
405             '+' => '',
406             '=' => '',
407             '' => $self->driver_base_class . '::',
408             },
409             $class,
410             );
411              
412 8         562 return $class, $arg;
413             }
414              
415             #pod =head2 driver_base_class
416             #pod
417             #pod This is the name of the name of the class which drivers are expected to
418             #pod subclass. By default it returns C.
419             #pod
420             #pod =cut
421              
422 16     16 1 97 sub driver_base_class { 'Mixin::ExtraFields::Driver' }
423              
424             #pod =head1 TODO
425             #pod
426             #pod =over
427             #pod
428             #pod =item * handle invocants without ids (classes) and drivers that don't need ids
429             #pod
430             #pod =back
431             #pod
432             #pod =cut
433              
434             1;
435              
436             __END__