Mercurial > pub > Impl
comparison lib/IMPL/Web/Application/Resource.pm @ 407:c6e90e02dd17 ref20150831
renamed Lib->lib
| author | cin |
|---|---|
| date | Fri, 04 Sep 2015 19:40:23 +0300 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 406:f23fcb19d3c1 | 407:c6e90e02dd17 |
|---|---|
| 1 package IMPL::Web::Application::Resource; | |
| 2 use strict; | |
| 3 | |
| 4 use constant { | |
| 5 ResourceClass => __PACKAGE__ | |
| 6 }; | |
| 7 use Scalar::Util qw(blessed); | |
| 8 | |
| 9 use IMPL::lang qw(:hash :base); | |
| 10 use IMPL::Const qw(:prop); | |
| 11 use IMPL::declare { | |
| 12 require => { | |
| 13 Exception => 'IMPL::Exception', | |
| 14 OpException => '-IMPL::InvalidOperationException', | |
| 15 NotFoundException => 'IMPL::Web::NotFoundException', | |
| 16 ResourceInterface => '-IMPL::Web::Application', | |
| 17 HttpResponse => 'IMPL::Web::HttpResponse', | |
| 18 HttpResponseResource => 'IMPL::Web::Application::HttpResponseResource', | |
| 19 Loader => 'IMPL::Code::Loader' | |
| 20 }, | |
| 21 base => [ | |
| 22 'IMPL::Web::Application::ResourceBase' => '@_' | |
| 23 ], | |
| 24 props => [ | |
| 25 access => PROP_RW, | |
| 26 verbs => PROP_RW, | |
| 27 children => PROP_RW | |
| 28 ] | |
| 29 }; | |
| 30 | |
| 31 __PACKAGE__->static_accessor(verbNames => [qw(get post put delete options head)]); | |
| 32 __PACKAGE__->static_accessor(httpMethodPrefix => 'Http'); | |
| 33 | |
| 34 sub CTOR { | |
| 35 my ($this, %args) = @_; | |
| 36 | |
| 37 my %verbs; | |
| 38 my $httpPrefix = $this->httpMethodPrefix; | |
| 39 | |
| 40 foreach my $verb (@{$this->verbNames}) { | |
| 41 my $method = exists $args{$verb} ? $args{$verb} : $this->can($httpPrefix . ucfirst($verb)); | |
| 42 $verbs{$verb} = $method | |
| 43 if $method; | |
| 44 } | |
| 45 | |
| 46 hashApply(\%verbs,$args{verbs}) | |
| 47 if ref($args{verbs}) eq 'HASH' ; | |
| 48 | |
| 49 $this->children($args{children} || $this->GetChildResources()); | |
| 50 | |
| 51 $this->access($args{access}) | |
| 52 if $args{access}; | |
| 53 | |
| 54 $this->verbs(\%verbs); | |
| 55 } | |
| 56 | |
| 57 sub _isInvokable { | |
| 58 my ($this,$method) = @_; | |
| 59 | |
| 60 return | |
| 61 (blessed($method) and $method->can('Invoke')) || | |
| 62 ref($method) eq 'CODE' | |
| 63 } | |
| 64 | |
| 65 sub _invoke { | |
| 66 my ($this,$method,@args) = @_; | |
| 67 | |
| 68 if(blessed($method) and $method->can('Invoke')) { | |
| 69 return $method->Invoke($this,@args); | |
| 70 } elsif(ref($method) eq 'CODE' || (not(ref($method)) and $this->can($method))) { | |
| 71 return $this->$method(@args); | |
| 72 } else { | |
| 73 die OpException->new("Can't invoke the specified method: $method"); | |
| 74 } | |
| 75 } | |
| 76 | |
| 77 sub HttpGet { | |
| 78 shift->model; | |
| 79 } | |
| 80 | |
| 81 sub AccessCheck { | |
| 82 my ($this,$verb) = @_; | |
| 83 | |
| 84 $this->_invoke($this->access,$verb) | |
| 85 if $this->access; | |
| 86 } | |
| 87 | |
| 88 sub Fetch { | |
| 89 my ($this,$childId) = @_; | |
| 90 | |
| 91 my $children = $this->children | |
| 92 or die NotFoundException->new( $this->location->url, $childId ); | |
| 93 | |
| 94 if (ref($children) eq 'HASH') { | |
| 95 if(my $child = $children->{$childId}) { | |
| 96 return $this->_isInvokable($child) ? $this->_invoke($child, $childId) : $child; | |
| 97 } else { | |
| 98 die NotFoundException->new( $this->location->url, $childId ); | |
| 99 } | |
| 100 } elsif($this->_isInvokable($children)) { | |
| 101 return $this->_invoke($children,$childId); | |
| 102 } else { | |
| 103 die OpException->new("Invalid resource description", $childId, $children); | |
| 104 } | |
| 105 } | |
| 106 | |
| 107 sub FetchChildResource { | |
| 108 my ($this,$childId) = @_; | |
| 109 | |
| 110 my $info = $this->Fetch($childId); | |
| 111 | |
| 112 return $info | |
| 113 if (is($info,ResourceInterface)); | |
| 114 | |
| 115 $info = { | |
| 116 response => $info, | |
| 117 class => HttpResponseResource | |
| 118 } | |
| 119 if is($info,HttpResponse); | |
| 120 | |
| 121 return $this->CreateChildResource($info, $childId) | |
| 122 if ref($info) eq 'HASH'; | |
| 123 | |
| 124 die OpException->new("Invalid resource description", $childId, $info); | |
| 125 } | |
| 126 | |
| 127 sub CreateChildResource { | |
| 128 my ($this,$info, $childId) = @_; | |
| 129 | |
| 130 my $params = hashApply( | |
| 131 { | |
| 132 parent => $this, | |
| 133 id => $childId, | |
| 134 request => $this->request, | |
| 135 class => ResourceClass | |
| 136 }, | |
| 137 $info | |
| 138 ); | |
| 139 | |
| 140 $params->{model} = $this->_invoke($params->{model}) | |
| 141 if $this->_isInvokable($params->{model}); | |
| 142 | |
| 143 my $factory = Loader->default->Require($params->{class}); | |
| 144 | |
| 145 return $factory->new(%$params); | |
| 146 } | |
| 147 | |
| 148 sub GetChildResources { | |
| 149 return {}; | |
| 150 } | |
| 151 | |
| 152 1; | |
| 153 | |
| 154 __END__ | |
| 155 | |
| 156 =pod | |
| 157 | |
| 158 =head1 NAME | |
| 159 | |
| 160 C<IMPL::Web::Application::Resource> - Ресурс C<REST> веб приложения | |
| 161 | |
| 162 =head1 SYNOPSIS | |
| 163 | |
| 164 =begin code | |
| 165 | |
| 166 use IMPL::require { | |
| 167 Resource => 'IMPL::Web::Application::Resource', | |
| 168 Security => 'IMPL::Security', | |
| 169 NotFoundException => 'IMPL::Web::NotFoundException', | |
| 170 ForbiddenException => 'IMPL::Web::ForbiddenException' | |
| 171 }; | |
| 172 | |
| 173 my $model = Resource->new( | |
| 174 get => sub { }, | |
| 175 verbs => { | |
| 176 # non-standart verbs placed here | |
| 177 myverb => sub { } | |
| 178 }, | |
| 179 #child resources can be a hash | |
| 180 children => { | |
| 181 user => { | |
| 182 # a resource class may be specified optionally | |
| 183 # class => Resource, | |
| 184 model => sub { | |
| 185 return Security->principal | |
| 186 }, | |
| 187 # the default get implementation is implied | |
| 188 # get => sub { shift->model }, | |
| 189 access => sub { | |
| 190 my ($this,$verb) = @_; | |
| 191 die ForbiddenException->new() | |
| 192 if Security->principal->isNobody | |
| 193 } | |
| 194 }, | |
| 195 catalog => { | |
| 196 get => sub { | |
| 197 my $ctx = shift->application->ConnectDb()->AutoPtr(); | |
| 198 | |
| 199 return $ctx->products->find_rs({ in_stock => 1 }); | |
| 200 }, | |
| 201 # chid resource may be created dynamically | |
| 202 children => sub { | |
| 203 # binds model against the parent reource and id | |
| 204 my ($this,$id) = @_; | |
| 205 | |
| 206 ($id) = ($id =~ /^(\w+)$/) | |
| 207 or die NotFoundException->new($id); | |
| 208 | |
| 209 my $ctx = shift->application->ConnectDb()->AutoPtr(); | |
| 210 | |
| 211 my $item = $ctx->products->fetch($id); | |
| 212 | |
| 213 die NotFoundException->new() | |
| 214 unless $item; | |
| 215 | |
| 216 # return parameters for the new resource | |
| 217 return { | |
| 218 model => $item, | |
| 219 get => sub { shift->model } | |
| 220 }; | |
| 221 } | |
| 222 }, | |
| 223 # dynamically binds whole child resource. The result of binding is | |
| 224 # the new resource or a hash with arguments to create one | |
| 225 posts => sub { | |
| 226 my ($this,$id) = @_; | |
| 227 | |
| 228 # this approach can be used to create a dynamic resource relaying | |
| 229 # on the type of the model | |
| 230 | |
| 231 return Resource->new( | |
| 232 id => $id, | |
| 233 parent => $this, | |
| 234 get => sub { shift->model } | |
| 235 ); | |
| 236 | |
| 237 # ditto | |
| 238 # parent and id will be mixed in automagically | |
| 239 # return { get => sub { shift->model} } | |
| 240 }, | |
| 241 post_only => { | |
| 242 get => undef, # remove GET verb implicitly | |
| 243 post => sub { | |
| 244 my ($this) = @_; | |
| 245 } | |
| 246 } | |
| 247 } | |
| 248 ); | |
| 249 | |
| 250 =end code | |
| 251 | |
| 252 Альтернативный вариант для создания класса ресурса. | |
| 253 | |
| 254 =begin code | |
| 255 | |
| 256 package MyResource; | |
| 257 | |
| 258 use IMPL::declare { | |
| 259 require => { | |
| 260 ForbiddenException => 'IMPL::Web::ForbiddenException' | |
| 261 }, | |
| 262 base => [ | |
| 263 'IMPL::Web::Application::Resource' => '@_' | |
| 264 ] | |
| 265 }; | |
| 266 | |
| 267 sub ds { | |
| 268 my ($this) = @_; | |
| 269 | |
| 270 $this->context->{ds} ||= $this->application->ConnectDb(); | |
| 271 } | |
| 272 | |
| 273 sub InvokeHttpVerb { | |
| 274 my $this = shift; | |
| 275 | |
| 276 $this->ds->Begin(); | |
| 277 | |
| 278 my $result = $this->next::method(@_); | |
| 279 | |
| 280 # in case of error the data context will be disposed and the transaction | |
| 281 # will be reverted | |
| 282 $this->ds->Commit(); | |
| 283 | |
| 284 return $result; | |
| 285 } | |
| 286 | |
| 287 # this method is inherited by default | |
| 288 # sub HttpGet { | |
| 289 # shift->model | |
| 290 # | |
| 291 # } | |
| 292 | |
| 293 sub HttpPost { | |
| 294 my ($this) = @_; | |
| 295 | |
| 296 my %data = map { | |
| 297 $_, | |
| 298 $this->request->param($_) | |
| 299 } qw(name description value); | |
| 300 | |
| 301 die ForbiddenException->new("The item with the scpecified name can't be created'") | |
| 302 if(not $data{name} or $this->ds->items->find({ name => $data{name})) | |
| 303 | |
| 304 $this->ds->items->insert(\%data); | |
| 305 | |
| 306 return $this->NoContent(); | |
| 307 } | |
| 308 | |
| 309 sub Fetch { | |
| 310 my ($this,$childId) = @_; | |
| 311 | |
| 312 my $item = $this->ds->items->find({name => $childId}) | |
| 313 or die NotFoundException->new(); | |
| 314 | |
| 315 # return parameters for the child resource | |
| 316 return { model => $item, role => "item food" }; | |
| 317 } | |
| 318 | |
| 319 =end code | |
| 320 | |
| 321 =head1 MEMBERS | |
| 322 | |
| 323 =head2 C<[get,set]verbs> | |
| 324 | |
| 325 Хеш с C<HTTP> методами. При попытке вызова C<HTTP> метода, которого нет в этом | |
| 326 хеше приводит к исключению C<IMPL::Web::NotAllowedException>. | |
| 327 | |
| 328 =head2 C<[get,set]access> | |
| 329 | |
| 330 Метод для проверки прав доступа. Если не задан, то доспуп возможен для всех. | |
| 331 | |
| 332 =head2 C<[get,set]children> | |
| 333 | |
| 334 Дочерние ресурсы. Дочерние ресурсы могут быть описаны либо в виде хеша, либо | |
| 335 в виде метода. | |
| 336 | |
| 337 =head3 C<HASH> | |
| 338 | |
| 339 Данный хещ содержит в себе таблицу идентификаторов дочерних ресурсов и их | |
| 340 описаний. | |
| 341 | |
| 342 Описание каждого ресурса представляет собой либо функцию, либо параметры для | |
| 343 создания ресурса C<CraeteChildResource>. Если описание в виде функции, то она | |
| 344 должна возвращать либо объект типа ресурс либо параметры для его создания. | |
| 345 | |
| 346 =head3 C<CODE> | |
| 347 | |
| 348 Если дочерние ресурсы описаны в виде функции (возможно использовать имя метода | |
| 349 класса текущего ресурса), то для получения дочернего ресурса будет вызвана | |
| 350 функция с параметрами C<($this,$childId)>, где C<$this> - текущий ресурс, | |
| 351 C<$childId> - идентификатор дочернего ресурса, который нужно вернуть. | |
| 352 | |
| 353 Данная функция должна возвратить либо объект типа ресурс, либо ссылку на хеш с | |
| 354 параметрами для создания оного при помощи метода | |
| 355 C<CreateChildResource($params,$childId)>. | |
| 356 | |
| 357 =head2 C<[virtual]Fetch($childId)> | |
| 358 | |
| 359 Метод для получения дочернего ресурса. | |
| 360 | |
| 361 Возвращает параметры для создания дочернего ресурса, либо уже созданный ресурс. | |
| 362 Создание дочернего ресурса происходит при помощи метода C<CreateChildResource()> | |
| 363 который добавляет недостающие параметры к возвращенным в данным методом и | |
| 364 создает новый ресурс | |
| 365 | |
| 366 =head2 C<CreateChildResource($params,$id)> | |
| 367 | |
| 368 Создает новый дочерний ресурс с указанным идентификатором и параметрами. | |
| 369 Автоматически заполняет параметры | |
| 370 | |
| 371 =over | |
| 372 | |
| 373 =item * C<parent> | |
| 374 | |
| 375 =item * C<id> | |
| 376 | |
| 377 =item * C<request> | |
| 378 | |
| 379 =back | |
| 380 | |
| 381 Тип создаваемого ресурса C<IMPL::Web::Application::Resource>, либо указывается | |
| 382 в параметре C<class>. | |
| 383 | |
| 384 =head2 C<[virtual]HttpGet()> | |
| 385 | |
| 386 Реализует C<HTTP> метод C<GET>. По-умолчанию возвращает модель. | |
| 387 | |
| 388 Данный метод нужен для того, чтобы ресурс по-умолчанию поддерживал метод C<GET>, | |
| 389 что является самым частым случаем, если нужно изменить данное поведение, нужно: | |
| 390 | |
| 391 =over | |
| 392 | |
| 393 =item * Передать в параметр конструктора C<get> значение undef | |
| 394 | |
| 395 =item * Переопределить метод C<HttpGet> | |
| 396 | |
| 397 =item * При проверке прав доступа выдать исключение | |
| 398 | |
| 399 =back | |
| 400 | |
| 401 =cut | |
| 402 |
