File Coverage

blib/lib/MooX/Struct.pm
Criterion Covered Total %
statement 243 262 92.7
branch 82 90 91.1
condition 39 51 76.4
subroutine 53 54 98.1
pod 9 17 52.9
total 426 474 89.8


line stmt bran cond sub pod time code
1             package MooX::Struct;
2              
3 16     16   1078499 use 5.008;
  16         170  
4 16     16   82 use strict;
  16         45  
  16         441  
5 16     16   82 use warnings;
  16         29  
  16         499  
6 16     16   9800 use utf8;
  16         227  
  16         76  
7              
8             BEGIN {
9 16     16   725 $MooX::Struct::AUTHORITY = 'cpan:TOBYINK';
10 16         346 $MooX::Struct::VERSION = '0.018';
11             }
12              
13 16     16   8690 use Moo 1.000;
  16         176476  
  16         94  
14 16     16   30640 use Types::TypeTiny 1.000 qw( HashLike ArrayLike );
  16         50577  
  16         92  
15 16     16   72788 use Types::Standard 1.000 qw( HashRef ArrayRef Num Ref );
  16         1113935  
  16         173  
16              
17             my $HashLike = HashLike | Ref['HASH'];
18             my $ArrayLike = ArrayLike | Ref['ARRAY'];
19              
20             use overload
21             q[""] => 'TO_STRING',
22 11     11   1679 q[bool] => sub { 1 },
23 16         143 q[@{}] => 'TO_ARRAY',
24             q[=] => 'CLONE',
25 16     16   19562 fallback => 1;
  16         45  
26              
27             METHODS: {
28 16     16   1778 no warnings;
  16         37  
  16         5924  
29 7     7 1 5521 sub OBJECT_ID { require Object::ID; goto \&Object::ID::object_id };
  7         18161  
30 24     24 1 86 sub FIELDS { qw() };
31 1     1 1 25 sub TYPE { +undef };
32 14     14 1 63 sub TO_ARRAY { [ map {; $_[0]->$_ } $_[0]->FIELDS ] };
  37         311  
33 2     2 1 7 sub TO_HASH { +{ map {; $_ => $_[0]->$_ } $_[0]->FIELDS } };
  6         55  
34 12     12 1 435 sub TO_STRING { join q[ ], @{ $_[0]->TO_ARRAY } };
  12         32  
35 2     2   549 sub CLONE { my $s = shift; ref($s)->new(%{$s->TO_HASH}, @_) };
  2         5  
  2         20  
36 6 50   6 1 82 sub CLASSNAME { ref($_[0]) or $_[0] };
37 6     6 1 8296 sub TYPE_TINY { Types::Standard::InstanceOf->parameterize(shift->CLASSNAME) };
38             };
39              
40             sub BUILDARGS
41             {
42 67     67 1 50157 my $class = shift;
43 67         188 my @fields = $class->FIELDS;
44            
45 67 100 100     597 if (
    100 100        
      66        
      66        
      100        
      100        
46             (@_==1 and ref $_[0] eq 'ARRAY') or # optimized usual case
47             (@_==1 and ArrayLike->check($_[0]) and not $HashLike->check($_[0]))
48             )
49             {
50 30         97 my @values = @{ $_[0] };
  30         70  
51 30 100       237 Carp::confess("too many values passed to constructor (expected @fields); stopped")
52             unless @fields >= @values;
53 16     16   122 no warnings;
  16         36  
  16         9767  
54             return +{
55             map {
56 29         166 $fields[$_] => $values[$_];
  55         606  
57             } 0 .. $#values
58             }
59             }
60              
61             elsif (@_ == 1 and ref $_[0] ne 'HASH' and $HashLike->check($_[0]))
62             {
63             # help Moo::Object!
64 1         200 @_ = +{ %{$_[0]} };
  1         27  
65             }
66              
67 37         921 my $hashref = $class->SUPER::BUILDARGS(@_);
68            
69             # my %tmp = map { $_ => 1 } keys %$hashref;
70             # delete $tmp{$_} for @fields;
71             # if (my @unknown = sort keys %tmp)
72             # {
73             # Carp::confess("unknown keys passed to constructor (@unknown); stopped");
74             # }
75            
76 36         826 return $hashref;
77             }
78              
79             sub EXTEND
80             {
81 4     4   191 my ($invocant, @args) = @_;
82 4         7 my $base = $invocant;
83 4 100       14 $base = ref $invocant if ref $invocant;
84            
85 4         98 my $processor = 'MooX::Struct::Processor'->new;
86 4         97 while (@args) {
87 5 100       20 last unless $args[0] =~ /^-(.+)$/;
88 1         7 $processor->flags->{ lc($1) } = !!shift @args;
89             }
90              
91 4         8 my $subname = undef;
92 4 50       11 $subname = ${ shift @args } if ref($args[0]) eq 'SCALAR';
  4         9  
93              
94 4         17 my $new_class = $processor->make_sub(
95             $subname,
96             [ -extends => [$base], @args ],
97             )->();
98 4 100       18 return $new_class unless ref $invocant;
99            
100 2         8 bless $invocant => $new_class;
101             }
102              
103             my $done = 0;
104             sub _data_printer
105             {
106 0     0   0 my ($self, $ddp) = @_;
107            
108 0         0 my @values;
109             my $label;
110            
111 0 0       0 if ($Data::Printer::VERSION lt '0.90') {
112 0         0 require Data::Printer::Filter;
113 0         0 require Term::ANSIColor;
114 0         0 @values = map scalar(&Data::Printer::p(\$_, return_value => 'dump')), @$self;
115 0   0     0 $label = Term::ANSIColor::colored($self->TYPE||'struct', 'bright_yellow');
116             }
117             else
118             {
119 0         0 @values = map $ddp->parse(\$_), @$self;
120 0   0     0 $label = $ddp->maybe_colorize($self->TYPE||'struct', 'MooX::Struct', 'bright_yellow');
121             }
122              
123 0 0       0 if (grep /\n/, @values) {
124             return sprintf(
125             "%s[\n\t%s,\n]",
126             $label,
127 0         0 join(qq[,\n\t], map { s/\n/\n\t/gm; $_ } @values),
  0         0  
  0         0  
128             );
129             }
130            
131 0         0 sprintf('%s[ %s ]', $label, join q[, ], @values);
132             }
133              
134 0         0 BEGIN {
135             package MooX::Struct::Processor;
136            
137             {
138 16     16   148 no warnings;
  16     16   34  
  16         2109  
  16         79  
139 16         244 our $AUTHORITY = 'cpan:TOBYINK';
140 16         38 our $VERSION = '0.018';
141             }
142            
143 38     38   75 sub _uniq { my %seen; grep { not $seen{$_}++ } @_ };
  38         102  
  73         279  
144            
145 16     16   130 use Moo 1.000;
  16         318  
  16         214  
146 16     16   6578 use Exporter::Tiny qw();
  16         53  
  16         752  
147 16     16   151 use Types::TypeTiny 1.000 qw( TypeTiny CodeLike HashLike ArrayLike );
  16         310  
  16         132  
148 16     16   7207 use Types::Standard 1.000 qw( HashRef ArrayRef CodeRef Num );
  16         225  
  16         97  
149 16     16   13257 use Carp qw(confess);
  16         34  
  16         993  
150 16     16   105 use Scalar::Util qw(blessed);
  16         43  
  16         777  
151 16     16   8295 use namespace::clean qw();
  16         187894  
  16         436  
152 16     16   116 use B::Hooks::EndOfScope qw(on_scope_end);
  16         33  
  16         113  
153            
154             sub __install_sub__ ($) {
155 114     114   204 my $args = shift;
156 114         290 my ($pkg, $name, $code) = @$args{qw(into as code)};
157 16     16   1712 no strict 'refs';
  16         36  
  16         35157  
158 114         158 *{"$pkg\::$name"} = $code;
  114         732  
159             }
160            
161             has flags => (
162             is => 'ro',
163             isa => HashLike,
164 28         1093 default => sub { +{} },
165 16         177 );
166            
167             has class_map => (
168             is => 'ro',
169             isa => HashLike,
170 32         1688 default => sub { +{} },
171 16         331688 );
172            
173             has base => (
174             is => 'ro',
175 31         48878 default => sub { 'MooX::Struct' },
176 16         8217 );
177            
178 16         4523 has trace => (
179             is => 'lazy',
180             );
181            
182             sub _build_trace
183             {
184             $ENV{PERL_MOOX_STRUCT_TRACE}
185 27 100   27   641 or shift->flags->{trace};
186             }
187            
188 16         12739 has trace_handle => (
189             is => 'lazy',
190             );
191            
192             sub _build_trace_handle
193             {
194 1     1   65 require IO::Handle;
195 1         9 \*STDERR;
196             }
197            
198 16         12395 my $counter = 0;
199             sub create_class
200             {
201 39     39 0 103 my ($self, $opts) = @_;
202 39         62 my $klass;
203 39         99 for my $o (@$opts) {
204 73 100       203 next unless $o->[0] eq '-class';
205 4 100       30 $klass = ArrayRef->check($o->[1]) ? join('::', @{$o->[1]}) : ${$o->[1]};
  1         71  
  3         135  
206 4         36 last;
207             }
208 39 100       329 $klass = sprintf('%s::__ANON__::%04d', $self->base, ++$counter) unless defined $klass;
209 39         282 "Moo"->_set_superclasses($klass, $self->base);
210 39         6612 "Moo"->_maybe_reset_handlemoose($klass);
211 39 100       1222 if ($self->trace)
212             {
213 2         49 $self->trace_handle->printf(
214             "package %s;\nuse Moo;\n",
215             $klass,
216             );
217             }
218 39         242 return $klass;
219             }
220            
221             sub process_meta
222             {
223 20     20 0 115 my ($self, $klass, $name, $val) = @_;
224            
225 20 100 100     166 if ($name eq '-extends' or $name eq '-isa')
    100          
    100          
226             {
227             my @parents = map {
228 13         38 exists $self->class_map->{$_}
229 13 100       107 ? $self->class_map->{$_}->()
230             : $_
231             } @$val;
232 13         68 "Moo"->_set_superclasses($klass, @parents);
233 13         4499 "Moo"->_maybe_reset_handlemoose($klass);
234            
235 13 100       392 if ($self->trace)
236             {
237 1         39 $self->trace_handle->printf(
238             "extends qw(%s)\n",
239             join(q[ ] => @parents),
240             );
241             }
242            
243 13 100       147 return map { $_->can('FIELDS') ? $_->FIELDS : () } @parents;
  13         146  
244             }
245             elsif ($name eq '-with')
246             {
247 2         15 require Moo::Role;
248 2         21 "Moo::Role"->apply_roles_to_package($klass, @$val);
249 2         2934 "Moo"->_maybe_reset_handlemoose($klass);
250            
251 2 100       59 if ($self->trace)
252             {
253 1         25 $self->trace_handle->printf(
254             "with qw(%s)\n",
255             join(q[ ] => @$val),
256             );
257             }
258            
259             return
260             # map { my $role = $_; grep { not ref $_ } @{ $Moo::Role::INFO{$role}{attributes} } }
261             # @$val;
262 2         35 }
263             elsif ($name eq '-class')
264             {
265             # skip; already handled by 'create_class' method (hopefully)
266             }
267             else
268             {
269 1         198 confess("option '$name' unknown");
270             }
271            
272 4         29 return;
273             }
274            
275             sub process_method
276             {
277 79     79 0 263 my ($self, $klass, $name, $coderef) = @_;
278 79         342 __install_sub__ {
279             into => $klass,
280             as => $name,
281             code => $coderef,
282             };
283 79 100       1757 if ($self->trace)
284             {
285 6         137 $self->trace_handle->printf(
286             "sub %s { ... }\n",
287             $name,
288             );
289 6 100       122 if ($self->flags->{deparse})
290             {
291 3         16 require B::Deparse;
292 3         3835 my $code = "B::Deparse"->new(qw(-q -si8T))->coderef2text($coderef);
293 3         49 $code =~ s/^/# /mig;
294 3         76 $self->trace_handle->printf("$code\n");
295             }
296             }
297 79         692 return;
298             }
299            
300 16         1350 my $ScalarLike;
301             sub process_spec
302             {
303 55     55 0 131 my ($self, $klass, $name, $val) = @_;
304            
305             my %spec = (
306 55 100       279 is => ($self->flags->{rw} ? 'rw' : 'ro'),
    100          
    100          
307             ( ArrayLike->check($val)
308             ? @$val
309             : ( HashLike->check($val) ? %$val : () )
310             ),
311             );
312            
313 55 100       11278 if ($name =~ /^(.+)\!$/)
314             {
315 3         10 $name = $1;
316 3         7 $spec{required} = 1;
317             }
318            
319 55 100       367 if ($name =~ /^\@(.+)/)
    100          
    100          
    100          
320             {
321 2         6 $name = $1;
322 2   66     20 $spec{isa} ||= ArrayLike;
323             }
324             elsif ($name =~ /^\%(.+)/)
325             {
326 2         6 $name = $1;
327 2   66     8 $spec{isa} ||= HashLike;
328             }
329             elsif ($name =~ /^\+(.+)/)
330             {
331 15         44 $name = $1;
332 15   66     93 $spec{isa} ||= Num;
333 15 100 100 10   210 $spec{default} ||= sub { 0 } unless $spec{required};
  10         566  
334             }
335             elsif ($name =~ /^\$(.+)/)
336             {
337 19   66     104 $ScalarLike ||= (~(ArrayRef|HashRef))->create_child_type(name => 'ScalarLike');
338 19         4051 $name = $1;
339 19   66     130 $spec{isa} ||= $ScalarLike;
340             }
341            
342 55         249 return ($name, \%spec);
343             }
344            
345             sub process_attribute
346             {
347 55     55 0 149 my ($self, $klass, $name, $val) = @_;
348 55         83 my $spec;
349 55         156 ($name, $spec) = $self->process_spec($klass, $name, $val);
350            
351 55 100       1251 if ($self->trace)
352             {
353 3         677 require Data::Dumper;
354 3         6907 my %sanespec = %$spec;
355 3 100       12 if (TypeTiny->check($sanespec{isa})) {
356 2         73 $sanespec{isa} = $sanespec{isa}->display_name;
357             }
358 3         45 my $spec_str = "Data::Dumper"->new([\%sanespec])->Terse(1)->Indent(0)->Sortkeys(1)->Dump;
359 3         281 $spec_str =~ s/(^\{)|(\}$)//g;
360 3         72 $self->trace_handle->printf(
361             "has %s => (%s);\n",
362             $name,
363             $spec_str,
364             );
365 3 50 66     79 if ($self->flags->{deparse} and CodeRef->check($spec->{isa}))
366             {
367 0         0 require B::Deparse;
368 0         0 my $code = "B::Deparse"->new(qw(-q -si8T))->coderef2text($spec->{isa});
369 0         0 $code =~ s/^/# /mig;
370 0         0 $self->trace_handle->printf("$code\n");
371             }
372             }
373            
374             "Moo"
375 55         618 ->_constructor_maker_for($klass)
376             ->register_attribute_specs($name, $spec);
377            
378 55         17590 "Moo"
379             ->_accessor_maker_for($klass)
380             ->generate_method($klass, $name, $spec);
381            
382 55         22937 "Moo"
383             ->_maybe_reset_handlemoose($klass);
384            
385 55         444 return $name;
386             }
387            
388             # returns a list of "fields" resulting from the argument
389             sub process_argument
390             {
391 78     78 0 139 my $self = shift;
392 78         170 my ($klass, $name, $val) = @_;
393            
394 78 100       236 return $self->process_method(@_) if CodeLike->check($val);
395 75 100       9616 return $self->process_meta(@_) if $name =~ /^-/;
396 55         177 return $self->process_attribute(@_);
397             }
398            
399             sub make_sub
400             {
401 39     39 0 88 my ($self, $subname, $proto) = @_;
402             return sub (;$)
403             {
404 117     117   31863 1; # bizarre, but necessary if $] < 5.014
405 117 100       327 if (ref $proto) # inflate!
406             {
407 39         153 my $opts = Exporter::Tiny::mkopt($proto);
408 39         1140 my $klass = $self->create_class($opts);
409 39         75 my $seen_extends;
410             my @fields = _uniq map {
411 39 100       101 ++$seen_extends if $_->[0] eq '-extends';
  78         238  
412 78         234 $self->process_argument($klass, @$_);
413             } @$opts;
414 38 100 100     452 unshift @fields, $self->base->FIELDS
415             if !$seen_extends && $self->base->can('FIELDS');
416 38     110   306 $self->process_method($klass, FIELDS => sub { @fields });
  110         4369  
417 38 50   4   318 $self->process_method($klass, TYPE => sub { $subname }) if defined $subname;
  4         34  
418 38         131 $proto = $klass;
419             }
420 116 100       663 return $proto->new(@_) if @_;
421 91         936 return $proto;
422             }
423 39         313 }
424            
425             sub process
426             {
427 25     25 0 666 my $self = shift;
428 25         48 my $caller = shift;
429            
430 25   100     218 while (@_ and $_[0] =~ /^-(.+)$/) {
431 2         20 $self->flags->{ lc($1) } = !!shift;
432             }
433            
434 25         63 foreach my $arg (@{ Exporter::Tiny::mkopt(\@_) })
  25         118  
435             {
436 35         607 my ($subname, $details) = @$arg;
437 35 100       94 $details = [] unless defined $details;
438            
439 35         95 $self->class_map->{ $subname } = $self->make_sub($subname, $details);
440             __install_sub__ {
441             into => $caller,
442             as => $subname,
443 35         171 code => $self->class_map->{ $subname },
444             };
445             }
446            
447             on_scope_end {
448             namespace::clean->clean_subroutines(
449             $caller,
450 18     18   3189 keys %{ $self->class_map },
  18         175  
451             );
452 25 100       2520 } unless $self->flags->{ retain };
453             }
454             };
455              
456             sub import
457             {
458 19     19   2137 my $caller = caller;
459 19         44 my $class = shift;
460 19         218 "$class\::Processor"->new->process($caller, @_);
461             }
462              
463 16     16   136 no Moo;
  16         32  
  16         110  
464             1;
465              
466             __END__