File Coverage

blib/lib/Object/HashBase.pm
Criterion Covered Total %
statement 215 246 87.4
branch 85 116 73.2
condition 22 40 55.0
subroutine 26 29 89.6
pod 1 4 25.0
total 349 435 80.2


line stmt bran cond sub pod time code
1             package Object::HashBase;
2 5     5   538784 use strict;
  5         11  
  5         212  
3 5     5   30 use warnings;
  5         34  
  5         509  
4              
5             our $VERSION = '0.016';
6             our $HB_VERSION = $VERSION;
7             # The next line is for inlining
8             # <-- START -->
9              
10             require Carp;
11             {
12 5     5   92 no warnings 'once';
  5         11  
  5         1388  
13             $Carp::Internal{+__PACKAGE__} = 1;
14             }
15              
16             BEGIN {
17             {
18             # Make sure none of these get messed up.
19 5     5   31 local ($SIG{__DIE__}, $@, $?, $!, $^E);
  5         63  
20 5 50       12 if (eval { require Class::XSAccessor; Class::XSAccessor->VERSION(1.19); 1 }) {
  5         2926  
  5         16453  
  5         29  
21             *CLASS_XS_ACCESSOR = sub() { 1 }
22 5         52 }
23             else {
24             *CLASS_XS_ACCESSOR = sub() { 0 }
25 0         0 }
26             }
27              
28             # these are not strictly equivalent, but for out use we don't care
29             # about order
30             *_isa = ($] >= 5.010 && require mro) ? \&mro::get_linear_isa : sub {
31 5     5   41 no strict 'refs';
  5         14  
  5         744  
32 0         0 my @packages = ($_[0]);
33 0         0 my %seen;
34 0         0 for my $package (@packages) {
35 0         0 push @packages, grep !$seen{$_}++, @{"$package\::ISA"};
  0         0  
36             }
37 0         0 return \@packages;
38             }
39 5 50 33     2875 }
40              
41             sub _is_role {
42 49     49   81 my $pkg = shift;
43 49 100       188 return 0 unless $INC{'Role/Tiny.pm'};
44 19 100       66 return Role::Tiny->is_role($pkg) ? 1 : 0;
45             }
46              
47             my %SPEC = (
48             '^' => {reader => 1, writer => 0, dep_writer => 1, read_only => 0, strip => 1},
49             '-' => {reader => 1, writer => 0, dep_writer => 0, read_only => 1, strip => 1},
50             '>' => {reader => 0, writer => 1, dep_writer => 0, read_only => 0, strip => 1},
51             '<' => {reader => 1, writer => 0, dep_writer => 0, read_only => 0, strip => 1},
52             '+' => {reader => 0, writer => 0, dep_writer => 0, read_only => 0, strip => 1},
53             '~' => {reader => 1, writer => 1, dep_writer => 0, read_only => 0, strip => 1, no_xs => 1},
54             );
55              
56 70     70 0 380 sub spec { \%SPEC }
57              
58             sub import {
59 50     50   14612 my $class = shift;
60 50         132 my $into = caller;
61 50         222 $class->do_import($into, @_);
62             }
63              
64             sub do_import {
65 50     50 0 145 my $class = shift;
66 50         91 my $into = shift;
67              
68             # Make sure we list the OLDEST version used to create this class.
69 50   33     166 my $ver = $Object::HashBase::HB_VERSION || $Object::HashBase::VERSION;
70 50 50 33     351 $Object::HashBase::VERSION{$into} = $ver if !$Object::HashBase::VERSION{$into} || $Object::HashBase::VERSION{$into} > $ver;
71              
72 50         96 my (@parents, @roles, @attrs);
73 50         124 for my $arg (@_) {
74 85 50 33     356 if (defined($arg) && length($arg)) {
75 85         180 my $p = substr($arg, 0, 1);
76 85 100       191 if ($p eq '@') {
77 4         12 push @parents, substr($arg, 1);
78 4         10 next;
79             }
80 81 100       188 if ($p eq '&') {
81 11         30 push @roles, substr($arg, 1);
82 11         27 next;
83             }
84             }
85 70         166 push @attrs, $arg;
86             }
87              
88 50         139 for my $parent (@parents) {
89 4         8 my $pm = $parent;
90 4         34 $pm =~ s{::}{/}g;
91 4         8 $pm .= '.pm';
92 4 100       16 unless ($INC{$pm}) {
93 1         3 local ($@);
94 1 50       3 unless (eval { require $pm; 1 }) {
  1         553  
  0         0  
95 1         243 Carp::croak("Could not load parent class '$parent': $@");
96             }
97             }
98 5     5   45 no strict 'refs';
  5         13  
  5         1620  
99 3 50       4 push @{"$into\::ISA"}, $parent unless grep { $_ eq $parent } @{"$into\::ISA"};
  3         40  
  1         5  
  3         19  
100             }
101              
102 49         294 my $isa = _isa($into);
103 49   50     252 my $attr_list = $Object::HashBase::ATTR_LIST{$into} ||= [];
104 49   50     192 my $attr_subs = $Object::HashBase::ATTR_SUBS{$into} ||= {};
105              
106 49         142 my @pre_init;
107             my @post_init;
108              
109 49 100       125 my $add_new = _is_role($into) ? 0 : 1;
110              
111 49 100 100     682 if ($add_new && (my $have_new = $into->can('new'))) {
112 11   50     36 my $new_lookup = $Object::HashBase::NEW_LOOKUP //= {};
113 11 100       48 $add_new = 0 unless $new_lookup->{$have_new};
114             }
115              
116             my %subs = (
117             ($add_new ? ($class->_build_new($into, \@pre_init, \@post_init)) : ()),
118 49 100       192 (map %{$Object::HashBase::ATTR_SUBS{$_} || {}}, @{$isa}[1 .. $#$isa]),
  11 50       80  
  49         190  
119             ($class->args_to_subs($attr_list, $attr_subs, \@attrs, $into)),
120             );
121              
122 5     5   97 no strict 'refs';
  5         13  
  5         2156  
123 49         234 while (my ($k, $v) = each %subs) {
124 413 100       1057 if (ref($v) eq 'CODE') {
125 286         442 *{"$into\::$k"} = $v;
  286         1642  
126             }
127             else {
128 127         426 my ($sub, @args) = @$v;
129 127         1340 $sub->(@args);
130             }
131             }
132              
133 49 100       6522 if (@roles) {
134 10 50       25 Carp::croak("Object::HashBase '&' role prefix requires Perl 5.010 or newer (this is $])")
135             if $] < 5.010;
136              
137 10 50       32 unless ($INC{'Role/Tiny.pm'}) {
138 0         0 local ($@);
139 0 0       0 unless (eval { require Role::Tiny; 1 }) {
  0         0  
  0         0  
140 0         0 Carp::croak("Object::HashBase '&' role prefix requires Role::Tiny but it could not be loaded: $@");
141             }
142             }
143              
144 10 50       101 unless (Role::Tiny->can('is_role')) {
145 0         0 Carp::croak("Object::HashBase '&' role prefix requires Role::Tiny 1.003000 or newer (is_role missing)");
146             }
147              
148 10         25 for my $role (@roles) {
149 11         21 my $pm = $role;
150 11         54 $pm =~ s{::}{/}g;
151 11         22 $pm .= '.pm';
152              
153 11 100       80 unless ($INC{$pm}) {
154 1         1 local ($@);
155 1 50       2 unless (eval { require $pm; 1 }) {
  1         87  
  0         0  
156 1         232 Carp::croak("Could not load role '$role': $@");
157             }
158             }
159              
160 10 100       33 Carp::croak("'$role' is not a Role::Tiny role")
161             unless Role::Tiny->is_role($role);
162              
163             Carp::croak("'$role' does not use Object::HashBase")
164 9 100       178 unless exists $Object::HashBase::VERSION{$role};
165              
166 8   50     19 my $role_subs = $Object::HashBase::ATTR_SUBS{$role} || {};
167              
168 5     5   48 no strict 'refs';
  5         10  
  5         10273  
169 8         25 for my $const (keys %$role_subs) {
170 8 100       11 next if defined &{"$into\::$const"}; # keep existing sub, no override, no warn
  8         45  
171 6         11 *{"$into\::$const"} = $role_subs->{$const};
  6         28  
172             }
173              
174 8   50     21 my $role_attr_list = $Object::HashBase::ATTR_LIST{$role} || [];
175 8   100     11 push @{$Object::HashBase::ROLE_ATTRS{$into} ||= []}, @$role_attr_list;
  8         57  
176             }
177              
178 7         14 my $key = "Object::HashBase::role_applier::$into";
179 7   33     46 my $applier = $^H{$key} ||= Object::HashBase::_RoleApplier->new($into);
180 7         25 $applier->add($_) for @roles;
181             }
182             }
183              
184             sub args_to_subs {
185 49     49 0 93 my $class = shift;
186 49         123 my ($attr_list, $attr_subs, $args, $into) = @_;
187              
188 49         272 my $use_gen = $class->can('gen_accessor') ;
189              
190 49         84 my %out;
191              
192 49         166 while (@$args) {
193 70         143 my $x = shift @$args;
194 70         159 my $p = substr($x, 0, 1);
195              
196 70   100     180 my $spec = $class->spec->{$p} || {reader => 1, writer => 1};
197 70 100       200 substr($x, 0, 1) = '' if $spec->{strip};
198              
199 70         162 push @$attr_list => $x;
200 70         182 my ($sub, $attr) = (uc $x, $x);
201              
202 70     0   681 $attr_subs->{$sub} = sub() { $attr };
  0         0  
203 70         183 $out{$sub} = $attr_subs->{$sub};
204              
205 70         135 my $copy = "$attr";
206 70 100       186 if ($spec->{reader}) {
207 66 50       203 if ($use_gen) {
    50          
208 0         0 $out{$attr} = $class->gen_accessor(reader => $copy, $spec, $args);
209             }
210             elsif (CLASS_XS_ACCESSOR && !$spec->{no_xs}) {
211 66         313 $out{$attr} = [\&Class::XSAccessor::newxs_getter, "$into\::$attr", $copy];
212             }
213             else {
214 0     0   0 $out{$attr} = sub { $_[0]->{$attr} };
  0         0  
215             }
216             }
217              
218 70 100       195 if ($spec->{writer}) {
    100          
    100          
219 61 50       172 if ($use_gen) {
    50          
220 0         0 $out{"set_$attr"} = $class->gen_accessor(writer => $copy, $spec, $args);
221             }
222             elsif(CLASS_XS_ACCESSOR && !$spec->{no_xs}) {
223 61         272 $out{"set_$attr"} = [\&Class::XSAccessor::newxs_setter, "$into\::set_$attr", $copy, 0];
224             }
225             else {
226 0     0   0 $out{"set_$attr"} = sub { $_[0]->{$attr} = $_[1] };
  0         0  
227             }
228             }
229             elsif($spec->{read_only}) {
230 3 50   1   2145 $out{"set_$attr"} = $use_gen ? $class->gen_accessor(read_only => $copy, $spec, $args) : sub { Carp::croak("'$attr' is read-only") };
  1         244  
231             }
232             elsif($spec->{dep_writer}) {
233 2 50   1   12 $out{"set_$attr"} = $use_gen ? $class->gen_accessor(dep_writer => $copy, $spec, $args) : sub { Carp::carp("set_$attr() is deprecated"); $_[0]->{$attr} = $_[1] };
  1         109  
  1         5  
234             }
235              
236 70 50       382 if ($spec->{custom}) {
237 0         0 my %add = $class->gen_accessor(custom => $copy, $spec, $args);
238 0         0 $out{$_} = $add{$_} for keys %add;
239             }
240             }
241              
242 49         401 return %out;
243             }
244              
245             sub attr_list {
246 6     6 1 1212 my $class = shift;
247              
248 6         48 my $isa = _isa($class);
249              
250 6         14 my %seen;
251             my @list;
252 6         15 for my $pkg (reverse @$isa) {
253 9 50 50     50 if (0.004 > ($Object::HashBase::VERSION{$pkg} || 0)) {
254 0         0 Carp::carp("$pkg uses an inlined version of Object::HashBase too old to support attr_list()");
255 0         0 next;
256             }
257 9         17 my $own = $Object::HashBase::ATTR_LIST{$pkg};
258 9   100     32 my $role_attrs = $Object::HashBase::ROLE_ATTRS{$pkg} || [];
259 9 50       23 for my $a (@$role_attrs, ($own ? @$own : ())) {
260 21 100       68 push @list, $a unless $seen{$a}++;
261             }
262             }
263              
264 6         30 return @list;
265             }
266              
267             sub _build_new {
268 38     38   79 my $class = shift;
269 38         94 my ($into, $pre_init, $post_init) = @_;
270              
271 38     3   232 my $add_pre_init = sub(&) { push @$pre_init => $_[-1] };
  3         9  
272 38     4   144 my $add_post_init = sub(&) { push @$post_init => $_[-1] };
  4         13  
273              
274 38         267 my $__pre_init = $into->can('_pre_init');
275 38 100   19   199 my $_pre_init = $__pre_init ? sub { ($__pre_init->(), @$pre_init) } : sub { @$pre_init };
  6         19  
  19         58  
276              
277 38         189 my $__post_init = $into->can('_post_init');
278 38 100   19   163 my $_post_init = $__post_init ? sub { ($__post_init->(), @$post_init) } : sub { @$post_init };
  6         14  
  19         44  
279              
280             my $new = sub {
281 20     20   324379 my $class = shift;
282              
283 20         54 my $self;
284              
285 20 100       64 if (@_ == 1) {
286 4         9 my $arg = shift;
287 4         9 my $type = ref($arg);
288              
289 4 100       12 if ($type eq 'HASH') {
290 1         5 $self = bless({%$arg}, $class);
291             }
292             else {
293 3 50       10 Carp::croak("Not sure what to do with '$type' in $class constructor")
294             unless $type eq 'ARRAY';
295              
296 3         6 my %proto;
297 3         8 my @attributes = attr_list($class);
298 3         9 while (@$arg) {
299 11         18 my $val = shift @$arg;
300 11 100       155 my $key = shift @attributes or Carp::croak("Too many arguments for $class constructor");
301 10         40 $proto{$key} = $val;
302             }
303              
304 2         9 $self = bless(\%proto, $class);
305             }
306             }
307             else {
308 16         53 $self = bless({@_}, $class);
309             }
310              
311             $Object::HashBase::CAN_CACHE{$class} = $self->can('init')
312 19 100       126 unless exists $Object::HashBase::CAN_CACHE{$class};
313              
314 19         52 $self->$_() for $_pre_init->();
315 19 100       57 $self->init() if $Object::HashBase::CAN_CACHE{$class};
316 19         43 $self->$_() for reverse $_post_init->();
317              
318 19         84 $self;
319 38         284 };
320              
321 38   100     118 my $new_lookup = $Object::HashBase::NEW_LOOKUP //= {};
322 38         133 $new_lookup->{$new} = 1;
323              
324 38         65 my %out;
325              
326             {
327 5     5   45 no strict 'refs';
  5         17  
  5         2990  
  38         59  
328 38 50       58 $out{new} = $new unless defined(&{"${into}\::new"});
  38         208  
329 38 50       57 $out{add_pre_init} = $add_pre_init unless defined(&{"${into}\::add_pre_init"});
  38         264  
330 38 50       56 $out{add_post_init} = $add_post_init unless defined(&{"${into}\::add_post_init"});
  38         227  
331 38 50       57 $out{_pre_init} = $_pre_init unless defined(&{"${into}\::_pre_init"});
  38         175  
332 38 50       111 $out{_post_init} = $_post_init unless defined(&{"${into}\::_post_init"});
  38         166  
333             }
334              
335 38         276 return %out;
336             }
337              
338             # _RoleApplier — deferred Role::Tiny composition.
339             #
340             # Object::HashBase's '&' import prefix copies role constants into the consumer
341             # eagerly (so `$self->{+FOO}` resolves at compile time), then defers actual
342             # Role::Tiny->apply_roles_to_package to end of consumer's compile scope by
343             # storing a blessed object in %^H. Perl destroys %^H entries at end of compile
344             # scope, triggering DESTROY here, which finally composes the role(s).
345              
346             package # hide from PAUSE indexer
347             Object::HashBase::_RoleApplier;
348              
349             sub new {
350 7     7   16 my ($class, $into) = @_;
351 7         2171 return bless { into => $into, roles => [] }, $class;
352             }
353              
354             sub add {
355 8     8   18 my ($self, $role) = @_;
356 8         277 push @{$self->{roles}}, $role
357 8 50       14 unless grep { $_ eq $role } @{$self->{roles}};
  1         4  
  8         24  
358             }
359              
360             sub DESTROY {
361 7     7   15 my $self = shift;
362 7 50       13 return unless @{$self->{roles}};
  7         20  
363 7         11 local $@;
364 7         15 my $ok = eval { Role::Tiny->apply_roles_to_package($self->{into}, @{$self->{roles}}); 1 };
  7         14  
  7         32  
  7         6710  
365 7 50       2812 unless ($ok) {
366 0   0       my $err = $@ || 'unknown error';
367 0           my $into = $self->{into};
368 0           my $roles = join(', ', @{$self->{roles}});
  0            
369 0           warn "Object::HashBase: failed to compose role(s) [$roles] into $into: $err";
370             }
371             }
372              
373             1;
374              
375             __END__