File Coverage

blib/lib/Params/Coerce.pm
Criterion Covered Total %
statement 79 88 89.7
branch 29 48 60.4
condition n/a
subroutine 20 20 100.0
pod 2 2 100.0
total 130 158 82.2


line stmt bran cond sub pod time code
1             package Params::Coerce; # git description: v0.14-9-g675637f
2             # ABSTRACT: Allows your classes to do coercion of parameters
3              
4             #pod =pod
5             #pod
6             #pod =head1 NAME
7             #pod
8             #pod
9             #pod =head1 SYNOPSIS
10             #pod
11             #pod # Coerce a object of class Foo to a Bar
12             #pod my $bar = Params::Coerce::coerce('Bar', $Foo)
13             #pod
14             #pod # Create a coercion param function
15             #pod use Params::Coerce '_Bar' => 'Bar';
16             #pod my $bar = _Bar($Foo);
17             #pod
18             #pod # Usage when Bar has a 'from' method
19             #pod my $bar = Bar->from($Foo);
20             #pod
21             #pod Real world example using L.
22             #pod
23             #pod # My class needs a URI
24             #pod package Web::Spider;
25             #pod
26             #pod use URI;
27             #pod use Params::Coerce 'coerce';
28             #pod
29             #pod sub new {
30             #pod my $class = shift;
31             #pod
32             #pod # Where do we start spidering
33             #pod my $start = coerce('URI', shift) or die "Wasn't passed a URI";
34             #pod
35             #pod bless { root => $start }, $class;
36             #pod }
37             #pod
38             #pod #############################################
39             #pod # Now we can do the following
40             #pod
41             #pod # Pass a URI as normal
42             #pod my $URI = URI->new('http://ali.as/');
43             #pod my $Spider1 = Web::Spider->new( $URI );
44             #pod
45             #pod # We can also pass anything that can be coerced into being a URI
46             #pod my $Website = HTML::Location->new( '/home/adam/public_html', 'http://ali.as' );
47             #pod my $Spider2 = Web::Spider->new( $Website );
48             #pod
49             #pod =head1 DESCRIPTION
50             #pod
51             #pod A big part of good API design is that we should be able to be flexible in
52             #pod the ways that we take parameters.
53             #pod
54             #pod Params::Coerce attempts to encourage this, by making it easier to take a
55             #pod variety of different arguments, while adding negligible additional complexity
56             #pod to your code.
57             #pod
58             #pod =head2 What is Coercion
59             #pod
60             #pod "Coercion" in computing terms generally refers to "implicit type
61             #pod conversion". This is where data and object are converted from one type to
62             #pod another behind the scenes, and you just just magically get what you need.
63             #pod
64             #pod The L pragma, and its string overloading is the form of coercion
65             #pod you are most likely to have encountered in Perl programming. In this case,
66             #pod your object is automatically (within perl itself) coerced into a string.
67             #pod
68             #pod C is intended for higher-order coercion between various
69             #pod types of different objects, for use mainly in subroutine and (mostly)
70             #pod method parameters, particularly on external APIs.
71             #pod
72             #pod =head2 __as_Another_Class Methods
73             #pod
74             #pod At the heart of C is the ability to transform objects from
75             #pod one thing to another. This can be done by a variety of different
76             #pod mechanisms.
77             #pod
78             #pod The preferred mechanism for this is by creating a specially named method
79             #pod in a class that indicates it can be coerced into another type of object.
80             #pod
81             #pod As an example, L provides an object method that returns an
82             #pod equivalent L object.
83             #pod
84             #pod # In the package HTML::Location
85             #pod
86             #pod # Coerce to a URI
87             #pod sub __as_URI {
88             #pod my $self = shift;
89             #pod return URI->new( $self->uri );
90             #pod }
91             #pod
92             #pod =head2 __from_Another_Class Methods
93             #pod
94             #pod From version 0.04 of C, you may now also provide
95             #pod __from_Another_Class methods as well. In the above example, rather then
96             #pod having to define a method in L, you may instead define
97             #pod one in L. The following code has an identical effect.
98             #pod
99             #pod # In the package URI
100             #pod
101             #pod # Coerce from a HTML::Location
102             #pod sub __from_HTML_Location {
103             #pod my $Location = shift;
104             #pod return URI->new( $Location->uri );
105             #pod }
106             #pod
107             #pod C will only look for the __from method, if it does not
108             #pod find a __as method.
109             #pod
110             #pod =head2 Loading Classes
111             #pod
112             #pod One thing to note with the C<__as_Another_Class> methods is that you are
113             #pod B required to load the class you are converting to in the class you
114             #pod are converting from.
115             #pod
116             #pod In the above example, L does B have to load the URI
117             #pod class. The need to load the classes for every object we might some day need
118             #pod to be coerced to would result in highly excessive resource usage.
119             #pod
120             #pod Instead, C guarantees that the class you are converting to
121             #pod C be loaded before it calls the __as_Another_Class method. Of course,
122             #pod in most situations you will have already loaded it for another purpose in
123             #pod either the From or To classes and this won't be an issue.
124             #pod
125             #pod If you make use of some class B the class you are being coerced
126             #pod to in the __as_Another_Class method, you will need to make sure that is loaded
127             #pod in your code, but it is suggested that you do it at run-time with a
128             #pod C if you are not using it already elsewhere.
129             #pod
130             #pod =head2 Coercing a Parameter
131             #pod
132             #pod The most explicit way of accessing the coercion functionality is with the
133             #pod Params::Coerce::coerce function. It takes as its first argument the name
134             #pod of the class you wish to coerce B, followed by the parameter to which you
135             #pod wish to apply the coercion.
136             #pod
137             #pod package My::Class;
138             #pod
139             #pod use URI ();
140             #pod use Params::Coerce '_URI' => 'URI';
141             #pod
142             #pod sub new {
143             #pod my $class = shift;
144             #pod
145             #pod # Take a URI argument
146             #pod my $URI = Params::Coerce::coerce('URI', shift) or return;
147             #pod
148             #pod ...
149             #pod }
150             #pod
151             #pod For people doing procedural programming, you may also import this function.
152             #pod
153             #pod # Import the coerce function
154             #pod use Params::Coerce 'coerce';
155             #pod
156             #pod Please note that the C function is the B function
157             #pod that can be imported, and that the two argument pragma (or the passing of
158             #pod two or more arguments to ->import) means something different entirely.
159             #pod
160             #pod =head2 Importing Parameter Coercion Methods
161             #pod
162             #pod The second way of using Params::Coerce, and the more common one for
163             #pod Object-Oriented programming, is to create method specifically for taking
164             #pod parameters in a coercing manner.
165             #pod
166             #pod package My::Class;
167             #pod
168             #pod use URI ();
169             #pod use Params::Coerce '_URI' => 'URI';
170             #pod
171             #pod sub new {
172             #pod my $class = shift;
173             #pod
174             #pod # Take a URI as parameter
175             #pod my $URI1 = $class->_URI(shift) or return;
176             #pod my $URI2 = _URI(shift) or return;
177             #pod ...
178             #pod }
179             #pod
180             #pod =head2 The C Constructor
181             #pod
182             #pod From version C<0.11> of C, an additional mechanism is
183             #pod available with the importable C constructor.
184             #pod
185             #pod package My::Class;
186             #pod
187             #pod use Params::Coerce 'from';
188             #pod
189             #pod package Other::Class;
190             #pod
191             #pod sub method {
192             #pod my $self = shift;
193             #pod my $My = My::Class->from(shift) or die "Bad param";
194             #pod ...
195             #pod }
196             #pod
197             #pod This is mainly a convenience. The above is equivalent to
198             #pod
199             #pod package My::Class;
200             #pod
201             #pod use Params::Coerce 'from' => 'Params::Coerce';
202             #pod
203             #pod In future versions, this C<-Efrom> syntax may also tweak the resolution
204             #pod order of the coercion.
205             #pod
206             #pod =head2 Chained Coercion
207             #pod
208             #pod While it is intended that Params::Coerce will eventually support coercion
209             #pod using multiple steps, like C<__as_HTML_Location->__as_URI>>,
210             #pod it is not currently capable of this. At this time only a single coercion
211             #pod step is supported.
212             #pod
213             #pod =head1 FUNCTIONS
214             #pod
215             #pod =cut
216              
217 3     3   211985 use 5.006;
  3         39  
218 3     3   17 use strict;
  3         5  
  3         94  
219 3     3   19 use Carp ();
  3         6  
  3         40  
220 3     3   16 use Scalar::Util ();
  3         25  
  3         120  
221 3         460 use Params::Util '_IDENTIFIER',
222             '_INSTANCE',
223 3     3   1567 '_CLASS';
  3         19698  
224              
225             # Load Overhead: 52k
226              
227             our $VERSION = '0.15';
228              
229             # The hint cache
230             my %hints = ();
231              
232              
233              
234              
235              
236             #####################################################################
237             # Use as a Pragma
238              
239             sub import {
240 8     8   66 my $class = shift;
241 8 100       731 my @param = @_ or return;
242 5 50       19 Carp::croak("Too many parameters") if @param > 2; # Um, what?
243              
244             # We'll need to know who is calling us
245 5         20 my $pkg = caller();
246              
247             # We export them the coerce function if they want it
248 5 100       14 if ( @param == 1 ) {
249 3 100       11 if ( $param[0] eq 'coerce' ) {
    50          
250 3     3   25 no strict 'refs';
  3         7  
  3         213  
251 1         3 *{"${pkg}::coerce"} = *coerce;
  1         5  
252 1         90 return 1;
253             } elsif ( $param[0] eq 'from' ) {
254             # They want a from constructor
255 3     3   20 no strict 'refs';
  3         7  
  3         1968  
256 2         3 *{"${pkg}::from"} = *from;
  2         12  
257 2         164 return 1;
258             } else {
259 0         0 Carp::croak "Params::Coerce does not export '$_[0]'";
260             }
261             }
262              
263             # The two argument form is 'method' => 'class'
264             # Check the values given to us.
265 2 50       7 my $method = _IDENTIFIER($param[0]) or Carp::croak "Illegal method name '$param[0]'";
266 2 50       36 my $want = _CLASS($param[1]) or Carp::croak "Illegal class name '$param[1]'";
267 2 50       20 _function_exists($pkg, $method) and Carp::croak "Cannot create '${pkg}::$method'. It already exists";
268              
269             # Make sure the class is loaded
270 2 50       5 unless ( _loaded($want) ) {
271 0         0 eval "require $want";
272 0 0       0 croak($@) if $@;
273             }
274              
275             # Create the method in our caller
276 2     2   147 eval "package $pkg;\nsub $method {\n\tParams::Coerce::_coerce('$want', \$_[-1])\n}";
  2     2   2105  
  2         2037  
277 2 50       9 Carp::croak("Failed to create coercion method '$method' in $pkg': $@") if $@;
278              
279 2         1570 1;
280             }
281              
282             #pod =pod
283             #pod
284             #pod =head2 coerce $class, $param
285             #pod
286             #pod The C function takes a class name and a single parameter and
287             #pod attempts to coerce the parameter into the intended class, or one of its
288             #pod subclasses.
289             #pod
290             #pod Please note that it is the responsibility of the consuming class to ensure
291             #pod that the class you wish to coerce to is loaded. C will check this
292             #pod and die is it is not loaded.
293             #pod
294             #pod Returns an instance of the class you specify, or one of its subclasses.
295             #pod Returns C if the parameter cannot be coerced into the class you wish.
296             #pod
297             #pod =cut
298              
299             sub coerce($$) {
300             # Check what they want properly first
301 5 50   5 1 3723 my $want = _CLASS($_[0]) or Carp::croak("Illegal class name '$_[0]'");
302 5 50       68 _loaded($want) or Carp::croak("Tried to coerce to unloaded class '$want'");
303              
304             # Now call the real function
305 5         12 _coerce($want, $_[1]);
306             }
307              
308             # The from method that is imported into the classes
309             sub from {
310 4 50   4 1 3493 @_ == 2 or Carp::croak("'->from must be called as a method with a single param");
311 4         15 _coerce(@_);
312             }
313              
314             # Internal version with less checks. Should ONLY be called once
315             # the first argument is FULLY validated.
316             sub _coerce {
317 13     13   23 my $want = shift;
318 13 50       53 my $have = Scalar::Util::blessed($_[0]) ? shift : return undef;
319              
320             # In the simplest case it is already what we need
321 13 100       60 return $have if $have->isa($want);
322              
323             # Is there a coercion hint for this combination
324 8         25 my $key = ref($have) . ',' . $want;
325 8 100       30 my $hint = exists $hints{$key} ? $hints{$key}
    50          
326             : _resolve($want, ref($have), $key)
327             or return undef;
328              
329             # Call the coercion function
330 8         25 my $type = substr($hint, 0, 1, '');
331 8 100       23 if ( $type eq '>' ) {
    50          
    0          
332             # Direct Push
333 7         23 $have = $have->$hint();
334             } elsif ( $type eq '<' ) {
335             # Direct Pull
336 1         5 $have = $want->$hint($have);
337             } elsif ( $type eq '^' ) {
338             # Third party
339 0         0 my ($pkg, $function) = $hint =~ m/^(.*)::(.*)\z/s;
340 0         0 require $pkg;
341 3     3   25 no strict 'refs';
  3         6  
  3         909  
342 0         0 $have = &{"${pkg}::${function}"}($have);
  0         0  
343             } else {
344 0         0 Carp::croak("Unknown coercion hint '$type$hint'");
345             }
346              
347             # Did we get what we wanted?
348 8         86 _INSTANCE($have, $want);
349             }
350              
351             # Try to work out how to get from one class to the other class
352             sub _resolve {
353 4     4   12 my ($want, $have, $key) = @_;
354              
355             # Look for a __as method
356 4         10 my $method = "__as_$want";
357 4         12 $method =~ s/::/_/g;
358 4 100       38 return _hint($key, ">$method") if $have->can($method);
359              
360             # Look for a direct __from method
361 1         3 $method = "__from_$have";
362 1         3 $method =~ s/::/_/g;
363 1 50       9 return _hint($key, "<$method") if $want->can($method);
364              
365             # Give up (and don't try again).
366             # We use zero specifically so it will return false in boolean context
367 0         0 _hint($key, '0');
368             }
369              
370             # For now just save to the memory hash.
371             # Later, this may also involve saving to a database somewhere.
372             sub _hint {
373 4     4   22 $hints{$_[0]} = $_[1];
374             }
375              
376              
377              
378              
379              
380             #####################################################################
381             # Support Functions
382              
383             # Is a class loaded.
384             sub _loaded {
385 3     3   23 no strict 'refs';
  3         14  
  3         331  
386 9     9   127 foreach ( keys %{"$_[0]::"} ) {
  9         52  
387 8 50       48 return 1 unless substr($_, -2, 2) eq '::';
388             }
389 1         17 '';
390             }
391              
392             # Does a function exist.
393             sub _function_exists {
394 3     3   22 no strict 'refs';
  3         7  
  3         241  
395 7     7   119 defined &{"$_[0]::$_[1]"};
  7         53  
396             }
397              
398             1;
399              
400             __END__