Redid conversion to Test::Fatal
[gitmo/Moose.git] / lib / Moose / Cookbook / Basics / Recipe5.pod
CommitLineData
471c4f09 1
2=pod
3
5547fba7 4=begin testing-SETUP
c79239a2 5
0adca353 6use Test::Requires {
7 'HTTP::Headers' => '0',
8 'Params::Coerce' => '0',
9 'URI' => '0',
10};
c79239a2 11
5547fba7 12=end testing-SETUP
c79239a2 13
471c4f09 14=head1 NAME
15
021b8139 16Moose::Cookbook::Basics::Recipe5 - More subtypes, coercion in a B<Request> class
471c4f09 17
18=head1 SYNOPSIS
19
20 package Request;
471c4f09 21 use Moose;
05d9eaf6 22 use Moose::Util::TypeConstraints;
c765b254 23
471c4f09 24 use HTTP::Headers ();
25 use Params::Coerce ();
26 use URI ();
c765b254 27
66b58567 28 subtype 'My::Types::HTTP::Headers' => as class_type('HTTP::Headers');
c765b254 29
66b58567 30 coerce 'My::Types::HTTP::Headers'
50ec5055 31 => from 'ArrayRef'
c765b254 32 => via { HTTP::Headers->new( @{$_} ) }
50ec5055 33 => from 'HashRef'
c765b254 34 => via { HTTP::Headers->new( %{$_} ) };
35
66b58567 36 subtype 'My::Types::URI' => as class_type('URI');
c765b254 37
66b58567 38 coerce 'My::Types::URI'
50ec5055 39 => from 'Object'
c765b254 40 => via { $_->isa('URI')
41 ? $_
42 : Params::Coerce::coerce( 'URI', $_ ); }
50ec5055 43 => from 'Str'
471c4f09 44 => via { URI->new( $_, 'http' ) };
c765b254 45
50ec5055 46 subtype 'Protocol'
c765b254 47 => as 'Str'
471c4f09 48 => where { /^HTTP\/[0-9]\.[0-9]$/ };
c765b254 49
66b58567 50 has 'base' => ( is => 'rw', isa => 'My::Types::URI', coerce => 1 );
51 has 'uri' => ( is => 'rw', isa => 'My::Types::URI', coerce => 1 );
c765b254 52 has 'method' => ( is => 'rw', isa => 'Str' );
53 has 'protocol' => ( is => 'rw', isa => 'Protocol' );
471c4f09 54 has 'headers' => (
55 is => 'rw',
66b58567 56 isa => 'My::Types::HTTP::Headers',
471c4f09 57 coerce => 1,
c765b254 58 default => sub { HTTP::Headers->new }
471c4f09 59 );
60
61=head1 DESCRIPTION
62
f07dc78e 63This recipe introduces type coercions, which are defined with the
64C<coerce> sugar function. Coercions are attached to existing type
65constraints, and define a (one-way) transformation from one type to
66another.
67
68This is very powerful, but it's also magical, so you have to
69explicitly ask for an attribute to be coerced. To do this, you must
16fb3624 70set the C<coerce> attribute option to a true value.
9deed647 71
f07dc78e 72First, we create the subtype to which we will coerce the other types:
50ec5055 73
66b58567 74 subtype 'My::Types::HTTP::Headers' => as class_type('HTTP::Headers');
3a4bb3ec 75
76We are creating a subtype rather than using C<HTTP::Headers> as a type
77directly. The reason we do this is coercions are global, and a
78coercion defined for C<HTTP::Headers> in our C<Request> class would
79then be defined for I<all> Moose-using classes in the current Perl
80interpreter. It's a L<best practice|Moose::Manual::BestPractices> to
81avoid this sort of namespace pollution.
50ec5055 82
3a4bb3ec 83The C<class_type> sugar function is simply a shortcut for this:
f07dc78e 84
85 subtype 'HTTP::Headers'
50ec5055 86 => as 'Object'
87 => where { $_->isa('HTTP::Headers') };
6aa9f385 88
f07dc78e 89Internally, Moose creates a type constraint for each Moose-using
90class, but for non-Moose classes, the type must be declared
91explicitly.
92
93We could go ahead and use this new type directly:
50ec5055 94
c765b254 95 has 'headers' => (
50ec5055 96 is => 'rw',
f07dc78e 97 isa => 'HTTP::Headers',
c765b254 98 default => sub { HTTP::Headers->new }
50ec5055 99 );
100
f07dc78e 101This creates a simple attribute which defaults to an empty instance of
102L<HTTP::Headers>.
50ec5055 103
f07dc78e 104The constructor for L<HTTP::Headers> accepts a list of key-value pairs
105representing the HTTP header fields. In Perl, such a list could be
106stored in an ARRAY or HASH reference. We want our C<headers> attribute
107to accept those data structure instead of an B<HTTP::Headers>
108instance, and just do the right thing. This is exactly what coercion
109is for:
50ec5055 110
66b58567 111 coerce 'My::Types::HTTP::Headers'
50ec5055 112 => from 'ArrayRef'
c765b254 113 => via { HTTP::Headers->new( @{$_} ) }
50ec5055 114 => from 'HashRef'
c765b254 115 => via { HTTP::Headers->new( %{$_} ) };
50ec5055 116
e39d2b6b 117The first argument to C<coerce> is the type I<to> which we are
f07dc78e 118coercing. Then we give it a set of C<from>/C<via> clauses. The C<from>
119function takes some other type name and C<via> takes a subroutine
120reference which actually does the coercion.
121
122However, defining the coercion doesn't do anything until we tell Moose
123we want a particular attribute to be coerced:
50ec5055 124
c765b254 125 has 'headers' => (
50ec5055 126 is => 'rw',
66b58567 127 isa => 'My::Types::HTTP::Headers',
50ec5055 128 coerce => 1,
c765b254 129 default => sub { HTTP::Headers->new }
50ec5055 130 );
131
f07dc78e 132Now, if we use an C<ArrayRef> or C<HashRef> to populate C<headers>, it
133will be coerced into a new L<HTTP::Headers> instance. With the
134coercion in place, the following lines of code are all equivalent:
50ec5055 135
c765b254 136 $foo->headers( HTTP::Headers->new( bar => 1, baz => 2 ) );
137 $foo->headers( [ 'bar', 1, 'baz', 2 ] );
138 $foo->headers( { bar => 1, baz => 2 } );
50ec5055 139
c765b254 140As you can see, careful use of coercions can produce a very open
141interface for your class, while still retaining the "safety" of your
f07dc78e 142type constraint checks. (1)
50ec5055 143
f07dc78e 144Our next coercion shows how we can leverage existing CPAN modules to
145help implement coercions. In this case we use L<Params::Coerce>.
50ec5055 146
f07dc78e 147Once again, we need to declare a class type for our non-Moose L<URI>
c765b254 148class:
50ec5055 149
66b58567 150 subtype 'My::Types::URI' => as class_type('URI');
50ec5055 151
f07dc78e 152Then we define the coercion:
50ec5055 153
66b58567 154 coerce 'My::Types::URI'
50ec5055 155 => from 'Object'
c765b254 156 => via { $_->isa('URI')
157 ? $_
158 : Params::Coerce::coerce( 'URI', $_ ); }
50ec5055 159 => from 'Str'
160 => via { URI->new( $_, 'http' ) };
161
f07dc78e 162The first coercion takes any object and makes it a C<URI> object. The
163coercion system isn't that smart, and does not check if the object is
164already a L<URI>, so we check for that ourselves. If it's not a L<URI>
165already, we let L<Params::Coerce> do its magic, and we just use its
166return value.
167
168If L<Params::Coerce> didn't return a L<URI> object (for whatever
169reason), Moose would throw a type constraint error.
c765b254 170
f07dc78e 171The other coercion takes a string and converts to a L<URI>. In this
172case, we are using the coercion to apply a default behavior, where a
173string is assumed to be an C<http> URI.
c765b254 174
f07dc78e 175Finally, we need to make sure our attributes enable coercion.
c765b254 176
66b58567 177 has 'base' => ( is => 'rw', isa => 'My::Types::URI', coerce => 1 );
178 has 'uri' => ( is => 'rw', isa => 'My::Types::URI', coerce => 1 );
c765b254 179
f07dc78e 180Re-using the coercion lets us enforce a consistent API across multiple
181attributes.
50ec5055 182
183=head1 CONCLUSION
12710e29 184
f07dc78e 185This recipe showed the use of coercions to create a more flexible and
186DWIM-y API. Like any powerful magic, we recommend some
187caution. Sometimes it's better to reject a value than just guess at
188how to DWIM.
189
190We also showed the use of the C<class_type> sugar function as a
191shortcut for defining a new subtype of C<Object>
192
193=head1 FOOTNOTES
50ec5055 194
f07dc78e 195=over 4
3824830b 196
f07dc78e 197=item (1)
198
199This particular example could be safer. Really we only want to coerce
200an array with an I<even> number of elements. We could create a new
201C<EvenElementArrayRef> type, and then coerce from that type, as
202opposed to from a plain C<ArrayRef>
203
204=back
205
206=head1 AUTHORS
471c4f09 207
208Stevan Little E<lt>stevan@iinteractive.comE<gt>
209
f07dc78e 210Dave Rolsky E<lt>autarch@urth.orgE<gt>
211
471c4f09 212=head1 COPYRIGHT AND LICENSE
213
7e0492d3 214Copyright 2006-2010 by Infinity Interactive, Inc.
471c4f09 215
216L<http://www.iinteractive.com>
217
218This library is free software; you can redistribute it and/or modify
219it under the same terms as Perl itself.
220
c79239a2 221=begin testing
222
223my $r = Request->new;
224isa_ok( $r, 'Request' );
225
226{
227 my $header = $r->headers;
228 isa_ok( $header, 'HTTP::Headers' );
229
230 is( $r->headers->content_type, '',
231 '... got no content type in the header' );
232
233 $r->headers( { content_type => 'text/plain' } );
234
235 my $header2 = $r->headers;
236 isa_ok( $header2, 'HTTP::Headers' );
237 isnt( $header, $header2, '... created a new HTTP::Header object' );
238
239 is( $header2->content_type, 'text/plain',
240 '... got the right content type in the header' );
241
242 $r->headers( [ content_type => 'text/html' ] );
243
244 my $header3 = $r->headers;
245 isa_ok( $header3, 'HTTP::Headers' );
246 isnt( $header2, $header3, '... created a new HTTP::Header object' );
247
248 is( $header3->content_type, 'text/html',
249 '... got the right content type in the header' );
250
251 $r->headers( HTTP::Headers->new( content_type => 'application/pdf' ) );
252
253 my $header4 = $r->headers;
254 isa_ok( $header4, 'HTTP::Headers' );
255 isnt( $header3, $header4, '... created a new HTTP::Header object' );
256
257 is( $header4->content_type, 'application/pdf',
258 '... got the right content type in the header' );
259
b10dde3a 260 isnt(
261 exception {
262 $r->headers('Foo');
263 },
264 undef,
265 '... dies when it gets bad params'
266 );
c79239a2 267}
268
269{
270 is( $r->protocol, undef, '... got nothing by default' );
271
b10dde3a 272 is(
273 exception {
274 $r->protocol('HTTP/1.0');
275 },
276 undef,
277 '... set the protocol correctly'
278 );
279
c79239a2 280 is( $r->protocol, 'HTTP/1.0', '... got nothing by default' );
281
b10dde3a 282 isnt(
283 exception {
284 $r->protocol('http/1.0');
285 },
286 undef,
287 '... the protocol died with bar params correctly'
288 );
c79239a2 289}
290
bd538e29 291{
292 $r->base('http://localhost/');
293 isa_ok( $r->base, 'URI' );
294
295 $r->uri('http://localhost/');
296 isa_ok( $r->uri, 'URI' );
297}
298
c79239a2 299=end testing
300
f891e7b7 301=cut