Froody

 view release on metacpan or  search on metacpan

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

package Froody::API::XML;
use strict;
use warnings;
use XML::LibXML;
use Froody::Method;
use Froody::ErrorType;
use Froody::Response::String;
use Froody::Logger;
my $logger = get_logger("froody.api.xml");

use base qw(Froody::API);
use Scalar::Util qw(weaken);

=head1 NAME

Froody::API::XML - Define a Froody API with xml

=head1 SYNOPSIS

  package MyAPI;
  use base qw{Froody::API::XML};
  
  sub xml { 
    return q{
      <spec>
        <methods>
          <method name="foo">....</method>
        </methods>
      </spec>
    };
   }
   
   1;

=head1 DESCRIPTION

This class is a helper base class for Froody::API.  It can parse a standard
format of XML and turn it into a bunch of Froody::Method objects.

=head1 METHODS

=over

=item xml

Subclasses must override this method to provide an XML specification.

=cut

sub xml {
  Froody::Error->throw("perl.use", "Please override the abstract method Froody::API::XML::xml()");
}

=item load( xml )

Calls C<load_spec()> with the return value of the C<xml> method.

=cut

sub load {
  my $class = shift;
  return $class->load_spec($class->xml);
}

=item load_spec($xml_string)

Turns a method spec xml string into an array of
C<Froody::Method> objects.

=cut

sub load_spec {

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

  my $parser = $class->parser;
  my $doc = UNIVERSAL::isa($xml, 'XML::LibXML::Node') ? $xml 
          : eval { $parser->parse_string($xml) };
  Froody::Error->throw("froody.xml.invalid", "Invalid xml passed: $@")
    if $@;
  $doc->indexElements;
  
  my $method_node_path = '/spec/methods/method';
  my $error_node_path = '/spec/errortypes/errortype';
  if  ($doc->documentElement->nodeName eq 'rsp') {
    $method_node_path = '/rsp'.$method_node_path;
    $error_node_path = '/rsp'.$error_node_path;
  }

  my @methods = map { $class->load_method($_) }
    $doc->findnodes($method_node_path)
      or Froody::Error->throw('froody.xml.nomethods', "no methods found in spec!");
    
  my @errortypes = map { $class->load_errortype($_) }
    $doc->findnodes($error_node_path);

  return (@methods, @errortypes);
}

=item load_method($element)

Passed an XML::LibXML::Element that represents a <method>...</method>,
this returns an instance of Froody::Method that represents that method.

=cut

sub load_method {
  my ($class, $method_element) = @_;
  unless (UNIVERSAL::isa($method_element, 'XML::LibXML::Element')) {
    Froody::Error->throw("perl.methodcall.param",
                          "we were expected to be passed a XML::LibXML::Element!");
  }
  
  # work out the name of the element
  my $full_name = $method_element->findvalue('./@name')
    or Froody::Error->throw("froody.xml",
       "Can't find the attribute 'name' for the method definition within "
       .$method_element->toString);


  # create a new method
  my $method = Froody::Method->new()
                  ->full_name($full_name)
                  ->arguments($class->_arguments($method_element))
                  ->errors($class->_errors($method_element))
                  ->needslogin($class->_needslogin($method_element));
  my ($response_element) = $class->_extract_response($method_element, $full_name);
  if ($response_element) {
    my ($structure, $example_data) = $class->_extract_structure($response_element);
    $method->structure($structure);
    my $example = Froody::Response::String->new;
    $example->set_string("<rsp status='ok'>".$response_element->toString(1)."</rsp>");
    $example->structure($method);
    
    $method->example_response($example);
    weaken($example->{structure});
  } else {
    $method->structure({})
  }

  my $desc = $method_element->findvalue("./description");
  $desc =~ s/^\s+//;
  $desc =~ s/\s+$//;
  $method->description($desc);

  return $method;
}

# okay, we're parsing this
#  <arguments>
#    <argument name="sex">male or female</argument>
#    <argument name="hair" optional="1">optionally list hair color</argument>
#    <argument name="hair" optional="1">optionally list hair color</argument>
#  </arguments>

sub _arguments {
  my ($class, $method_element) = @_;

  # get all of the argument elements
  my @argument_elements = $method_element->findnodes('./arguments/argument');

  # convert them into a big old hash
  my %arguments;
  foreach my $argument_element (@argument_elements)
  {
    # pull our the attributes
    
    my $name     = $argument_element->findvalue('./@name');
    my $optional = $argument_element->findvalue('./@optional') || 0;
    my $type     = $argument_element->findvalue('./@type') || 'text';

    # Ugh.  Track this down.
    $type = 'text' if $type eq 'scalar';
    my @types = split /,/, $type;

    # extract the contents of <argument>...</argument> as the description
    my $description = $argument_element->findvalue('./text()');

    $arguments{$name}{multiple} = 1 unless $type eq 'text';
    $arguments{$name}{optional} = $optional;
    $arguments{$name}{doc}      = $description;
    $arguments{$name}{type} = \@types;

    # XXX: compose the list in Froody::Argument
    require Froody::Argument;
    my @TYPES = keys %{ Froody::Argument->_types() };
    push @TYPES, 'remaining'; # a special case.
    for my $_type (@types) {
      Froody::Error->throw("froody.api.unsupportedtype", "The type '$_type' is unsupported")
        unless grep { $_type eq $_ } @TYPES;
      if ($_type eq 'remaining') {
        $arguments{$name}{optional} = 1;
      }
    }

  }

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

# returns true if the method element passed needs a login, i.e. has
# an attribute <method needslogin="1">.  Returns false in all other cases
sub _needslogin {
  my ($class, $method_element) = @_;
  return $method_element->findvalue('./@needslogin') || 0;
}

=item load_errortype

Passed an XML::LibXML::Element that represents an <errortype>...</errortype>,
this returns an instance of Froody::ErrorType that represents that error type.

=cut

sub load_errortype  {
  my ($class, $et_element) = @_;
  
  unless (UNIVERSAL::isa($et_element, 'XML::LibXML::Element')) {
    Froody::Error->throw("perl.methodcall.param",
                          "we were expected to be passed a XML::LibXML::Element!");
  }
  
  # work out the name of the element
  my $code = $et_element->findvalue('./@code') || '';
  Carp::cluck "no code in ".$et_element->toString(1) unless defined $code;

  my $et = Froody::ErrorType->new;
  $et->name($code);


  unless (grep { !UNIVERSAL::isa($_, "XML::LibXML::Text") } $et_element->childNodes) {
    my $et_str = $et_element->textContent;
    local $@;
    eval { 
      my $new_et = $class->parser()->parse_string(qq{<errortype code="$code">$et_str</errortype>}); 
      $et_element = $new_et->documentElement();
    };
    if ($@) {
      $logger->warn($@);
    }
  }

  my ($spec, $example_data) = $class->_extract_structure($et_element);
  foreach (keys %$spec) {
    my $val = delete $spec->{$_};
    s{^errortype}{err};  # 'errortype's are really 'err's
    $spec->{$_} = $val;
  }
  $spec->{''}{elts} = [ 'err' ];
  
  # enforce msg (code is already in here!)
  push @{ $spec->{err}{attr} }, "msg";
  
  $et->structure($spec);

  my $example = Froody::Response::String->new;
  $et_element->setNodeName("err");
  my $text = "<rsp status='fail'>".$et_element->toString(1)."</rsp>";
  $example->set_bytes($text);
  $example->structure($et);
  weaken($example->{structure});
  
  $et->example_response($example);

  return $et;
}

=item parser

This method returns the parser we're using.  It's an instance of XML::LibXML.

=cut

{
  my $parser = XML::LibXML->new;
  $parser->expand_entities(1);
  $parser->keep_blanks(0);
  sub parser { $parser }
}

=back

=head1 SPEC OVERVIEW

The specs handed to C<register_spec()> should be on this form:

  <spec>
    <methods>
      <method ...> </method>
      ...
    </methods>
    <errortypes>
       <errortype code="error.subtype">...</errortype>
       ...
    </errortypes>
  </spec>


=head2 <method>

Each method take this form:

  <method name="foo.bar.quux" needslogin="1">
    <description>Very short description of method's basic behaviour</description>
    <keywords>...</keywords>
    <arguments>...</arguments>
    <response>...</response>
    <errors>...</errors>
  </method>

=over

=item <keywords>

A space-separated list of keywords of the concepts touched upon
by this method. As an example, clockthat.events.addTags would
have "events tags" as its keywords. This way we can easily list
all methods that deals with tags, no matter where in the
namespace they live.

=item <arguments>



( run in 2.253 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )