Froody

 view release on metacpan or  search on metacpan

lib/Froody/API/Reflection.pm  view on Meta::CPAN

        <argument name="code" optional="0">The code of the error type whose information is being requested.</argument>
      </arguments>
      <response>
        $ERROR_INFO
      </response>
      <errors>
        <error code="froody.error.notfound.errortype" message="Error Type not Found"/>
      </errors>
    </method>
    <method name="froody.reflection.getSpecification">
      <description>Request the full public specification for a froody endpoint.</description>
      <response>
        <spec>
          <methods>
            $METHOD_INFO
            $METHOD_INFO
          </methods>
          <errortypes>
            $ERROR_INFO
            $ERROR_INFO
          </errortypes>

lib/Froody/Dispatch.pm  view on Meta::CPAN

  my $dispatcher = Froody::Dispatch->new();
  my $response = $dispatcher->call( "foo.bar.baz",
    fred => "wilma" 
  );

or, as a client:

  $client = Froody::Dispatch->new;

  # uses reflection to load methods from the server
  $client->add_endpoint( "uri" ); 

  # look mah, no arguments!
  $rsp = $invoker->invoke($client, 'service.wibble');

  # ok, take some arguments then.
  $rsp = $invoker->invoke($client, 'service.devide', divisor => 1, dividend => 2);

  # alternatively, args can be passed as a hashref:
  $args = { devisor => 1, devidend => 2 }; 
  $rsp = $invoker->invoke($client, 'service.devide', $args);

lib/Froody/Dispatch.pm  view on Meta::CPAN


use Froody::Response::Terse;
use Froody::Repository;
use Froody::Response::Error;
use Froody::Invoker::Remote;
use Froody::Error qw(err);
use Froody::Logger;

my $logger = get_logger("froody.dispatch");

__PACKAGE__->mk_accessors(qw{response_class error_class endpoints filters});

=head1 METHODS

=head2 Class Methods

=over 4

=item new

Create a new instance of the dispatcher

=cut

sub new {
  my $class = shift;
  my $self = $class->SUPER::new(@_);
  $self->endpoints({}) unless $self->endpoints;
  $self->error_class('Froody::Response::Error') unless $self->error_class;
  $self;
}

=item parse_cli (@args) 

Parses a list of files, urls, modules, method filter expressions, and paths.

What you're able to do:

lib/Froody/Dispatch.pm  view on Meta::CPAN

    $self = $self->new; # we should be an instance.
  }
  my $repository = Froody::Repository->new;
  $self->repository($repository);
  for (@{ $args->{includes} }) {
    unshift @INC, $_; #extend the search path
  }
  my @filters = @{ $args->{filters} || [] };
  $self->filters(@filters);
  for (@{ $args->{urls} || [] }) { 
    $self->add_endpoint( $_, @filters );
  }
#  warn Dumper($args->{modules}); use Data::Dumper;

  # implementation has-a api_specification
  # implementation has-a list of filters
  
  my %api;
  for my $module (@{ $args->{modules} || [] }) {
  
    # make sure implementation is loaded for isa-checks etc.

lib/Froody/Dispatch.pm  view on Meta::CPAN

}

=item add_implementation 

Adds an implementation's methods to this dispatcher.

=cut

sub add_implementation {
  my ($self, $module, @filters) = @_;
  return if $self->endpoints->{$module}{loaded};
  
  eval " use $module; 1" or die $@;
  my ($api, $invoker);
  my $repository = $self->repository;
  $invoker = $self->endpoints->{$module}{invoker};
  if ($module->isa('Froody::API')) {
    $api = $module;
    $invoker ||= (bless {}, "Froody::Invoker");  
    $self->repository->register_api( $api, $invoker, @filters );
  } elsif ($module->isa("Froody::Implementation")) {
    my @imp_filters;
    $module->register_in_repository($repository, @filters);
  }
  $self->endpoints->{$module} = { invoker => $invoker, loaded => 1 };
}

=item cli_config

Parses arguments with L<parse_cli|/Froody::Dispatch>, and then calls
config with the arguments. This is intended to be used for parsing command
line options, and directly creating configuration details

Returns a dispatch object, and the parsed options.

lib/Froody/Dispatch.pm  view on Meta::CPAN

sub default_repository
{
  require Carp;
  Carp::confess "Don't expect the default repo to introspect.";
}

=item call_via ($invoker, $method, [@ARGS, $args])

Calls C<$method> with C<$invoker>.  If $invoker or $method are not
instances of C<Froody::Invoker> and C<Froody::Method> respectively then
this method will attempt to discover them in the registered list of endpoints
and the method repository.

Returns a Froody::Response object.

=cut

sub call_via {
  my ($self, $invoker, $method )  = splice @_, 0, 3; #pull off first three args.
  my $args  = ref $_[0] eq 'HASH' ? $_[0] 
                                  : { @_ };

  Carp::confess "You must provide an invoker" unless $invoker;
  
  if (!UNIVERSAL::isa($invoker, 'Froody::Invoker')) {
    $self->endpoints({}) unless $self->endpoints();
    $invoker = $self->endpoints->{$invoker}{invoker}
  }

  $method = $self->get_method($method, $args)
    unless UNIVERSAL::isa($method, 'Froody::Method');
  
  my $meta = {
    dispatcher => $self,
    method => $method,
    params => $args,
    repository => $self->repository,
  };
  return $invoker->invoke($method, $args, $meta);
}

=item add_endpoint( "url" )

Registers all methods from a remote repository within this one.

TODO:  add regex filtering of methods.

=cut

sub add_endpoint {
  my ($self,$url, @method_filters) = @_;
  die "add_endpoint requires an url" unless $url;
  
  my $endpoints = $self->endpoints;
  if ($endpoints->{$url}{loaded}) {
    $logger->warn("Attempted to add endpoint($url) more than once.");
    return $self;
  }
  $self->endpoints->{ $url }{invoker} ||= Froody::Invoker::Remote->new()->url($url);

  $self->load_specification($url, @method_filters);
 
  return $self;
}

=item load_specification ($name, @method_filters) 

Load method and errortype specifications from a named endpoint

=cut

sub load_specification {
  my ($self, $name, @method_filters) = @_;
  
  my $endpoint = $self->endpoints->{$name};
  my $invoker = $endpoint->{invoker};
  
  my $repo = $self->repository;
  my $response = $self->call_via($invoker, 'froody.reflection.getSpecification' );
  if ($response->as_xml->status eq 'ok') {
    my @structures =  Froody::API::XML->load_spec($response->as_xml->xml);
    $repo->load($invoker, \@structures, @method_filters);
    $endpoint->{$endpoint}{loaded} = time;
  } 
}

=back

=head2 Instance Methods

=over

=item get_method( 'full_name', args )

lib/Froody/Dispatch.pm  view on Meta::CPAN

  my $repo = $self->repository;
  my $method = eval { $repo->get_method($name, $args) };
  return $method if $method;

  # We didn't find it?  Maybe it's been declared in our spec that may have
  # changed since we last loaded it.  Let's look for it in here!
  # TODO: Improve this so it DTRT more (though works for now)
  if (err('froody.invoke.nosuchmethod')) {
    local $@ = $@;
    my $get_info = $repo->get_method("froody.reflection.getMethodInfo", $args);
    for (keys %{$self->endpoints || {}}) {
      my $invoker = $self->endpoints->{$_}{invoker};

      my $info = $self->call_via($invoker, $get_info, { %$args, method_name => $name })->as_xml;
      if ($info->status eq 'ok') {
        $method = Froody::API::XML->load_method( $info->xml->findnodes("/rsp/method") );
        $method->invoker($invoker);
        $repo->register_method($method);
      }
      return $method if $method;
    }
  }

lib/Froody/Invoker/Remote.pm  view on Meta::CPAN

=head1 NAME

Froody::Invoker::Remote - invoker that calls methods remotely

=head1 SYNOPSIS

  use Froody::Invoker::Remote;

  my $remote = Froody::Invoker::Remote
                 ->new()
                 ->url("http://someserver.com/restendpoint/");
  
  my $method = Froody::Method->new()
                             ->full_name("fred.bar.baz")
                             ->invoker($remote);
  
  my $response = $method->call({ foo => "wibble" });

=head1 DESCRIPTION

An Invoker that calls a remote server to get the Froody::Response.

=head2 Accessors

=over

=item url

The URL of the REST endpoint.

=item source

Returns where this invoker originated

=back

=head1 BUGS

None known.

lib/Froody/QuickStart.pod  view on Meta::CPAN

Froody::QuickStart - the froody Quick Start tutorial

=head1 DESCRIPTION

At the core of Froody is the concept of Froody Methods, methods that you
can call remotely over the web.

For example, we have a hypothetical method called "examples.myapi.greet" that
can return us greetings.  We need to pass it one parameter, called "who", that
is the name of the person we're greeting. These two things are passed as CGI
parameters to a froody endpoint (a cgi script, a mod_perl server, whatever)
sitting at "http://myserver.com/fe"

  bash$ lwp-request 'http://myserver.com/fe?method=examples.myapi.greet&who=Mark'
  <?xml version="1.0" encoding="utf-8"?>
  <rsp stat="ok">
    <greeting>Hello Mark!</greeting>
  </rsp>

We get a chunk of XML back that is the result of calling the function.

=head1 Hello World Server Example

The easiest way to show you how Froody works is to expand the above example and
demonstrate how we might implement that on the server.

=head2 Writing The API

Firstly we have to define a class that builds an API - a way of describing what
methods can be called on our endpoint, along with what they expect to be passed
and what they're going to return.  This is normally done by subclassing
Froody::API::XML and implementing C<xml> to return a block of XML that describes 
the method(s):

  package MyAPI;
  use base qw(Froody::API::XML);
  1;
  
  sub xml {
  return <<"XML";

lib/Froody/QuickStart.pod  view on Meta::CPAN

  </rsp>

The C<-M> option tells C<froody> to load a module.  If a Froody::Implementation
has been loaded into memory then the client will automatically use that to make
local froody calls against.

=head2 Setting up the Webserver

Local implementations are all very well and good, but the point is to make
Froody methods avalible across the web.  To do this we need to set up our
endpoint so that it knows about the Froody Methods it's supposed to serve.

For performance reasons this should really be run from a mod_perl handler,
which can be configured with a section in your httpd.conf like so:

  PerlModule SayHello;
  PerlModule Froody::Server;
  
  <Location /fe>
    SetHandler perl
    PerlHandler Froody::Server->handler
  </Location>

This is all documented in L<Froody::Server>.  If you need a little more control
over your server setup (i.e. you need multiple Froody endpoints for any one
Apache) then you should read L<Froody::Server>.

For testing or an infrequently called script, we could run our endpoint as
a CGI script:

  #!/usr/bin/perl
  use warnings;
  use strict;
  use Froody::Server;
  Froody::Server->dispatch();

Either way, we can now make calls to our server:

lib/Froody/QuickStart.pod  view on Meta::CPAN

  <rsp stat="ok">
    <greeting>Hello Mark!</greeting>
  </rsp>

=head2 Using Froody as a Client

Froody is useful as a client as well as a server:

  # create a client and then tell it where to get its methods from
  my $client = Froody::Dispatch->new();
  $client->add_endpoint("http://myserver.com/fe");
  
  # call the method on the remote server
  my $response = $client->dispatch( 
    method => "example.myapi.greet", 
    params => { who => "Mark" },
  );

  # print out the same XML as above
  print $response->render;
  

lib/Froody/QuickStart.pod  view on Meta::CPAN


which is equivalient to:

  my $rsp = $client->dispatch( 
    method => "example.myapi.greet", 
    params => { who => "Mark" },
  );
  my $response = $rsp->as_terse->content;


The terse response makes use of Reflection. The 'add_endpoint()' method calls
the server to ask it what methods it provides and what the responses look like.
It then uses these specifications (like the one we declared at the top of this
document) to do the conversion back to the expected response.

=head1 A More Complicated Example

Of course, there would be little point returning XML if all we were going to do was
return simple strings.  We can return much more complicated data structures:

  package Corelist::API;

lib/Froody/Server.pm  view on Meta::CPAN

A Froody server. use as:

  #!/usr/bin/perl

  use warnings;
  use strict;
  
  use Froody::Server;
  Froody::Server->dispatch();

..in a CGI script that is a Froody endpoint.

This server accepts a CGI request as the Froody request, and will dispatch
the method, and return the XML of the response as the result of the HTTP
request. If the dispatcher throws an error, we catch it and wrap it in
a L<Froody::Response> object that represents the error.

You can pass a namespace to dispatch into to the L<dispatch()> call, to
override the default L<Froody::Dispatch> namespace. This is strongly
recommended, or your code will be fairly useless.

lib/Froody/Server/Test.pm  view on Meta::CPAN

    my $server = Froody::Server::Standalone->new();
    $server->config({ 
        modules => \@impl, 
    });
    $server->port($port);
    $server->run;
  }
  return $child;
}

=item endpoint

Returns the endpoint for the Test server

=cut

sub endpoint {
    my $class = shift;
    return "http://localhost:$port/";
}

# start the web server

=item client

Returns a C<Froody::Dispatch> based client that points to the test
server. As a side-effect, it will start up the test server if it has

lib/Froody/Server/Test.pm  view on Meta::CPAN

=cut

sub client {
  my ($class, @impl) = @_;
  
  $class->start(@impl);
  
  sleep 1;
  my $client = Froody::Dispatch->new();
  $client->repository( Froody::Repository->new() );
  $client->add_endpoint("http://localhost:$port");
  return $client;
}

# stop is documented
sub stop {
  if ($child) {
    # what the gosh-darn signals numbered on this box then?
    use Config;
    defined $Config{sig_name} || die "No sigs?";
    my ($i, %signo);

lib/Froody/SimpleClient.pm  view on Meta::CPAN

=head1 SYNOPSIS

=head1 DESCRIPTION

=cut

package Froody::SimpleClient;
use warnings;
use strict;
use base qw( Class::Accessor::Chained::Fast );
__PACKAGE__->mk_accessors(qw( endpoint ));

use LWP::UserAgent;
use JSON::Syck;
use HTTP::Request::Common;
use Encode qw( encode_utf8 );

=head1 ATTRIBUTES

=over

=item endpoint

=back

=head1 METHODS

=over

=item new( endpoint, [ timeout => 30 ] )

We take an endpoint here. Optionally, we also take a timeout in seconds.

=cut

sub new {
  my $class = shift;
  my $endpoint = shift;
  my $self = $class->SUPER::new({ endpoint => $endpoint });
  $self->{ua} = LWP::UserAgent->new( @_ );
  return $self;
}

=item call( method, arg => value, arg => value, etc... )

=cut

sub call {
  my $self = shift;

lib/Froody/SimpleClient.pm  view on Meta::CPAN


makes a raw call, returns the exact thing returned by the server.

=cut

sub call_raw {
  my $self = shift;
  my $method = shift;
  my %args = ref $_[0] eq 'HASH' ? %{ $_[0] } : @_;
  
  die "no endpoint set" unless $self->endpoint;
  die "no UA" unless $self->{ua};

  # fudge args so we can do the Right Thing with lists and uploads.
  my $hash_expansion;
  for (keys %args) {
    my $value = $args{$_};
    if (!defined $value) {
      delete $args{$_};
      next;
    }

lib/Froody/SimpleClient.pm  view on Meta::CPAN

      die "can't handle type of argument '$_' (is a ".ref($args{$_}).")";
    }
  }
  foreach (keys %args) {
    unless (ref $args{$_}) {
      $args{$_} = encode_utf8($args{$_});
    }
  }

  # make the request
  my $request = POST( $self->endpoint,
    Content_Type => 'form-data',
    Content => [ method => $method, %args ] );
  my $response = $self->{ua}->request( $request );

  Froody::Error->throw('froody.invoke.remote', 'Bad response from server: '. $response->status_line)
    unless $response->is_success;

  return $response->content;
}

t/froodydoc_pod_js.t  view on Meta::CPAN

=head3 Errors

=over

=item froody.error.notfound.errortype - Error Type not Found

=back

=head2 froody.reflection.getSpecification

Request the full public specification for a froody endpoint.

=head3 Arguments

None.

=head3 Response


  <response>
    <spec>

t/froodydoc_wiky_js.t  view on Meta::CPAN

  <errortype code="mycode">Internal structure of your error type goes here (including XML)</errortype>
</response>
%]

== Errors ==

* *froody.error.notfound.errortype* - Error Type not Found

= froody.reflection.getSpecification =

Request the full public specification for a froody endpoint.

== Arguments ==

None.

== Response ==

[%
<response>
  <spec>

t/high-level.t  view on Meta::CPAN

use lib 't/lib';
use DTest;
use LWP::Simple ();

use Froody::Server::Test;
use Froody::SimpleClient;

ok( Froody::Server::Test->start( "DTest::Test" ), 'got interface to client');
ok( my $real_client = Froody::Server::Test->client, 'got interface to client');

like( LWP::Simple::get(Froody::Server::Test->endpoint . '?foo=1'),
	qr/froody.invoke.nomethod/, 'get a froody error');

is $real_client->repository->get_methods, 14, "Right number of methods.";

eq_or_diff [ sort map { $_->full_name } $real_client->repository->get_errortypes],  
           [ '',  'perl.methodcall.param','test.error',],
           "Right number of error types.";
sleep(1);
ok( my $simple_client = Froody::SimpleClient->new( Froody::Server::Test->endpoint, 
                                                   timeout => 1 ), "got simpleclient" );

my $first = 1;

for my $client ( $real_client, $simple_client ) {
#for my $client ($simple_client ) {
  my $answer;

  lives_ok {
    $client->call('froody.reflection.getErrorTypeInfo', code=> 'test.error');



( run in 1.551 second using v1.01-cache-2.11-cpan-2b1a40005be )