Commit | Line | Data |
471c4f09 |
1 | |
2 | =pod |
3 | |
4 | =head1 NAME |
5 | |
021b8139 |
6 | Moose::Cookbook::Basics::Recipe5 - More subtypes, coercion in a B<Request> class |
471c4f09 |
7 | |
8 | =head1 SYNOPSIS |
9 | |
10 | package Request; |
471c4f09 |
11 | use Moose; |
05d9eaf6 |
12 | use Moose::Util::TypeConstraints; |
c765b254 |
13 | |
471c4f09 |
14 | use HTTP::Headers (); |
15 | use Params::Coerce (); |
16 | use URI (); |
c765b254 |
17 | |
3a4bb3ec |
18 | subtype 'My.HTTP::Headers' => as class_type('HTTP::Headers'); |
c765b254 |
19 | |
3a4bb3ec |
20 | coerce 'My.HTTP::Headers' |
50ec5055 |
21 | => from 'ArrayRef' |
c765b254 |
22 | => via { HTTP::Headers->new( @{$_} ) } |
50ec5055 |
23 | => from 'HashRef' |
c765b254 |
24 | => via { HTTP::Headers->new( %{$_} ) }; |
25 | |
3a4bb3ec |
26 | subtype 'My.URI' => as class_type('HTTP::Headers'); |
c765b254 |
27 | |
3a4bb3ec |
28 | coerce 'My.URI' |
50ec5055 |
29 | => from 'Object' |
c765b254 |
30 | => via { $_->isa('URI') |
31 | ? $_ |
32 | : Params::Coerce::coerce( 'URI', $_ ); } |
50ec5055 |
33 | => from 'Str' |
471c4f09 |
34 | => via { URI->new( $_, 'http' ) }; |
c765b254 |
35 | |
50ec5055 |
36 | subtype 'Protocol' |
c765b254 |
37 | => as 'Str' |
471c4f09 |
38 | => where { /^HTTP\/[0-9]\.[0-9]$/ }; |
c765b254 |
39 | |
3a4bb3ec |
40 | has 'base' => ( is => 'rw', isa => 'My.URI', coerce => 1 ); |
41 | has 'uri' => ( is => 'rw', isa => 'My.URI', coerce => 1 ); |
c765b254 |
42 | has 'method' => ( is => 'rw', isa => 'Str' ); |
43 | has 'protocol' => ( is => 'rw', isa => 'Protocol' ); |
471c4f09 |
44 | has 'headers' => ( |
45 | is => 'rw', |
3a4bb3ec |
46 | isa => 'My.HTTP::Headers', |
471c4f09 |
47 | coerce => 1, |
c765b254 |
48 | default => sub { HTTP::Headers->new } |
471c4f09 |
49 | ); |
50 | |
51 | =head1 DESCRIPTION |
52 | |
f07dc78e |
53 | This recipe introduces type coercions, which are defined with the |
54 | C<coerce> sugar function. Coercions are attached to existing type |
55 | constraints, and define a (one-way) transformation from one type to |
56 | another. |
57 | |
58 | This is very powerful, but it's also magical, so you have to |
59 | explicitly ask for an attribute to be coerced. To do this, you must |
60 | set the C<coerce> attribute parameter to a true value. |
9deed647 |
61 | |
f07dc78e |
62 | First, we create the subtype to which we will coerce the other types: |
50ec5055 |
63 | |
3a4bb3ec |
64 | subtype 'My.HTTP::Headers' => as class_type('HTTP::Headers'); |
65 | |
66 | We are creating a subtype rather than using C<HTTP::Headers> as a type |
67 | directly. The reason we do this is coercions are global, and a |
68 | coercion defined for C<HTTP::Headers> in our C<Request> class would |
69 | then be defined for I<all> Moose-using classes in the current Perl |
70 | interpreter. It's a L<best practice|Moose::Manual::BestPractices> to |
71 | avoid this sort of namespace pollution. |
50ec5055 |
72 | |
3a4bb3ec |
73 | The C<class_type> sugar function is simply a shortcut for this: |
f07dc78e |
74 | |
75 | subtype 'HTTP::Headers' |
50ec5055 |
76 | => as 'Object' |
77 | => where { $_->isa('HTTP::Headers') }; |
6aa9f385 |
78 | |
f07dc78e |
79 | Internally, Moose creates a type constraint for each Moose-using |
80 | class, but for non-Moose classes, the type must be declared |
81 | explicitly. |
82 | |
83 | We could go ahead and use this new type directly: |
50ec5055 |
84 | |
c765b254 |
85 | has 'headers' => ( |
50ec5055 |
86 | is => 'rw', |
f07dc78e |
87 | isa => 'HTTP::Headers', |
c765b254 |
88 | default => sub { HTTP::Headers->new } |
50ec5055 |
89 | ); |
90 | |
f07dc78e |
91 | This creates a simple attribute which defaults to an empty instance of |
92 | L<HTTP::Headers>. |
50ec5055 |
93 | |
f07dc78e |
94 | The constructor for L<HTTP::Headers> accepts a list of key-value pairs |
95 | representing the HTTP header fields. In Perl, such a list could be |
96 | stored in an ARRAY or HASH reference. We want our C<headers> attribute |
97 | to accept those data structure instead of an B<HTTP::Headers> |
98 | instance, and just do the right thing. This is exactly what coercion |
99 | is for: |
50ec5055 |
100 | |
3a4bb3ec |
101 | coerce 'My.HTTP::Headers' |
50ec5055 |
102 | => from 'ArrayRef' |
c765b254 |
103 | => via { HTTP::Headers->new( @{$_} ) } |
50ec5055 |
104 | => from 'HashRef' |
c765b254 |
105 | => via { HTTP::Headers->new( %{$_} ) }; |
50ec5055 |
106 | |
f07dc78e |
107 | The first argument to c<coerce> is the type I<to> which we are |
108 | coercing. Then we give it a set of C<from>/C<via> clauses. The C<from> |
109 | function takes some other type name and C<via> takes a subroutine |
110 | reference which actually does the coercion. |
111 | |
112 | However, defining the coercion doesn't do anything until we tell Moose |
113 | we want a particular attribute to be coerced: |
50ec5055 |
114 | |
c765b254 |
115 | has 'headers' => ( |
50ec5055 |
116 | is => 'rw', |
3a4bb3ec |
117 | isa => 'My.HTTP::Headers', |
50ec5055 |
118 | coerce => 1, |
c765b254 |
119 | default => sub { HTTP::Headers->new } |
50ec5055 |
120 | ); |
121 | |
f07dc78e |
122 | Now, if we use an C<ArrayRef> or C<HashRef> to populate C<headers>, it |
123 | will be coerced into a new L<HTTP::Headers> instance. With the |
124 | coercion in place, the following lines of code are all equivalent: |
50ec5055 |
125 | |
c765b254 |
126 | $foo->headers( HTTP::Headers->new( bar => 1, baz => 2 ) ); |
127 | $foo->headers( [ 'bar', 1, 'baz', 2 ] ); |
128 | $foo->headers( { bar => 1, baz => 2 } ); |
50ec5055 |
129 | |
c765b254 |
130 | As you can see, careful use of coercions can produce a very open |
131 | interface for your class, while still retaining the "safety" of your |
f07dc78e |
132 | type constraint checks. (1) |
50ec5055 |
133 | |
f07dc78e |
134 | Our next coercion shows how we can leverage existing CPAN modules to |
135 | help implement coercions. In this case we use L<Params::Coerce>. |
50ec5055 |
136 | |
f07dc78e |
137 | Once again, we need to declare a class type for our non-Moose L<URI> |
c765b254 |
138 | class: |
50ec5055 |
139 | |
3a4bb3ec |
140 | subtype 'My.URI' => as class_type('HTTP::Headers'); |
50ec5055 |
141 | |
f07dc78e |
142 | Then we define the coercion: |
50ec5055 |
143 | |
3a4bb3ec |
144 | coerce 'My.URI' |
50ec5055 |
145 | => from 'Object' |
c765b254 |
146 | => via { $_->isa('URI') |
147 | ? $_ |
148 | : Params::Coerce::coerce( 'URI', $_ ); } |
50ec5055 |
149 | => from 'Str' |
150 | => via { URI->new( $_, 'http' ) }; |
151 | |
f07dc78e |
152 | The first coercion takes any object and makes it a C<URI> object. The |
153 | coercion system isn't that smart, and does not check if the object is |
154 | already a L<URI>, so we check for that ourselves. If it's not a L<URI> |
155 | already, we let L<Params::Coerce> do its magic, and we just use its |
156 | return value. |
157 | |
158 | If L<Params::Coerce> didn't return a L<URI> object (for whatever |
159 | reason), Moose would throw a type constraint error. |
c765b254 |
160 | |
f07dc78e |
161 | The other coercion takes a string and converts to a L<URI>. In this |
162 | case, we are using the coercion to apply a default behavior, where a |
163 | string is assumed to be an C<http> URI. |
c765b254 |
164 | |
f07dc78e |
165 | Finally, we need to make sure our attributes enable coercion. |
c765b254 |
166 | |
3a4bb3ec |
167 | has 'base' => ( is => 'rw', isa => 'My.URI', coerce => 1 ); |
168 | has 'uri' => ( is => 'rw', isa => 'My.URI', coerce => 1 ); |
c765b254 |
169 | |
f07dc78e |
170 | Re-using the coercion lets us enforce a consistent API across multiple |
171 | attributes. |
50ec5055 |
172 | |
173 | =head1 CONCLUSION |
12710e29 |
174 | |
f07dc78e |
175 | This recipe showed the use of coercions to create a more flexible and |
176 | DWIM-y API. Like any powerful magic, we recommend some |
177 | caution. Sometimes it's better to reject a value than just guess at |
178 | how to DWIM. |
179 | |
180 | We also showed the use of the C<class_type> sugar function as a |
181 | shortcut for defining a new subtype of C<Object> |
182 | |
183 | =head1 FOOTNOTES |
50ec5055 |
184 | |
f07dc78e |
185 | =over 4 |
3824830b |
186 | |
f07dc78e |
187 | =item (1) |
188 | |
189 | This particular example could be safer. Really we only want to coerce |
190 | an array with an I<even> number of elements. We could create a new |
191 | C<EvenElementArrayRef> type, and then coerce from that type, as |
192 | opposed to from a plain C<ArrayRef> |
193 | |
194 | =back |
195 | |
196 | =head1 AUTHORS |
471c4f09 |
197 | |
198 | Stevan Little E<lt>stevan@iinteractive.comE<gt> |
199 | |
f07dc78e |
200 | Dave Rolsky E<lt>autarch@urth.orgE<gt> |
201 | |
471c4f09 |
202 | =head1 COPYRIGHT AND LICENSE |
203 | |
2840a3b2 |
204 | Copyright 2006-2009 by Infinity Interactive, Inc. |
471c4f09 |
205 | |
206 | L<http://www.iinteractive.com> |
207 | |
208 | This library is free software; you can redistribute it and/or modify |
209 | it under the same terms as Perl itself. |
210 | |
f891e7b7 |
211 | =cut |