view Lib/IMPL/Web/Application/RestResource.pm @ 213:d6e2ea24af08

sync
author sergey
date Fri, 03 Aug 2012 01:15:15 +0400
parents 292226770180
children e9fd7ff3f54c
line wrap: on
line source

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

use IMPL::lang qw(:declare :constants is :hash);
use IMPL::Exception();

use IMPL::declare {
	require => {
		ForbiddenException => 'IMPL::Web::ForbiddenException',
		NotFoundException => 'IMPL::Web::NotFoundException',
		InvalidOpException => '-IMPL::InvalidOperationException',
		ArgumentException => '-IMPL::InvalidArgumentException',
		TTransform => '-IMPL::Transform',
		TResolve => '-IMPL::Config::Resolve',
		CustomResource => 'IMPL::Web::Application::RestCustomResource'
	},
	base => {
		'IMPL::Web::Application::RestCustomResource' => '@_'
	}
};

BEGIN {
	public property target => PROP_GET | PROP_OWNERSET;
	public property index => PROP_GET | PROP_OWNERSET;
	public property fetch => PROP_GET | PROP_OWNERSET;
	
	public property methods => PROP_GET | PROP_OWNERSET;
	
	public property childRegex => PROP_GET | PROP_OWNERSET;
	public property enableForms => PROP_GET | PROP_OWNERSET;
	
}

sub CTOR {
	my ($this,%args) = @_;
	
	die ArgumentException->new("target") unless $this->target;
	
	$this->final($this->childRegex ? 0 : 1);
	$this->methods({}) unless $this->methods;
	
	$this->index($this->get) unless defined $this->index;
	
	if ($this->enableForms) {
        $this->methods->{create} = {
        	get => $this->get,
        	post => $this->post,
        	final => 1 # this resource doesn't have any children
        } if $this->post;
        
        $this->methods->{edit} = {
        	get => $this->get,
        	post => $this->put,
        	final => 1 # this resource doesn't have any children
        } if $this->put;
        
        $this->methods->{delete} {
        	get => $this->get,
        	post => $this->delete,
        	final => 1 # this resource doesn't have any children
        } if $this->delete;
	}
}

# создает дочерний ресурс из описания, однако все методы созданного
# ресурса переадресуются к его родителю, это нужно, чтобы публиковать
# методы и свойства объекта
sub _CreateSubResource {
	my ($this,$resource,$id) = @_;
	
	my %methods = map {
        my $method = $resource->{$_};
        $_ => sub {
            my ($this,$action) = @_;
            return $this->parent->InvokeMember($method,$action);
        };           
    } grep $resource->{$_}, qw(get post put delete);
        
    return CustomResource->new(
        %methods,
        final => $resource->{final},
        parent => $this,
        id => $id,
        contract => $this->contract
    );
}

sub FetchChildResource {
	my ($this,$id,$action) = @_;
	
	my $rx = $this->childRegex;
	
	my $res;
	
	if (length $id == 0) {
		
		my $method = $this->index;
		die ForbiddenException->new() unless $method;
		
		$res = $this->InvokeMember($method,$action);
		
	} elsif ($this->methods and my $resource = $this->methods->{$id}) {
		
		return $this->_CreateSubResource($resource,$id);
		
	} elsif ($rx and $id =~ m/^$rx$/ and my $method = $this->fetch) {
		
		$method = {
			method => $method,
			parameters => 'id'
		} unless ref $method;
		
		$res = $this->InvokeMember($method,$action, { id => $id } );
		
	}
	
    die NotFoundException->new() unless defined $res;
        
    return $this->contract->Transform($res, {parent => $this, id => $id} );
}

sub InvokeMember {
    my ($this,$method,$action,$predefined) = @_;
    
    die ArgumentException->new("method","No method information provided") unless $method;
    
    #normalize method info
    if (not ref $method) {
        $method = {
            method => $method
        };
    }
    
    if (ref $method eq 'HASH') {
        my $member = $method->{method} or die InvalidOpException->new("A member name isn't specified");
        
        $member = $member->Invoke($this) if eval { $member->isa(TResolve) };
        
        my @args;
    
        if (my $params = $method->{parameters}) {
            if (ref $params eq 'HASH') {
                @args = map {
                    $_,
                    $this->MakeParameter($params->{$_},$action,$predefined)
                } keys %$params;                
            } elsif (ref $params eq 'ARRAY') {
                @args = map $this->MakeParameter($_,$action,$predefined), @$params;
            } else {
                @args = ($this->MakeParameter($params,$action,$predefined)); 
            }
        }
        return $this->target->$member(@args);
    } elsif (ref $method eq TResolve) {
        return $method->Invoke($this);
    } elsif (ref $method eq 'CODE') {
        return $method->($this,$action);
    } else {
        die InvalidOpException->new("Unsupported type of the method information", ref $method);
    }
}

sub MakeParameter {
    my ($this,$param,$action,$predefined) = @_;
    
    my $params = hashApply(
	    {
	    	id => $this->id,
	    	action => $action,
	    	query => $action->query
	    },
	    $predefined || {}
	);
    
    
    
    if ($param) {
        if (is $param, TTransform ) {
            return $param->Transform($action->query);
        } elsif ($param and not ref $param) {
            return $params->{$param} || $action->query->param($param);
        } else {
            die InvalidOpException->new("Unsupported parameter mapping", $param);
        }
    } else {
        return undef;
    }
}

1;

__END__

=pod

=head1 NAME

C<IMPL::Web::Application::RestResource> - ресурс Rest вебсервиса.

=head1 SYNOPSIS

=begin text

[REQUEST]
GET /artists

[RESPONSE]
<artists>
    <artist id="1">
        <name>The Beatles <name/>
    </atrist>
    <artist id="2">
        <name>Bonobo</name>
    </artist>
</artists>

[REQUEST]
GET /artists/1/cds?title='Live at BBC'

[RESPONSE]
<cds>
    <cd id="14">
        <title>Live at BBC 1</title>
    </cd>
    <cd id="15">
        <title>Live at BBC 2</title>
    </cd>
</cds>

[REQUEST]
GET /cds/15

[RESPONSE]
<cd id="15">
    <title>Live at BBC 2</title>
</cd>

=end text

=begin code

use IMPL::require {
	TRes => 'IMPL::Web:Application::RestResource',
	DataContext => 'My::App::DataContext'
};

my $cds = TRes->new(
    DataContext->Default,
    {
    	methods => {
    		history => {
    			get => {
	    			method => 'GetHistory',
	    			parameters => [qw(from to)]
    			}, 
    		},
    		rating => {
    			get => {
    				method => 'GetRating'
    			}
    			post => {
    				method => 'Vote',
    				parameters => [qw(id rating comment)]
    			}
    		}
    	}
    	index => {
    		method => 'search',
    		paremeters => [qw(filter page limit)]
    	},
    	fetch => 'GetItemById'
    }   
);

=end code

=head1 DESCRIPTION

Каждый ресурс представляет собой коллекцию и реализует методы C<HTTP> C<GET,POST,PUT,DELETE>.

Вызов каждого из этих методов позволяет выполнить одну из операций над ресурсом, однако
операций может быть больше, для этого создаются дочерние ресурсы (каждый из которых также
может иметь четыре метода C<GET,POST,PUT,DELETE>), однако обращения к методам у дочерних
ресурсов отображаются в вызовы методов у родительского ресурса.

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

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


=head2 HTTP METHODS

=head3 C<GET>

Возвращает данные из текущего ресурса. Обращение к данному методу не должно вносить
изменений в ресурсы. 

=head3 C<PUT>

Обновляет ресурс. Повторное обращение к данному методу должно приводить к одному и
томуже результату.

=head3 C<DELETE>

Удаляет ресурс.

=head3 C<POST>

Данный метод может вести себя как угодно, однако обычно он используется для добавления
нового дочернего ресурса в коллекцию,также может использоваться для вызова метода, в случае
если происходит публикация методов в качестве дочерних ресурсов.

=head1 BROWSER COMPATIBILITY

Однако существует проблема с браузерами, поскольку тег C<< <form> >> реализет только методы
C<GET,POST>. Для решения данной проблемы используется режим совместимости C<enableForms>. В
случае когда данный режим активен, автоматически публикуются дочерние ресурсы C<create,edit,delete>.

Данные ресуры пбликуются как методы, что означает то, что обращения к ним будут превращены в
выполнения соответсвующих методов на родительском объекте.

=head2 C<create>

По сути данные ресурс не является необходимостью, однако создается для целостности модели.

=head3 C<GET>

Передает управление методу C<get>

=head3 C<POST>

Передает управление методу C<post>

=head2 C<edit>

=head3 C<GET>

Передает управление методу C<get>

=head3 C<POST>

Передает управление методу C<put>, как если бы он был выполнен непосредственно у
родительского ресурса.

=head2 C<delete>

=head3 C<GET>

Передает управление методу C<get>

=head3 C<POST>

Передает управление методу C<delete>, , как если бы он был выполнен непосредственно у
родительского ресурса.

=head1 METHOD DEFINITIONS

Все методы ресурсов данного типа задаются описаниями, хранящимися в соответствующих
свойствах. Когда наступает необходимость вызова соответствующего метода, его описание
бедется из свойства и передается методу C<InvokeMember>, который и производит вызов.

=head2 C<HASH>

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

=over

=item C<method>

Имя метода который будет вызван.

=item C<paremeters>

Описание параметров метода, может быть либо массивом, либо хешем, либо простым
значением.

=over

=item C<ARRAY>

Метод получает список параметров, каждый элемент данного массива будет превращен
в параметр при помощи метода C<MakeParameter>

=item C<HASH>

Метод получает список параметров, который состоит пар ключ-значение, каждое значение 
данного хеша будет превращено в зачение параметра метода при помощи метода C<MakeParameter>.
Ключи хеша изменениям не подвергаются.

=item Простое значение

Метод получает одно значение, которое будет получено из текущего при помощи C<MakeParameter>.

=back

=back

=head2 C<CODE>

Если в описании метода находится ссылка на функцию, то эта функция будет вызвана с параметрами.
Данный вариант полезен когда ресурсы создаются програмно обычного механизма описаний не достаточно
для реализации требуемого функционала.

=over

=item C<$resource>

Текущий ресурс у которого производится вызов метода.

=item C<$action>

Текущий запрос C<IMPL::Web::Application::Action>.

=back

=head2 Простое значение

Интерпретируется как имя метода у объекта данных текущего ресурса.

=head1 MEMBERS

=head2 C<[get]id>

Идентификатор текущего ресурса.

=head2 C<[get]target>

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

=head2 C<[get]parent>

Родительский ресурс, в котором находится текущий ресурс. Может быть C<undef>,
если текущий ресурс является корнем.

=head2 C<[get]methods>

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

=head2 C<[get]childRegex>

Содержит регулярное выражение для идентификаторов дочерних объектов. Если оно
не задано, то данный ресурс не является коллекцией.

=head2 C<[get]fetch>

Содержит описание метода для получения дочернего объекта. Если данный метод
отсутствует, то дочерние ресурсы не получится адресовать относительно данного.
По умолчанию получает идентификатор дочернего ресурса первым параметром.  

=head2 C<[get]index>

Описание метода для получения списка дочерних объектов. По умолчанию не
получает параметров.

=head2 C<[get]post>

Описание метода для добавление дочернего ресурса. По умолчанию получает
объект C<CGI> описывабщий текущий запрос первым параметром.

=head2 C<[get]put>

Описание метода для обновления дочернего ресурса. По умолчанию получает
объект C<CGI> текущего запроса.

=head2 C<[get]delete>

Описание метода для удаления дочернего ресурса. По умолчанию не получает
параметров.

=head2 C<GetImpl($action)>

=over

=item C<$action>

Текущий запрос C<IMPL::Web::Application::Action>.

=back

Переадресует запрос нужному методу внутреннего объекта C<target> при
помощи C<InvokeMember>.

=head2 C<PutImpl($action)>

=over

=item C<$action>

Текущий запрос C<IMPL::Web::Application::Action>.

=back

Переадресует запрос нужному методу внутреннего объекта C<target> при
помощи C<InvokeMember>.

=head2 C<PostImpl($action)>

=over

=item C<$action>

Текущий запрос C<IMPL::Web::Application::Action>.

=back

Переадресует запрос нужному методу внутреннего объекта C<target> при
помощи C<InvokeMember>.

=head2 C<DeleteImpl($action)>

=over

=item C<$action>

Текущий запрос C<IMPL::Web::Application::Action>.

=back

Переадресует запрос нужному методу внутреннего объекта C<target> при
помощи C<InvokeMember>.

=head2 C<InvokeMember($memberInfo,$action,$params)>

=over

=item C<$memberInfo>

Описание члена внутреннего объекта C<target>, который нужно вызвать.

=item C<$action>

Текущий запрос C<IMPL::Web::Application::Action>.

=item C<$params>

Ссылка на хеш с предопределенными параметрами.

=back

Вызывает метод внутреннего объекта C<target>, предварительно подготовив
параметры на основе описания C<$memberInfo> и при помощи С<MakeParameter()>.

=head2 C<MakeParameter($paramDef,$action)>

=over

=item C<$paramDef>

Описание параметра, может быть C<IMPL::Transform> или простая строка.

Если описание параметра - простая строка, то ее имя либо

=over

=item C<id>

Идентификатор ресурса

=item C<query>

Объект C<CGI> текущего запроса

=item C<action>

Текущий запрос C<IMPL::Web::Application::Action>

=item C<любое другое значение>

Интерпретируется как параметр текущего запроса.

=back

Если описание параметра - объект C<IMPL::Transform>, то будет выполнено это преобразование над C<CGI>
объектом текущего запроса C<< $paramDef->Transform($action->query) >>.

=item C<$action>

Текущий запрос

=back

=cut