Mercurial > pub > Impl
comparison Lib/IMPL/Web/Application/ResourceContract.pm @ 229:47f77e6409f7
heavily reworked the resource model of the web application:
*some ResourcesContraact functionality moved to Resource
+Added CustomResource
*Corrected action handlers
author | sergey |
---|---|
date | Sat, 29 Sep 2012 02:34:47 +0400 |
parents | 431db7034a88 |
children | 6d8092d8ce1b |
comparison
equal
deleted
inserted
replaced
228:431db7034a88 | 229:47f77e6409f7 |
---|---|
1 package IMPL::Web::Application::ResourceContract; | 1 package IMPL::Web::Application::ResourceContract; |
2 use strict; | 2 use strict; |
3 use IMPL::lang qw(:declare); | 3 use IMPL::lang qw(:constants); |
4 use IMPL::declare { | 4 use IMPL::declare { |
5 require => { | 5 require => { |
6 'Exception' => 'IMPL::Exception', | 6 'Exception' => 'IMPL::Exception', |
7 'ArgumentException' => '-IMPL::ArgumentException', | 7 'ArgumentException' => '-IMPL::ArgumentException', |
8 'KeyNotFoundException' => '-IMPL::KeyNotFoundException', | 8 'KeyNotFoundException' => '-IMPL::KeyNotFoundException', |
9 'ResourceClass' => 'IMPL::Web::Application::Resource' | 9 'ResourceClass' => 'IMPL::Web::Application::Resource' |
10 }, | 10 }, |
11 base => [ | 11 base => [ 'IMPL::Object' => undef ], |
12 'IMPL::Object' => undef | 12 props => [ |
13 ] | 13 resourceFactory => PROP_ALL, |
14 verbs => PROP_ALL, | |
15 _namedResources => PROP_ALL, | |
16 _regexpResources => PROP_ALL | PROP_LIST, | |
17 ] | |
14 }; | 18 }; |
15 | |
16 BEGIN { | |
17 public property resourceFactory => PROP_ALL; | |
18 public property operations => PROP_ALL; | |
19 private property _namedResources => PROP_ALL; | |
20 private property _regexpResources => PROP_ALL | PROP_LIST; | |
21 } | |
22 | 19 |
23 sub CTOR { | 20 sub CTOR { |
24 my $this = shift; | 21 my $this = shift; |
25 my %args = @_; | 22 my %args = @_; |
26 | 23 |
27 $this->resourceFactory( $args{resourceFactory} || ResourceClass ); | 24 $this->resourceFactory( $args{resourceFactory} || ResourceClass ); |
28 | 25 |
29 my $resources = $args{resources} || []; | 26 my $resources = $args{resources} || []; |
30 my $operations = $args{operations} || {}; | 27 my $verbs = $args{verbs} || {}; |
31 | 28 |
32 die ArgumentException->new(resources => 'resources parameter must be a reference to an array') | 29 die ArgumentException->new( |
33 unless ref $resources eq 'ARRAY'; | 30 resources => 'resources parameter must be a reference to an array' ) |
34 | 31 unless ref $resources eq 'ARRAY'; |
35 die ArgumentException->new(opearations => 'operations parameter must be a reference to a hash') | 32 |
36 unless ref $operations eq 'HASH'; | 33 die ArgumentException->new( |
37 | 34 opearations => 'operations parameter must be a reference to a hash' ) |
35 unless ref $verbs eq 'HASH'; | |
36 | |
37 $this->verbs( | |
38 { map { lc($_), $verbs->{$_} } keys %$verbs } ); | |
39 | |
38 my %nameMap; | 40 my %nameMap; |
39 | 41 |
40 foreach my $res (@$resources) { | 42 foreach my $res (@$verbs) { |
41 next unless $res->{contract}; | 43 next unless $res->{contract}; |
42 if(my $name = $res->{name}) { | 44 if ( my $name = $res->{name} ) { |
43 $nameMap{$name} = $res; | 45 $nameMap{$name} = $res; |
44 } | 46 } |
45 if($res->{match}) { | 47 if ( $res->{match} ) { |
46 $this->_regexpResources->Append($res); | 48 $this->_regexpResources->Append($res); |
47 } | 49 } |
48 } | 50 } |
49 | 51 |
50 $this->_namedResources(\%nameMap); | 52 $this->_namedResources( \%nameMap ); |
53 } | |
54 | |
55 sub AddChildResourceContract { | |
56 my ($this,$res) = @_; | |
57 | |
58 die ArgumentException->new(res => "A valid child resource definition") | |
59 unless ref $res eq 'HASH'; | |
60 | |
61 $this->_namedResources->{$res->{name}} = $res if $res->{name}; | |
62 $this->_regexpResources->Append($res) if $res->{match}; | |
63 | |
64 return; | |
51 } | 65 } |
52 | 66 |
53 sub CreateResource { | 67 sub CreateResource { |
54 my $this = shift; | 68 my $this = shift; |
55 my %args = @_; | 69 my %args = @_; |
56 | 70 |
57 return $this->resourceFactory->new ( | 71 return $this->resourceFactory->new( %args, contract => $this ); |
58 %args, | 72 } |
59 contract => $this | 73 |
60 ); | 74 sub FindChildResourceInfo { |
61 } | 75 my ( $this, $name ) = @_; |
62 | 76 |
63 sub FindChildResourceContractInfo { | 77 if ( my $info = $this->_namedResources->{$name} ) { |
64 my ($this,$name) = @_; | 78 return $info, [$name]; |
65 | 79 } |
66 if(my $contract = $this->_namedResources->{$name}) { | 80 else { |
67 return $contract; | |
68 } else { | |
69 foreach my $info ( $this->_regexpResources ) { | 81 foreach my $info ( $this->_regexpResources ) { |
70 my $rx = $info->{match}; | 82 my $rx = $info->{match}; |
71 return $info if $name =~ m/$rx/; | 83 if(my @childId = $name =~ m/$rx/) { |
84 return $info, \@childId; | |
85 } | |
72 } | 86 } |
73 } | 87 } |
74 | 88 |
75 return undef; | 89 return; |
76 } | |
77 | |
78 sub CreateChildResource { | |
79 my $this = @_; | |
80 my %args = @_; | |
81 | |
82 my $id = $args{id} or die ArgumentException( id => 'id parameter must be specified'); | |
83 my $parent = $args{parent}; | |
84 my $model = $parent->model if $parent; | |
85 my $binding, $childContract, @bindingVars; | |
86 | |
87 if(my $info = $this->_namedResources->{$id}) { | |
88 @bindingVars = ($id); | |
89 $childContract = $info->{contract}; | |
90 $binding = $info->{bind}; | |
91 } else { | |
92 foreach my $info ( $this->_regexpResources ) { | |
93 my $rx = $info->{match}; | |
94 next unless $rx; | |
95 if( @bindingVars = ($id =~ m/$rx/) ) { | |
96 $childContract = $info->{contract}; | |
97 $binding = $info->{bind}; | |
98 } | |
99 } | |
100 } | |
101 | |
102 if ($childContract) { | |
103 my $childModel = $binding ? $binding->($parent,$model,@bindingVars) : undef; | |
104 | |
105 return $childContract->CreateResource( | |
106 %args, | |
107 model => $childModel | |
108 ); | |
109 } else { | |
110 die KeyNotFoundException->new($id); | |
111 } | |
112 } | 90 } |
113 | 91 |
114 1; | 92 1; |
115 | 93 |
116 __END__ | 94 __END__ |
129 ResourceContract => 'IMPL::Web::Application::ResourceContract', | 107 ResourceContract => 'IMPL::Web::Application::ResourceContract', |
130 OperationContract => 'IMPL::Web::Application::OperationContract' | 108 OperationContract => 'IMPL::Web::Application::OperationContract' |
131 }; | 109 }; |
132 | 110 |
133 my $contract = ResourceContract->new( | 111 my $contract = ResourceContract->new( |
134 operations => { | 112 verbs => { |
135 get => OperationContract->new( | 113 get => OperationContract->new( |
136 bind => sub { | 114 binding => sub { |
115 my ($resource,$action) = @_; | |
137 return "Hello!"; | 116 return "Hello!"; |
138 } | 117 } |
139 ) | 118 ), |
119 post => OperationContract->new( | |
120 parameters => [ | |
121 IMPL::Transform::DataToModel->new() # создаем преобразование для формы | |
122 ], | |
123 binding => sub { | |
124 my ($resource,$action,$data) = @_; | |
125 return $resource->model->AddItem($data); | |
126 }, | |
127 success => sub { | |
128 my ($resource,$action,$result) = @_; | |
129 return IMPL::Web::HttpResponse->Redirect( | |
130 location => $resource->location->Child($result->id) | |
131 ) | |
132 }, | |
133 error => sub { | |
134 my ($resource,$action,$error) = @_; | |
135 $action->errors->Append($error); | |
136 return $resource->model; | |
137 } | |
138 | |
139 ), | |
140 }, | 140 }, |
141 resources => [ | 141 resources => [ |
142 { | 142 { |
143 name => 'info', | 143 name => 'info', |
144 bind => sub { | 144 binding => sub { |
145 return $_[0]->model->info; | 145 return $_[0]->model->info; |
146 }, | 146 }, |
147 contract => ResourceContract->new( | 147 contract => ResourceContract->new( |
148 get => OperationContract->new( | 148 verbs => { |
149 bind => sub { | 149 get => OperationContract->new( |
150 my ($resource,$model) = @_; | 150 binding => sub { |
151 return $model; # or the same: $resource->model; | 151 my ($resource,$action) = @_; |
152 } | 152 return $resource->model; |
153 ) | 153 } |
154 ) | |
155 } | |
154 ) | 156 ) |
155 } | 157 } |
156 ] | 158 ] |
157 ) | 159 ) |
158 | 160 |
162 model => $obj, | 164 model => $obj, |
163 parent => $prentResource, | 165 parent => $prentResource, |
164 id => 'item-something' | 166 id => 'item-something' |
165 ); | 167 ); |
166 | 168 |
167 my $child = $contract->CreateChildResource( | 169 my $child = $contract->FetchChildResource('info'); |
168 parent => $resource, | |
169 id => 'info' | |
170 ); | |
171 | 170 |
172 =end code | 171 =end code |
173 | 172 |
174 =head1 DESCRIPTION | 173 =head1 DESCRIPTION |
175 | 174 |
175 Контракт описывает структуру Веб-ресурса и отображение операций над ним в методы | |
176 предметной области. Контракты используются инфраструктурой и пользователь | |
177 не использует их напрямую, до тех пор пока не требуется изменить стандартный | |
178 функционал. | |
179 | |
180 | |
181 Ресурс представляе собой набор контрактов операций, соответствующих методам | |
182 C<HTTP> которые доступны у данного ресурса. Кроме операций ресурс состоит из | |
183 дочерних ресурсов, которые могут соответствовать регулярным выражениям, либо | |
184 иметь фиксированные имена. | |
185 | |
186 Каждая операция над ресурсом C<IMPL::Web::Application::OperationContract> | |
187 описывается преобразованием параметров, привязкой к предметной области, | |
188 дополнительным обработчиком результата выполнения привязки, либо обработчиком | |
189 исключения, если привязку не удалось выполнить. | |
190 | |
191 Основное назначение контракта - создавать объекты ресурсов, над которыми | |
192 контроллер запросов C<HTTP> сможет выполнить операцию. Контракт может создавать | |
193 дочерние ресурсы, на основе указанного родительского ресурса и идетификатора | |
194 нового ресурса. При этом будет найден подходящий контракт для дочернего ресурса | |
195 и с его помощью создан дочерний ресурс. | |
196 | |
197 =head2 Динамический контракт | |
198 | |
199 Основная функция контракта - превращать данные модели предметной области в | |
200 данные ресурсной модели, тоесть в ресурсы, для чего каждый контракт обязан | |
201 реализовывать метод C<CreateResource(%args)>. | |
202 | |
203 Результатом выполнения этого метода должен быть Web-ресурс, см. | |
204 C<IMPL::Web::Application::Resource>. Другими словами не существует жесткого | |
205 требования к реализации самого контракта, как и того, что созданный ресурс | |
206 должен ссылаться именно на этот контракт (да и вообще ссылаться на контракт). | |
207 | |
208 Таким образом можно реализовать контракт, который выполняет роль посредника, | |
209 ниже приведен пример, который выбирает нужный контракт на основе типа модели | |
210 переданной для создания ресурса. | |
211 | |
212 =begin code | |
213 | |
214 package My::Web::Application::ContractMapper; | |
215 use strict; | |
216 use IMPL::lang qw(:constants); | |
217 use IMPL::declare { | |
218 require => { | |
219 ForbiddenException => 'IMPL::Web::Forbidden' | |
220 }, | |
221 base => [ | |
222 'IMPL::Object' => undef, | |
223 'IMPL::Object::Autofill' => '@_' | |
224 ], | |
225 props => [ | |
226 map => PROP_GET | PROP_OWNERSET | |
227 ] | |
228 } | |
229 | |
230 sub CreateResource { | |
231 my ($this,%args) = @_; | |
232 | |
233 my $type = ref $args{model} || '_default'; | |
234 | |
235 my $contract = $this->map->{$type}; | |
236 | |
237 die ForbiddenException->new() | |
238 unless $contract; | |
239 | |
240 return $contract->CreateResource(%args); | |
241 } | |
242 | |
243 =end code | |
244 | |
245 =head1 MEMBERS | |
246 | |
247 =head2 C<CTOR(%args)> | |
248 | |
249 =over | |
250 | |
251 =item * C<resourceFactory> | |
252 | |
253 Фабрика объектов C<IMPL::Object::Factory> которая будет использоваться при | |
254 создании новых ресурсов. По-умолчанию C<IMPL::Web::Application::Resource>. | |
255 | |
256 =item * C<operations> | |
257 | |
258 Хеш с доступными действиями над C<HTTP> ресурсом, ключом является имя ресурса, | |
259 значением C<IMPL::Web::Application::OperationContract>. | |
260 | |
261 =item * C<resources> | |
262 | |
263 Ссылка на массив хешей, каждый хеш описывает правила, как получить дочерний | |
264 ресурс и связать его с контрактом. Ниже преведено описание элементов хеша. | |
265 | |
266 =over | |
267 | |
268 =item * C<name> | |
269 | |
270 Имя дочернегно ресурса. | |
271 | |
272 =item * C<match> | |
273 | |
274 Регулярное выражение, которому должно удовлетворять имя дочернего ресурса. | |
275 | |
276 =item * C<bind> | |
277 | |
278 Делегат, получающий модель для дочернего ресурса. Первым параметром ему | |
279 передается родительский объект, далее передаются граппы из регулярного | |
280 выражения, если это ресурс с именем удовлетворяющим регулярному выражению из | |
281 элемента C<match>, либо имя ресурса, если это ресурс с именем, указанным в | |
282 элементе C<name>. | |
283 | |
284 =item * C<contract> | |
285 | |
286 Ссылка на C<IMPL::Web::Application::ResourceContract> для дочернего ресурса. | |
287 У данного контракта используется только метод C<CreateContract>. | |
288 | |
289 =back | |
290 | |
291 По крайней мере C<name> или C<match> должны присутсвовать. | |
292 | |
293 =back | |
294 | |
295 =head2 C<CreateResource(%args)> | |
296 | |
297 Создает ресурс, параметры C<%args> будут переданы напрямую констркутору | |
298 ресурса, для создания ресурса используется фабрика C<resourceFactory>. | |
299 При создании, конгструктору ресурса, будет передана ссылка на текущй контракт. | |
300 | |
301 По-сути никакого дополнительного функционала данный метод не несет. | |
302 | |
303 =head2 C<FindChildResourceInfo($childId)> | |
304 | |
305 Используется для поиска информации о дочернем ресурсе, возвращает список из двух | |
306 элементов. C<($info,$childIdParts)> | |
307 | |
308 =over | |
309 | |
310 =item * C<$info> | |
311 | |
312 Информация о контракте дочернего ресурса, как правило это ссылка на хеш, похожий | |
313 по формату на | |
314 | |
315 =back | |
316 | |
317 =head2 C<[get]verbs> | |
318 | |
319 Хеш с доступными действиями над C<HTTP> ресурсом, все имена операций приведены | |
320 к нижнему регистру. | |
321 | |
322 =begin code | |
323 | |
324 my $result = $contract->verbs->{get}->Invoke($resource,$action); | |
325 | |
326 =end code | |
327 | |
176 =cut | 328 =cut |