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