File Coverage

blib/lib/Class/Tiny/ConstrainedAccessor.pm
Criterion Covered Total %
statement 104 108 96.3
branch 45 46 97.8
condition 10 12 83.3
subroutine 19 21 90.4
pod n/a
total 178 187 95.1


line stmt bran cond sub pod time code
1             package Class::Tiny::ConstrainedAccessor;
2              
3 14     14   2352206 use 5.006;
  14         161  
4 14     14   79 use strict;
  14         30  
  14         295  
5 14     14   70 use warnings;
  14         40  
  14         354  
6 14     14   6126 use Class::Tiny;
  14         23661  
  14         67  
7 14     14   3634 use Types::TypeTiny ();
  14         18255  
  14         2839  
8              
9             our $VERSION = '0.000014'; # TRIAL
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 26     26   5213 my $target = caller;
130 26         689 my $package = shift;
131              
132 26         57 my %opts = ();
133 26 100       98 %opts = @{+shift} if ref $_[0] eq 'ARRAY';
  1         3  
134              
135 26         46 my %constraints;
136 26 100 66     117 if (ref $_[0] eq 'HASH' && scalar @_ == 1) {
137 1         2 %constraints = %{$_[0]};
  1         5  
138             } else {
139 25 100       103 die "Need 'name => \$Constraint' pairs" if @_%2;
140 23         108 %constraints = @_;
141             }
142              
143             # --- Make the accessors ---
144 24         44 my %accessors; # constraint => [checker, get_message]
145 24         79 foreach my $k (keys(%constraints)) {
146 47         108 my $constraint = $constraints{$k};
147              
148 47         113 my ($checker, $get_message) =
149             _get_constraint_sub($k, $constraint); # dies on failure
150              
151 36         19790 my $accessor = _make_accessor($k, $checker, $get_message);
152 36         161 $accessors{$k} = [$checker, $get_message]; # Save for BUILD()
153              
154             { # Install the accessor
155 14     14   113 no strict 'refs';
  14         33  
  14         903  
  36         72  
156 36         72 *{ "$target\::$k" } = $accessor;
  36         282  
157             }
158             } #foreach constraint
159              
160             # --- Make BUILD ---
161             my $has_build =
162 14     14   102 do { no warnings 'once'; no strict 'refs'; *{"$target\::BUILD"}{CODE} }
  14     14   38  
  14         595  
  14         98  
  14         30  
  14         947  
163 13   100     44 || $opts{NOBUILD}; # NOBUILD => pretend BUILD() already exists.
164 13         128 my $build = _make_build(%accessors);
165             {
166 14     14   93 no strict 'refs';
  14         29  
  14         12145  
  13         45  
167 13 100       27 *{ $has_build ? "$target\::_check_all_constraints" :
  13         714  
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 47     47   101 my ($type_name, $type) = @_;
178              
179             # Get type's name, if any
180 47 100       84 my $name = eval { $type->can('name') || $type->can('description') };
  47         283  
181 47 100       2644 $name = $type->$name() if $name;
182              
183             # Set default message
184 47 100       189 $name = $type_name unless $name;
185 47     182   161 my $default_get_message = sub { "Value is not a $name" };
  182         4539  
186              
187             # Handle the custom-constraint format
188 47 100       149 if(ref $type eq 'ARRAY') {
189 7 100       35 die "Custom constraint $type_name must have two elements: checker, get_message"
190             unless scalar @$type == 2;
191 5 100       23 die "$type_name: checker must be a subroutine" unless ref($type->[0]) eq 'CODE';
192 4 100       17 die "$type_name: get_message must be a subroutine" unless ref($type->[1]) eq 'CODE';
193 3         9 $type = bless [@$type], 'Class::Tiny::ConstrainedAccessor::CustomConstraint';
194 3         38 return @$type;
195             }
196              
197             # Handle Specio::Constraint::Simple, which Types::TypeTiny::to_TypeTiny()
198             # does not (https://rt.cpan.org/Ticket/Display.html?id=131011)
199              
200 40 100 100     137 if( (ref $type eq 'Specio::Constraint::Simple') ||
201 37         162 eval { $type->can('value_is_valid') } ) {
202 6   66     10 $type_name = eval { $type->name } || $type_name;
203             return (
204 276     276   935 sub { $type->value_is_valid(@_) },
205 6         38 $default_get_message
206             );
207             }
208              
209             # Handle MooX::Types::MooseLike as well as other types of coderef.
210             # A $orig_coderef may indicate failure by dying or returning 0 ---
211             # there's no way to know. We assume that:
212             # - Express undef is success (e.g., MooX::Types::MooseLike)
213             # - die() is failure (ditto)
214             # - Returning a defined value is a success/failure indication
215             # (as expected by Types::TypeTiny::to_TypeTiny(\&)).
216 34 100       2549 if(ref $type eq 'CODE') {
217 3         6 my $orig_coderef = $type;
218             my $new_type = sub {
219 229     229   2654 local $@;
220 229         417 my $is_ok = eval { $orig_coderef->(@_) };
  229         626  
221 229 100       155551 if($@) {
    50          
222 182         352 $is_ok = 0; # die() => failure
223             } elsif(!defined $is_ok) {
224 47         83 $is_ok = 1; # undef => success
225             }
226 229         648 return $is_ok;
227 3         10 };
228 3         6 $type = $new_type;
229             }
230              
231 34         108 my $impl = Types::TypeTiny::to_TypeTiny($type);
232 34 100       99490 die "I couldn't understand the constraint for $type_name"
233             unless ref $impl eq 'Type::Tiny';
234              
235 27     726   82 return ( $impl->compiled_check(), sub { $impl->get_message($_[0]) } );
  726         11311  
236             } #_get_constraint_sub()
237              
238             # _make_accessor($name, \&checker, \&get_message): Make an accessor.
239             sub _make_accessor {
240 36     36   93 my ($k, $checker, $get_message) = @_;
241              
242             # The accessor --- modified from the Class::Tiny docs based on
243             # the source for C::T::__gen_accessor() and C::T::__gen_sub_body().
244             return sub {
245 1458     1458   1134435 my $self_ = shift;
246 1458 100       4169 if (@_) { # Set
    100          
247 1386 100       4054 $checker->($_[0]) or die $get_message->($_[0]);
248 440         6594 return $self_->{$k} = $_[0];
249              
250             } elsif ( exists $self_->{$k} ) { # Get
251 25         154 return $self_->{$k};
252              
253             } else { # Get default
254 47         252 my $defaults_ =
255             Class::Tiny->get_all_attribute_defaults_for( ref $self_ );
256              
257 47         1782 my $def_ = $defaults_->{$k};
258 47 100       246 $def_ = $def_->() if ref $def_ eq 'CODE';
259              
260 47 100       222 $checker->($def_) or die $get_message->($def_);
261 44         849 return $self_->{$k} = $def_;
262             }
263 36         138 }; #accessor()
264             } #_make_accessor()
265              
266             # _make_build(%accessors): Make a BUILD subroutine that will check
267             # the constraints from the constructor arguments.
268             # The resulting subroutine takes ($self, {args}).
269             sub _make_build {
270 13     13   94 my %accessors = @_;
271              
272             return sub {
273 84     84   41377 my ($self, $args) = @_;
274 84         265 foreach my $k (keys %$args) {
275 138 100       830 next unless exists $accessors{$k};
276 84         145 my ($checker, $get_message) = @{$accessors{$k}};
  84         271  
277 84 100       284 $checker->($args->{$k}) or die $get_message->($args->{$k});
278             }
279             } #BUILD()
280 13         95 } #_make_build()
281              
282             ############################################################################
283             # A package to bless custom constraints ([\&check, \&get_message]) into
284             {
285             package Class::Tiny::ConstrainedAccessor::CustomConstraint;
286              
287             sub check {
288 0     0     my ($self, $value) = @_;
289 0           $self->[0]->($value);
290             }
291              
292             sub get_message {
293 0     0     my ($self, $value) = @_;
294 0           $self->[1]->($value);
295             }
296              
297             } # end of package ...::CustomConstraint
298              
299             1; # End of Class::Tiny::ConstrainedAccessor
300             # Rest of the docs {{{1
301             __END__
302              
303             =head1 AUTHORS
304              
305             Created by Christopher White, C<< <cxwembedded at gmail.com> >>. Thanks to
306             Toby Inkster (TOBYINK) and Ivan Butorin (FISHBONE) for code contributions.
307              
308             =head1 BUGS
309              
310             Please report any bugs or feature requests through the GitHub Issues interface
311             at L<https://github.com/cxw42/Class-Tiny-ConstrainedAccessor/issues>. I will be
312             notified, and then you'll automatically be notified of progress on your bug as
313             I make changes.
314              
315             =head1 SUPPORT
316              
317             You can find documentation for this module with the perldoc command.
318              
319             perldoc Class::Tiny::ConstrainedAccessor
320              
321             You can also look for information at:
322              
323             =over 4
324              
325             =item * GitHub (report bugs here)
326              
327             L<https://github.com/cxw42/Class-Tiny-ConstrainedAccessor>
328              
329             =item * MetaCPAN
330              
331             L<https://metacpan.org/pod/Class::Tiny::ConstrainedAccessor>
332              
333             =back
334              
335             =head1 LICENSE
336              
337             Copyright 2019 Christopher White and contributors.
338              
339             This program is free software; you can redistribute it and/or modify it
340             under the terms of the the Apache License (2.0). You may obtain a
341             copy of the full license at:
342              
343             L<https://www.apache.org/licenses/LICENSE-2.0>
344              
345             Unless required by applicable law or agreed to in writing, software
346             distributed under the License is distributed on an "AS IS" BASIS,
347             WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
348             See the License for the specific language governing permissions and
349             limitations under the License.
350              
351             =cut
352              
353             # }}}1
354             # vi: set fdm=marker: