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