File Coverage

blib/lib/Object/Extend.pm
Criterion Covered Total %
statement 82 92 89.1
branch 16 26 61.5
condition n/a
subroutine 20 21 95.2
pod 2 2 100.0
total 120 141 85.1


line stmt bran cond sub pod time code
1             package Object::Extend;
2              
3 4     4   50908 use 5.008;
  4         12  
  4         162  
4 4     4   22 use strict;
  4         5  
  4         117  
5 4     4   19 use warnings;
  4         11  
  4         107  
6 4     4   19 use base qw(Exporter);
  4         6  
  4         681  
7              
8             use constant {
9 4         314 SINGLETON => sprintf('%s::_Singleton', __PACKAGE__ ),
10             METHOD_NAME => qr{^[a-zA-Z_]\w*$},
11 4     4   21 };
  4         6  
12              
13 4     4   21 use B qw(perlstring);
  4         5  
  4         330  
14 4     4   19 use Carp qw(confess);
  4         8  
  4         231  
15 4     4   23 use Scalar::Util qw(blessed);
  4         8  
  4         617  
16 4     4   7310 use Storable qw(freeze);
  4         32044  
  4         799  
17              
18             our @EXPORT_OK = qw(extend with SINGLETON);
19             our $VERSION = '0.4.0';
20              
21             my $ID = 0;
22             my %CACHE;
23              
24             # find/create a unique class name for the supplied object's class/methods combination.
25             #
26             # Eigenclasses are immutable i.e. once an eigenclass has been created,
27             # its @ISA and installed methods never change. This means we can reuse/recycle
28             # an eigenclass if we're passed the same superclass/methods combo.
29             #
30             # Note: we need to identify the subs in the method hash by value (deparse)
31             # rather than by reference (refaddr), since ref addresses can be recycled
32             # (and frequently are for anonymous subs).
33             #
34             # Note: the SINGLETON class added to the eigenclass's @ISA doesn't
35             # implement any methods: we just use it as metadata to indicate that
36             # the object has been extended.
37              
38             sub _eigenclass($$) {
39 35     35   60 my ($class, $methods) = @_;
40              
41 35         55 my $key = do {
42 4     4   79 no warnings qw(once);
  4         42  
  4         1326  
43              
44 35         63 local $Storable::Deparse = 1;
45             # XXX squashed bugs 1) sort hash keys
46             # 2) freeze the $hashref, not the %$hash!
47 35         58 local $Storable::canonical = 1;
48              
49 35         181 freeze [ $class, $methods ];
50             };
51              
52 35         39126 my $eigenclass = $CACHE{$key};
53              
54 35 100       116 unless ($eigenclass) {
55 17         109 $eigenclass = sprintf '%s::_%x', SINGLETON, ++$ID;
56 17         64 $CACHE{$key} = $eigenclass;
57              
58 17 100       194 if ($class->isa(SINGLETON)) {
59 4         18 _set_isa($eigenclass, [ $class ]);
60             } else {
61 13         60 _set_isa($eigenclass, [ $class, SINGLETON ]);
62             }
63              
64 17         78 while (my ($name, $sub) = each(%$methods)) {
65 26         92 _install_sub("$eigenclass\::$name", $sub);
66             }
67             }
68              
69 35         95 return $eigenclass;
70             }
71              
72             # install the supplied sub in the supplied class.
73             # "extend" is a pretty clear statement of intent, so
74             # we don't issue a warning if the sub already exists
75             #
76             # XXX if we used Exporter::Tiny, we could
77             # allow the redefine warning to be enabled e.g.:
78             #
79             # use Object::Extend extend => { warn_on_redefine => 1 };
80              
81             sub _install_sub($$) {
82 26     26   43 my ($class, $sub) = @_;
83 4     4   25 no warnings 'redefine';
  4         41  
  4         148  
84 4     4   22 no strict 'refs';
  4         7  
  4         271  
85 26         237 *$class = $sub;
86             }
87              
88             # set a class's @ISA array
89             sub _set_isa($$) {
90 17     17   34 my ($class, $isa) = @_;
91 4     4   25 no strict 'refs';
  4         5  
  4         2549  
92 17         26 *{"$class\::ISA"} = $isa;
  17         336  
93             }
94              
95             # return true if $ref ISA $class - works with non-references,
96             # unblessed references and objects
97             sub _isa($$) {
98 88     88   146 my ($ref, $class) = @_;
99 88 100       4379 return blessed($ref) ? $ref->isa($class) : ref($ref) eq $class;
100             }
101              
102             # confess with a message whose string parameters are quoted
103             sub _error($;@) {
104 0     0   0 my $template = shift;
105 0 0       0 my @args = map { defined($_) ? perlstring($_) : 'undef' } @_;
  0         0  
106 0         0 confess sprintf($template, @args);
107             }
108              
109             # sanity check the arguments to extend
110             sub _validate(@) {
111 35     35   65 my $object = shift;
112 35         153 my $class = blessed($object);
113              
114 35 50       123 unless ($class) {
115 0         0 _error(
116             "invalid 'object' parameter: expected blessed reference, got: %s",
117             ref($object)
118             );
119             }
120              
121 35         63 my $methods;
122              
123 35 100       132 if (@_ == 1) {
    50          
124 27         51 $methods = shift;
125             } elsif (@_ % 2 == 0) {
126 8         30 $methods = { @_ };
127             }
128              
129 35 50       104 unless (_isa($methods, 'HASH')) {
130 0         0 _error(
131             "invalid 'methods' parameter: expected a hashref, got: %s",
132             ref($methods)
133             );
134             }
135              
136 35         184 for my $name (keys %$methods) {
137 52 50       299 if (!defined($name)) {
    50          
138 0         0 _error 'invalid method name (undef)';
139             } elsif ($name !~ METHOD_NAME) {
140 0         0 _error(
141             'invalid method name (%s): name must match %s',
142             $name,
143             METHOD_NAME
144             );
145             } else {
146 52         105 my $method = $methods->{$name};
147              
148 52 50       108 unless (_isa($method, 'CODE')) {
149 0         0 _error(
150             'invalid method value for %s: expected a coderef, got: %s',
151             $name,
152             ref($method),
153             );
154             }
155             }
156             }
157              
158 35         174 return ($object, $class, $methods);
159             }
160              
161             # dummy sub to optionally make the syntax
162             # a bit more DSL-ish: extend $object => with ...
163             sub with($) {
164 1     1 1 7892 my $methods = shift;
165              
166 1 50       5 unless (_isa($methods, 'HASH')) {
167 0         0 _error(
168             "invalid 'methods' parameter: expected a hashref, got: %s",
169             ref($methods)
170             );
171             }
172              
173 1         5 return $methods;
174             }
175              
176             # find/create an eigenclass for the object's class/methods and bless the object into it
177             sub extend($;@) {
178 35     35 1 127625 my ($object, $class, $methods) = _validate(@_);
179              
180 35 50       125 if (%$methods) {
181 35         99 my $eigenclass = _eigenclass($class, $methods);
182 35         128 bless $object, $eigenclass;
183             } # else return the original object unchanged
184              
185 35         91 return $object;
186             }
187              
188             1;
189              
190             =head1 NAME
191              
192             Object::Extend - add and override per-object methods
193              
194             =head1 SYNOPSIS
195              
196             use Object::Extend qw(extend);
197              
198             my $foo1 = Foo->new;
199             my $foo2 = Foo->new;
200              
201             extend $foo1 => {
202             bar => sub { ... },
203             };
204              
205             $foo1->bar; # OK
206             $foo2->bar; # error
207              
208             =head1 DESCRIPTION
209              
210             This module allows objects to be extended with per-object methods, similar to the use of
211             L
212             in Ruby. Object methods are added to an object-specific shim class (known as an C),
213             which extends the object's original class. The original class is left unchanged.
214              
215             =head2 EXPORTS
216              
217             =head3 extend
218              
219             C takes an object and a hash or hashref of method names and method values (coderefs) and adds
220             the methods to the object's shim class. The object is then blessed into this class and returned.
221              
222             It can be used in standalone statements:
223              
224             extend $object, foo => sub { ... }, bar => \&bar;
225              
226             Or expressions:
227              
228             return extend($object => { bar => sub { ... } })->bar;
229              
230             In both cases, C operates on and returns the supplied object i.e. a new object is never created.
231             If a new object is needed, it can be created manually e.g.:
232              
233             my $object2 = Object->new($object1);
234             my $object3 = clone($object1);
235              
236             extend($object2, foo => sub { ... })->foo;
237             return extend($object3 => ...);
238              
239             Objects can be extended multiple times with new or overridden methods:
240              
241             # call the original method
242             my $object = Foo->new;
243             $object->foo;
244              
245             # override the original method
246             extend $object, foo => sub { ... };
247             $object->foo;
248              
249             # add a new method
250             extend $object, bar => sub { ... };
251             $object->bar;
252              
253             =head3 with
254              
255             This sub can optionally be imported to make the use of C more descriptive. It takes and
256             returns a hashref of method names/coderefs:
257              
258             use Object::Extend qw(extend with);
259              
260             extend $object => with { foo => sub { ... } };
261              
262             =head3 SINGLETON
263              
264             Every extended object's shim class includes an additional (empty) class in its C<@ISA> which indicates
265             that the object has been extended. The name of this class can be accessed by importing the C
266             constant e.g.:
267              
268             use Object::Extend qw(SINGLETON);
269              
270             if ($object->isa(SINGLETON)) { ... } # object extended with object-specific methods
271              
272             =head1 VERSION
273              
274             0.4.0
275              
276             =head1 SEE ALSO
277              
278             =over
279              
280             =item * L
281              
282             =item * L
283              
284             =item * L
285              
286             =item * L
287              
288             =back
289              
290             =head1 AUTHOR
291              
292             chocolateboy
293              
294             =head1 COPYRIGHT AND LICENSE
295              
296             Copyright (C) 2013 by chocolateboy
297              
298             This library is free software; you can redistribute it and/or modify
299             it under the same terms as Perl itself, either Perl version 5.14.2 or,
300             at your option, any later version of Perl 5 you may have available.
301              
302             =cut