Commit | Line | Data |
471c4f09 |
1 | |
2 | =pod |
3 | |
4 | =head1 NAME |
5 | |
3824830b |
6 | Moose::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 |
59 | This recipe introduces the idea of type coercions, and the C<coerce> |
60 | keyword. Coercions can be attached to pre-existing type constraints, |
61 | and can be used to transform input of one type, into input of another |
62 | type. This can be an extremely powerful tool if used correctly, which |
63 | is why, by default, it is off. If you want your accessor to attempt |
64 | a coercion, you must specifically ask for it with the B<coerce> option. |
9deed647 |
65 | |
50ec5055 |
66 | Now, onto the coercions. |
67 | |
68 | First we need to create a subtype to attach our coercion too. Here we |
69 | create a basic I<Header> subtype, which matches any instance of the |
70 | class B<HTTP::Headers>. |
71 | |
72 | subtype 'Header' |
73 | => as 'Object' |
74 | => where { $_->isa('HTTP::Headers') }; |
75 | |
76 | The simplest thing from here would be create an accessor declaration |
77 | like so: |
78 | |
79 | has 'headers' => ( |
80 | is => 'rw', |
81 | isa => 'Header', |
82 | default => sub { HTTP::Headers->new } |
83 | ); |
84 | |
85 | We would then have a self-validating accessor whose default value is |
86 | an empty instance of B<HTTP::Headers>. This is nice, but it is not |
87 | ideal. |
88 | |
89 | The constructor for B<HTTP::Headers> accepts a list of key-value pairs |
90 | representing the fields in an HTTP header. With Perl such a list could |
91 | easily be stored into an ARRAY or HASH reference. We would like our |
92 | class's interface to be able to accept this list of key-value pairs |
93 | in place of the B<HTTP::Headers> instance, and just DWIM. This is where |
94 | coercion 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 | |
102 | We first tell it that we are attaching the coercion to the 'Header' |
103 | subtype. We then give is a set of C<from> clauses which map other |
104 | subtypes to coercion routines (through the C<via> keyword). Fairly |
105 | simple really, however, this alone does nothing. We have to tell |
106 | our 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 | |
115 | This will coerce any B<ArrayRef> or B<HashRef> which is passed into |
116 | the C<headers> accessor into an instance of B<HTTP::Headers>. So that |
117 | the 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 | |
123 | As you can see, careful use of coercions can produce an very open |
124 | interface for your class, while still retaining the "safety" of |
125 | your type constraint checks. |
126 | |
127 | Our next coercion takes advantage of the power of CPAN to handle |
128 | the details of our coercion. In this particular case it uses the |
129 | L<Params::Coerce> module, which fits in rather nicely with L<Moose>. |
130 | |
131 | Again, we create a simple subtype to represent instance of the |
132 | B<URI> class. |
133 | |
134 | subtype 'Uri' |
135 | => as 'Object' |
136 | => where { $_->isa('URI') }; |
137 | |
138 | Then 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 | |
148 | The first C<from> clause we introduce is for the 'Object' subtype, |
149 | an 'Object' is simply, anything which is C<bless>ed. This means |
150 | that if the coercion encounters another object, it should use this |
151 | clause. Now we look at the C<via> block so what it does. First |
152 | it checks to see if its a B<URI> instance. Since the coercion |
153 | process happens prior to any type constraint checking, it is entirely |
154 | possible for this to happen. And if it does happen, we simple want |
155 | to pass the instance on through. However, if it is not an instance |
156 | of B<URI>, then we need to coerce it. This is where L<Params::Coerce> |
157 | can do it's magic, and we can just use it's return value. Simple |
158 | really, and much less work since we use a module from CPAN :) |
159 | |
160 | The second C<from> clause is attached to the 'Str' subtype, and |
161 | illustrates how coercions can also be used to handle certain |
162 | 'default' behaviors. In this coercion, we simple take any string |
163 | and pass it into the B<URI> constructor along with the default |
164 | 'http' scheme type. |
165 | |
166 | And of course, our coercions do nothing unless they are told to, |
167 | like so: |
168 | |
169 | has 'base' => (is => 'rw', isa => 'Uri', coerce => 1); |
170 | has 'uri' => (is => 'rw', isa => 'Uri', coerce => 1); |
171 | |
172 | As you can see, re-using the coercion allows us to enforce a |
173 | consistent and very flexible API across multiple accessors. |
174 | |
175 | =head1 CONCLUSION |
176 | |
177 | This recipe illustrated the power of coercions to build a more |
178 | flexible and open API for your accessors, while still retaining |
179 | all the safety that comes from using Moose's type constraints. |
180 | Using coercions it becomes simple to manage (from a single |
181 | location) a consisten API not only across multiple accessors, |
182 | but across multiple classes as well. |
183 | |
184 | In the next recipe, we will introduce roles, a concept originally |
185 | borrowed from Smalltalk, which made it's way into Perl 6, and |
186 | now into Moose. |
3824830b |
187 | |
471c4f09 |
188 | =head1 AUTHOR |
189 | |
190 | Stevan Little E<lt>stevan@iinteractive.comE<gt> |
191 | |
192 | =head1 COPYRIGHT AND LICENSE |
193 | |
194 | Copyright 2006 by Infinity Interactive, Inc. |
195 | |
196 | L<http://www.iinteractive.com> |
197 | |
198 | This library is free software; you can redistribute it and/or modify |
199 | it under the same terms as Perl itself. |
200 | |
201 | =cut |