File Coverage

blib/lib/App/hopen/Util/XML/FromPerl.pm
Criterion Covered Total %
statement 55 74 74.3
branch 13 28 46.4
condition 2 3 66.6
subroutine 10 12 83.3
pod 2 2 100.0
total 82 119 68.9


line stmt bran cond sub pod time code
1             package App::hopen::Util::XML::FromPerl;
2              
3             our $VERSION = '0.02';
4              
5 1     1   7 use strict;
  1         3  
  1         30  
6 1     1   6 use warnings;
  1         2  
  1         56  
7              
8             # === Warnings ==============================================================
9              
10             # Set up for warnings. We can't do this in a separate package because
11             # warnings::enabled() and related rely on caller being the package that
12             # invoked this one, not this package itself.
13 1     1   740 use if $] ge '5.014', qw(warnings::register undefined);
  1         13  
  1         7  
14 1     1   228 use if $] lt '5.014', qw(warnings::register);
  1         3  
  1         6  
15              
16             # @_warning_category is the category in which we will warn, or an empty list.
17             # @_warning_categories is the list of categories we need to check to see
18             # if we should warn.
19 1     1   42 use vars qw(@_warning_category @_warning_categories);
  1         2  
  1         232  
20              
21             if($] ge '5.014') {
22             @_warning_category = (__PACKAGE__ . '::undefined');
23             @_warning_categories = (__PACKAGE__, @_warning_category);
24             } else {
25             @_warning_category = ();
26             @_warning_categories = __PACKAGE__;
27             }
28              
29             # Emit a warning and return a value. Call via goto. Usage:
30             # @_ = ("warning message", $return_value);
31             # goto &_emit_warning;
32              
33             sub _emit_warning {
34 0     0   0 my ($message, $retval) = @_;
35              
36             # Are all the categories of interest enabled?
37 0         0 my $should_emit = 1;
38 0         0 foreach(@_warning_categories) {
39 0 0       0 if(!warnings::enabled($_)) {
40 0         0 $should_emit = 0;
41 0         0 last;
42             }
43             }
44              
45 0 0       0 warnings::warn(@_warning_category, $message) if $should_emit;
46              
47 0         0 return $retval;
48             } #_emit_warning
49              
50             # === Code ==================================================================
51              
52 1     1   741 use XML::LibXML;
  1         31970  
  1         7  
53              
54 1     1   175 use parent 'Exporter';
  1         2  
  1         9  
55             our @EXPORT_OK = qw(xml_from_perl xml_node_from_perl);
56              
57             # Fill in the children of the given node from the passed value.
58             # No return value.
59             sub _fill_node_children {
60 5     5   12 my ($doc, $parent, $data) = @_;
61 5 50       14 unless(defined $data) {
62 0         0 @_ = ("I can't create an XML node from undefined data", undef);
63 0         0 goto &_emit_warning;
64             }
65              
66 5         8 my ($one, $has_attrs);
67 5 50       15 if(ref $data eq 'ARRAY') {
68 5         10 $one = $data->[1];
69 5         11 $has_attrs = ref $one eq 'HASH';
70             }
71              
72 5         8 my $new_node;
73 5 100 66     28 if (ref $data eq 'ARRAY' && $data->[0] eq '!--') { # Comment
    50          
74 1 50       12 my $separ = defined $, ? $, : ' ';
75              
76             # Grab the plain text nodes and paste them together.
77             my $text = join $separ,
78 1         5 map { $data->[$_] }
79 1 50       6 grep { defined $data->[$_] and not ref $data->[$_] }
  1 50       9  
80             (($has_attrs ? 2 : 1) .. $#$data);
81              
82 1         16 $new_node = $doc->createComment($text);
83              
84             } elsif (ref $data eq 'ARRAY') { # Regular node
85 4         22 $new_node = $doc->createElement($data->[0]);
86              
87 4 50       12 if ($has_attrs) {
88 4         13 my @keys = keys %$one;
89 4 50       18 @keys = sort @keys unless tied %$one;
90 4         9 for (@keys) {
91 5 50       31 if (defined (my $v = $one->{$_})) {
92 5         17 $new_node->setAttribute($_, $v);
93             }
94             }
95             }
96              
97             _fill_node_children($doc, $new_node, $data->[$_])
98 4 50       179 for grep { defined $data->[$_] } (($has_attrs ? 2 : 1) .. $#$data);
  5         71  
99              
100             } else { # Text node
101 0         0 $new_node = $doc->createTextNode("$data");
102             }
103              
104 5         176 $parent->appendChild($new_node);
105 5         31 return undef;
106             } #_fill_node_children
107              
108             # Create a phony element we can use as a temporary parent node
109             sub _create_phony {
110 1     1   3 my $doc = shift;
111 1         21 return $doc->createElementNS('https://metacpan.org/pod/XML::FromPerl',
112             'phony_root');
113             } #_create_phony
114              
115             sub xml_node_from_perl {
116 0     0 1 0 my $doc = shift;
117 0         0 my $data = shift;
118 0         0 my $parent;
119              
120 0         0 $parent = _create_phony($doc);
121 0         0 _fill_node_children $doc, $parent, $data;
122 0         0 return $parent->firstChild;
123             } #xml_node_from_perl
124              
125             sub xml_from_perl {
126 1     1 1 3 my $data = shift;
127 1         32 my $doc = XML::LibXML::Document->new(@_);
128              
129 1 50       7 unless(defined $data) {
130 0         0 @_ = ("I can't create an XML document from undefined data", $doc);
131 0         0 goto &_emit_warning;
132             }
133              
134 1         6 my $parent = _create_phony($doc);
135 1         18 $doc->setDocumentElement($parent);
136 1         24 _fill_node_children $doc, $parent, $data;
137              
138 1         40 $doc->setDocumentElement($parent->firstChild);
139 1         42 return $doc;
140             } #xml_from_perl
141              
142             1;
143             __END__
144              
145             =head1 NAME
146              
147             XML::FromPerl - Generate XML from simple Perl data structures
148              
149             =head1 SYNOPSIS
150              
151             use XML::FromPerl qw(xml_from_perl);
152              
153             my $doc = xml_from_perl
154             [ Foo => { attr1 => val1, attr2 => val2},
155             [ Bar => { attr3 => val3, ... },
156             [ '!--', 'some comment, indicated by tag name "!--"' ],
157             [ Bar => { ... },
158             "Some Text here",
159             [Doz => { ... },
160             [ Bar => { ... }, [ ... ] ] ] ] ] ];
161              
162             $doc->toFile("foo.xml");
163             # -> <Foo attr1="val1" attr2="val2">
164             # <Bar attr3="val3">
165             # <!--some comment...-->
166             # ...
167             # </Bar>
168             # </Foo>
169              
170             =head1 DESCRIPTION
171              
172             This module is able to generate XML described using simple Perl data
173             structures.
174              
175             XML nodes are declared as arrays where the first slot is the tag name,
176             the second is a HASH containing tag attributes and the rest are its
177             children. Perl scalars are used for text sections.
178              
179             =head1 EXPORTABLE FUNCTIONS
180              
181             =head2 xml_from_perl $data
182              
183             Converts the given perl data structure into a L<XML::LibXML::Document>
184             object.
185              
186             If C<$data> is undefined, the document will have no root element
187             or other contents.
188             A warning in category C<'XML::FromPerl::undefined'> will be issued.
189              
190             =head2 xml_node_from_perl $doc, $data
191              
192             Converts the given perl data structure into a L<XML::LibXML::Node>
193             object linked to the document passed.
194              
195             If C<$data> is undefined, or is an arrayref including any undefined
196             entries, the undefined entries will be ignored.
197             A warning in category C<'XML::FromPerl::undefined'> will be issued for
198             each C<undef> item processed.
199              
200             =head1 NOTES
201              
202             =head2 Namespaces
203              
204             I have not made my mind yet about how to handle XML namespaces other
205             than stating them explicitly in the names or setting the C<xmlns>
206             attribute.
207              
208             =head2 Attribute order
209              
210             If attribute order is important to you, declare then using
211             L<Tie::IxHash>:
212              
213             For instance:
214              
215             use Tie::IxHash;
216             sub attrs {
217             my @attrs = @_;
218             tie my(%attrs), 'Tie::Hash', @attrs;
219             \%attrs
220             }
221              
222             my $doc = xml_from_perl [ Foo => attrs(attr1 => val1, attrs2 => val2), ...];
223              
224             Otherwise attributes are sorted in lexicographical order.
225              
226             =head2 Memory usage
227              
228             This module is not very memory efficient. At some point it is going to
229             keep in memory both the original perl data structure and the
230             XML::LibXML one.
231              
232             Anyway, nowadays that shouldn't be a problem unless your data is
233             really huge.
234              
235             =head2 Comments
236              
237             Any attributes or children of a comment node will be ignored.
238             So, for example,
239              
240             [ '!--',
241             { attr => 'val' },
242             [ Foo => { attr => "hello" } ]
243             ]
244              
245             will produce
246              
247             <!---->
248              
249             not
250              
251             <!--<Foo attr="hello"/>-->
252              
253             This is due to a limitation of L<XML::LibXML>:
254             C<XML::LibXML::Comment::appendChild()> is a no-op.
255              
256             Any text elements in a comment node will be joined together by the
257             value of C<$,>, or a single space if C<$,> is undefined. For example,
258             if C<$, eq '#'>,
259              
260             [ '!--', qw(hello there world) ]
261              
262             will produce
263              
264             <!--hello#there#world-->
265              
266             =head1 SEE ALSO
267              
268             L<XML::LibXML>, L<XML::LibXML::Document>, L<XML::LibXML::Node>.
269              
270             Other modules for generating XML are L<XML::Writer> and
271             L<XML::Generator>. Check also L<XML::Compile>.
272              
273             A related PerlMonks discussion:
274             L<http://www.perlmonks.org/?node_id=1195009>.
275              
276             =head1 COPYRIGHT AND LICENSE
277              
278             Copyright (C) 2017, 2019 by
279             Salvador FandiE<ntilde>o E<lt>sfandino@yahoo.comE<gt> and
280             Christopher White E<lt>cxw@cpan.orgE<gt>.
281              
282             This library is free software; you can redistribute it and/or modify
283             it under the same terms as Perl itself, either Perl version 5.24.1 or,
284             at your option, any later version of Perl 5 you may have available.
285              
286             =cut