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