File Coverage

blib/lib/MooX/Struct.pm
Criterion Covered Total %
statement 244 263 92.7
branch 83 90 92.2
condition 41 54 75.9
subroutine 53 54 98.1
pod 9 17 52.9
total 430 478 89.9


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