File Coverage

blib/lib/Object/Declare.pm
Criterion Covered Total %
statement 140 146 95.8
branch 37 56 66.0
condition 9 20 45.0
subroutine 29 29 100.0
pod n/a
total 215 251 85.6


line stmt bran cond sub pod time code
1             package Object::Declare;
2              
3 1     1   60233 use 5.006;
  1         11  
4 1     1   5 use strict;
  1         1  
  1         17  
5 1     1   4 use warnings;
  1         1  
  1         293  
6              
7             our $VERSION = '0.24';
8              
9             sub import
10             {
11 1     1   9 my $class = shift;
12 1 50 33     11 my %args = ( ( @_ and ref( $_[0] ) ) ? ( mapping => $_[0] ) : @_ )
    50          
13             or return;
14 1         3 my $from = caller;
15              
16 1 50       3 my $mapping = $args{mapping} or return;
17 1   50     3 my $aliases = $args{aliases} || {};
18 1   50     16 my $declarator = $args{declarator} || ['declare'];
19 1   50     4 my $copula = $args{copula} || [ 'is', 'are' ];
20              
21             # Both declarator and copula can contain more than one entries;
22             # normalize into an arrayref if we only have on entry.
23 1 50       2 $mapping = [$mapping] unless ref($mapping);
24 1 50       3 $declarator = [$declarator] unless ref($declarator);
25 1 50       1 $copula = [$copula] unless ref($copula);
26              
27 1 50       5 if ( ref($mapping) eq 'ARRAY' )
28             {
29             # rewrite "MyApp::Foo" into simply "foo"
30             $mapping = {
31             map {
32 0         0 my $helper = $_;
  0         0  
33 0         0 $helper =~ s/.*:://;
34 0         0 ( lc($helper) => $_ );
35             } @$mapping
36             };
37             }
38              
39             # Convert mapping targets into instantiation closures
40 1 50       2 if ( ref($mapping) eq 'HASH' )
41             {
42 1         3 foreach my $key ( keys %$mapping )
43             {
44 2         5 my $val = $mapping->{$key};
45 2 100       4 next if ref($val); # already a callback, don't bother
46 1     8   13 $mapping->{$key} = sub { scalar( $val->new(@_) ) };
  8         22  
47             }
48             }
49              
50 1 50       4 if ( ref($copula) eq 'ARRAY' )
51             {
52             # add an empty prefix to all copula
53 0         0 $copula = { map { $_ => '' } @$copula };
  0         0  
54             }
55              
56             # Install declarator functions into caller's package, remembering
57             # the mapping and copula set for this declarator.
58 1         3 foreach my $sym (@$declarator)
59             {
60             ## no critic (ProhibitNoStrict)
61 1     1   6 no strict 'refs';
  1         1  
  1         96  
62              
63 1         5 *{"$from\::$sym"} = sub (&) {
64 2     2   1804 unshift @_, ( $mapping, $copula, $aliases );
65 2         8 goto &_declare;
66 1         2 };
67             ## use critic
68             }
69              
70             # Establish prototypes (same as "use subs") so Sub::Override can work
71             {
72             ## no critic (ProhibitNoStrict)
73 1     1   20 no strict 'refs';
  1         2  
  1         119  
  1         2  
74             _predeclare(
75 2         20 ( map { "$from\::$_" } keys %$mapping ),
76 1         2 ( map { ( "UNIVERSAL::$_", "$_\::AUTOLOAD" ) } keys %$copula ),
  2         6  
77             );
78             ## use critic
79             }
80             }
81              
82             # Same as "use sub". All is fair if you predeclare.
83             sub _predeclare
84             {
85             ## no critic (ProhibitNoStrict)
86 1     1   6 no strict 'refs';
  1         2  
  1         30  
87 1     1   4 no warnings 'redefine';
  1         2  
  1         96  
88 13     13   19 foreach my $sym (@_)
89             {
90 18         67 *$sym = \&$sym;
91             }
92             ## use critic
93             }
94              
95             sub _declare
96             {
97 2     2   5 my ( $mapping, $copula, $aliases, $code ) = @_;
98 2         5 my $from = caller;
99              
100             # Table of collected objects.
101 2         4 my @objects;
102              
103             # Establish a lexical extent for overrided symbols; they will be
104             # restored automagically upon scope exit.
105             my %subs_replaced;
106             my $replace = sub {
107             ## no critic (ProhibitNoStrict)
108 1     1   5 no strict 'refs';
  1         2  
  1         29  
109 1     1   5 no warnings 'redefine';
  1         2  
  1         323  
110 12     12   16 my ( $sym, $code ) = @_;
111              
112             # Do the "use subs" predeclaration again before overriding, because
113             # Sub::Override cannot handle empty symbol slots. This is normally
114             # redundant (&import already did that), but we do it here anyway to
115             # guard against runtime deletion of symbol table entries.
116 12         22 _predeclare($sym);
117              
118             # Now replace the symbol for real.
119 12   50     55 $subs_replaced{$sym} ||= *$sym{CODE};
120 12         37 *$sym = $code;
121             ## use critic
122 2         7 };
123              
124             # In DSL (domain-specific language) mode; install AUTOLOAD to handle all
125             # unrecognized calls for "foo is 1" (which gets translated to "is->foo(1)",
126             # and UNIVERSAL to collect "is foo" (which gets translated to "foo->is".
127             # The arguments are rolled into a Katamari structure for later analysis.
128 2         10 while ( my ( $sym, $prefix ) = each %$copula )
129             {
130             $replace->(
131             "UNIVERSAL::$sym" => sub {
132              
133             # Turn "is some_field" into "some_field is 1"
134 14 50   14   87 my ( $key, @vals ) =
    50          
135             ref($prefix) ? $prefix->(@_) : ( $prefix . $_[0] => 1 )
136             or return;
137              
138             # If the copula returns a ready-to-use katamari object,
139             # don't try to roll it by ourself.
140 14 50 33     24 return $key
141             if ref($key) && ref($key) eq 'Object::Declare::Katamari';
142 14 50 33     39 $key = $aliases->{$key} if $aliases and exists $aliases->{$key};
143 14         23 unshift @vals, $key;
144 14         81 bless( \@vals => 'Object::Declare::Katamari' );
145             }
146 4         21 );
147             $replace->(
148             "$sym\::AUTOLOAD" => sub {
149              
150             # Handle "some_field is $some_value"
151 12     12   19 shift;
152              
153 12         13 my $field = our $AUTOLOAD;
154 12 50       26 return if $field =~ /DESTROY$/;
155              
156 12         78 $field =~ s/^\Q$sym\E:://;
157              
158 12 50       45 my ( $key, @vals ) =
    50          
159             ref($prefix)
160             ? $prefix->( $field, @_ )
161             : ( $prefix . $field => @_ )
162             or return;
163              
164 12 100 66     35 $key = $aliases->{$key} if $aliases and exists $aliases->{$key};
165 12         21 unshift @vals, $key;
166 12         45 bless( \@vals, 'Object::Declare::Katamari' );
167             }
168 4         16 );
169             }
170              
171 2         7 my @overridden = map { "$from\::$_" } keys %$mapping;
  4         12  
172              
173             # Now install the collector symbols from class mappings
174             my $toggle_subs = sub {
175 20     20   23 foreach my $sym (@overridden)
176             {
177             ## no critic (ProhibitNoStrict)
178 1     1   15 no strict 'refs';
  1         2  
  1         31  
179 1     1   5 no warnings 'redefine';
  1         2  
  1         127  
180             ( $subs_replaced{$sym}, *$sym ) =
181 40         109 ( *$sym{CODE}, $subs_replaced{$sym} );
182             ## use critic
183             }
184 2         7 };
185              
186 2         8 while ( my ( $sym, $build ) = each %$mapping )
187             {
188 4         9 $replace->(
189             "$from\::$sym" => _make_object( $build => \@objects, $toggle_subs )
190             );
191             }
192              
193             # Let's play Katamari!
194 2         6 &$code;
195              
196             # Restore overriden subs
197 2         13 while ( my ( $sym, $code ) = each %subs_replaced )
198             {
199             ## no critic (ProhibitNoStrict)
200 1     1   6 no strict 'refs';
  1         1  
  1         22  
201 1     1   4 no warnings 'redefine';
  1         1  
  1         97  
202 12         59 *$sym = $code;
203             ## use critic
204             }
205              
206             # In scalar context, returns hashref; otherwise preserve ordering
207 2 100       18 return ( wantarray ? @objects : {@objects} );
208             }
209              
210             # Make a star from the Katamari!
211             sub _make_object
212             {
213 4     4   8 my ( $build, $schema, $toggle_subs ) = @_;
214              
215             return sub {
216              
217             # Restore overriden subs
218             ## no critic (ProhibitNoStrict)
219 1     1   6 no strict 'refs';
  1         2  
  1         20  
220 1     1   4 no warnings 'redefine';
  1         2  
  1         185  
221             ## use critic
222              
223 10 100   10   20 my $name = ( ref( $_[0] ) ? undef : shift );
224 10         13 my $args = \@_;
225             my $damacy = bless(
226             sub {
227 10         17 $toggle_subs->();
228              
229             my $rv = $build->(
230             ( $_[0] ? ( name => $_[0] ) : () ),
231 10 100       19 map { $_->unroll } @$args
  16         27  
232             );
233              
234 10         59 $toggle_subs->();
235              
236 10         33 return $rv;
237 10         39 } => 'Object::Declare::Damacy'
238             );
239              
240 10 100       16 if (wantarray)
241             {
242 6         27 return ($damacy);
243             }
244             else
245             {
246 4         8 push @$schema, $name => $damacy->($name);
247             }
248 4         16 };
249             }
250              
251             package Object::Declare::Katamari;
252              
253 1     1   1013 use overload "!" => \&negation, fallback => 1;
  1         811  
  1         10  
254              
255             sub negation
256             {
257 4 50   4   5 my @katamari = @{ $_[0] } or return ();
  4         12  
258 4         9 $katamari[1] = !$katamari[1];
259 4         14 return bless( \@katamari, ref( $_[0] ) );
260             }
261              
262             # Unroll a Katamari structure into constructor arguments.
263             sub unroll
264             {
265 26 50   26   26 my @katamari = @{ $_[0] } or return ();
  26         53  
266 26 50       46 my $field = shift @katamari or return ();
267 26         26 my @unrolled;
268              
269 26         49 unshift @unrolled, pop(@katamari)->unroll
270             while ref( $katamari[-1] ) eq __PACKAGE__;
271              
272 26 100       37 if ( @katamari == 1 )
273             {
274             # single value: "is foo"
275 22 100       29 if ( ref( $katamari[0] ) eq 'Object::Declare::Damacy' )
276             {
277 2         4 $katamari[0] = $katamari[0]->($field);
278             }
279 22         71 return ( $field => @katamari, @unrolled );
280             }
281             else
282             {
283             # Multiple values: "are qw( foo bar baz )"
284 4         5 foreach my $kata (@katamari)
285             {
286 8 100       14 $kata = $kata->() if ref($kata) eq 'Object::Declare::Damacy';
287             }
288 4         17 return ( $field => \@katamari, @unrolled );
289             }
290             }
291              
292             1;
293              
294             __END__