File Coverage

blib/lib/XML/LibXML/Ferry.pm
Criterion Covered Total %
statement 94 94 100.0
branch 52 52 100.0
condition 6 6 100.0
subroutine 13 13 100.0
pod n/a
total 165 165 100.0


line stmt bran cond sub pod time code
1             =encoding utf-8
2              
3             =head1 NAME
4              
5             XML::LibXML::Ferry - Marshall LibXML nodes and native objects
6              
7             =head1 SYNOPSIS
8              
9             use XML::LibXML::Ferry; # Implies use XML::LibXML
10              
11             =head1 DESCRIPTION
12              
13             Adds higher-level methods to L<XML::LibXML::Element> to very expressively
14             traverse and create XML fragments to/from your custom objects.
15              
16             =cut
17              
18             # Nothing but $VERSION actually goes here
19              
20             use 5.006;
21 3     3   360060 use strict;
  3         32  
22 3     3   14 use warnings;
  3         5  
  3         59  
23 3     3   12 use Scalar::Util qw(blessed);
  3         6  
  3         66  
24 3     3   13  
  3         6  
  3         153  
25             use XML::LibXML;
26 3     3   557  
  3         45544  
  3         15  
27             BEGIN {
28             our $VERSION = 'v0.8.5';
29 3     3   3494 }
30              
31             =head1 METHODS
32              
33             =over
34              
35             =item C<B<XML::LibXML::Element::attr>( [I<%attributes>] )>
36              
37             If I<C<%attributes>> is not empty: each key/value pair is added/replaced in
38             the element. Undefined values are skipped. Returns the element itself, for
39             possible chaining.
40              
41             If I<C<%attributes>> is missing/empty: returns a hashref of all of the
42             element's attributes (although that is redundant with simply using the element
43             as a hashref directly, as explained in L<XML::LibXML::Element/OVERLOADING>).
44              
45             =cut
46              
47             my ($self, %attrs) = @_;
48             if (%attrs) {
49 7     7   2673 foreach (keys %attrs) {
50 7 100       14 $self->setAttribute($_, $attrs{$_}) if defined $attrs{$_};
51 6         14 };
52 8 100       42 return $self;
53             } else {
54 6         44 return \%$self;
55             };
56 1         37 }
57              
58             =item C<B<XML::LibXML::Element::create>( I<$name>, [I<$text>], [I<%attributes>] )>
59              
60             Create a stand-alone element named I<C<$name>> with I<C<%attributes>> set, in
61             the same document as the current element.
62              
63             To create an element with attributes but no text content, specify an undefined
64             I<C<$text>>.
65              
66             Returns the new element.
67              
68             =cut
69              
70             my ($self, $name, $text, %attrs) = @_;
71             my $el = $self->ownerDocument->createElement($name);
72             $el->appendTextNode($text) if defined $text;
73 5     5   1306 return %attrs ? $el->attr(%attrs) : $el;
74 5         32 }
75 5 100       26  
76 5 100       17 =item C<B<XML::LibXML::Element::add>( I<$name>, [I<$text>], [I<%attributes>] )>
77              
78             Wrapper around L</XML::LibXML::Element::create()>, which also appends the new
79             element to the children of the current element. Returns the new element.
80              
81             =item C<B<XML::LibXML::Element::add>( I<$node> )>
82              
83             With an element as its only argument, convenience wrapper for
84             S<C<<<< $node->appendChild() >>>>>.
85              
86             =cut
87              
88             my ($self, $name, $text, %attrs) = @_;
89             if (ref $name) {
90             $self->appendChild($name);
91             return $name;
92 3     3   4058 };
93 3 100       7 my $el = $self->create($name, $text, %attrs);
94 1         5 $self->appendChild($el);
95 1         2 return $el;
96             };
97 2         7  
98 2         16 =item C<B<XML::LibXML::Node::textNodeContent>()>
99 2         5  
100             Iterate through each of the element's immediate children and create a string
101             from text nodes found. The result is stripped of leading and trailing
102             whitespace.
103              
104             =cut
105              
106             my ($self) = @_;
107             my $text = '';
108             foreach ($self->childNodes) {
109             $text .= $_->textContent if ($_->nodeType == XML_TEXT_NODE);
110             };
111 99     99   319 $text =~ s/^\s+|\s+$//g;
112 99         108 return $text;
113 99         148 }
114 100 100       509  
115             =item C<B<XML::LibXML::Element::ferry>( I<$obj>, [I<$exceptions>] )>
116 99         490  
117 99         427 Iterate through each of the element's attributes (including namespaced ones),
118             then each of its child nodes.
119              
120             The lowercased attribute or node name is matched against I<C<$obj>>'s methods.
121             Without a match, a second try is made with an append C<s>. With still no
122             match, the same two names are matched against I<C<$obj>>'s hash keys. With
123             still no match, the value is ignored.
124              
125             If we matched a method, it is passed the attribute's text or node's text
126             content as single argument. (Or your subroutine results or new class
127             instance, see below.) If we matched a direct hash key, it is overwritten with
128             that new content, or if the existing hash value is an arrayref, the new
129             content is pushed to it instead.
130              
131             I<C<$exceptions>> is a hashref associating attributes and child node names
132             with one of three possible types:
133              
134             =over
135              
136             =item B<String property name>
137              
138             Alternative method/key name to use instead of the lowercased, possibly plural
139             form of the attribute/node name. Great for shortening verbose names in your
140             API.
141              
142             Since non-existent property names are safely ignored, you can make sure that a
143             node or attribute will be ignored by specifying an unknown name. To keep
144             Ferry-using code consistent and explicit, parts of the DTD you're working
145             with, but which you are not implementing, should be set to C<__UNIMPLEMENTED>,
146             parts which are ignored because they are obsolete to C<__OBSOLETE> and parts
147             which are skipped for any other reason to C<__IGNORE>.
148              
149             =item B<Arrayref>
150              
151             Two items are expected:
152              
153             =over 4
154              
155             =item B<String property name>
156              
157             As above, alternative name.
158              
159             =item B<Subroutine reference> OR B<String class name>
160              
161             If a subroutine is given, it is called with two arguments: I<C<$obj>> and the
162             current XML node I<or> attribute string. If your subroutine returns
163             something, it will be used as the value to save. It is thus impossible to
164             store C<undef> by returning it. Note that if a subroutine is associated with
165             an unknown property name (i.e. C<__IGNORE>), it will still be invoked and its
166             return value ignored, which is useful for cases where you have nothing
167             meaningful to return.
168              
169             If a class name is given, a new instance of it is created with:
170             S<C<<<< $classname->new($val) >>>>>
171             where I<C<$val>> is either an attribute's string content or a
172             L<XML::LibXML::Element>. The created object will be used as the value to
173             save. This is key to allow creating various classes representing different
174             parts of a DTD: with each class creator internally calling
175             S<C<<<< $node->ferry(...) >>>>>,
176             one can end up with any arbitrary structure matching that of the XML document.
177              
178             =back
179              
180             =item B<Hashref>
181              
182             Recursion: the hashref will be treated like I<C<$exceptions>> into the
183             I<current> I<C<$obj>>. This is useful to flatten small but deep structures
184             without having to use multiple classes. An empty hashref still triggers this
185             behavior.
186              
187             =back
188              
189             Optional key C<__text> should contain a property name. This is necessary for
190             getting the direct text content of a node along with any attributes.
191              
192             Optional key C<__meta_name> alters the above behavior slightly: the element
193             being processed is handled like a key-value tag. (Like HTML's C<META> or
194             cXML's C<Extrinsic>.) The key to search in I<C<$exceptions>> will be the
195             content of its attribute named in C<__meta_name> instead of its C<nodeName>.
196             So a hypothetical
197             S<C<<<< <meta property="foo">bar</meta> >>>>>
198             with a meta name C<property> would be treated as if it actually were
199             S<C<<<< <foo>bar</foo> >>>>>
200             .
201              
202             Optional key C<__meta_content> works in conjunction with C<__meta_name> above,
203             and adds that the value will also come from the specified attribute. For
204             example, a meta name C<property> and content C<value> would treat
205             S<C<<<< <meta property="foo" value="bar">ignored</meta> >>>>>
206             as if it actually were
207             S<C<<<< <foo>bar</foo> >>>>>
208             .
209              
210             See L</EXAMPLES> for a detailed example.
211              
212             =cut
213              
214             my ($self, $obj, $ex) = @_;
215              
216             # Reduce various key/value sources down to a single list.
217             #
218             # Because some targets can be opaque methods or arrayrefs, we allow
219             # multiple keys, hence the use of a 2D array instead of a hash.
220 27     27   13731 #
221             # Each item is an ARRAYREF:
222             # [0] - Raw input attribute/node name
223             # [1] - String content or XML::LibXML::Element child
224             my @store;
225             if (exists $ex->{__meta_name}) {
226             # Process as a single META tag, ignoring other attributes
227             push @store, [
228             $self->{ $ex->{__meta_name} },
229             (defined $ex->{__meta_content} ? $self->{ $ex->{__meta_content} } : $self),
230 27         28 ];
231 27 100       45 } else {
232             push @store, [ $_, $self->{$_} ] foreach (%$self);
233             push @store, [ $_->nodeName, $_ ] foreach ($self->childNodes);
234             push @store, [ $ex->{__text}, $self->textNodeContent ] if exists $ex->{__text};
235 12 100       26 # Namespaced attributes we're explicitly looking for
236             # (XML::LibXML::AttributeHash uses Clark notation.)
237             foreach (grep { /:/ } keys %{ $ex }) {
238 15         30 push @store, [ $_, $self->getAttribute($_) ] if $self->hasAttribute($_);
239 15         1970 };
240 15 100       307 };
241              
242             # Process each key/value found
243 15         17 foreach (@store) {
  24         57  
  15         40  
244 2 100       23 my ($key, $val) = @{ $_ };
245             my $sub = undef;
246              
247             # Rename key, identify SUBREF, recurse HASHREFs
248             if (exists $ex->{ $key }) {
249 27         713 my $e = $ex->{ $key };
250 133         155 if (ref($e) eq 'ARRAY') {
  133         239  
251 133         172 $key = $e->[0]; # Override key
252             $sub = $e->[1]; # SUBREF or class name string
253             } elsif (ref($e) eq 'HASH') {
254 133 100       223 # Safely ignore the invalid case of setting a HASHREF on an attribute key
255 30         42 $val->ferry($obj, $e) if ref($val); # <-- RECURSION
256 30 100       60 next;
    100          
257 3         5 } else {
258 3         4 $key = $e; # Override key
259             };
260             } else {
261 24 100       91 $key = lc($key);
262 24         285 };
263              
264 3         14 # Reduce value to a string through SUB or textNodeContent()
265             if (ref $sub) {
266             $val = $sub->($obj, $val);
267 103         147 } elsif ($sub) {
268             my $file = $sub;
269             $file =~ s|::|/|g;
270             require "$file.pm";
271 109 100       183 $val = $sub->new($val); # No eval: we want this to fail if the class doesn't exist
    100          
272 1         5 } else {
273             $val = $val->textNodeContent if ref($val);
274 2         4 };
275 2         7  
276 2         349 # Save value if it wasn't eaten up by SUB
277 2         234 if (defined $val) {
278             my $m = $key;
279 106 100       203 $m .= 's' unless blessed($obj) && $obj->can($m);
280             if (blessed($obj) && $obj->can($m)) {
281             $obj->$m($val);
282             } else {
283 109 100       199 $key .= 's' unless exists $obj->{$key};
284 95         105 if (exists $obj->{$key}) {
285 95 100 100     345 if (ref($obj->{$key}) eq 'ARRAY') {
286 95 100 100     304 push @{ $obj->{$key} }, $val;
287 6         15 } else {
288             $obj->{$key} = $val;
289 89 100       182 };
290 89 100       197 };
291 11 100       22 };
292 4         6 };
  4         13  
293              
294 7         22 };
295             }
296              
297             =item C<B<XML::LibXML::Element::toHash>()>
298              
299             Convert an XML element tree into a recursive hash. Each attribute is in a key
300             C<__attributes> and each child node is recursively put in an array in its
301             name. Key C<__text> contains the merged text nodes directly in the element,
302             with intial and trailing whitespace stripped.
303              
304             The resulting format is a bit verbose, but ideal for using L<Test::Deep> to
305             compare XML fragments and for quick inspections. (See L</EXAMPLES>.)
306              
307             =cut
308              
309             my ($self) = @_;
310             my $hash = { __attributes => {} };
311              
312             # Grab attributes
313             $hash->{__attributes}{$_} = $self->{$_} foreach (keys %$self);
314              
315             # Grab childNodes
316 25     25   2986 if ($self->hasChildNodes) {
317 25         41 foreach ($self->childNodes) {
318             if ($_->nodeType == XML_ELEMENT_NODE) {
319             $hash->{$_->nodeName} = [] unless exists $hash->{$_->nodeName};
320 25         53 my $newhash = $_->toHash;
321             push(@{ $hash->{$_->nodeName} }, $newhash);
322             };
323 25 100       2044 };
324 20         34 };
325 64 100       246 $hash->{__text} = $self->textNodeContent;
326 23 100       93 return $hash;
327 23         44 }
328 23         30  
  23         78  
329             =item C<B<XML::LibXML::Document::toHash>()>
330              
331             Convenience wrapper which invokes L<XML::LibXML::Element::toHash()> above on
332 25         57 the document's C<documentElement>.
333 25         115  
334             =cut
335              
336             my ($self) = @_;
337             return $self->documentElement->toHash;
338             }
339              
340             =back
341              
342             =head1 EXAMPLES
343              
344 1     1   19 B<ferry():>
345 1         6  
346             Given the following XML fragment as I<C<$root>>, an L<XML::LibXML::Element>:
347              
348             <Example weirdName="test-example">
349             <Attribute name="location">1234 Main St</Attribute>
350             <Attribute name="phone">1-800-555-1212</Attribute>
351             <Bars>
352             <Bar name="first bar">
353             <Description>
354             <Text>This is the first bar!</Text>
355             </Description>
356             </Bar>
357             <Bar>
358             <Description>
359             <Text>The second bar is unnamed.</Text>
360             </Description>
361             </Bar>
362             </Bars>
363             </Example>
364              
365             We could write the following to clearly map C<Example> to a C<Mystuff::Thingy>
366             also containing some C<Mystuff::Otherthing>s:
367              
368             use XML::LibXML::Ferry;
369              
370             my $thing = new Mystuff::Thing 'thingy', $root;
371              
372             package Mystuff::Thing
373             sub new {
374             my ($class, $name, $node) = @_;
375             my $self = {
376             foo => undef,
377             location => undef,
378             phone_number => undef,
379             bar => [],
380             };
381             bless $self, $class;
382             $node->ferry($self, {
383             __attributes => {
384             weirdName => 'foo',
385             },
386             Attribute => {
387             __meta_name => 'name',
388             # 'location' will implicitly match our property
389             phone => 'phone_number',
390             },
391             Bars => {
392             Bar => [ 'bars', 'Mystuff::Otherthing' ],
393             },
394             });
395             return $self;
396             }
397              
398             package Mystuff::Otherthing;
399             sub new {
400             my ($class, $name, $node) = @_;
401             my $self = {
402             name => undef,
403             description => undef,
404             };
405             bless $self, $class;
406             $node->ferry($self, {
407             # Attribute 'name' will implicitly match our property
408             Description => {
409             Text => 'description',
410             },
411             });
412             return $self;
413             }
414              
415             This would make I<C<$thing>> contain:
416              
417             $VAR1 = bless( {
418             'foo' => 'test-example',
419             'location' => '1234 Main St',
420             'phone_number' => '1-800-555-1212',
421             'bar' => [
422             bless( {
423             'name' => 'first bar',
424             'description' => 'This is the first bar!'
425             }, 'Mystuff::Otherthing' ),
426             bless( {
427             'name' => undef,
428             'description' => 'The second bar is unnamed.'
429             }, 'Mystuff::Otherthing' )
430             ],
431             }, 'Mystuff::Thing' );
432              
433              
434              
435              
436              
437              
438              
439             B<toHash():>
440              
441             Given the following XML fragment:
442              
443             <Example weirdName="test-example">
444             <Attribute name="location">1234 Main St</Attribute>
445             <Attribute name="phone">1-800-555-1212</Attribute>
446             </Example>
447              
448             L</toHash()> would return:
449              
450             $VAR1 = {
451             '__attributes' => {
452             'weirdName' => 'test-example',
453             },
454             'Attribute' => [
455             {
456             '__attributes' => {
457             'name' => 'location',
458             },
459             '__text' => '1234 Main St',
460             },
461             {
462             '__attributes' => {
463             'name' => 'phone',
464             },
465             '__text' => '1-800-555-1212',
466             },
467             ],
468             };
469              
470             =head1 AUTHOR
471              
472             Stéphane Lavergne L<https://github.com/vphantom>
473              
474             =head1 ACKNOWLEDGEMENTS
475              
476             Graph X Design Inc. L<https://www.gxd.ca/> sponsored this project.
477              
478             =head1 COPYRIGHT & LICENSE
479              
480             Copyright (c) 2017-2018 Stéphane Lavergne L<https://github.com/vphantom>
481              
482             Permission is hereby granted, free of charge, to any person obtaining a copy
483             of this software and associated documentation files (the "Software"), to deal
484             in the Software without restriction, including without limitation the rights
485             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
486             copies of the Software, and to permit persons to whom the Software is
487             furnished to do so, subject to the following conditions:
488              
489             The above copyright notice and this permission notice shall be included in all
490             copies or substantial portions of the Software.
491              
492             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
493             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
494             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
495             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
496             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
497             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
498             SOFTWARE.
499              
500             =cut
501              
502             1;