Froody
view release on metacpan or search on metacpan
lib/Froody/Walker.pm view on Meta::CPAN
=head1 NAME
Froody::Walker;
=head1 SYNOPSIS
my $spec = $froody_method->structure;
my $method_name = $froody_method->name;
# create a new walker that knows about the spec
my $walker = Froody::Walker->new($spec, "method name");
my $terse = Froody::Walker::Terse->new;
my $xml = Froody::Walker::XML->new;
$walker->from($terse)->to($xml);
# walk $source turning it into xml
my $xml = $walker->walk($data);
=head1 DESCRIPTION
Walker classes are constucted with a description of the structure it
will generate. That structure is a very specific grammar listing
paths into the object that can be built.
Setting L<from|/METHODS> and L<to|/METHODS> designates the source and
targets for the transformation, respectively. We currently provide
two data structures C<Froody::Walker::Terse> and C<Froody::Walker::XML>
as transformation engines, which will both work bi-directionally.
=cut
package Froody::Walker;
use strict;
use warnings;
use base 'Class::Accessor::Chained::Fast';
__PACKAGE__->mk_accessors(qw{spec method});
use Froody::Logger;
use Froody::Error;
use Scalar::Util qw(weaken);
my $logger = get_logger("froody.walker");
=head2 METHODS
=over
=item $self->walk($spec, $data)
Walks the structure of data with the source C<Froody::Walker::Driver>,
returning a transformed version of the data as per the designation of
the target.
=cut
sub walk {
my ($self, $source) = @_;
$source = $self->from->init_source($source);
return $self->to->init_target('') unless $self->toplevels;
my $result = $self->walk_node($source, ($self->toplevels)[0]);
Froody::Error->throw('froody.xml', "walk_node did not return a response")
unless defined($result);
# hey, at this point, we have multiple top-level elements!
return $result; # $result->{ $toplevel[0] }; # Terse specific. move to XMLToTerse
}
=item $self->walk_node($spec, $source, $xpath_key, [ $parent_target ])
Walks the data structure this object holds with the specification, starting at
the part of the spec indicated by $xpath_key.
C<$parent_target> is used for accumulation of results at the current level.
=cut
sub walk_node {
my ($self, $source, $xpath_key, $parent_target) = @_;
$logger->debug("Walking path '$xpath_key' into source '@{[ $source || '' ]}'");
my $target = $self->to->init_target($xpath_key, $parent_target)
or Froody::Error->throw('froody.xml', "init_target failed to create a target");
# get the part of the spec for where we are now looking at
my $global_spec = $self->spec || {};
my $spec = $global_spec->{ $xpath_key };
$self->from->validate_source($source, $xpath_key);
# get text node (simplest case)
if (!$spec or $spec->{text}) {
$logger->debug("getting text node");
my $value = $self->from->read_text($source, $xpath_key);
if (defined($value)) {
$value = "$value";
$logger->debug(" got '$value'");
$target = $self->to->write_text($target, $xpath_key, $value);
Froody::Error->throw('froody.xml', "write_text did not return a target")
unless defined($target);
}
# if the spec is empty, assume a single text node.
return $target unless $spec;
}
# the attributes are easy. We just pass them each on.
# TODO: Work out if we need to encode these
# HANDLE all simple values.
for my $attr (reverse @{ $spec->{attr} }) {
$logger->debug("Getting attribute '$attr'");
my $value = $self->from->read_attribute($source, $xpath_key, $attr);
next unless defined($value);
$value = "$value";
$logger->debug(" got $value");
$target = $self->to->write_attribute($target, $xpath_key, $attr, $value)
or Froody::Error->throw('froody.xml',"write_attribute did not return a target");
}
for my $element (@{ $spec->{elts} }) {
$logger->debug("getting element $element");
my $local_xpath = $xpath_key ? "$xpath_key/$element" : $element;
my @local_source = $self->from->child_sources($source, $xpath_key, $element )
# if there's no source, we don't make a target - no empty hashes,
# xml nodes, etc, etc.
or next;
#warn "Here in elts for $local_xpath";
if (@local_source > 1 and !$global_spec->{$local_xpath}{multi}) {
Froody::Error->throw('froody.xml',
"got multiple entries for path '$local_xpath', but the spec suggests there should be only one");
}
for my $this_source (@local_source) {
Froody::Error->throw('froody.xml', "source for path '$local_xpath' is undefined")
unless defined($this_source);
$logger->debug("local source '$this_source'");
my $local_target = $self->walk_node( $this_source, $local_xpath, $target );
$target = $self->to->add_child_to_target( $target, $xpath_key,
$element, $local_target )
or Froody::Error->throw('froody.xml', "add_child_to_target did not return a target");
}
}
$logger->debug("done walking path $xpath_key");
return $target;
}
=item from (Froody::Walker::Driver)
Sets the source driver.
=cut
sub from {
my $self = shift;
if (@_) {
$self->{from} = shift;
$self->{from}{walker} = $self;
weaken( $self->{from}{walker} );
return $self
}
return $self->{from};
}
=item to (Froody::Walker::Driver)
Sets the target driver.
=cut
sub to {
my $self = shift;
if (@_) {
$self->{to} = shift;
$self->{to}{walker} = $self;
weaken( $self->{to}{walker} );
return $self;
}
return $self->{to};
}
=back
=head2 Utility methods
Small methods that get called a lot from subclasses.
=over
=item spec_for_xpath( path )
Returns the local method spec for the given xpath. Returns the
default method spec (text-only node) if there is no spac for that
path.
=cut
sub spec_for_xpath {
my ($self, $xpath) = @_;
my $spec = $self->spec->{$xpath} if $self->spec && $xpath;
return $spec;
}
=item toplevels()
return a list of the top-level node names in the response XML.
=cut
sub toplevels {
my $self = shift;
# look for the toplevel in the spec. If we've got more than one, panic!
my @toplevel = grep m{^[^/]+$}, keys %{ $self->spec };
if (@toplevel > 1) {
warn Dumper($self->spec); use Data::Dumper;
Froody::Error->throw("froody.xml",
"invalid Response spec (multiple toplevel nodes!)")
}
$self->{spec}{''} = {
elts => \@toplevel,
attr => [],
text => 0,
};
return @toplevel;
}
=back
=head1 BUGS
None known.
( run in 3.440 seconds using v1.01-cache-2.11-cpan-524268b4103 )