File Coverage

blib/lib/Class/Tiny/ConstrainedAccessor.pm
Criterion Covered Total %
statement 97 98 98.9
branch 43 44 97.7
condition 5 6 83.3
subroutine 17 18 94.4
pod n/a
total 162 166 97.5


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