File Coverage

blib/lib/Class/Tiny/ConstrainedAccessor.pm
Criterion Covered Total %
statement 115 115 100.0
branch 54 54 100.0
condition 5 6 83.3
subroutine 20 20 100.0
pod n/a
total 194 195 99.4


line stmt bran cond sub pod time code
1             package Class::Tiny::ConstrainedAccessor;
2              
3 13     13   2640544 use 5.006;
  13         90  
4 13     13   67 use strict;
  13         22  
  13         246  
5 13     13   58 use warnings;
  13         29  
  13         367  
6 13     13   8259 use Class::Tiny;
  13         20449  
  13         49  
7              
8             our $VERSION = '0.000013';
9              
10             # Docs {{{1
11              
12             =head1 NAME
13              
14             Class::Tiny::ConstrainedAccessor - Generate Class::Tiny accessors that apply type constraints
15              
16             =head1 SYNOPSIS
17              
18             L<Class::Tiny> uses custom accessors if they are defined before the
19             C<use Class::Tiny> statement in a package. This module creates custom
20             accessors that behave as standard C<Class::Tiny> accessors except that
21             they apply type constraints (C<isa> relationships). Type constraints
22             can come from L<Type::Tiny>, L<MooseX::Types>, L<MooX::Types::MooseLike>,
23             L<MouseX::Types>, or L<Specio>. Alternatively, constraints can be applied
24             using the technique described in
25             L<"Constraints without a type system"|/CONSTRAINTS WITHOUT A TYPE SYSTEM>.
26              
27             Example of a class using this package:
28              
29             package SampleClass;
30             use Scalar::Util qw(looks_like_number);
31              
32             use Type::Tiny;
33              
34             # Create the type constraint
35             use vars::i '$MediumInteger' = Type::Tiny->new(
36             name => 'MediumInteger',
37             constraint => sub { looks_like_number($_) and $_ >= 10 and $_ < 20 }
38             );
39              
40             use Class::Tiny::ConstrainedAccessor {
41             medint => $MediumInteger, # create accessor sub medint()
42             med_with_default => $MediumInteger,
43             };
44              
45             # After using ConstrainedAccessor, actually define the class attributes.
46             use Class::Tiny qw(medint regular), {
47             med_with_default => 12,
48             };
49              
50             =head1 CONSTRAINTS WITHOUT A TYPE SYSTEM
51              
52             If you don't want to use L<Type::Tiny> or one of the other type systems listed
53             above, you can create your own constraints as two-element arrayrefs. Example:
54              
55             use Class::Tiny::ConstrainedAccessor
56             'field' => [ \&checker_sub, \&message_sub ];
57              
58             C<checker_sub> and C<message_sub> are used as follows to check C<$value>:
59              
60             checker_sub($value) or die get_message($value);
61              
62             Therefore, C<checker_sub> must return truthy if C<$_[0]> passes the constraint,
63             or else falsy. C<get_message> must return something that can be passed to
64             C<die()>, when given a C<$_[0]> that has failed the constraint.
65              
66             If your profile ever tells you that constraint-checks are on the critical
67             path, try custom constraints. They may give you more control or opportunity
68             for optimization than general-purpose type systems.
69              
70             =head1 SUBROUTINES
71              
72             =head2 import
73              
74             Creates the accessors you have requested. Constraints can be passed as a list
75             or hashref of variable/constraint pairs. Basic usage:
76              
77             # Constraints are passed as a list of pairs
78             use Class::Tiny::ConstrainedAccessor
79             name => constraint
80             [, name2 => constraint ...]; # ... any number of name=>constraint pairs
81              
82             # Constraints are passed as a hashref
83             use Class::Tiny::ConstrainedAccessor {
84             name => constraint,
85             [, name2 => constraint ...]; # ... any number of name=>constraint pairs
86             };
87              
88             This also creates a L<BUILD()|Class::Tiny/BUILD> subroutine to check the
89             constructor parameters, if a C<BUILD()> doesn't already exist.
90              
91             If a C<BUILD()> does exist (e.g., you said C<use subs 'BUILD';>), this package
92             will create the same function, taking the same parameters as C<BUILD()> would,
93             but call it C<_check_all_constraints()>. You can call this checker from your
94             own C<BUILD()> if you want to.
95              
96             =head1 OPTIONS
97              
98             To specify options, pass an B<arrayref> as the first argument on the `use`
99             line. This is because a hashref carries attributes and constraints.
100             For example:
101              
102             use Class::Tiny::ConstrainedAccessor [ OPTION=>value ],
103             name => constraint ...;
104              
105             Valid options are:
106              
107             =over
108              
109             =item NOBUILD
110              
111             If C<< NOBUILD => 1 >> is given, the constructor-parameter-checker
112             is created as C<_check_all_constraints> regardless of whether C<BUILD()>
113             exists or not. Example:
114              
115             package MyClass;
116             use Class::Tiny::ConstrainedAccessor
117             [NOBUILD => 1],
118             foo => SomeConstraint;
119             # Now $object->_check_all_constraints($args) exists, but not BUILD().
120              
121             =back
122              
123             =cut
124              
125             # }}}1
126              
127             sub import {
128 25     25   7570 my $target = caller;
129 25         572 my $package = shift;
130              
131 25         47 my %opts = ();
132 25 100       75 %opts = @{+shift} if ref $_[0] eq 'ARRAY';
  1         3  
133              
134 25         39 my %constraints;
135 25 100 66     96 if (ref $_[0] eq 'HASH' && scalar @_ == 1) {
136 1         2 %constraints = %{$_[0]};
  1         4  
137             } else {
138 24 100       87 die "Need 'name => \$Constraint' pairs" if @_%2;
139 22         374 %constraints = @_;
140             }
141              
142             # --- Make the accessors ---
143 23         44 my %accessors; # constraint => [checker, get_message]
144 23         62 foreach my $k (keys(%constraints)) {
145 45         87 my $constraint = $constraints{$k};
146              
147 45         96 my ($checker, $get_message) =
148             _get_constraint_sub($k, $constraint); # dies on failure
149              
150 34         77 my $accessor = _make_accessor($k, $checker, $get_message);
151 34         107 $accessors{$k} = [$checker, $get_message]; # Save for BUILD()
152              
153             { # Install the accessor
154 13     13   6198 no strict 'refs';
  13         30  
  13         975  
  34         65  
155 34         47 *{ "$target\::$k" } = $accessor;
  34         272  
156             }
157             } #foreach constraint
158              
159             # --- Make BUILD ---
160             my $has_build =
161 13     13   73 do { no warnings 'once'; no strict 'refs'; *{"$target\::BUILD"}{CODE} }
  13     13   26  
  13         471  
  13         73  
  13         32  
  13         714  
162 12   100     32 || $opts{NOBUILD}; # NOBUILD => pretend BUILD() already exists.
163 12         61 my $build = _make_build(%accessors);
164             {
165 13     13   130 no strict 'refs';
  13         27  
  13         11045  
  12         30  
166 12 100       18 *{ $has_build ? "$target\::_check_all_constraints" :
  12         691  
167             "$target\::BUILD" } = $build;
168             }
169              
170             } #import()
171              
172             # _get_constraint_sub: Get the subroutine for a constraint.
173             # Takes the constraint name (for debug messages) and the constraint.
174             # Returns two coderefs: checker and get_message.
175             sub _get_constraint_sub {
176 45     45   91 my ($type_name, $type) = @_;
177 45         68 my ($checker, $get_message);
178              
179             # Handle the custom-constraint format
180 45 100       120 if(ref $type eq 'ARRAY') {
181 7 100       94 die "Custom constraint $type_name must have two elements: checker, get_message"
182             unless scalar @$type == 2;
183 5 100       19 die "$type_name: checker must be a subroutine" unless ref($type->[0]) eq 'CODE';
184 4 100       16 die "$type_name: get_message must be a subroutine" unless ref($type->[1]) eq 'CODE';
185 3         6 return @$type;
186             }
187              
188             # Get type's name, if any
189 38 100       64 my $name = eval { $type->can('name') || $type->can('description') };
  38         219  
190 38 100       2109 $name = $type->$name() if $name;
191              
192             # Set default message
193 38 100       147 $name = $type_name unless $name;
194 38     455   117 $get_message = sub { "Value is not a $name" };
  455         126911  
195              
196             # Try the different types of constraints we know about
197             DONE: {
198              
199 38 100       67 if (ref($type) eq 'CODE') { # MooX::Types::MooseLike
  38         87  
200 3     138   9 $checker = sub { eval { $type->(@_); 1 } };
  138         194  
  138         390  
  47         2079  
201             # In profiling, seems to be about on par with `&$type;`.
202 3         5 last DONE;
203             }
204              
205 35 100       97 die "I don't know how to handle non-reference constraint $type_name"
206             unless ref $type; # All the rest of the checks use $type->can()
207              
208 32 100       44 if ( my $method = eval { $type->can('compiled_check') }) { # Type::Tiny
  32         104  
209 15         132 $checker = $type->$method();
210 15     362   422 $get_message = sub { $type->get_message($_[0]) };
  362         4831  
211 15         31 last DONE;
212             }
213              
214 17 100       1835 if (my $method = eval { $type->can('inline_check') || $type->can('_inline_check') }) { # Specio::Constraint::Simple
  17 100       89  
215 7         3570 $checker = eval {
216 7         31 my $text = $type->$method('$value');
217             # $method() will fail if type cannot be inlined.
218 2     2   81 local $SIG{__WARN__} = sub { }; # Eval failure => silent
219 2         157 eval "sub { my \$value = shift; $text }"
220             };
221 7 100       113183 last DONE if $checker;
222             }
223              
224 16 100       30 if (my $method = eval { $type->can('check') } ) { # Moose, Mouse
  16         145  
225 9     349   1885 $checker = sub { unshift @_, $type; goto &$method; };
  349         680  
  349         1460  
226             # Like $type->check(@_), but profiles as much faster on my
227             # test system.
228 9         40 last DONE;
229             }
230              
231 7 100       11 if(eval { $type->can('value_is_valid') }) { # Specio::Constraint::Simple
  7         38  
232 3     138   7 $checker = sub { $type->value_is_valid(@_) };
  138         519  
233 3         5 last DONE;
234             }
235              
236             } #DONE
237              
238 35 100       113 die "$type_name: I don't know how to use type " . ref($type)
239             unless $checker;
240              
241 31         78 return ($checker, $get_message);
242             } #_get_constraint_sub()
243              
244             # _make_accessor($name, \&checker, \&get_message): Make an accessor.
245             sub _make_accessor {
246 34     34   65 my ($k, $checker, $get_message) = @_;
247              
248             # The accessor --- modified from the Class::Tiny docs based on
249             # the source for C::T::__gen_accessor() and C::T::__gen_sub_body().
250             return sub {
251 1326     1326   1192826 my $self_ = shift;
252 1326 100       4001 if (@_) { # Set
    100          
253 1260 100       4357 $checker->($_[0]) or die $get_message->($_[0]);
254 400         37511 return $self_->{$k} = $_[0];
255              
256             } elsif ( exists $self_->{$k} ) { # Get
257 23         123 return $self_->{$k};
258              
259             } else { # Get default
260 43         313 my $defaults_ =
261             Class::Tiny->get_all_attribute_defaults_for( ref $self_ );
262              
263 43         1370 my $def_ = $defaults_->{$k};
264 43 100       193 $def_ = $def_->() if ref $def_ eq 'CODE';
265              
266 43 100       386 $checker->($def_) or die $get_message->($def_);
267 40         3991 return $self_->{$k} = $def_;
268             }
269 34         132 }; #accessor()
270             } #_make_accessor()
271              
272             # _make_build(%accessors): Make a BUILD subroutine that will check
273             # the constraints from the constructor arguments.
274             # The resulting subroutine takes ($self, {args}).
275             sub _make_build {
276 12     12   37 my %accessors = @_;
277              
278             return sub {
279 76     76   52412 my ($self, $args) = @_;
280 76         200 foreach my $k (keys %$args) {
281 123 100       17209 next unless exists $accessors{$k};
282 76         124 my ($checker, $get_message) = @{$accessors{$k}};
  76         161  
283 76 100       193 $checker->($args->{$k}) or die $get_message->($args->{$k});
284             }
285             } #BUILD()
286 12         72 } #_make_build()
287              
288             1; # End of Class::Tiny::ConstrainedAccessor
289             # Rest of the docs {{{1
290             __END__
291              
292             =head1 AUTHORS
293              
294             Created by Christopher White, C<< <cxwembedded at gmail.com> >>. Thanks to
295             Toby Inkster (TOBYINK) and Ivan Butorin (FISHBONE) for code contributions.
296              
297             =head1 BUGS
298              
299             Please report any bugs or feature requests through the GitHub Issues interface
300             at L<https://github.com/cxw42/Class-Tiny-ConstrainedAccessor/issues>. I will be
301             notified, and then you'll automatically be notified of progress on your bug as
302             I make changes.
303              
304             =head1 SUPPORT
305              
306             You can find documentation for this module with the perldoc command.
307              
308             perldoc Class::Tiny::ConstrainedAccessor
309              
310             You can also look for information at:
311              
312             =over 4
313              
314             =item * GitHub (report bugs here)
315              
316             L<https://github.com/cxw42/Class-Tiny-ConstrainedAccessor>
317              
318             =item * MetaCPAN
319              
320             L<https://metacpan.org/pod/Class::Tiny::ConstrainedAccessor>
321              
322             =back
323              
324             =head1 LICENSE
325              
326             Copyright 2019 Christopher White and contributors.
327              
328             This program is free software; you can redistribute it and/or modify it
329             under the terms of the the Apache License (2.0). You may obtain a
330             copy of the full license at:
331              
332             L<https://www.apache.org/licenses/LICENSE-2.0>
333              
334             Unless required by applicable law or agreed to in writing, software
335             distributed under the License is distributed on an "AS IS" BASIS,
336             WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
337             See the License for the specific language governing permissions and
338             limitations under the License.
339              
340             =cut
341              
342             # }}}1
343             # vi: set fdm=marker: