From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

#
# This file is part of CatalystX-ExtJS
#
# This software is Copyright (c) 2011 by Moritz Onken.
#
# This is free software, licensed under:
#
# The (three-clause) BSD License
#
BEGIN {
$CatalystX::Controller::ExtJS::Direct::API::VERSION = '2.0.0'; # TRIAL
}
# ABSTRACT: API and router controller for Ext.Direct
use Moose;
use List::Util qw(first);
__PACKAGE__->config(
action => {
end => { ActionClass => '+CatalystX::Action::ExtJS::Serialize' },
begin => { ActionClass => '+CatalystX::Action::ExtJS::Deserialize' },
index => { Path => undef },
router => { Path => 'router' },
src => { Local => undef },
},
default => 'application/json'
);
has 'api' => ( is => 'rw', lazy_build => 1 );
has 'routes' => ( is => 'rw', isa => 'HashRef', default => sub { {} } );
has 'namespace' => ( is => 'rw' );
sub index { }
sub src {
my ($self, $c) = @_;
$c->res->content_type('application/javascript');
$c->res->body( 'Ext.app.REMOTING_API = ' . $self->encoded_api($c) . ';' );
}
sub _build_api {
my ($self) = @_;
my $c = $self->_app;
my $data = {};
foreach my $name ( $c->controllers ) {
my $controller = $c->controller($name);
$name =~ s/^API:://;
$name =~ s/:://g;
my $meta = $controller->meta;
next
unless ( $controller->can('is_direct') || $meta->does_role('CatalystX::Controller::ExtJS::Direct') );
my @methods;
foreach my $method ( $controller->get_action_methods() ) {
next
unless ( my $action = $controller->action_for( $method->name ) );
next unless ( exists $action->attributes->{Direct} );
my @routes =
CatalystX::Controller::ExtJS::Direct::Route::Factory->build(
$c->dispatcher, $action );
foreach my $route (@routes) {
$self->routes->{$name}->{ $route->name } = $route;
push( @methods, $route->build_api );
}
}
$data->{$name} = [@methods];
}
return {
url => $c->dispatcher->uri_for_action( $self->action_for('router') )
->as_string,
type => 'remoting',
actions => $data
};
}
sub encoded_api {
my ( $self, $c ) = @_;
return JSON::Any->new->to_json( $self->set_namespace( $self->api, $c ? $c->req->params->{namespace} : () ) );
}
sub router {
my ( $self, $c ) = @_;
my $reqs = ref $c->req->data eq 'ARRAY' ? $c->req->data : [ $c->req->data ];
my $api = $self->api; # populates $self->routes
my $routes = $self->routes;
if ( keys %{ $c->req->body_params }
&& ( my $params = $c->req->body_params ) )
{
$reqs = [
{
map {
my $orig = $_;
$orig =~ s/^ext//;
( lc($orig) => delete $params->{$_} )
} qw(extType extAction extMethod extTID extUpload)
}
];
if ( $params->{extData} ) {
$reqs->[0]->{data} = JSON::Any->new->decode( delete $params->{extData} );
} else {
$reqs->[0]->{data} = [{%$params}];
}
}
my @requests;
foreach my $req (@$reqs) {
unless ( $req && $req->{action}
&& exists $routes->{ $req->{action} }
&& exists $routes->{ $req->{action} }->{ $req->{method} } )
{
$self->status_bad_request( $c, { message => sprintf('method %s in action %s does not exist', $req->{method} || '', $req->{action} || '') } );
return;
}
my $route = $routes->{ $req->{action} }->{ $req->{method} };
push(@requests, $route->prepare_request($req));
}
my @res;
REQUESTS:
foreach my $req (@requests) {
$req->{data} = [$req->{data}] if(ref $req->{data} ne "ARRAY");
$c->stash->{upload} = 1 if ( $req->{upload} );
my $route = $routes->{ $req->{action} }->{ $req->{method} };
my $params = @{$req->{data}} && ref $req->{data}->[-1] eq 'HASH' ? $req->{data}->[-1] : undef;
my $body;
{
local $c->{response} = $c->response_class->new({});
local $c->{stash} = {};
local $c->{request} = $c->req;
local $c->{error} = undef;
$c->req->parameters($params);
$c->req->body_parameters($params);
my %req = $route->request($req);
$c->req($c->request_class->new(%{$c->req}, %req));
eval {
$c->visit($route->build_url( $req->{data} ));
my $response = $c->res;
if ( $response->content_type eq 'application/json' ) {
(my $res_body = $response->body) =~ s/^\xEF\xBB\xBF//; # remove BOM
my $json = JSON::Any->new->decode( $res_body );
$body = $json;
} else {
$body = $response->body;
}
if(@{$c->error}) { 0 }
elsif($response->status >= 400) {
$c->error($body);
0;
} else { 1 }
} or do {
my $msg;
if(@{ $c->error } && List::MoreUtils::all { ref $_ } @{ $c->error }) {
$msg = @{$c->error} == 1 ? $c->error->[0] : $c->error;
} elsif(scalar @{ $c->error }) {
$msg = join "\n", @{ $c->error };
} else {
$msg = join("\n", "$@", $c->response->body || ());
}
push(@res, { type => 'exception', tid => $req->{tid}, message => $msg, status => $c->res->status });
$c->log->debug($msg) if($c->debug);
next REQUESTS;
};
}
my $res = { map { $_ => $req->{$_} } qw(action method tid type) };
push( @res, { %$res, result => $body } );
}
$c->stash->{rest} = @res != 1 ? \@res : $res[0];
}
sub set_namespace {
my ($self, $api, $namespace) = @_;
return $api unless($namespace && $namespace =~ /^\w+(\.\w+)?$/);
return {%$api, namespace => $namespace };
}
sub end {
my ( $self, $c ) = @_;
$c->stash->{rest} ||= $self->set_namespace( $self->api, $c->req->params->{namespace} );
}
1;
=pod
=head1 NAME
CatalystX::Controller::ExtJS::Direct::API - API and router controller for Ext.Direct
=head1 VERSION
version 2.0.0
=head1 SYNOPSIS
package MyApp::Controller::API;
use Moose;
extends 'CatalystX::Controller::ExtJS::Direct::API';
1;
<script type="text/javascript" src="/api/src?namespace=MyApp.Direct"></script>
<script>Ext.Direct.addProvider(Ext.app.REMOTING_API);</script>
=head1 ACTIONS
=head2 router
Every request to the API is going to hit this action, since the API's url will point to this action.
You can change the url to this action via the class configuration.
Example:
package MyApp::Controller::API;
__PACKAGE__->config( action => { router => { Path => 'callme' } } );
1;
The router is now available at C<< /api/callme >>.
=head2 src
Provides the API as JavaScript. Include this action in your web application as shown in the L</SYNOPSIS>.
To set the namespace for the API, pass a C<namespace> query parameter:
<script type="text/javascript" src="/api/src?namespace=MyApp.Direct"></script>
=head2 index
This action is called when you access the namespace of the API. It will load L</api> and return
the JSON encoded API to the client. Since this class utilizes L<Catalyst::Controller::REST> you
can specify a content type in the request header and get the API encoded accordingly.
=head1 METHODS
=head2 api
Returns the API as a HashRef.
Example:
{
url => '/api/router',
type => 'remote',
actions => {
Calc => {
methods => [
{ name => 'add', len => 2 },
{ name => 'subtract', len => 0 }
]
}
}
}
=head2 encoded_api
This method returns the JSON encoded API which is useful when you want to include the API in a JavaScript file.
Example:
Ext.app.REMOTING_API = [% c.controller('API').encoded_api %];
Ext.Direct.addProvider(Ext.app.REMOTING_API);
Calc.add(1, 3, function(provider, response) {
// process response
});
=head1 AUTHOR
Moritz Onken <onken@netcubed.de>
=head1 COPYRIGHT AND LICENSE
This software is Copyright (c) 2011 by Moritz Onken.
This is free software, licensed under:
The (three-clause) BSD License
=cut
__END__