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 |
