File Coverage

blib/lib/Mooish/AttributeBuilder.pm
Criterion Covered Total %
statement 49 49 100.0
branch 6 8 75.0
condition n/a
subroutine 14 14 100.0
pod 4 8 50.0
total 73 79 92.4


line stmt bran cond sub pod time code
1             package Mooish::AttributeBuilder;
2             $Mooish::AttributeBuilder::VERSION = '1.001'; # TRIAL
3 11     11   741637 use v5.10;
  11         135  
4 11     11   58 use strict;
  11         24  
  11         229  
5 11     11   64 use warnings;
  11         19  
  11         445  
6              
7 11     11   76 use Exporter qw(import);
  11         46  
  11         408  
8 11     11   62 use Carp qw(croak);
  11         29  
  11         550  
9 11     11   64 use Scalar::Util qw(blessed);
  11         21  
  11         14377  
10              
11             our @EXPORT = qw(
12             field
13             param
14             option
15             extended
16             );
17              
18             # List of available types. May be extended if a custom function will call
19             # expand_shortcuts
20             our %TYPES = (
21             field => {
22             is => 'ro',
23             init_arg => undef,
24             },
25             param => {
26             is => 'ro',
27             required => 1,
28             },
29             option => {
30             is => 'ro',
31             required => 0,
32             predicate => 1,
33             },
34             extended => {},
35             );
36              
37             # Prefix of protected methods. Will be joined with the rest of the method name
38             # with an underscore, so an empty prefix means starting with an underscore
39             our $PROTECTED_PREFIX = '';
40              
41             # The list of methods which are protected by default
42             our %PROTECTED_METHODS = map { $_ => 1 } qw(builder trigger);
43              
44             # The list of method name prefixes. Undef means no prefix at all, just use
45             # attribute name
46             our %METHOD_PREFIXES = (
47             reader => 'get',
48             writer => 'set',
49             clearer => 'clear',
50             predicate => 'has',
51             builder => 'build',
52             trigger => 'trigger',
53             init_arg => undef,
54             );
55              
56             my @shortcuts;
57             my @builtin_shortcuts = (
58              
59             # expand attribute type
60             sub {
61             my ($name, %args) = @_;
62             my $type = delete $args{_type};
63              
64             if ($type && $TYPES{$type}) {
65             %args = (
66             %{$TYPES{$type}},
67             %args,
68             );
69             }
70              
71             return %args;
72             },
73              
74             # merge lazy + default / lazy + builder
75             sub {
76             my ($name, %args) = @_;
77              
78             if ($args{lazy}) {
79             my $lazy = $args{lazy};
80             $args{lazy} = 1;
81              
82             if (ref $lazy eq 'CODE') {
83             check_and_set(\%args, $name, default => $lazy);
84             }
85             else {
86             check_and_set(\%args, $name, builder => $lazy);
87             }
88             }
89              
90             return %args;
91             },
92              
93             # merge coerce + isa
94             sub {
95             my ($name, %args) = @_;
96              
97             if (blessed $args{coerce}) {
98             check_and_set(\%args, $name, isa => $args{coerce});
99             $args{coerce} = 1;
100             }
101              
102             return %args;
103             },
104              
105             # make sure params with defaults are not required
106             sub {
107             my ($name, %args) = @_;
108              
109             if ($args{required} && (exists $args{default} || $args{builder})) {
110             delete $args{required};
111             }
112              
113             return %args;
114             },
115              
116             # method names from shortcuts
117             sub {
118             my ($name, %args) = @_;
119              
120             # initialized lazily
121             my $normalized_name;
122             my $protected_field;
123              
124             # inflate names from shortcuts
125             foreach my $method_type (keys %METHOD_PREFIXES) {
126             next unless defined $args{$method_type};
127             next if ref $args{$method_type};
128             next unless grep { $_ eq $args{$method_type} } '1', -public, -hidden;
129              
130             $normalized_name //= get_normalized_name($name, $method_type);
131             $protected_field //= $name ne $normalized_name;
132              
133             my $is_protected =
134             $args{$method_type} eq -hidden
135             || (
136             $args{$method_type} eq '1'
137             && ($protected_field || $PROTECTED_METHODS{$method_type})
138             );
139              
140             $args{$method_type} = join '_', grep { defined }
141             ($is_protected ? $PROTECTED_PREFIX : undef),
142             $METHOD_PREFIXES{$method_type},
143             $normalized_name;
144             }
145              
146             # special treatment for trigger
147             if ($args{trigger} && !ref $args{trigger}) {
148             my $trigger = $args{trigger};
149             $args{trigger} = sub {
150             return shift->$trigger(@_);
151             };
152             }
153              
154             return %args;
155             },
156              
157             # literal parameters (prepended with -)
158             sub {
159             my ($name, %args) = @_;
160              
161             foreach my $literal (keys %args) {
162             if ($literal =~ m{\A - (.+) \z}x) {
163             $args{$1} = delete $args{$literal};
164             }
165             }
166              
167             return %args;
168             },
169              
170             );
171              
172             sub field
173             {
174 33     33 1 78673 my ($name, %args) = @_;
175              
176 33         150 return ($name, expand_shortcuts(field => $name, %args));
177             }
178              
179             sub param
180             {
181 7     7 1 10192 my ($name, %args) = @_;
182              
183 7         31 return ($name, expand_shortcuts(param => $name, %args));
184             }
185              
186             sub option
187             {
188 2     2 1 3434 my ($name, %args) = @_;
189              
190 2         9 return ($name, expand_shortcuts(option => $name, %args));
191             }
192              
193             sub extended
194             {
195 4     4 1 6554 my ($name, %args) = @_;
196              
197 4         9 my $extended_name;
198 4 100       16 if (ref $name eq 'ARRAY') {
199 2         5 $extended_name = [map { "+$_" } @{$name}];
  4         13  
  2         6  
200             }
201             else {
202 2         6 $extended_name = "+$name";
203             }
204              
205 4         17 return ($extended_name, expand_shortcuts(extended => $name, %args));
206             }
207              
208             sub add_shortcut
209             {
210 3     3 0 128 my ($sub) = @_;
211              
212 3 50       11 croak 'Custom shortcut passed to add_shortcut must be a coderef'
213             unless ref $sub eq 'CODE';
214              
215 3         5 push @shortcuts, $sub;
216 3         8 return;
217             }
218              
219             # Helpers - not part of the interface
220              
221             sub check_and_set
222             {
223 5     5 0 17 my ($hash_ref, $name, %pairs) = @_;
224              
225 5         13 foreach my $key (keys %pairs) {
226             croak "Could not expand shortcut: $key already exists for $name"
227 5 50       14 if exists $hash_ref->{$key};
228              
229 5         12 $hash_ref->{$key} = $pairs{$key};
230             }
231              
232 5         15 return;
233             }
234              
235             sub get_normalized_name
236             {
237 30     30 0 69 my ($name, $for) = @_;
238              
239 30 100       285 croak "Could not use attribute shortcut with array fields: $for is not supported"
240             if ref $name;
241              
242 29         75 $name =~ s/^_//;
243 29         100 return $name;
244             }
245              
246             sub expand_shortcuts
247             {
248 46     46 0 140 my ($attribute_type, $name, %args) = @_;
249              
250 46         101 $args{_type} = $attribute_type;
251              
252             # NOTE: builtin shortcuts are executed after custom shortcuts
253 46         114 foreach my $sub (@shortcuts, @builtin_shortcuts) {
254 278         1624 %args = $sub->($name, %args);
255             }
256              
257 45         248 return %args;
258             }
259              
260             1;
261              
262             # ABSTRACT: build Mooish attribute definitions with less boilerplate
263