7 eval 'use HTTP::Headers; use Params::Coerce; use URI;';
9 diag 'HTTP::Headers, Params::Coerce & URI required for this test';
19 Moose::Cookbook::Basics::Recipe5 - More subtypes, coercion in a B<Request> class
25 use Moose::Util::TypeConstraints;
28 use Params::Coerce ();
31 subtype 'My::Types::HTTP::Headers' => as class_type('HTTP::Headers');
33 coerce 'My::Types::HTTP::Headers'
35 => via { HTTP::Headers->new( @{$_} ) }
37 => via { HTTP::Headers->new( %{$_} ) };
39 subtype 'My::Types::URI' => as class_type('URI');
41 coerce 'My::Types::URI'
43 => via { $_->isa('URI')
45 : Params::Coerce::coerce( 'URI', $_ ); }
47 => via { URI->new( $_, 'http' ) };
51 => where { /^HTTP\/[0-9]\.[0-9]$/ };
53 has 'base' => ( is => 'rw', isa => 'My::Types::URI', coerce => 1 );
54 has 'uri' => ( is => 'rw', isa => 'My::Types::URI', coerce => 1 );
55 has 'method' => ( is => 'rw', isa => 'Str' );
56 has 'protocol' => ( is => 'rw', isa => 'Protocol' );
59 isa => 'My::Types::HTTP::Headers',
61 default => sub { HTTP::Headers->new }
66 This recipe introduces type coercions, which are defined with the
67 C<coerce> sugar function. Coercions are attached to existing type
68 constraints, and define a (one-way) transformation from one type to
71 This is very powerful, but it's also magical, so you have to
72 explicitly ask for an attribute to be coerced. To do this, you must
73 set the C<coerce> attribute option to a true value.
75 First, we create the subtype to which we will coerce the other types:
77 subtype 'My::Types::HTTP::Headers' => as class_type('HTTP::Headers');
79 We are creating a subtype rather than using C<HTTP::Headers> as a type
80 directly. The reason we do this is coercions are global, and a
81 coercion defined for C<HTTP::Headers> in our C<Request> class would
82 then be defined for I<all> Moose-using classes in the current Perl
83 interpreter. It's a L<best practice|Moose::Manual::BestPractices> to
84 avoid this sort of namespace pollution.
86 The C<class_type> sugar function is simply a shortcut for this:
88 subtype 'HTTP::Headers'
90 => where { $_->isa('HTTP::Headers') };
92 Internally, Moose creates a type constraint for each Moose-using
93 class, but for non-Moose classes, the type must be declared
96 We could go ahead and use this new type directly:
100 isa => 'HTTP::Headers',
101 default => sub { HTTP::Headers->new }
104 This creates a simple attribute which defaults to an empty instance of
107 The constructor for L<HTTP::Headers> accepts a list of key-value pairs
108 representing the HTTP header fields. In Perl, such a list could be
109 stored in an ARRAY or HASH reference. We want our C<headers> attribute
110 to accept those data structure instead of an B<HTTP::Headers>
111 instance, and just do the right thing. This is exactly what coercion
114 coerce 'My::Types::HTTP::Headers'
116 => via { HTTP::Headers->new( @{$_} ) }
118 => via { HTTP::Headers->new( %{$_} ) };
120 The first argument to C<coerce> is the type I<to> which we are
121 coercing. Then we give it a set of C<from>/C<via> clauses. The C<from>
122 function takes some other type name and C<via> takes a subroutine
123 reference which actually does the coercion.
125 However, defining the coercion doesn't do anything until we tell Moose
126 we want a particular attribute to be coerced:
130 isa => 'My::Types::HTTP::Headers',
132 default => sub { HTTP::Headers->new }
135 Now, if we use an C<ArrayRef> or C<HashRef> to populate C<headers>, it
136 will be coerced into a new L<HTTP::Headers> instance. With the
137 coercion in place, the following lines of code are all equivalent:
139 $foo->headers( HTTP::Headers->new( bar => 1, baz => 2 ) );
140 $foo->headers( [ 'bar', 1, 'baz', 2 ] );
141 $foo->headers( { bar => 1, baz => 2 } );
143 As you can see, careful use of coercions can produce a very open
144 interface for your class, while still retaining the "safety" of your
145 type constraint checks. (1)
147 Our next coercion shows how we can leverage existing CPAN modules to
148 help implement coercions. In this case we use L<Params::Coerce>.
150 Once again, we need to declare a class type for our non-Moose L<URI>
153 subtype 'My::Types::URI' => as class_type('URI');
155 Then we define the coercion:
157 coerce 'My::Types::URI'
159 => via { $_->isa('URI')
161 : Params::Coerce::coerce( 'URI', $_ ); }
163 => via { URI->new( $_, 'http' ) };
165 The first coercion takes any object and makes it a C<URI> object. The
166 coercion system isn't that smart, and does not check if the object is
167 already a L<URI>, so we check for that ourselves. If it's not a L<URI>
168 already, we let L<Params::Coerce> do its magic, and we just use its
171 If L<Params::Coerce> didn't return a L<URI> object (for whatever
172 reason), Moose would throw a type constraint error.
174 The other coercion takes a string and converts to a L<URI>. In this
175 case, we are using the coercion to apply a default behavior, where a
176 string is assumed to be an C<http> URI.
178 Finally, we need to make sure our attributes enable coercion.
180 has 'base' => ( is => 'rw', isa => 'My::Types::URI', coerce => 1 );
181 has 'uri' => ( is => 'rw', isa => 'My::Types::URI', coerce => 1 );
183 Re-using the coercion lets us enforce a consistent API across multiple
188 This recipe showed the use of coercions to create a more flexible and
189 DWIM-y API. Like any powerful magic, we recommend some
190 caution. Sometimes it's better to reject a value than just guess at
193 We also showed the use of the C<class_type> sugar function as a
194 shortcut for defining a new subtype of C<Object>
202 This particular example could be safer. Really we only want to coerce
203 an array with an I<even> number of elements. We could create a new
204 C<EvenElementArrayRef> type, and then coerce from that type, as
205 opposed to from a plain C<ArrayRef>
211 Stevan Little E<lt>stevan@iinteractive.comE<gt>
213 Dave Rolsky E<lt>autarch@urth.orgE<gt>
215 =head1 COPYRIGHT AND LICENSE
217 Copyright 2006-2009 by Infinity Interactive, Inc.
219 L<http://www.iinteractive.com>
221 This library is free software; you can redistribute it and/or modify
222 it under the same terms as Perl itself.
226 my $r = Request->new;
227 isa_ok( $r, 'Request' );
230 my $header = $r->headers;
231 isa_ok( $header, 'HTTP::Headers' );
233 is( $r->headers->content_type, '',
234 '... got no content type in the header' );
236 $r->headers( { content_type => 'text/plain' } );
238 my $header2 = $r->headers;
239 isa_ok( $header2, 'HTTP::Headers' );
240 isnt( $header, $header2, '... created a new HTTP::Header object' );
242 is( $header2->content_type, 'text/plain',
243 '... got the right content type in the header' );
245 $r->headers( [ content_type => 'text/html' ] );
247 my $header3 = $r->headers;
248 isa_ok( $header3, 'HTTP::Headers' );
249 isnt( $header2, $header3, '... created a new HTTP::Header object' );
251 is( $header3->content_type, 'text/html',
252 '... got the right content type in the header' );
254 $r->headers( HTTP::Headers->new( content_type => 'application/pdf' ) );
256 my $header4 = $r->headers;
257 isa_ok( $header4, 'HTTP::Headers' );
258 isnt( $header3, $header4, '... created a new HTTP::Header object' );
260 is( $header4->content_type, 'application/pdf',
261 '... got the right content type in the header' );
266 '... dies when it gets bad params';
270 is( $r->protocol, undef, '... got nothing by default' );
273 $r->protocol('HTTP/1.0');
275 '... set the protocol correctly';
276 is( $r->protocol, 'HTTP/1.0', '... got nothing by default' );
279 $r->protocol('http/1.0');
281 '... the protocol died with bar params correctly';