view Lib/IMPL/Web/Application/ResourceContract.pm @ 324:b1e7b55b4a38

reverted previous changes
author cin
date Sat, 25 May 2013 01:57:49 +0400
parents 673581380e79
children fe725fad2d90
line wrap: on
line source

package IMPL::Web::Application::ResourceContract;
use strict;

use IMPL::lang;
use IMPL::Const qw(:prop);
use IMPL::declare {
	require => {
		Exception            => 'IMPL::Exception',
		ArgumentException    => '-IMPL::ArgumentException',
		KeyNotFoundException => '-IMPL::KeyNotFoundException',
		ResourceClass        => 'IMPL::Web::Application::Resource'
	  },
	  base  => [ 'IMPL::Object' => undef ],
	  props => [
		resourceFactory  => PROP_ALL,
		verbs            => PROP_ALL,
		_namedResources  => PROP_ALL,
		_regexpResources => PROP_ALL | PROP_LIST,
	  ]
};

sub CTOR {
	my $this = shift;
	my %args = @_;

	$this->resourceFactory( $args{resourceFactory} || ResourceClass );

	my $resources  = $args{resources}  || [];
	my $verbs = $args{verbs} || {};

	die ArgumentException->new(
		resources => 'resources parameter must be a reference to an array' )
	  unless ref $resources eq 'ARRAY';

	die ArgumentException->new(
		opearations => 'operations parameter must be a reference to a hash' )
	  unless ref $verbs eq 'HASH';

	$this->verbs(
		{ map { lc($_), $verbs->{$_} } keys %$verbs } );

	my %nameMap;

	foreach my $res (@$resources) {
		if (ref $res->{contract} eq 'HASH') {
		    $res->{contract} = __PACKAGE__->new(%{$res->{contract}});
		}
		
		next unless $res->{contract};
		
		if ( my $name = $res->{name} ) {
			$nameMap{$name} = $res;
		}
		if ( $res->{match} ) {
			$this->_regexpResources->Append($res);
		}
	}

	$this->_namedResources( \%nameMap );
}

sub AddChildResourceContract {
    my ($this,$res) = @_;
    
    die ArgumentException->new(res => "A valid child resource definition")
        unless ref $res eq 'HASH';
        
    $this->_namedResources->{$res->{name}} = $res if $res->{name};
    $this->_regexpResources->Append($res) if $res->{match};
    
    return; 
}

sub CreateResource {
	my $this = shift;
	my %args = @_;

	return $this->resourceFactory->new( %args, contract => $this );
}

sub FindChildResourceInfo {
	my ( $this, $name ) = @_;

	if ( my $info = $this->_namedResources->{$name} ) {
		return $info, [$name];
	}
	else {
		foreach my $info ( $this->_regexpResources ) {
			my $rx = $info->{match};
			if(my @childId = $name =~ m/$rx/) {
			    return $info, \@childId; 
			}
		}
	}

	return;
}

1;

__END__

=pod

=head1 NAME

C<IMPL::Web::Application::ResourceContract> - описание ресурса

=head1 SYNIOSIS

=begin code

use IMPL::require {
	ResourceContract => 'IMPL::Web::Application::ResourceContract',
	OperationContract => 'IMPL::Web::Application::OperationContract'
};

my $contract = ResourceContract->new(
    verbs => {
    	get => OperationContract->new(
            binding => sub {
            	my ($resource,$action) = @_;
                return "Hello!";
            }
        ),
        post => OperationContract->new(
            parameters => [
                IMPL::Transform::DataToModel->new() # создаем преобразование для формы
            ],
            binding => sub {
            	my ($resource,$action,$data) = @_;
            	return $resource->model->AddItem($data);
            },
            success => sub {
            	my ($resource,$action,$result) = @_;
            	return IMPL::Web::HttpResponse->Redirect(
            	   location => $resource->location->Child($result->id)
            	)
            },
            error => sub {
            	my ($resource,$action,$error) = @_;
            	$action->errors->Append($error);
            	return $resource->model;
            }
            
        ),
    },
    resources => [
        {
        	name => 'info',
        	binding => sub {
        		return $_[0]->model->info;
        	},
        	contract => ResourceContract->new(
        	   verbs => {
        	       # using method references is also possible
	        	   get => sub {
	        	   	   my ($resource,$action) = @_;
	        	   	   return $resource->model;
	        	   }
	        	   
        	   }
        	)
        }
    ]
)

my $obj = My::App::Data->fetch('something');

my $resource = $contract->CreateResource(
    model => $obj,
    parent => $prentResource,
    id => 'item-something'
);

my $child = $contract->FetchChildResource('info');

=end code 

=head1 DESCRIPTION

Контракт описывает структуру Веб-ресурса и отображение операций над ним в методы
предметной области. Контракты используются инфраструктурой и пользователь
не использует их напрямую, до тех пор пока не требуется изменить стандартный
функционал. 


Ресурс представляе собой набор контрактов операций, соответствующих методам
C<HTTP> которые доступны у данного ресурса. Кроме операций ресурс состоит из
дочерних ресурсов, которые могут соответствовать регулярным выражениям, либо
иметь фиксированные имена.

Каждая операция над ресурсом C<IMPL::Web::Application::OperationContract>
описывается преобразованием параметров, привязкой к предметной области,
дополнительным обработчиком результата выполнения привязки, либо обработчиком
исключения, если привязку не удалось выполнить.

Основное назначение контракта - создавать объекты ресурсов, над которыми
контроллер запросов C<HTTP> сможет выполнить операцию. Контракт может создавать
дочерние ресурсы, на основе указанного родительского ресурса и идетификатора
нового ресурса. При этом будет найден подходящий контракт для дочернего ресурса
и с его помощью создан дочерний ресурс.

=head2 Динамический контракт

Основная функция контракта - превращать данные модели предметной области в
данные ресурсной модели, тоесть в ресурсы, для чего каждый контракт обязан
реализовывать метод C<CreateResource(%args)>.

Результатом выполнения этого метода должен быть Web-ресурс, см.
C<IMPL::Web::Application::Resource>. Другими словами не существует жесткого
требования к реализации самого контракта, как и того, что созданный ресурс
должен ссылаться именно на этот контракт (да и вообще ссылаться на контракт).

Таким образом можно реализовать контракт, который выполняет роль посредника,
ниже приведен пример, который выбирает нужный контракт на основе типа модели
переданной для создания ресурса. 

=begin code

package My::Web::Application::ContractMapper;
use strict;
use IMPL::Const qw(:prop);
use IMPL::declare {
    require => {
        ForbiddenException => 'IMPL::Web::ForbiddenException'  
    },
    base => [
        'IMPL::Object' => undef,
        'IMPL::Object::Autofill' => '@_'
    ],
    props => [
        map => PROP_GET | PROP_OWNERSET
    ]
}

sub CreateResource {
    my ($this,%args) = @_;
    
    my $type = ref $args{model} || '_default';
    
    my $contract = $this->map->{$type};
    
    die ForbiddenException->new()
        unless $contract;
    
    return $contract->CreateResource(%args);
} 

=end code

=head1 MEMBERS

=head2 C<CTOR(%args)>

=over

=item * C<resourceFactory>

Фабрика объектов C<IMPL::Object::Factory> которая будет использоваться при
создании новых ресурсов. По-умолчанию C<IMPL::Web::Application::Resource>.

=item * C<operations>

Хеш с доступными действиями над C<HTTP> ресурсом, ключом является имя ресурса,
значением C<IMPL::Web::Application::OperationContract>.

=item * C<resources>

Ссылка на массив хешей, каждый хеш описывает правила, как получить дочерний
ресурс и связать его с контрактом. Ниже преведено описание элементов хеша.

=over

=item * C<name>

Имя дочернегно ресурса.

=item * C<match>

Регулярное выражение, которому должно удовлетворять имя дочернего ресурса. 

=item * C<bind>

Делегат, получающий модель для дочернего ресурса. Первым параметром ему
передается родительский объект, далее передаются граппы из регулярного
выражения, если это ресурс с именем удовлетворяющим регулярному выражению из
элемента C<match>, либо имя ресурса, если это ресурс с именем, указанным в
элементе C<name>.

=item * C<contract>

Ссылка на C<IMPL::Web::Application::ResourceContract> для дочернего ресурса.
У данного контракта используется только метод C<CreateContract>.

Для реализации отложенной загрузки контракта (чтобы снизить ресурсоемкость
запуска приложения) вместо ссылки на контракт может быть либо ссылка на
процедуру, либо объект имеющий метод C<Invoke>.

=back

По крайней мере C<name> или C<match> должны присутсвовать.

=back

=head2 C<CreateResource(%args)>

Создает ресурс, параметры C<%args> будут переданы напрямую констркутору
ресурса, для создания ресурса используется фабрика C<resourceFactory>.
При создании, конгструктору ресурса, будет передана ссылка на текущй контракт.

По-сути никакого дополнительного функционала данный метод не несет.

=head2 C<FindChildResourceInfo($childId)>

Используется для поиска информации о дочернем ресурсе, возвращает список из двух
элементов. C<($info,$childIdParts)>

=over

=item * C<$info>

Информация о контракте дочернего ресурса, как правило это ссылка на хеш, похожий
по формату на 

=back

=head2 C<[get]verbs>

Хеш с доступными действиями над C<HTTP> ресурсом, все имена операций приведены
к нижнему регистру.

=begin code

my $result = $contract->verbs->{get}->Invoke($resource,$action);

=end code

=cut