File Coverage

blib/lib/Object/Adhoc.pm
Criterion Covered Total %
statement 100 121 82.6
branch 40 64 62.5
condition 15 29 51.7
subroutine 17 19 89.4
pod 2 2 100.0
total 174 235 74.0


line stmt bran cond sub pod time code
1 4     4   533948 use 5.008;
  4         16  
2 4     4   29 use strict;
  4         35  
  4         130  
3 4     4   24 use warnings;
  4         8  
  4         523  
4              
5             package Object::Adhoc;
6              
7             our $AUTHORITY = 'cpan:TOBYINK';
8             our $VERSION = '0.008';
9              
10 4     4   30 use Digest::MD5 qw( md5_hex );
  4         11  
  4         376  
11 4     4   37 use Scalar::Util qw( refaddr );
  4         26  
  4         424  
12 4     4   2379 use Exporter::Shiny qw( object make_class );
  4         32115  
  4         29  
13             our @EXPORT = qw( object );
14              
15             BEGIN {
16             *USE_XS = eval 'use Class::XSAccessor 1.19 (); 1'
17             ? sub () { !!1 }
18 4 50   4   1028 : sub () { !!0 };
  4     4   2613  
  4         13689  
  4         78  
19             };
20              
21             BEGIN {
22 4     4   2927 require Hash::Util;
23             *lock_ref_keys = 'Hash::Util'->can('lock_ref_keys')
24 4   33     25044 || sub { return; };
25             };
26              
27             our $RESERVED_REGEXP;
28              
29             # Yes, you can push extra methods onto this array if you need to,
30             # but if you do that, then set $RESERVED_REGEXP to undef so that
31             # make_class will rebuild it!
32             #
33             our @RESERVED_METHODS = qw(
34             import unimport
35             DESTROY
36             AUTOLOAD
37             isa DOES does can VERSION
38             meta new
39             TO_JSON
40             );
41             #
42             # Note that tie-related stuff isn't on the list of reserved methods
43             # because people using those names isn't likely to cause any actual
44             # harm.
45              
46             sub object {
47 11 100   11 1 734242 my ($data, $keys, %opts) =
    50          
48             (@_ == 0) ? _croak('Expected hashref') :
49             (@_ <= 2) ? @_ : ($_[0], undef, @_[1..$#_]);
50 11   50     91 $keys ||= delete($opts{keys}) || [ keys %$data ];
      66        
51 11         39 bless $data, make_class($keys, %opts);
52 11 100       34 if ($opts{recurse}) {
53 7 50 33     48 _croak('Bad recurse option') if ref($opts{recurse}) || $opts{recurse} !~ /\A[0-9]+\z/;
54 7 100       11 my %seen = (refaddr($data) => undef, %{ delete($opts{seen}) or {}});
  7         30  
55 7         39 for my $k (keys %$data) {
56 11 100       23 ref $data->{$k} or next;
57 6         16 $data->{$k} = _recurse($data->{$k}, %opts, seen => \%seen);
58             }
59             }
60 11         43 lock_ref_keys($data, @$keys);
61 11         397 $data;
62             }
63              
64             sub _recurse {
65 14     14   33 my ($ref, %opts) = @_;
66 14         20 my $lvl = $opts{recurse} - 1;
67 14         19 my $reftype = ref $ref;
68 14 50       14 my %seen = %{ delete($opts{seen}) or {} };
  14         33  
69 14 50 66     62 return $ref if $lvl < 1 || !$reftype || exists $seen{refaddr($ref)};
      66        
70            
71 11         22 $seen{refaddr($ref)} = undef;
72            
73 11 100       17 if ($reftype eq 'ARRAY') {
74 3 100       4 @$ref = map { ref($_) ? _recurse($_, %opts, recurse => $lvl, seen => \%seen) : $_ } @$ref;
  6         12  
75 3         6 return $ref;
76             }
77            
78 8 100       15 if ($reftype eq 'HASH') {
79 6         18 return object($ref, %opts, recurse => $lvl, seen => \%seen);
80             }
81            
82 2 100       6 if ($reftype eq 'CODE') {
83             return sub {
84 2     2   6835 my $wa = wantarray;
85 2 50       12 if ($wa) {
    50          
86 0 0       0 return map { ref($_) ? _recurse($_, %opts, recurse => $lvl, seen => \%seen) : $_ } $ref->(@_);
  0         0  
87             }
88             elsif (defined $wa) {
89 2         7 local $_ = $ref->(@_);
90 2 50       20 return ref($_) ? _recurse($_, %opts, recurse => $lvl, seen => \%seen) : $_;
91             }
92 0         0 goto $ref;
93 1         5 };
94             }
95              
96 1 50       13 if ($reftype eq 'REF') {
97 1 50       3 ($$ref) = map { ref($_) ? _recurse($_, %opts, recurse => $lvl, seen => \%seen) : $_ } $$ref;
  1         5  
98 1         2 return $ref;
99             }
100              
101 0         0 return $ref;
102             }
103              
104             my %made;
105             sub make_class {
106 13     13 1 219030 my ($keys, %opts) = @_;
107 13         51 my $joined = join "|", sort(@$keys);
108 13 100       39 $joined .= '*CTOR' if $opts{ctor};
109 13 100       39 return $made{$joined} if $made{$joined};
110            
111 9         73 my $class = sprintf('%s::__ANON__::%s', __PACKAGE__, md5_hex($joined));
112            
113 9         53 my %getters = map(+($_ => $_), @$keys);
114 9 50       75 my %predicates = map(+((/^_/?"_has$_":"has_$_")=> $_), @$keys);
115            
116 9   66     36 $RESERVED_REGEXP ||= do {
117 4         51 my $re = join "|", map quotemeta($_), @RESERVED_METHODS;
118 4         398 qr/\A(?:$re)\z/;
119             };
120            
121 9         29 for my $key (@$keys) {
122 17 50       40 if (exists $predicates{$key}) {
123 0         0 delete $predicates{$key};
124 0         0 require Carp;
125 0         0 Carp::carp("Ambiguous method '$key' is getter, not predicate");
126             }
127 17 50 33     156 if ($key !~ /^[^\W0-9]\w*$/s or $key =~ $RESERVED_REGEXP) {
128 0         0 require Carp;
129 0         0 Carp::carp("Key '$key' would be bad method name, not generating methods");
130 0 0       0 my $predicate = ($key =~ /^_/) ? "_has$key" : "has_$key";
131 0         0 delete $getters{$key};
132 0         0 delete $predicates{$predicate};
133             }
134             }
135            
136 9         14 if (USE_XS) {
137 9         99 'Class::XSAccessor'->import(
138             class => $class,
139             getters => \%getters,
140             exists_predicates => \%predicates,
141             );
142             }
143             else {
144             require B;
145             my $code = "package $class;\n";
146             while (my ($predicate, $key) = each %predicates) {
147             my $qkey = B::perlstring($key);
148             $code .= "sub $predicate :method { &Object::Adhoc::_usage if \@_ > 1; CORE::exists \$_[0]{$qkey} }\n";
149             }
150             while (my ($getter, $key) = each %getters) {
151             my $qkey = B::perlstring($key);
152             $code .= "sub $getter :method { &Object::Adhoc::_usage if \@_ > 1; \$_[0]{$qkey} }\n";
153             }
154             $code .= "1;\n";
155             eval($code) or die($@);
156             }
157            
158 9         2421 do {
159 4     4   37 no strict 'refs';
  4         8  
  4         2970  
160 9         21 *{"$class\::DOES"} = \&_DOES;
  9         47  
161 9         81 *{"$class\::does"} = \&_DOES;
  9         39  
162 9         15 *{"$class\::VERSION"} = \$VERSION;
  9         34  
163 9         14 *{"$class\::TO_JSON"} = \&_TO_JSON;
  9         31  
164            
165 9 100       28 if ( $opts{ctor} ) {
166 1         5 my $re = join "|", map quotemeta($_), @$keys;
167 1         4 *{"$class\::new"} = sub {
168 3 50 66 3   2381 my ($class, %hash) = (@_ == 2 and ref $_[1] eq 'HASH') ? ($_[0], %{$_[1]}) : (@_ % 2 == 1) ? @_ : _usage('class', 'hashref');
  1 100       4  
169 3         9 for (keys %hash) {
170 7 100       56 /\A(?:$re)\z/ or _croak("Bad key: $_");
171             }
172 2   33     11 return bless(\%hash, ref($class) || $class);
173 1         4 };
174             }
175             };
176            
177 9         53 $made{$joined} = $class;
178             }
179              
180             sub _usage {
181 0     0   0 my $caller = (caller(1))[3];
182 0         0 require Carp;
183 0         0 local $Carp::CarpLevel = 1 + $Carp::CarpLevel;
184 0 0       0 my @fields = @_ ? @_ : ('self');
185 0         0 Carp::croak("Usage: $caller\(@{[join q[, ], @fields]})"); # mimic XS usage message
  0         0  
186             }
187              
188             sub _croak {
189 1     1   7 require Carp;
190 1         192 goto \&Carp::croak;
191             }
192              
193             sub _DOES {
194 0 0   0   0 return !!1 if $_[1] eq __PACKAGE__;
195 0 0       0 return !!1 if $_[1] eq 'HASH';
196 0         0 shift->isa(@_);
197             }
198              
199             sub _TO_JSON {
200 1     1   132 my %hash = %{ +shift };
  1         6  
201 1         3 \%hash;
202             }
203              
204             1;
205              
206             __END__