File Coverage

blib/lib/Mooish/AttributeBuilder.pm
Criterion Covered Total %
statement 117 117 100.0
branch 32 38 84.2
condition 27 34 79.4
subroutine 30 30 100.0
pod 0 10 0.0
total 206 229 89.9


line stmt bran cond sub pod time code
1             package Mooish::AttributeBuilder;
2             $Mooish::AttributeBuilder::VERSION = '1.003';
3 15     15   1047870 use v5.10;
  15         189  
4 15     15   81 use strict;
  15         35  
  15         375  
5 15     15   103 use warnings;
  15         30  
  15         545  
6              
7 15     15   113 use Carp qw(croak);
  15         41  
  15         913  
8 15     15   117 use Scalar::Util qw(blessed);
  15         28  
  15         1491  
9              
10             my $set_subname;
11             BEGIN {
12 15 50 33 15   64 if (eval { require Sub::Util } && Sub::Util->VERSION >= 1.40) {
  15         5980  
13 15         8414 $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 121     121 0 1391 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 34 return '';
46             }
47              
48             # The list of methods which are hidden by default
49             sub hidden_methods
50             {
51             return {
52 15     15 0 87 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 52     52 0 460 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   1066 my ($self, $caller) = (shift, scalar caller);
77 17         46 state $export_cache = {};
78              
79 17         42 my %flags = map { $_ => $_ } @_;
  1         4  
80              
81 17   100     132 my $cache_key = $self . ($flags{-standard} || '');
82 17         37 foreach my $type (keys %{$self->attribute_types}) {
  17         47  
83             my $function = $export_cache->{$cache_key . $type} //= sub {
84 52     52   124475 my ($name, %args) = @_;
        52      
        52      
        52      
        52      
        1      
85 52         481 return $self->expand_shortcuts($flags{-standard}, $type => $name, %args);
86 69   100     475 };
87              
88 69 50       483 $set_subname->("${self}::${type}", $function)
89             if $set_subname;
90              
91             NO_STRICT: {
92 15     15   115 no strict 'refs';
  15         51  
  15         17780  
  69         120  
93 69         99 *{"${caller}::${type}"} = $function;
  69         18115  
94             }
95             }
96             }
97              
98             my @custom_shortcuts;
99              
100             sub custom_shortcuts
101             {
102 51     51 0 128 return [@custom_shortcuts];
103             }
104              
105             sub add_shortcut
106             {
107 4     4 0 200 my ($sub) = @_;
108              
109 4 50       31 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 52     52 0 125 my ($self) = @_;
119              
120             return [
121             # expand attribute type
122             sub {
123 52     52   189 my ($name, %args) = @_;
124 52         132 my $type = delete $args{_type};
125              
126 52 50 33     288 if ($type && $self->attribute_types->{$type}) {
127             %args = (
128 52         134 %{$self->attribute_types->{$type}},
  52         122  
129             %args,
130             );
131             }
132              
133 52         375 return %args;
134             },
135              
136             # merge lazy + default / lazy + builder
137             sub {
138 52     52   166 my ($name, %args) = @_;
139              
140 52 100 100     201 if ($args{lazy} && !exists $args{default} && !$args{builder}) {
      100        
141 5         11 my $lazy = $args{lazy};
142 5         10 $args{lazy} = 1;
143              
144 5 100       16 if (ref $lazy eq 'CODE') {
145 3         10 check_and_set(\%args, $name, default => $lazy);
146             }
147             else {
148 2         6 check_and_set(\%args, $name, builder => $lazy);
149             }
150             }
151              
152 52         226 return %args;
153             },
154              
155             # merge coerce + isa
156             sub {
157 52     52   152 my ($name, %args) = @_;
158              
159 52 100       258 if (blessed $args{coerce}) {
160 1         5 check_and_set(\%args, $name, isa => $args{coerce});
161 1         2 $args{coerce} = 1;
162             }
163              
164 52         263 return %args;
165             },
166              
167             # make sure params with defaults are not required
168             sub {
169 52     52   157 my ($name, %args) = @_;
170              
171 52 100 66     164 if ($args{required} && (exists $args{default} || $args{builder})) {
      66        
172 1         3 delete $args{required};
173             }
174              
175 52         198 return %args;
176             },
177              
178             # method names from shortcuts
179             sub {
180 52     52   143 my ($name, %args) = @_;
181              
182             # initialized lazily
183 52         133 my $normalized_name;
184             my $hidden_field;
185              
186             # inflate names from shortcuts
187 52         81 my %prefixes = %{$self->method_prefixes};
  52         133  
188 52         258 foreach my $method_type (keys %prefixes) {
189 363 100       762 next unless defined $args{$method_type};
190 34 50       87 next if ref $args{$method_type};
191 34 100       73 next unless grep { $_ eq $args{$method_type} } '1', -public, -hidden;
  102         313  
192              
193 32   100     158 $normalized_name //= get_normalized_name($name, $method_type);
194 31   100     155 $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     201 && ($hidden_field || $self->hidden_methods->{$method_type})
201             );
202              
203 93         243 $args{$method_type} = join '_', grep { defined }
204             ($is_hidden ? $self->hidden_prefix : undef),
205 31 100       122 $prefixes{$method_type},
206             $normalized_name;
207             }
208              
209             # special treatment for trigger
210 51 100 66     214 if ($args{trigger} && !ref $args{trigger}) {
211 2         16 my $trigger = $args{trigger};
212             $args{trigger} = sub {
213 2         11833 return shift->$trigger(@_);
214 2         11 };
215             }
216              
217 51         292 return %args;
218             },
219              
220             # literal parameters (prepended with -)
221             sub {
222 51     51   151 my ($name, %args) = @_;
223              
224 51         150 foreach my $literal (keys %args) {
225 155 100       379 if ($literal =~ m{\A - (.+) \z}x) {
226 2         7 $args{$1} = delete $args{$literal};
227             }
228             }
229              
230 51         222 return %args;
231             },
232 52         777 ];
233             }
234              
235             sub expand_shortcuts
236             {
237 52     52 0 230 my ($self, $standard, $attribute_type, $name, %args) = @_;
238              
239 52         126 $args{_type} = $attribute_type;
240              
241             # NOTE: don't use custom shortcuts if we stick to the standard
242 52         94 my @filters;
243 52 100       190 push @filters, @{$self->custom_shortcuts} unless $standard;
  51         136  
244 52         98 push @filters, @{$self->standard_shortcuts};
  52         133  
245              
246             # NOTE: builtin shortcuts are executed after custom shortcuts
247 52         167 foreach my $sub (@filters) {
248 314         1735 %args = $sub->($name, %args);
249             }
250              
251             # TODO: dirty hack for 'extended' attribute. Can be done better?
252 51 100       166 if ($attribute_type eq 'extended') {
253 4 100       18 if (ref $name eq 'ARRAY') {
254 2         6 $name = [map { "+$_" } @{$name}];
  4         13  
  2         6  
255             }
256             else {
257 2         6 $name = "+$name";
258             }
259             }
260              
261 51         1173 return ($name, %args);
262             }
263              
264             ### Helpers - not called in pkg context
265              
266             sub check_and_set
267             {
268 6     6 0 20 my ($hash_ref, $name, %pairs) = @_;
269              
270 6         20 foreach my $key (keys %pairs) {
271             croak "Could not expand shortcut: $key already exists for $name"
272 6 50       17 if exists $hash_ref->{$key};
273              
274 6         16 $hash_ref->{$key} = $pairs{$key};
275             }
276              
277 6         22 return;
278             }
279              
280             sub get_normalized_name
281             {
282 31     31 0 78 my ($name, $for) = @_;
283              
284 31 100       304 croak "Could not use attribute shortcut with array fields: $for is not supported"
285             if ref $name;
286              
287 30         81 $name =~ s/^_//;
288 30         109 return $name;
289             }
290              
291             1;
292              
293             # ABSTRACT: build Mooish attribute definitions with less boilerplate
294