The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

XML::LibXML::Ferry - Marshall LibXML nodes and native objects

SYNOPSIS

use XML::LibXML::Ferry;  # Implies use XML::LibXML

DESCRIPTION

Adds higher-level methods to XML::LibXML::Element to very expressively traverse and create XML fragments to/from your custom objects.

METHODS

XML::LibXML::Element::attr( [%attributes] )

If %attributes is not empty: each key/value pair is added/replaced in the element. Undefined values are skipped. Returns the element itself, for possible chaining.

If %attributes is missing/empty: returns a hashref of all of the element's attributes (although that is redundant with simply using the element as a hashref directly, as explained in "OVERLOADING" in XML::LibXML::Element).

XML::LibXML::Element::create( $name, [$text], [%attributes] )

Create a stand-alone element named $name with %attributes set, in the same document as the current element.

To create an element with attributes but no text content, specify an undefined $text.

Returns the new element.

XML::LibXML::Element::add( $name, [$text], [%attributes] )

Wrapper around "XML::LibXML::Element::create()", which also appends the new element to the children of the current element. Returns the new element.

XML::LibXML::Element::add( $node )

With an element as its only argument, convenience wrapper for $node->appendChild().

XML::LibXML::Node::textNodeContent()

Iterate through each of the element's immediate children and create a string from text nodes found. The result is stripped of leading and trailing whitespace.

XML::LibXML::Element::ferry( $obj, [$exceptions] )

Iterate through each of the element's attributes (including namespaced ones), then each of its child nodes.

The lowercased attribute or node name is matched against $obj's methods. Without a match, a second try is made with an append s. With still no match, the same two names are matched against $obj's hash keys. With still no match, the value is ignored.

If we matched a method, it is passed the attribute's text or node's text content as single argument. (Or your subroutine results or new class instance, see below.) If we matched a direct hash key, it is overwritten with that new content, or if the existing hash value is an arrayref, the new content is pushed to it instead.

$exceptions is a hashref associating attributes and child node names with one of three possible types:

String property name

Alternative method/key name to use instead of the lowercased, possibly plural form of the attribute/node name. Great for shortening verbose names in your API.

Since non-existent property names are safely ignored, you can make sure that a node or attribute will be ignored by specifying an unknown name. To keep Ferry-using code consistent and explicit, parts of the DTD you're working with, but which you are not implementing, should be set to __UNIMPLEMENTED, parts which are ignored because they are obsolete to __OBSOLETE and parts which are skipped for any other reason to __IGNORE.

Arrayref

Two items are expected:

String property name

As above, alternative name.

Subroutine reference OR String class name

If a subroutine is given, it is called with two arguments: $obj and the current XML node or attribute string. If your subroutine returns something, it will be used as the value to save. It is thus impossible to store undef by returning it. Note that if a subroutine is associated with an unknown property name (i.e. __IGNORE), it will still be invoked and its return value ignored, which is useful for cases where you have nothing meaningful to return.

If a class name is given, a new instance of it is created with: $classname->new($val) where $val is either an attribute's string content or a XML::LibXML::Element. The created object will be used as the value to save. This is key to allow creating various classes representing different parts of a DTD: with each class creator internally calling $node->ferry(...), one can end up with any arbitrary structure matching that of the XML document.

Hashref

Recursion: the hashref will be treated like $exceptions into the current $obj. This is useful to flatten small but deep structures without having to use multiple classes. An empty hashref still triggers this behavior.

Optional key __text should contain a property name. This is necessary for getting the direct text content of a node along with any attributes.

Optional key __meta_name alters the above behavior slightly: the element being processed is handled like a key-value tag. (Like HTML's META or cXML's Extrinsic.) The key to search in $exceptions will be the content of its attribute named in __meta_name instead of its nodeName. So a hypothetical <meta property="foo">bar</meta> with a meta name property would be treated as if it actually were <foo>bar</foo> .

Optional key __meta_content works in conjunction with __meta_name above, and adds that the value will also come from the specified attribute. For example, a meta name property and content value would treat <meta property="foo" value="bar">ignored</meta> as if it actually were <foo>bar</foo> .

See "EXAMPLES" for a detailed example.

XML::LibXML::Element::toHash()

Convert an XML element tree into a recursive hash. Each attribute is in a key __attributes and each child node is recursively put in an array in its name. Key __text contains the merged text nodes directly in the element, with intial and trailing whitespace stripped.

The resulting format is a bit verbose, but ideal for using Test::Deep to compare XML fragments and for quick inspections. (See "EXAMPLES".)

XML::LibXML::Document::toHash()

Convenience wrapper which invokes XML::LibXML::Element::toHash() above on the document's documentElement.

EXAMPLES

ferry():

Given the following XML fragment as $root, an XML::LibXML::Element:

<Example weirdName="test-example">
	<Attribute name="location">1234 Main St</Attribute>
	<Attribute name="phone">1-800-555-1212</Attribute>
	<Bars>
		<Bar name="first bar">
			<Description>
				<Text>This is the first bar!</Text>
			</Description>
		</Bar>
		<Bar>
			<Description>
				<Text>The second bar is unnamed.</Text>
			</Description>
		</Bar>
	</Bars>
</Example>

We could write the following to clearly map Example to a Mystuff::Thingy also containing some Mystuff::Otherthings:

use XML::LibXML::Ferry;

my $thing = new Mystuff::Thing 'thingy', $root;

package Mystuff::Thing
sub new {
	my ($class, $name, $node) = @_;
	my $self = {
		foo          => undef,
		location     => undef,
		phone_number => undef,
		bar          => [],
	};
	bless $self, $class;
	$node->ferry($self, {
		__attributes => {
			weirdName => 'foo',
		},
		Attribute => {
			__meta_name => 'name',
			# 'location' will implicitly match our property
			phone       => 'phone_number',
		},
		Bars => {
			Bar => [ 'bars', 'Mystuff::Otherthing' ],
		},
	});
	return $self;
}

package Mystuff::Otherthing;
sub new {
	my ($class, $name, $node) = @_;
	my $self = {
		name        => undef,
		description => undef,
	};
	bless $self, $class;
	$node->ferry($self, {
		# Attribute 'name' will implicitly match our property
		Description => {
			Text => 'description',
		},
	});
	return $self;
}

This would make $thing contain:

$VAR1 = bless( {
	'foo' => 'test-example',
	'location' => '1234 Main St',
	'phone_number' => '1-800-555-1212',
	'bar' => [
		bless( {
			'name' => 'first bar',
			'description' => 'This is the first bar!'
		}, 'Mystuff::Otherthing' ),
		bless( {
			'name' => undef,
			'description' => 'The second bar is unnamed.'
		}, 'Mystuff::Otherthing' )
	],
}, 'Mystuff::Thing' );

toHash():

Given the following XML fragment:

<Example weirdName="test-example">
	<Attribute name="location">1234 Main St</Attribute>
	<Attribute name="phone">1-800-555-1212</Attribute>
</Example>

"toHash()" would return:

$VAR1 = {
	'__attributes' => {
		'weirdName' => 'test-example',
	},
	'Attribute' => [
		{
			'__attributes' => {
				'name' => 'location',
			},
			'__text' => '1234 Main St',
		},
		{
			'__attributes' => {
				'name' => 'phone',
			},
			'__text' => '1-800-555-1212',
		},
	],
};

AUTHOR

Stéphane Lavergne https://github.com/vphantom

ACKNOWLEDGEMENTS

Graph X Design Inc. https://www.gxd.ca/ sponsored this project.

COPYRIGHT & LICENSE

Copyright (c) 2017-2018 Stéphane Lavergne https://github.com/vphantom

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.