File Coverage

blib/lib/Mooish/AttributeBuilder.pm
Criterion Covered Total %
statement 117 117 100.0
branch 32 38 84.2
condition 21 28 75.0
subroutine 30 30 100.0
pod 0 10 0.0
total 200 223 89.6


line stmt bran cond sub pod time code
1             package Mooish::AttributeBuilder;
2             $Mooish::AttributeBuilder::VERSION = '1.002'; # TRIAL
3 15     15   1046660 use v5.10;
  15         189  
4 15     15   95 use strict;
  15         31  
  15         362  
5 15     15   90 use warnings;
  15         40  
  15         550  
6              
7 15     15   114 use Carp qw(croak);
  15         41  
  15         947  
8 15     15   103 use Scalar::Util qw(blessed);
  15         37  
  15         1505  
9              
10             my $set_subname;
11             BEGIN {
12 15 50 33 15   56 if (eval { require Sub::Util } && Sub::Util->VERSION >= 1.40) {
  15         5906  
13 15         8667 $set_subname = \&Sub::Util::set_subname;
14             }
15             }
16              
17             ### These subs can be extended in subclasses
18              
19             # List of available attribute types. May be extended if a custom function will
20             # call expand_shortcuts
21             sub attribute_types
22             {
23             return {
24 117     117 0 1270 field => {
25             is => 'ro',
26             init_arg => undef,
27             },
28             param => {
29             is => 'ro',
30             required => 1,
31             },
32             option => {
33             is => 'ro',
34             required => 0,
35             predicate => 1,
36             },
37             extended => {},
38             };
39             }
40              
41             # Prefix of hidden methods. Will be joined with the rest of the method name
42             # with an underscore, so an empty prefix means starting with an underscore
43             sub hidden_prefix
44             {
45 13     13 0 32 return '';
46             }
47              
48             # The list of methods which are hidden by default
49             sub hidden_methods
50             {
51             return {
52 15     15 0 99 builder => 1,
53             trigger => 1,
54             };
55             }
56              
57             # The list of method name prefixes. Undef means no prefix at all, just use
58             # attribute name
59             sub method_prefixes
60             {
61             return {
62 50     50 0 387 reader => 'get',
63             writer => 'set',
64             clearer => 'clear',
65             predicate => 'has',
66             builder => 'build',
67             trigger => 'trigger',
68             init_arg => undef,
69             };
70             }
71              
72             ### General functions called in sub context
73              
74             sub import
75             {
76 17     17   1005 my ($self, $caller) = (shift, scalar caller);
77 17         39 state $export_cache = {};
78              
79 17         48 my %flags = map { $_ => $_ } @_;
  1         5  
80              
81 17   100     126 my $cache_key = $self . ($flags{-standard} || '');
82 17         32 foreach my $type (keys %{$self->attribute_types}) {
  17         51  
83             my $function = $export_cache->{$cache_key . $type} //= sub {
84 50     50   113610 my ($name, %args) = @_;
        50      
        50      
        50      
        50      
        1      
85 50         471 return $self->expand_shortcuts($flags{-standard}, $type => $name, %args);
86 69   100     475 };
87              
88 69 50       509 $set_subname->("${self}::${type}", $function)
89             if $set_subname;
90              
91             NO_STRICT: {
92 15     15   118 no strict 'refs';
  15         31  
  15         17844  
  69         109  
93 69         163 *{"${caller}::${type}"} = $function;
  69         18633  
94             }
95             }
96             }
97              
98             my @custom_shortcuts;
99              
100             sub custom_shortcuts
101             {
102 49     49 0 120 return [@custom_shortcuts];
103             }
104              
105             sub add_shortcut
106             {
107 4     4 0 191 my ($sub) = @_;
108              
109 4 50       43 croak 'Custom shortcut passed to add_shortcut must be a coderef'
110             unless ref $sub eq 'CODE';
111              
112 4         8 push @custom_shortcuts, $sub;
113 4         10 return;
114             }
115              
116             sub standard_shortcuts
117             {
118 50     50 0 121 my ($self) = @_;
119              
120             return [
121             # expand attribute type
122             sub {
123 50     50   161 my ($name, %args) = @_;
124 50         116 my $type = delete $args{_type};
125              
126 50 50 33     277 if ($type && $self->attribute_types->{$type}) {
127             %args = (
128 50         119 %{$self->attribute_types->{$type}},
  50         131  
129             %args,
130             );
131             }
132              
133 50         365 return %args;
134             },
135              
136             # merge lazy + default / lazy + builder
137             sub {
138 50     50   166 my ($name, %args) = @_;
139              
140 50 100       161 if ($args{lazy}) {
141 5         11 my $lazy = $args{lazy};
142 5         10 $args{lazy} = 1;
143              
144 5 100       23 if (ref $lazy eq 'CODE') {
145 3         11 check_and_set(\%args, $name, default => $lazy);
146             }
147             else {
148 2         5 check_and_set(\%args, $name, builder => $lazy);
149             }
150             }
151              
152 50         257 return %args;
153             },
154              
155             # merge coerce + isa
156             sub {
157 50     50   145 my ($name, %args) = @_;
158              
159 50 100       243 if (blessed $args{coerce}) {
160 1         5 check_and_set(\%args, $name, isa => $args{coerce});
161 1         2 $args{coerce} = 1;
162             }
163              
164 50         253 return %args;
165             },
166              
167             # make sure params with defaults are not required
168             sub {
169 50     50   150 my ($name, %args) = @_;
170              
171 50 100 66     159 if ($args{required} && (exists $args{default} || $args{builder})) {
      66        
172 1         3 delete $args{required};
173             }
174              
175 50         197 return %args;
176             },
177              
178             # method names from shortcuts
179             sub {
180 50     50   137 my ($name, %args) = @_;
181              
182             # initialized lazily
183 50         89 my $normalized_name;
184             my $hidden_field;
185              
186             # inflate names from shortcuts
187 50         87 my %prefixes = %{$self->method_prefixes};
  50         122  
188 50         240 foreach my $method_type (keys %prefixes) {
189 344 100       815 next unless defined $args{$method_type};
190 33 50       90 next if ref $args{$method_type};
191 33 100       67 next unless grep { $_ eq $args{$method_type} } '1', -public, -hidden;
  99         298  
192              
193 32   100     161 $normalized_name //= get_normalized_name($name, $method_type);
194 31   100     159 $hidden_field //= $name ne $normalized_name;
195              
196             my $is_hidden =
197             $args{$method_type} eq -hidden
198             || (
199             $args{$method_type} eq '1'
200 31   100     235 && ($hidden_field || $self->hidden_methods->{$method_type})
201             );
202              
203 93         261 $args{$method_type} = join '_', grep { defined }
204             ($is_hidden ? $self->hidden_prefix : undef),
205 31 100       120 $prefixes{$method_type},
206             $normalized_name;
207             }
208              
209             # special treatment for trigger
210 49 100 66     176 if ($args{trigger} && !ref $args{trigger}) {
211 2         5 my $trigger = $args{trigger};
212             $args{trigger} = sub {
213 2         12243 return shift->$trigger(@_);
214 2         16 };
215             }
216              
217 49         287 return %args;
218             },
219              
220             # literal parameters (prepended with -)
221             sub {
222 49     49   162 my ($name, %args) = @_;
223              
224 49         168 foreach my $literal (keys %args) {
225 147 100       395 if ($literal =~ m{\A - (.+) \z}x) {
226 2         7 $args{$1} = delete $args{$literal};
227             }
228             }
229              
230 49         219 return %args;
231             },
232 50         756 ];
233             }
234              
235             sub expand_shortcuts
236             {
237 50     50 0 223 my ($self, $standard, $attribute_type, $name, %args) = @_;
238              
239 50         121 $args{_type} = $attribute_type;
240              
241             # NOTE: don't use custom shortcuts if we stick to the standard
242 50         102 my @filters;
243 50 100       156 push @filters, @{$self->custom_shortcuts} unless $standard;
  49         130  
244 50         95 push @filters, @{$self->standard_shortcuts};
  50         120  
245              
246             # NOTE: builtin shortcuts are executed after custom shortcuts
247 50         167 foreach my $sub (@filters) {
248 302         1640 %args = $sub->($name, %args);
249             }
250              
251             # TODO: dirty hack for 'extended' attribute. Can be done better?
252 49 100       142 if ($attribute_type eq 'extended') {
253 4 100       15 if (ref $name eq 'ARRAY') {
254 2         4 $name = [map { "+$_" } @{$name}];
  4         13  
  2         7  
255             }
256             else {
257 2         7 $name = "+$name";
258             }
259             }
260              
261 49         1110 return ($name, %args);
262             }
263              
264             ### Helpers - not called in pkg context
265              
266             sub check_and_set
267             {
268 6     6 0 24 my ($hash_ref, $name, %pairs) = @_;
269              
270 6         19 foreach my $key (keys %pairs) {
271             croak "Could not expand shortcut: $key already exists for $name"
272 6 50       19 if exists $hash_ref->{$key};
273              
274 6         15 $hash_ref->{$key} = $pairs{$key};
275             }
276              
277 6         16 return;
278             }
279              
280             sub get_normalized_name
281             {
282 31     31 0 80 my ($name, $for) = @_;
283              
284 31 100       253 croak "Could not use attribute shortcut with array fields: $for is not supported"
285             if ref $name;
286              
287 30         79 $name =~ s/^_//;
288 30         134 return $name;
289             }
290              
291             1;
292              
293             # ABSTRACT: build Mooish attribute definitions with less boilerplate
294