recipe 5
[gitmo/Moose.git] / lib / Moose / Cookbook / Recipe5.pod
CommitLineData
471c4f09 1
2=pod
3
4=head1 NAME
5
3824830b 6Moose::Cookbook::Recipe5 - More subtypes, coercion in a B<Request> class
471c4f09 7
8=head1 SYNOPSIS
9
10 package Request;
11 use strict;
12 use warnings;
13 use Moose;
05d9eaf6 14 use Moose::Util::TypeConstraints;
471c4f09 15
16 use HTTP::Headers ();
17 use Params::Coerce ();
18 use URI ();
19
50ec5055 20 subtype 'Header'
21 => as 'Object'
471c4f09 22 => where { $_->isa('HTTP::Headers') };
23
50ec5055 24 coerce 'Header'
25 => from 'ArrayRef'
471c4f09 26 => via { HTTP::Headers->new( @{ $_ } ) }
50ec5055 27 => from 'HashRef'
471c4f09 28 => via { HTTP::Headers->new( %{ $_ } ) };
29
50ec5055 30 subtype 'Uri'
31 => as 'Object'
471c4f09 32 => where { $_->isa('URI') };
33
50ec5055 34 coerce 'Uri'
35 => from 'Object'
36 => via { $_->isa('URI')
37 ? $_
38 : Params::Coerce::coerce( 'URI', $_ ) }
39 => from 'Str'
471c4f09 40 => via { URI->new( $_, 'http' ) };
41
50ec5055 42 subtype 'Protocol'
471c4f09 43 => as Str
44 => where { /^HTTP\/[0-9]\.[0-9]$/ };
45
471c4f09 46 has 'base' => (is => 'rw', isa => 'Uri', coerce => 1);
50ec5055 47 has 'uri' => (is => 'rw', isa => 'Uri', coerce => 1);
471c4f09 48 has 'method' => (is => 'rw', isa => 'Str');
49 has 'protocol' => (is => 'rw', isa => 'Protocol');
50 has 'headers' => (
51 is => 'rw',
52 isa => 'Header',
53 coerce => 1,
54 default => sub { HTTP::Headers->new }
55 );
56
57=head1 DESCRIPTION
58
50ec5055 59This recipe introduces the idea of type coercions, and the C<coerce>
60keyword. Coercions can be attached to pre-existing type constraints,
61and can be used to transform input of one type, into input of another
62type. This can be an extremely powerful tool if used correctly, which
63is why, by default, it is off. If you want your accessor to attempt
64a coercion, you must specifically ask for it with the B<coerce> option.
9deed647 65
50ec5055 66Now, onto the coercions.
67
68First we need to create a subtype to attach our coercion too. Here we
69create a basic I<Header> subtype, which matches any instance of the
70class B<HTTP::Headers>.
71
72 subtype 'Header'
73 => as 'Object'
74 => where { $_->isa('HTTP::Headers') };
75
76The simplest thing from here would be create an accessor declaration
77like so:
78
79 has 'headers' => (
80 is => 'rw',
81 isa => 'Header',
82 default => sub { HTTP::Headers->new }
83 );
84
85We would then have a self-validating accessor whose default value is
86an empty instance of B<HTTP::Headers>. This is nice, but it is not
87ideal.
88
89The constructor for B<HTTP::Headers> accepts a list of key-value pairs
90representing the fields in an HTTP header. With Perl such a list could
91easily be stored into an ARRAY or HASH reference. We would like our
92class's interface to be able to accept this list of key-value pairs
93in place of the B<HTTP::Headers> instance, and just DWIM. This is where
94coercion can help. First, lets declare our coercion:
95
96 coerce 'Header'
97 => from 'ArrayRef'
98 => via { HTTP::Headers->new( @{ $_ } ) }
99 => from 'HashRef'
100 => via { HTTP::Headers->new( %{ $_ } ) };
101
102We first tell it that we are attaching the coercion to the 'Header'
103subtype. We then give is a set of C<from> clauses which map other
104subtypes to coercion routines (through the C<via> keyword). Fairly
105simple really, however, this alone does nothing. We have to tell
106our attribute declaration to actually use the coercion, like so:
107
108 has 'headers' => (
109 is => 'rw',
110 isa => 'Header',
111 coerce => 1,
112 default => sub { HTTP::Headers->new }
113 );
114
115This will coerce any B<ArrayRef> or B<HashRef> which is passed into
116the C<headers> accessor into an instance of B<HTTP::Headers>. So that
117the following lines of code are all equivalent:
118
119 $foo->headers(HTTP::Headers->new(bar => 1, baz => 2));
120 $foo->headers([ 'bar', 1, 'baz', 2 ]);
121 $foo->headers({ bar => 1, baz => 2 });
122
123As you can see, careful use of coercions can produce an very open
124interface for your class, while still retaining the "safety" of
125your type constraint checks.
126
127Our next coercion takes advantage of the power of CPAN to handle
128the details of our coercion. In this particular case it uses the
129L<Params::Coerce> module, which fits in rather nicely with L<Moose>.
130
131Again, we create a simple subtype to represent instance of the
132B<URI> class.
133
134 subtype 'Uri'
135 => as 'Object'
136 => where { $_->isa('URI') };
137
138Then we add the coercion:
139
140 coerce 'Uri'
141 => from 'Object'
142 => via { $_->isa('URI')
143 ? $_
144 : Params::Coerce::coerce( 'URI', $_ ) }
145 => from 'Str'
146 => via { URI->new( $_, 'http' ) };
147
148The first C<from> clause we introduce is for the 'Object' subtype,
149an 'Object' is simply, anything which is C<bless>ed. This means
150that if the coercion encounters another object, it should use this
151clause. Now we look at the C<via> block so what it does. First
152it checks to see if its a B<URI> instance. Since the coercion
153process happens prior to any type constraint checking, it is entirely
154possible for this to happen. And if it does happen, we simple want
155to pass the instance on through. However, if it is not an instance
156of B<URI>, then we need to coerce it. This is where L<Params::Coerce>
157can do it's magic, and we can just use it's return value. Simple
158really, and much less work since we use a module from CPAN :)
159
160The second C<from> clause is attached to the 'Str' subtype, and
161illustrates how coercions can also be used to handle certain
162'default' behaviors. In this coercion, we simple take any string
163and pass it into the B<URI> constructor along with the default
164'http' scheme type.
165
166And of course, our coercions do nothing unless they are told to,
167like so:
168
169 has 'base' => (is => 'rw', isa => 'Uri', coerce => 1);
170 has 'uri' => (is => 'rw', isa => 'Uri', coerce => 1);
171
172As you can see, re-using the coercion allows us to enforce a
173consistent and very flexible API across multiple accessors.
174
175=head1 CONCLUSION
176
177This recipe illustrated the power of coercions to build a more
178flexible and open API for your accessors, while still retaining
179all the safety that comes from using Moose's type constraints.
180Using coercions it becomes simple to manage (from a single
181location) a consisten API not only across multiple accessors,
182but across multiple classes as well.
183
184In the next recipe, we will introduce roles, a concept originally
185borrowed from Smalltalk, which made it's way into Perl 6, and
186now into Moose.
3824830b 187
471c4f09 188=head1 AUTHOR
189
190Stevan Little E<lt>stevan@iinteractive.comE<gt>
191
192=head1 COPYRIGHT AND LICENSE
193
194Copyright 2006 by Infinity Interactive, Inc.
195
196L<http://www.iinteractive.com>
197
198This library is free software; you can redistribute it and/or modify
199it under the same terms as Perl itself.
200
201=cut