File Coverage

blib/lib/Mic/Assembler.pm
Criterion Covered Total %
statement 334 349 95.7
branch 110 136 80.8
condition 32 49 65.3
subroutine 63 69 91.3
pod 0 3 0.0
total 539 606 88.9


line stmt bran cond sub pod time code
1             package Mic::Assembler;
2              
3 33     33   193 use strict;
  33         116  
  33         964  
4 33     33   8621 use Class::Method::Modifiers qw(install_modifier);
  33         41105  
  33         1599  
5 33     33   217 use Carp;
  33         60  
  33         1403  
6 33     33   10374 use Hash::Util qw( lock_keys );
  33         68309  
  33         193  
7 33     33   14076 use Hash::Merge qw( merge );
  33         65849  
  33         1866  
8 33     33   10317 use List::MoreUtils qw( any uniq );
  33         217646  
  33         327  
9 33     33   22133 use Module::Runtime qw( require_module );
  33         68  
  33         235  
10 33     33   1657 use Params::Validate qw(:all);
  33         56  
  33         4737  
11 33     33   8667 use Package::Stash;
  33         39442  
  33         995  
12 33     33   203 use Scalar::Util qw( reftype );
  33         63  
  33         1500  
13 33     33   12250 use Storable qw( dclone );
  33         75342  
  33         1876  
14 33     33   9001 use Sub::Name;
  33         12038  
  33         1428  
15              
16 33     33   9048 use Mic::_Guts;
  33         79  
  33         101257  
17              
18             sub new {
19 49     49 0 142 my ($class, %arg) = @_;
20              
21             my $obj = {
22             spec => $arg{-spec} || {},
23 49   50     222 };
24 49         157 bless $obj;
25             }
26              
27             sub load_spec_from {
28 47     47 0 115 my ($self, $package) = @_;
29              
30 47         160 my $spec = $self->{spec};
31 47         689 my $cls_stash = Package::Stash->new($package);
32              
33 47 50       159 $spec = { %$spec, %{ $cls_stash->get_symbol('%__meta__') || {} } };
  47         865  
34 47         151 $spec->{name} = $package;
35 47         80 $self->{cls_stash} = $cls_stash;
36 47         72 $self->{spec} = $spec;
37 47         124 return $spec;
38             }
39              
40             sub assemble {
41 49     49 0 111 my ($self) = @_;
42              
43 49         118 my $spec = $self->{spec};
44 49   66     237 $self->{cls_stash} ||= Package::Stash->new($spec->{name});
45              
46 49         78 my $obj_stash;
47              
48 49   66     220 my $pkg = $Mic::Bound_implementation_of{ $spec->{name} } || $spec->{implementation};
49             $pkg ne $spec->{name}
50 49 50       134 or confess "$spec->{name} cannot be its own implementation.";
51 49         136 my $stash = _get_stash($pkg);
52              
53 49         216 my $meta = $stash->get_symbol('%__meta__');
54              
55             $spec->{implementation} = {
56             package => $pkg,
57             methods => $stash->get_all_symbols('CODE'),
58             has => {
59 49 50       360 %{ $meta->{has} || { } },
60             },
61             arrayimp => $meta->{arrayimp},
62             slot_offset => $meta->{slot_offset},
63 49         384 };
64 49         223 _collect_non_instance_methods($spec, $meta);
65 49         458 $obj_stash = Package::Stash->new("$spec->{implementation}{package}::__Assembled");
66              
67 49         188 _prep_interface($spec);
68 49         129 _merge_interfaces($spec);
69              
70 49         101 my $cls_stash = $self->{cls_stash};
71 49         414 $cls_stash->add_symbol('$__Obj_pkg', $obj_stash->name);
72 49 50       387 $cls_stash->add_symbol('%__meta__', $spec) if @_ > 0;
73              
74 49         160 _add_methods($spec, $obj_stash);
75 49         1354 _make_builder_class($spec);
76 49         162 _add_class_methods($spec, $cls_stash);
77 49         1586 _check_interface($spec);
78 48         1286 return $spec->{name};
79             }
80              
81             sub _collect_non_instance_methods {
82 49     49   109 my ($spec, $meta) = @_;
83              
84 49         139 my $is_classmethod = _interface($meta, 'classmethod');
85              
86 49         91 foreach my $sub ( keys %{ $spec->{implementation}{methods} } ) {
  49         173  
87 134         174 my $type;
88 134 100       255 if ( $is_classmethod->{$sub} ) {
89 1         1 $type = 'classmethod';
90             }
91 134 100       296 if ($type) {
92 1         3 $spec->{implementation}{$type}{$sub} = delete $spec->{implementation}{methods}{$sub};
93             }
94             }
95             }
96              
97             sub _get_stash {
98 49     49   80 my $pkg = shift;
99              
100 49         329 my $stash = Package::Stash->new($pkg); # allow for inlined pkg
101              
102 49 100       590 if ( ! $stash->has_symbol('%__meta__') ) {
103 44         201 require_module($pkg);
104 44         801 $stash = Package::Stash->new($pkg);
105             }
106 49 50       558 if ( ! $stash->has_symbol('%__meta__') ) {
107 0         0 confess "Package $pkg has no %__meta__";
108             }
109 49         145 return $stash;
110             }
111              
112             sub _interface {
113 98     98   180 my ($spec, $type) = @_;
114              
115 98   100     332 $type ||= 'interface';
116 98         328 my %must_allow = (
117             interface => [qw( AUTOLOAD can DOES DESTROY )],
118             classmethod => [ ],
119             );
120 98 100 66     458 if ( $type eq 'interface' && ref $spec->{$type} eq 'HASH') {
121 49         119 $spec->{interface_meta} = do {
122 49         100 my @args = %{ $spec->{$type} };
  49         176  
123 49         1103 validate(@args, {
124             object => { type => HASHREF },
125             class => { type => HASHREF },
126             extends => { type => ARRAYREF, optional => 1 },
127             invariant => { type => HASHREF, optional => 1 },
128             });
129 49         488 $spec->{$type};
130             };
131 49         96 $spec->{$type} = [ keys %{ $spec->{$type}{object} } ];
  49         195  
132 49         148 $Mic::Spec_for{ $spec->{name} }{interface} = $spec->{interface_meta};
133             }
134 98         146 return { map { $_ => 1 } @{ $spec->{$type} }, @{ $must_allow{$type} } };
  391         798  
  98         192  
  98         262  
135             }
136              
137             sub _prep_interface {
138 49     49   98 my ($spec) = @_;
139              
140 49 100       181 return if ref $spec->{interface};
141 3         7 my $count = 0;
142             {
143              
144 3 100       6 if (my $methods = $Mic::Spec_for{ $spec->{interface} }{interface}) {
  4         19  
145 3         9 $spec->{interface_name} = $spec->{interface};
146 3         9 $spec->{interface} = $methods;
147             }
148             else {
149 1 50       2 $count > 0
150             and confess "Invalid interface: $spec->{interface}";
151 1         3 require_module($spec->{interface});
152 1         7 $count++;
153 1         1 redo;
154             }
155             }
156             }
157              
158             sub _merge_interfaces {
159 49     49   92 my ($spec) = @_;
160              
161 49 100       74 foreach my $super (@{ $spec->{interface}{extends} || [] }) {
  49         282  
162 2         10 require_module($super);
163             my $declared_interface = $Mic::Spec_for{ $super }{interface}
164 2 50       51 or confess "Could not find interface '$super'";
165 2         13 $spec->{interface} = merge($spec->{interface}, $declared_interface);
166 2         5974 $spec->{does}{$super} = 1;
167             }
168             }
169              
170             sub _check_interface {
171 49     49   98 my ($spec) = @_;
172 49         106 my $count = 0;
173 49         82 foreach my $method ( @{ $spec->{interface} } ) {
  49         122  
174 193 100       401 defined $spec->{implementation}{methods}{$method}
175             or confess "Interface method '$method' is not implemented.";
176 192         262 ++$count;
177             }
178 48 50       155 $count > 0 or confess "Cannot have an empty interface.";
179             }
180              
181             sub _add_methods {
182 49     49   95 my ($spec, $stash) = @_;
183              
184 49         105 my $in_interface = _interface($spec);
185              
186             $spec->{implementation}{methods}{DOES} = sub {
187 8     8   4208 my ($self, $r) = @_;
        6      
        8      
188              
189 8 100       21 if ( ! $r ) {
190             my @items = (( $spec->{interface_name} ? $spec->{interface_name} : () ),
191 2 50       8 $spec->{name}, sort keys %{ $spec->{does} });
  2         11  
192 2 50       7 return unless defined wantarray;
193 2 50       19 return wantarray ? @items : \@items;
194             }
195              
196             return $r eq $spec->{interface_name}
197             || $spec->{name} eq $r
198 6   33     55 || $spec->{does}{$r}
199             || $self->isa($r);
200 49         259 };
201             $spec->{implementation}{methods}{can} = sub {
202 75     75   9919 my ($self, $f) = @_;
        75      
        75      
203              
204 75 50       189 if ( ! $f ) {
205 0         0 my @items = sort @{ $spec->{interface} };
  0         0  
206 0 0       0 return unless defined wantarray;
207 0 0       0 return wantarray ? @items : \@items;
208             }
209 75         377 return UNIVERSAL::can($self, $f);
210 49         196 };
211              
212 49         112 while ( my ($name, $meta) = each %{ $spec->{implementation}{has} } ) {
  113         535  
213              
214 64         244 _validate_slot_def($meta);
215 64 100 66     624 if ( ! $spec->{implementation}{methods}{$name}
      100        
216             && $meta->{reader}
217             && $in_interface->{ $meta->{reader} } ) {
218              
219 14         50 my $obfu_name = Mic::_Guts::obfu_name($name, $spec);
220             $spec->{implementation}{methods}{ $meta->{reader} } = sub {
221 144     144   2311 my ($self) = @_;
        27      
222              
223 144 100       346 if ( reftype $self eq 'HASH' ) {
224 143         1958 return $self->{$obfu_name};
225             }
226 1         12 return $self->[ $spec->{implementation}{slot_offset}{$name} ];
227 14         78 };
228             }
229              
230 64 100 66     385 if ( ! $spec->{implementation}{methods}{$name}
      66        
231             && $meta->{property}
232             && $in_interface->{ $meta->{property} } ) {
233              
234 1 50       4 confess "'property' can only be used from Perl 5.16 onwards"
235             if $] lt '5.016';
236 1         5 my $obfu_name = Mic::_Guts::obfu_name($name, $spec);
237             $spec->{implementation}{methods}{ $meta->{property} } = sub : lvalue {
238 2     2   11 my ($self) = @_;
239              
240 2 50       7 if ( reftype $self eq 'HASH' ) {
241 0         0 return $self->{$obfu_name};
242             }
243 2         10 return $self->[ $spec->{implementation}{slot_offset}{$name} ];
244 1         5 };
245             }
246              
247 64 100 66     302 if ( ! $spec->{implementation}{methods}{$name}
      66        
248             && $meta->{writer}
249             && $in_interface->{ $meta->{writer} } ) {
250              
251             $spec->{implementation}{methods}{ $meta->{writer} } = sub {
252 1     1   5 my ($self, $new_val) = @_;
        12      
253              
254 1 50       4 if ( reftype $self eq 'HASH' ) {
255 0         0 $self->{ Mic::_Guts::obfu_name($name, $spec) } = $new_val;
256             }
257             else {
258 1         8 $self->[ $spec->{implementation}{slot_offset}{$name} ] = $new_val;
259             }
260 1         4 return $self;
261 1         4 };
262             }
263 64         188 _add_delegates($spec, $meta, $name);
264             }
265              
266 49         226 while ( my ($name, $sub) = each %{ $spec->{implementation}{methods} } ) {
  347         1141  
267 298 100       595 next unless $in_interface->{$name};
268 292         3210 $stash->add_symbol("&$name", subname $stash->name."::$name" => $sub);
269             }
270              
271 49         107 foreach my $name ( @{ $spec->{interface} } ) {
  49         117  
272 194         2406 _add_pre_conditions($spec, $stash, $name, 'object');
273 194         855 _add_post_conditions($spec, $stash, $name, 'object');
274             }
275 49         1002 _add_invariants($spec, $stash);
276             }
277              
278             sub _validate_slot_def {
279 64     65   1283 validate(@_, {
280             default => { type => SCALAR | CODEREF, optional => 1 },
281             handles => { type => ARRAYREF | HASHREF, optional => 1 },
282             init_arg => { type => SCALAR, optional => 1 },
283             property => { type => SCALAR, optional => 1 },
284             reader => { type => SCALAR, optional => 1 },
285             writer => { type => SCALAR, optional => 1 },
286             });
287             }
288              
289             sub _add_invariants {
290 49     49   105 my ($spec, $stash) = @_;
291              
292 49 100       198 return unless $Mic::Contracts_for{ $spec->{name} }{invariant};
293             my $inv_hash =
294             (! ref $spec->{interface}
295             && $Mic::Spec_for{ $spec->{interface} }{interface_meta}{invariant})
296              
297             || $spec->{interface_meta}{invariant}
298 5 50 33     53 or return;
299              
300             $spec->{invariant_guard} ||= sub {
301             # skip methods called by the invariant
302 687 100   687   23024 return if (caller 1)[0] eq $spec->{name};
303              
304 125         1022 foreach my $desc (keys %{ $inv_hash }) {
  125         235  
305 125         176 my $sub = $inv_hash->{$desc};
306 125 100       241 $sub->(@_)
307             or confess "Invariant '$desc' violated";
308             }
309 5   50     56 };
310 5         12 foreach my $type ( qw[before after] ) {
311 10         755 install_modifier($stash->name, $type, @{ $spec->{interface} }, $spec->{invariant_guard});
  10         58  
312             }
313             }
314              
315              
316             sub _add_pre_conditions {
317 243     243   452 my ($spec, $stash, $name, $type) = @_;
318              
319 243 100       611 return unless $Mic::Contracts_for{ $spec->{name} }{pre};
320              
321 21         83 _validate_contract_def($spec->{interface_meta}{$type}{$name});
322             my $pre_cond_hash = $spec->{interface_meta}{$type}{$name}{require}
323 21 100       85 or return;
324              
325             my $guard = sub {
326 13     13   3140 foreach my $desc (keys %{ $pre_cond_hash }) {
  13         44  
327 13         24 my $sub = $pre_cond_hash->{$desc};
328 13 100       44 $sub->(@_)
329             or confess "Method '$name' failed precondition '$desc'";
330             }
331 6         33 };
332 6         29 install_modifier($stash->name, 'before', $name, $guard);
333             }
334              
335             sub _add_post_conditions {
336 243     243   401 my ($spec, $stash, $name, $type) = @_;
337              
338 243 100       594 return unless $Mic::Contracts_for{ $spec->{name} }{post};
339              
340 42         97 _validate_contract_def($spec->{interface_meta}{$type}{$name});
341             my $post_cond_hash = $spec->{interface_meta}{$type}{$name}{ensure}
342 42 100       188 or return;
343              
344 18         37 my $constructor_spec = _constructor_spec($spec);
345              
346             my $guard = sub {
347 30     30   4400 my $orig = shift;
348 30         50 my $self = shift;
349              
350 30         41 my @old;
351 30         46 my @invocant = ($self);
352 30 100       72 if ($type eq 'object') {
353 24         1177 @old = ( dclone($self) );
354             }
355 30         92 my $results = [$orig->($self, @_)];
356 30         476 my $results_to_check = $results;
357              
358 30 100 66     106 if ($type eq 'class' && $name eq $constructor_spec->{name}) {
359 6         13 $results_to_check = $results->[0];
360 6         16 @invocant = ();
361             }
362              
363 30         40 foreach my $desc (keys %{ $post_cond_hash }) {
  30         82  
364 45         158 my $sub = $post_cond_hash->{$desc};
365 45 100       124 $sub->(@invocant, @old, $results_to_check, @_)
366             or confess "Method '$name' failed postcondition '$desc'";
367             }
368 26 100       442 return unless defined wantarray;
369 7 100       59 return wantarray ? @$results : $results->[0];
370 18         102 };
371 18         78 install_modifier($stash->name, 'around', $name, $guard);
372             }
373              
374             sub _validate_contract_def {
375 63     63   628 validate(@_, {
376             ensure => { type => HASHREF, optional => 1 },
377             require => { type => HASHREF, optional => 1 },
378             });
379             }
380              
381             sub _make_builder_class {
382 49     49   97 my ($spec) = @_;
383              
384 49         467 my $stash = Package::Stash->new("$spec->{name}::__Util");
385 49         193 $Mic::Util_class{ $spec->{name} } = $stash->name;
386              
387 49         152 my $constructor_spec = _constructor_spec($spec);
388              
389 49         162 my %method = (
390             new_object => \&_object_maker,
391             );
392              
393 49     52   208 $method{main_class} = sub { $spec->{name} };
  52     52   134  
        32      
394              
395             $method{build} = sub {
396 51     51   123 my (undef, $obj, $arg) = @_;
        51      
        32      
397              
398 51         125 my $impl_pkg = $spec->{implementation}{package};
399 51 100       577 if ( my $builder = $impl_pkg->can('BUILD') ) {
400 6         21 $builder->($obj, $arg);
401             }
402 49         189 };
403              
404             $method{check_invariants} = sub {
405 51     51   83 shift;
        51      
        32      
406 51         101 my ($obj) = @_;
407              
408 51 100       199 return unless exists $spec->{invariant_guard};
409 5         18 $spec->{invariant_guard}->($obj);
410 49         179 };
411              
412 49         344 my $class_var_stash = Package::Stash->new("$spec->{name}::__ClassVar");
413              
414             $method{get_var} = sub {
415 0     0   0 my ($class, $name) = @_;
        0      
        0      
416 0         0 $class_var_stash->get_symbol($name);
417 49         226 };
418              
419             $method{set_var} = sub {
420 0     0   0 my ($class, $name, $val) = @_;
        0      
        0      
421 0         0 $class_var_stash->add_symbol($name, $val);
422 49         180 };
423              
424 49         204 foreach my $sub ( keys %method ) {
425 294         1821 $stash->add_symbol("&$sub", $method{$sub});
426 294         1903 subname $stash->name."::$sub", $method{$sub};
427             }
428             }
429              
430             sub _add_class_methods {
431 49     49   105 my ($spec, $stash) = @_;
432              
433 49         150 $spec->{class_methods} = $spec->{implementation}{classmethod};
434 49         157 _add_default_constructor($spec);
435              
436 49         78 foreach my $sub ( keys %{ $spec->{class_methods} } ) {
  49         162  
437 49         398 $stash->add_symbol("&$sub", $spec->{class_methods}{$sub});
438 49         370 subname "$spec->{name}::$sub", $spec->{class_methods}{$sub};
439 49         156 _add_pre_conditions($spec, $stash, $sub, 'class');
440 49         563 _add_post_conditions($spec, $stash, $sub, 'class');
441             }
442             }
443              
444             sub _add_delegates {
445 64     64   149 my ($spec, $meta, $name) = @_;
446              
447 64 100       265 if ( $meta->{handles} ) {
448 15         28 my $method;
449 15         31 my $target_method = {};
450 15 100       64 if ( ref $meta->{handles} eq 'ARRAY' ) {
    50          
451 14         28 $method = { map { $_ => 1 } @{ $meta->{handles} } };
  49         118  
  14         35  
452             }
453             elsif( ref $meta->{handles} eq 'HASH' ) {
454 1         2 $method = $meta->{handles};
455 1         1 $target_method = $method;
456             }
457              
458 15         37 foreach my $meth ( keys %{ $method } ) {
  15         50  
459 51 50       121 if ( defined $spec->{implementation}{methods}{$meth} ) {
460 0         0 confess "Cannot override implemented method '$meth' with a delegated method";
461             }
462             else {
463 51         113 my $obfu_name = Mic::_Guts::obfu_name($name, $spec);
464 51   66     154 my $target = $target_method->{$meth} || $meth;
465             $spec->{implementation}{methods}{$meth} = sub {
466 288     288   8952 my $obj = shift;
        90      
        89      
467              
468             my $delegate = reftype $obj eq 'HASH'
469             ? $obj->{$obfu_name}
470 288 100       812 : $obj->[ $spec->{implementation}{slot_offset}{ $name } ];
471 288 100       480 if (wantarray) {
    100          
472 187         430 my @results = $delegate->$target(@_);
473 187         4145 return @results;
474             }
475             elsif( defined wantarray ) {
476 86         251 return $delegate->$target(@_);
477             }
478             else {
479 15         52 $delegate->$target(@_);
480 15         193 return;
481             }
482             }
483 51         268 }
484             }
485             }
486             }
487              
488             sub _constructor_spec {
489 116     177   197 my ($spec) = @_;
490              
491 116 50       304 if(! ref $spec->{interface}) {
492 0         0 my $s;
493             $s = $Mic::Spec_for{ $spec->{interface} }{constructor}
494 0 0       0 and return $s;
495             }
496 116   100     397 $spec->{constructor} ||= {};
497 116         202 return $spec->{constructor};
498             }
499              
500             sub _add_default_constructor {
501 49     49   92 my ($spec) = @_;
502              
503 49         114 my $constructor_spec = _constructor_spec($spec);
504              
505 49   50     303 $constructor_spec->{name} ||= 'new';
506 49         79 my $sub_name = $constructor_spec->{name};
507 49 100       180 if ( ! exists $spec->{class_methods}{$sub_name} ) {
508             $spec->{class_methods}{$sub_name} = sub {
509 51     51   7948 my $class = shift;
        51      
        16      
510 51         90 my ($arg);
511              
512 51 100       241 if ( scalar @_ == 1 ) {
    100          
513 18         38 $arg = shift;
514             }
515             elsif ( scalar @_ > 1 ) {
516 2         7 $arg = [@_];
517             }
518              
519 51         181 my $builder = Mic::builder_for($class);
520 51         311 my $obj = $builder->new_object;
521 51 100       171 my $kv_args = ref $arg eq 'HASH' ? $arg : {};
522 51         86 for my $name ( keys %{ $kv_args } ) {
  51         159  
523              
524             # handle init_args
525 33         119 my ($attr, $dup) = grep { $spec->{implementation}{has}{$_}{init_arg} eq $name }
526 18         40 keys %{ $spec->{implementation}{has} };
  18         64  
527 18 50       69 if ( $dup ) {
528 0         0 confess "Cannot have same init_arg '$name' for attributes '$attr' and '$dup'";
529             }
530 18 100       51 if ( $attr ) {
531 17         40 my $attr_val = $arg->{$name};
532 17 100       110 if ( reftype $obj eq 'HASH' ) {
533 14         49 my $obfu_name = Mic::_Guts::obfu_name($attr, $spec);
534 14         46 $obj->{$obfu_name} = $attr_val;
535             }
536             else {
537 3         18 $obj->[ $spec->{implementation}{slot_offset}{$attr} ] = $attr_val;
538             }
539             }
540             }
541              
542 51         196 $builder->build($obj, $arg);
543 51         291 $builder->check_invariants($obj);
544 51         266 return $obj;
545 48         300 };
546             }
547             }
548              
549             sub _object_maker {
550 52     52   137 my ($builder, $init) = @_;
551              
552 52         165 my $class = $builder->main_class;
553              
554 52         535 my $stash = Package::Stash->new($class);
555              
556 52         460 my $spec = $stash->get_symbol('%__meta__');
557 52         210 my $pkg_key = Mic::_Guts::obfu_name('', $spec);
558             my $obj = $spec->{implementation}{arrayimp}
559 52 100       212 ? [ ]
560             : { };
561              
562 52         94 while ( my ($attr, $meta) = each %{ $spec->{implementation}{has} } ) {
  119         501  
563             my $init_val = $init->{$attr}
564             ? $init->{$attr}
565             : (ref $meta->{default} eq 'CODE'
566             ? $meta->{default}->()
567 67 100       397 : $meta->{default});
    100          
568 67 100       244 if ( $spec->{implementation}{arrayimp} ) {
569 11         25 my $offset = $spec->{implementation}{slot_offset}{$attr};
570 11         36 $obj->[$offset] = $init_val;
571             }
572             else {
573 56         140 my $obfu_name = Mic::_Guts::obfu_name($attr, $spec);
574 56         177 $obj->{$obfu_name} = $init_val;
575             }
576             }
577              
578 52         96 bless $obj => ${ $stash->get_symbol('$__Obj_pkg') };
  52         432  
579 52         167 $Mic::_Guts::Implementation_meta{ref $obj} = $spec->{implementation};
580              
581 52 100       268 if ( reftype $obj eq 'HASH' ) {
582 42         329 lock_keys(%$obj);
583             }
584 52         777 return $obj;
585             }
586              
587             1;
588              
589             __END__