File Coverage

blib/lib/Object/Declare.pm
Criterion Covered Total %
statement 140 146 95.8
branch 37 56 66.0
condition 8 20 40.0
subroutine 29 29 100.0
pod n/a
total 214 251 85.2


line stmt bran cond sub pod time code
1             package Object::Declare;
2              
3 1     1   91185 use 5.006;
  1         3  
4 1     1   6 use strict;
  1         1  
  1         28  
5 1     1   5 use warnings;
  1         2  
  1         679  
6              
7             $Object::Declare::VERSION = '0.23';
8              
9             sub import {
10 1     1   13 my $class = shift;
11 1 50 33     17 my %args = ((@_ and ref($_[0])) ? (mapping => $_[0]) : @_) or return;
    50          
12 1         3 my $from = caller;
13              
14 1 50       6 my $mapping = $args{mapping} or return;
15 1   50     4 my $aliases = $args{aliases} || {};
16 1   50     6 my $declarator = $args{declarator} || ['declare'];
17 1   50     66 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       9 $mapping = [$mapping] unless ref($mapping);
22 1 50       4 $declarator = [$declarator] unless ref($declarator);
23 1 50       3 $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       7 if (ref($mapping) eq 'HASH') {
38 1         5 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   6 $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         3 foreach my $sym (@$declarator) {
53 1     1   7 no strict 'refs';
  1         2  
  1         128  
54              
55 1         9 *{"$from\::$sym"} = sub (&) {
56 2     2   2134 unshift @_, ($mapping, $copula, $aliases);
57 2         11 goto &_declare;
58 1         5 };
59             }
60              
61             # Establish prototypes (same as "use subs") so Sub::Override can work
62             {
63 1     1   6 no strict 'refs';
  1         1  
  1         165  
  1         3  
64             _predeclare(
65 2         9 (map { "$from\::$_" } keys %$mapping),
66 1         3 (map { ("UNIVERSAL::$_", "$_\::AUTOLOAD") } keys %$copula),
  2         9  
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         45  
74 1     1   5 no warnings 'redefine';
  1         2  
  1         143  
75 13     13   23 foreach my $sym (@_) {
76 18         97 *$sym = \&$sym;
77             }
78             }
79              
80             sub _declare {
81 2     2   5 my ($mapping, $copula, $aliases, $code) = @_;
82 2         5 my $from = caller;
83              
84             # Table of collected objects.
85 2         5 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   6 no strict 'refs';
  1         1  
  1         34  
92 1     1   5 no warnings 'redefine';
  1         2  
  1         880  
93 12     12   14 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         22 _predeclare($sym);
100              
101             # Now replace the symbol for real.
102 12   50     69 $subs_replaced{$sym} ||= *$sym{CODE};
103 12         51 *$sym = $code;
104 2         10 };
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         13 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   72 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     40 return $key
117             if ref($key) && ref($key) eq 'Object::Declare::Katamari';
118 14 50 33     49 $key = $aliases->{$key} if $aliases and exists $aliases->{$key};
119 14         23 unshift @vals, $key;
120 14         129 bless( \@vals => 'Object::Declare::Katamari' );
121 4         26 } );
122             $replace->( "$sym\::AUTOLOAD" => sub {
123             # Handle "some_field is $some_value"
124 12     12   14 shift;
125              
126 12         14 my $field = our $AUTOLOAD;
127 12 50       32 return if $field =~ /DESTROY$/;
128              
129 12         140 $field =~ s/^\Q$sym\E:://;
130              
131 12 50       61 my ($key, @vals) = ref($prefix) ? $prefix->($field, @_) : ($prefix.$field => @_) or return;
    50          
132              
133 12 100 33     44 $key = $aliases->{$key} if $aliases and exists $aliases->{$key};
134 12         23 unshift @vals, $key;
135 12         76 bless( \@vals, 'Object::Declare::Katamari' );
136 4         31 } );
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   26 foreach my $sym (@overridden) {
143 1     1   17 no strict 'refs';
  1         2  
  1         41  
144 1     1   6 no warnings 'redefine';
  1         1  
  1         165  
145 40         151 ($subs_replaced{$sym}, *$sym) = (*$sym{CODE}, $subs_replaced{$sym});
146             }
147 2         11 };
148              
149 2         14 while (my ($sym, $build) = each %$mapping) {
150 4         16 $replace->("$from\::$sym" => _make_object($build => \@objects, $toggle_subs));
151             }
152              
153             # Let's play Katamari!
154 2         6 &$code;
155              
156             # Restore overriden subs
157 2         19 while (my ($sym, $code) = each %subs_replaced) {
158 1     1   5 no strict 'refs';
  1         2  
  1         106  
159 1     1   6 no warnings 'redefine';
  1         1  
  1         127  
160 12         82 *$sym = $code;
161             }
162              
163             # In scalar context, returns hashref; otherwise preserve ordering
164 2 100       31 return(wantarray ? @objects : { @objects });
165             }
166              
167             # Make a star from the Katamari!
168             sub _make_object {
169 4     4   5 my ($build, $schema, $toggle_subs) = @_;
170              
171             return sub {
172             # Restore overriden subs
173 1     1   6 no strict 'refs';
  1         1  
  1         195  
174 1     1   6 no warnings 'redefine';
  1         6  
  1         262  
175              
176 10 100   10   20 my $name = ( ref( $_[0] ) ? undef : shift );
177 10         16 my $args = \@_;
178             my $damacy = bless(sub {
179 10         15 $toggle_subs->();
180              
181             my $rv = $build->(
182             ( $_[0] ? ( name => $_[0] ) : () ),
183 10 100       26 map { $_->unroll } @$args
  16         33  
184             );
185              
186 10         79 $toggle_subs->();
187              
188 10         45 return $rv;
189 10         66 } => 'Object::Declare::Damacy');
190              
191 10 100       20 if (wantarray) {
192 6         45 return ($damacy);
193             } else {
194 4         10 push @$schema, $name => $damacy->($name);
195             }
196 4         24 };
197             }
198              
199             package Object::Declare::Katamari;
200              
201 1     1   2323 use overload "!" => \&negation, fallback => 1;
  1         1694  
  1         11  
202              
203             sub negation {
204 4 50   4   5 my @katamari = @{$_[0]} or return ();
  4         22  
205 4         9 $katamari[1] = !$katamari[1];
206 4         23 return bless(\@katamari, ref($_[0]));
207             }
208              
209             # Unroll a Katamari structure into constructor arguments.
210             sub unroll {
211 26 50   26   21 my @katamari = @{$_[0]} or return ();
  26         89  
212 26 50       53 my $field = shift @katamari or return ();
213 26         26 my @unrolled;
214              
215 26         63 unshift @unrolled, pop(@katamari)->unroll
216             while ref($katamari[-1]) eq __PACKAGE__;
217              
218 26 100       45 if (@katamari == 1) {
219             # single value: "is foo"
220 22 100       48 if ( ref( $katamari[0] ) eq 'Object::Declare::Damacy' ) {
221 2         6 $katamari[0] = $katamari[0]->($field);
222             }
223 22         99 return($field => @katamari, @unrolled);
224             }
225             else {
226             # Multiple values: "are qw( foo bar baz )"
227 4         6 foreach my $kata (@katamari) {
228 8 100       28 $kata = $kata->() if ref($kata) eq 'Object::Declare::Damacy';
229             }
230 4         27 return($field => \@katamari, @unrolled);
231             }
232             }
233              
234             1;
235              
236             __END__
237              
238             =pod
239              
240             =head1 NAME
241              
242             Object::Declare - Declarative object constructor
243              
244             =head1 VERSION
245              
246             version 0.23
247              
248             =head1 SYNOPSIS
249              
250             use Object::Declare ['MyApp::Column', 'MyApp::Param'];
251              
252             my %objects = declare {
253              
254             param foo =>
255             !is global,
256             is immutable,
257             valid_values are qw( more values );
258              
259             column bar =>
260             field1 is 'value',
261             field2 is 'some_other_value',
262             sub_params are param( is happy ), param ( is sad );
263              
264             };
265              
266             print $objects{foo}; # a MyApp::Param object
267             print $objects{bar}; # a MyApp::Column object
268              
269             # Assuming that MyApp::Column::new simply blesses into a hash...
270             print $objects{bar}{sub_params}[0]; # a MyApp::Param object
271             print $objects{bar}{sub_params}[1]; # a MyApp::Param object
272              
273             =head1 DESCRIPTION
274              
275             This module exports one function, C<declare>, for building named
276             objects with a declarative syntax, similar to how L<Jifty::DBI::Schema>
277             defines its columns.
278              
279             In list context, C<declare> returns a list of name/object pairs in the
280             order of declaration (allowing duplicates), suitable for putting into a hash.
281             In scalar context, C<declare> returns a hash reference.
282              
283             Using a flexible C<import> interface, one can change exported helper
284             functions names (I<declarator>), words to link labels and values together
285             (I<copula>), and the table of named classes to declare (I<mapping>):
286              
287             use Object::Declare
288             declarator => ['declare'], # list of declarators
289             copula => { # list of words, or a map
290             is => '', # from copula to label prefixes,
291             are => '', # or to callback that e.g. turns
292             has => sub { has => @_ }, # "has X" to "has is X" and
293             # "X has 1" to "has is [X => 1]"
294             },
295             aliases => { # list of label aliases:
296             more => 'less', # turns "is more" into "is less"
297             # and "more is 1" into "less is 1"
298             },
299             mapping => {
300             column => 'MyApp::Column', # class name to call ->new to
301             param => sub { # arbitrary coderef also works
302             bless(\@_, 'MyApp::Param');
303             },
304             };
305              
306             After the declarator block finishes execution, all helper functions are
307             removed from the package. Same-named functions (such as C<&is> and C<&are>)
308             that existed before the declarator's execution are restored correctly.
309              
310             =head1 NOTES
311              
312             If you export the declarator to another package via C<@EXPORT>, be sure
313             to export all mapping keys as well. For example, this will work for the
314             example above:
315              
316             our @EXPORT = qw( declare column param );
317              
318             But this will not:
319              
320             our @EXPORT = qw( declare );
321              
322             The copula are not turned into functions, so there is no need to export them.
323              
324             =head1 AUTHORS
325              
326             Audrey Tang E<lt>cpan@audreyt.orgE<gt>
327              
328             =head1 COPYRIGHT
329              
330             Copyright 2006, 2007 by Audrey Tang <cpan@audreyt.org>.
331              
332             This software is released under the MIT license cited below.
333              
334             =head2 The "MIT" License
335              
336             Permission is hereby granted, free of charge, to any person obtaining a copy
337             of this software and associated documentation files (the "Software"), to deal
338             in the Software without restriction, including without limitation the rights
339             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
340             copies of the Software, and to permit persons to whom the Software is
341             furnished to do so, subject to the following conditions:
342              
343             The above copyright notice and this permission notice shall be included in
344             all copies or substantial portions of the Software.
345              
346             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
347             OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
348             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
349             THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
350             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
351             FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
352             DEALINGS IN THE SOFTWARE.
353              
354             =head1 AUTHOR
355              
356             Shlomi Fish <shlomif@cpan.org>
357              
358             =head1 COPYRIGHT AND LICENSE
359              
360             This software is Copyright (c) 2006 by Audrey Tang.
361              
362             This is free software, licensed under:
363              
364             The MIT (X11) License
365              
366             =head1 BUGS
367              
368             Please report any bugs or feature requests on the bugtracker website
369             http://rt.cpan.org/NoAuth/Bugs.html?Dist=Object-Declare or by email to
370             bug-object-declare@rt.cpan.org.
371              
372             When submitting a bug or request, please include a test-file or a
373             patch to an existing test-file that illustrates the bug or desired
374             feature.
375              
376             =for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan
377              
378             =head1 SUPPORT
379              
380             =head2 Perldoc
381              
382             You can find documentation for this module with the perldoc command.
383              
384             perldoc Object::Declare
385              
386             =head2 Websites
387              
388             The following websites have more information about this module, and may be of help to you. As always,
389             in addition to those websites please use your favorite search engine to discover more resources.
390              
391             =over 4
392              
393             =item *
394              
395             MetaCPAN
396              
397             A modern, open-source CPAN search engine, useful to view POD in HTML format.
398              
399             L<http://metacpan.org/release/Object-Declare>
400              
401             =item *
402              
403             Search CPAN
404              
405             The default CPAN search engine, useful to view POD in HTML format.
406              
407             L<http://search.cpan.org/dist/Object-Declare>
408              
409             =item *
410              
411             RT: CPAN's Bug Tracker
412              
413             The RT ( Request Tracker ) website is the default bug/issue tracking system for CPAN.
414              
415             L<https://rt.cpan.org/Public/Dist/Display.html?Name=Object-Declare>
416              
417             =item *
418              
419             AnnoCPAN
420              
421             The AnnoCPAN is a website that allows community annotations of Perl module documentation.
422              
423             L<http://annocpan.org/dist/Object-Declare>
424              
425             =item *
426              
427             CPAN Ratings
428              
429             The CPAN Ratings is a website that allows community ratings and reviews of Perl modules.
430              
431             L<http://cpanratings.perl.org/d/Object-Declare>
432              
433             =item *
434              
435             CPAN Forum
436              
437             The CPAN Forum is a web forum for discussing Perl modules.
438              
439             L<http://cpanforum.com/dist/Object-Declare>
440              
441             =item *
442              
443             CPANTS
444              
445             The CPANTS is a website that analyzes the Kwalitee ( code metrics ) of a distribution.
446              
447             L<http://cpants.cpanauthors.org/dist/Object-Declare>
448              
449             =item *
450              
451             CPAN Testers
452              
453             The CPAN Testers is a network of smokers who run automated tests on uploaded CPAN distributions.
454              
455             L<http://www.cpantesters.org/distro/O/Object-Declare>
456              
457             =item *
458              
459             CPAN Testers Matrix
460              
461             The CPAN Testers Matrix is a website that provides a visual overview of the test results for a distribution on various Perls/platforms.
462              
463             L<http://matrix.cpantesters.org/?dist=Object-Declare>
464              
465             =item *
466              
467             CPAN Testers Dependencies
468              
469             The CPAN Testers Dependencies is a website that shows a chart of the test results of all dependencies for a distribution.
470              
471             L<http://deps.cpantesters.org/?module=Object::Declare>
472              
473             =back
474              
475             =head2 Bugs / Feature Requests
476              
477             Please report any bugs or feature requests by email to C<bug-object-declare at rt.cpan.org>, or through
478             the web interface at L<https://rt.cpan.org/Public/Bug/Report.html?Queue=Object-Declare>. You will be automatically notified of any
479             progress on the request by the system.
480              
481             =head2 Source Code
482              
483             The code is open to the world, and available for you to hack on. Please feel free to browse it and play
484             with it, or whatever. If you want to contribute patches, please send me a diff or prod me to pull
485             from your repository :)
486              
487             L<https://github.com/shlomif/perl-Object-Declare>
488              
489             git clone git://github.com/shlomif/perl-Object-Declare.git
490              
491             =cut