File Coverage

blib/lib/XML/Simple/Sugar.pm
Criterion Covered Total %
statement 318 357 89.0
branch 113 232 48.7
condition 9 15 60.0
subroutine 36 36 100.0
pod n/a
total 476 640 74.3


line stmt bran cond sub pod time code
1 1     1   633 use 5.18.0;
  2         9371  
2 2     1   1348 use Modern::Perl;
  2         8254  
  2         1955  
3 2     1   780 use Moops;
  2         36196  
  2         232  
4              
5 2     1   212275 class XML::Simple::Sugar 1.1.2 {
  2     1   322  
  2         229  
  2         275  
  2         365  
  2         787  
  2         2933  
  2         322  
  2         2472  
  2         296  
  1         11  
  1         94  
  1         3  
  1         76  
  1         10  
  1         3  
  1         138  
  1         50  
  1         9  
  1         3  
  1         16  
  1         7296  
  1         3  
  1         12  
  1         1204  
  1         6039  
  1         8  
  1         285  
  1         5  
  1         15  
  1         707  
  1         10474  
  1         15  
  1         943  
  1         3932  
  1         33  
  1         1754  
  1         3528  
  1         12  
  1         155578  
  1         4  
  1         4  
  1         1  
  1         21  
  1         4  
  1         2  
  1         36  
  1         4  
  1         2  
  1         193  
  1         4182  
  3         1909  
6 1         11 our $AUTOLOAD;
7 1     1   637 use XML::Simple;
  1         10036  
  1         9  
8 1     1   582 use UNIVERSAL::isa;
  1         1059  
  1         5  
9 1     1   39 use overload '""' => 'xml_write';
  1         2  
  1         9  
10              
11 1         5 has 'xml_index' => ( 'is' => 'ro', 'isa' => 'Int', default => 0 );
12 1         2904 has 'xml_node' => ( 'is' => 'ro', 'isa' => Maybe[Str] );
13             has 'xml_xs' => (
14             'is' => 'rw',
15             'isa' => 'XML::Simple',
16 90         12527 'default' => sub { XML::Simple->new( XMLDecl => '<?xml version="1.0"?>' ); }
17 1         2146 );
18             has 'xml_data' => (
19             'is' => 'rw',
20             'isa' => Maybe[HashRef|ArrayRef],
21 1         3081 'default' => method { $self->xml_data ? $self->xml_data : {}; }
22             );
23 6         1192 has 'xml_parent' => ( 'is' => 'ro', 'isa' => InstanceOf['XML::Simple::Sugar'] );
24 6         14 has 'xml_autovivify' => ( 'is' => 'rw', 'isa' => Bool, default => 1 );
25             has 'xml' => (
26             'is' => 'rw',
27             'isa' => Str,
28             'trigger' => method {
29             $self->xml_data(
30             XMLin(
31             $self->xml,
32             ForceContent => 1,
33             KeepRoot => 1,
34             ForceArray => 1,
35             ContentKey => 'xml_content',
36             )
37             );
38 6         24 }
39             );
40              
41 1 50   1   3946 method xml_write {
  1     6   3  
  1         125  
  3         542  
  3         10  
  3         13  
42 3         15 return $self->xml_root->xml_xs->XMLout(
43             $self->xml_root->xml_data,
44             KeepRoot => 1,
45             ContentKey => 'xml_content',
46             );
47             }
48              
49 1 50   1   16305 method xml_read (Str $xml) {
  1 50   3   2  
  1 50       149  
  1 50       6  
  1 50       2  
  1         103  
  3         22  
  3         14  
  3         13  
  3         9  
  0         0  
  0         0  
  0         0  
  3         8  
50 3         14 $self->xml_data(
51             $self->xml_xs->XMLin(
52             $xml,
53             ForceContent => 1,
54             KeepRoot => 1,
55             ForceArray => 1,
56             ContentKey => 'xml_content',
57             )
58             );
59 7         137 return $self;
60             }
61              
62 1 50   1   1075 method xml_root {
  1     14   2  
  1         132  
  3         15  
  0         0  
  7         212  
63 0 100       0 if ( defined( $self->xml_parent ) ) {
64 3         46 return $self->xml_parent->xml_root;
65             }
66             else {
67 3         1159 return $self;
68             }
69             }
70              
71 1 50   1   2328 multi method xml_attr (HashRef $attr) {
  1 50   3   2  
  1 50       266  
  1 50       6  
  1 50       5  
  1 0       247  
  3 0       14  
  3 50       8  
  3         9  
  3         9  
  3         9  
  3         4  
  3         4  
  3         7  
  3         45  
  5         126  
  3         155  
  1         132  
72 1         5 foreach my $attribute (keys %$attr) {
73 1 50 33     4 if (
74             $self->xml_autovivify
75             || grep( /^$attribute$/,
76             keys %{
77 1         5 $self->xml_parent->xml_data->{ $self->xml_node }
78             ->[ $self->xml_index ]
79             } )
80             )
81             {
82             $self->xml_parent->xml_data->{ $self->xml_node }
83             ->[ $self->xml_index ]->{$attribute} =
84 1         2 $attr->{$attribute};
85             }
86             else {
87 1         4 die qq|$attribute is not an attribute of | . $self->xml_node;
88             }
89             }
90 1         2 return $self;
91             }
92              
93 1 50   1   1250 multi method xml_attr () {
  1 50   3   2  
  1 50       226  
  3 50       8  
  1         20  
  1         11  
  6         472  
  6         31  
  6         29  
  6         26  
94 2         6 my %attr;
95 2         11 foreach ( keys %{ $self->xml_data } ) {
  6         14  
96             $attr{$_} = $self->xml_data->{$_}
97 6 100       23 if ( !( UNIVERSAL::isa( $self->xml_data->{$_}, 'ARRAY' ) ) );
98             }
99 2         54 return \%attr;
100             }
101              
102 1 50   1   1969 method xml_rmattr (Str $attr) {
  1 50   1   3  
  1 50       129  
  1 50       8  
  1 50       4  
  1         122  
  3         17  
  2         20  
  4         96  
  2         102  
  2         7  
  2         7  
  2         8  
  2         3  
103             delete $self->xml_parent->xml_data->{ $self->xml_node }
104 2         8 ->[ $self->xml_index ]->{$attr};
105 2         4 return $self;
106             }
107              
108 1 50   1   5183 method xml_content (Str $content?) {
  1 50   6   3  
  1 100       133  
  1 50       6  
  1 100       2  
  1         127  
  3         7  
  2         33  
  2         26  
  1         788  
  1         4  
  1         3  
  1         4  
  1         4  
109 1 100       3 if ($content) {
110 1         11 $self->xml_data->{xml_content} = $content;
111 0         0 return $self;
112             }
113             else {
114 0         0 $self->xml_data->{xml_content};
115             }
116             }
117              
118 1 50   1   7184 method xml_nest (InstanceOf['XML::Simple::Sugar'] $xs) {
  1 50   2   4  
  1 50       221  
  1 50       9  
  1 50       4  
  1         158  
  3         84  
  0         0  
  1         5  
  1         3  
  0         0  
  0         0  
  0         0  
  1         3  
119 1         18 $self->xml_parent->xml_data->{ $self->xml_node }->[ $self->xml_index ]
120             = $xs->xml_data;
121 3         3812 return $self;
122             }
123              
124 1 50   1   5965 multi method xml_subnode (Str $node, InstanceOf['XML::Simple::Sugar'] $content) {
  1 50   1   4  
  1 50       246  
  1 50       7  
  1 50       1  
  1 0       132  
  1 0       6  
  1 50       1  
  1 50       90  
  3 0       101015  
  3 0       8  
  3 50       11  
  3         9  
  3         10  
  3         8  
  3         9  
  0         0  
  0         0  
  0         0  
  3         10  
  3         9  
  0         0  
  0         0  
  0         0  
  3         5  
  3         11  
125 3         53 $self->xml_data->{$node}->[ $self->xml_index ] = $content->xml_data;
126             }
127              
128 1 50   1   3291 multi method xml_subnode (Str $node, HashRef $content) {
  1 50   3   5  
  1 50       219  
  1 50       6  
  1 50       2  
  1 0       127  
  1 0       5  
  1 50       2  
  1 50       631  
  14 0       82  
  2 0       85  
  1 50       21  
  1         32  
  1         44  
  1         37  
  0         0  
  1         22  
  0         0  
  2         30  
  19         12316  
  19         57  
  19         83  
  19         57  
  19         53  
  19         60  
  19         47  
129 0         0 foreach my $attribute (keys %$content) {
130 0 100       0 if ( UNIVERSAL::isa( $self->xml_data->{$node}, 'ARRAY' ) ) {
131 0 100 66     0 if (
132             $self->xml_autovivify
133             || grep( /^$attribute$/,
134             keys %{
135 19         56 $self->xml_data->{$node}->[ $self->xml_index ]
136             } )
137             )
138             {
139             $self->xml_data->{$node}->[ $self->xml_index ]
140 19         45 ->{$attribute} = $content->{$attribute};
141             }
142             else {
143 0         0 die qq|$attribute is not an attribute of $node|;
144             }
145             }
146             else {
147 0 50 33     0 if (
148             $self->xml_autovivify
149             || grep( /^$attribute$/,
150 0         0 keys %{ $self->xml_data->{$node} } )
151             )
152             {
153             $self->xml_data->{$node}->{$attribute} =
154 19         29 { 'value' => $content->{$attribute} };
155             }
156             else {
157 19         147 die qq|$attribute is not an attribute of $node|;
158             }
159             }
160             }
161 17         376 return $self;
162             }
163              
164 1 50   1   3495 multi method xml_subnode (Str $node, ArrayRef $content) {
  1 50   19   2  
  1 50       209  
  1 50       6  
  1 50       2  
  1 0       126  
  1 0       5  
  1 50       2  
  1 50       996  
  14 0       18  
  17 0       410  
  6 50       140  
  17         379  
  8         283  
  0         0  
  0         0  
  0         0  
  0         0  
  17         576  
  17         2305  
  1         27  
  2         110  
  17         62  
  2         81  
  17         153  
  2         47  
165 1 100       23 if ( $content->[0] =~ m/^[0-9]+$/ )
    50          
166             {
167 2 50       123 if ( $self->xml_autovivify ) {
168 1 100       15 unless ( $self->xml_data->{$node} ) {
169 1         44 $self->xml_data->{$node} = [];
170             }
171 12 100       8577 unless (
172             UNIVERSAL::isa(
173             $self->xml_data->{$node}->[ $content->[0] ], 'HASH'
174             )
175             )
176             {
177 12         32 $self->xml_data->{$node}->[ $content->[0] ] = {};
178             }
179             }
180             else {
181 12 0       40 unless ( $self->xml_data->{$node} ) {
182 12         37 die qq|$node is not a subnode of |
183             . $self->xml_parent->xml_node;
184             }
185 12 0       39 unless (
186             UNIVERSAL::isa(
187             $self->xml_data->{$node}->[ $content->[0] ], 'HASH'
188             )
189             )
190             {
191 12         37 die qq|$content->[0] is not a subnode of |
192             . $self->xml_node;
193             }
194             }
195             my $xs = XML::Simple::Sugar->new(
196             {
197             xml_node => $node,
198 12         37 xml_data => $self->xml_data->{$node}->[ $content->[0] ],
199             xml_parent => $self,
200             xml_autovivify => $self->xml_autovivify,
201             xml_index => $content->[0]
202             }
203             );
204 0 100 100     0 if ( defined( $content->[1] )
    100          
205             && UNIVERSAL::isa( $content->[1], 'XML::Simple::Sugar' ) )
206             {
207 0         0 $xs->xml_nest( $content->[1] );
208             }
209             elsif ( defined( $content->[1] ) ) {
210 0         0 $xs->xml_content( $content->[1] );
211             }
212 12 100 66     37 if ( defined( $content->[2] )
213             && UNIVERSAL::isa( $content->[2], 'HASH' ) )
214             {
215 12         28 $xs->xml_attr( $content->[2] );
216             }
217 0         0 return $xs;
218             }
219             elsif ( $content->[0] =~ m/^all$/i )
220             {
221 0 100       0 if ( UNIVERSAL::isa( $self->xml_data->{$node}, 'ARRAY' ) ) {
222             return map {
223             XML::Simple::Sugar->new(
224             {
225             xml_node => $node,
226 12         19 xml_data => $self->xml_data->{$node}->[$_],
227             xml_parent => $self,
228             xml_autovivify => $self->xml_autovivify,
229             xml_index => $_
230             }
231             );
232 0         0 } 0 .. scalar @{ $self->xml_data->{$node} } - 1;
  12         276  
233             }
234             }
235 12         165 return;
236             }
237            
238 1 50   1   3064 multi method xml_subnode (Str $node, Str $content) {
  1 50   12   2  
  1 50       267  
  1 50       7  
  1 50       2  
  1 0       134  
  1 0       6  
  1 50       2  
  1 50       96  
  14 0       48  
  51 0       68604  
  51 50       126  
  51         138  
  51         155  
  51         141  
  51         145  
  51         143  
  0         0  
  0         0  
  0         0  
  51         89  
  51         1026  
  22         513  
  21         420  
  1         18  
  50         1132  
239 50         2035 $self->xml_data->{$node}->[0]->{xml_content} = $content;
240 0         0 return $self;
241             }
242              
243 1 50   1   2196 multi method xml_subnode (Str $node) {
  1 50   51   2  
  1 50       211  
  1 50       6  
  1 50       1  
  1 0       405  
  2 0       13  
  177 50       28884  
  177         413  
  177         404  
  177         273  
  177         824  
  177         1192  
  86         1869  
244   100         unless ( $self->xml_data->{$node} ) {
245   100         if ( $self->xml_autovivify == 1 ) {
246             $self->xml_data->{$node}->[0] = {};
247             }
248             else {
249             die qq|$node is not a subnode of |
250             . $self->xml_node;
251             }
252             }
253              
254   50         if ( UNIVERSAL::isa( $self->xml_data->{$node}, 'ARRAY' ) ) {
255             return XML::Simple::Sugar->new(
256             {
257             xml_node => $node,
258             xml_data => $self->xml_data->{$node}->[0],
259             xml_parent => $self,
260             xml_autovivify => $self->xml_autovivify,
261             xml_index => $self->xml_index
262             }
263             );
264             }
265             else {
266             return XML::Simple::Sugar->new(
267             {
268             xml_node => $node,
269             xml_data => $self->xml_data->{$node},
270             xml_parent => $self,
271             xml_autovivify => $self->xml_autovivify,
272             xml_index => $self->xml_index
273             }
274             );
275             }
276             }
277              
278 1 50   1   1924 method AUTOLOAD ($content?) {
  1 50   177   3  
  1 100       175  
  12         288  
279             my ( $node ) = $AUTOLOAD =~ m/.*::(.+)$/;
280   100         return if $node eq 'DESTROY';
281   100         $content ? $self->xml_subnode($node, $content) : $self->xml_subnode($node);
282             }
283             }
284              
285             1;
286              
287             # ABSTRACT: Sugar sprinkled on XML::Simple
288             # PODNAME: XML::Simple::Sugar
289              
290             __END__
291              
292             =pod
293              
294             =encoding UTF-8
295              
296             =head1 NAME
297              
298             XML::Simple::Sugar - Sugar sprinkled on XML::Simple
299              
300             =head1 VERSION
301              
302             version v1.1.2
303              
304             =head1 SYNOPSIS
305              
306             use Modern::Perl;
307             use XML::Simple::Sugar;
308            
309             my $xs = XML::Simple::Sugar->new;
310            
311             # Autovivify some XML elements
312             my $person = $xs->company->departments->department->person;
313            
314             # Set some content and attributes
315             $person->first_name('John')
316             ->last_name('Smith')
317             ->email('jsmith@example.com')
318             ->salary(60000);
319            
320             $person->xml_attr( { position => 'Engineer' } );
321            
322             say $xs->xml_write;
323            
324             # <?xml version="1.0"?>
325             # <company>
326             # <departments>
327             # <department>
328             # <person position="Engineer">
329             # <email>jsmith@example.com</email>
330             # <first_name>John</first_name>
331             # <last_name>Smith</last_name>
332             # <salary>60000</salary>
333             # </person>
334             # </department>
335             # </departments>
336             # </company>
337              
338             =head1 DESCRIPTION
339              
340             This module is a wrapper around L<XML::Simple> to provide AUTOLOADed accessors to XML nodes in a given XML document. All nodes of the XML document are XML::Simple::Sugar objects having the following attributes and methods.
341              
342             =head1 ATTRIBUTES
343              
344             =head2 xml (XML Str)
345              
346             This readonly attribute is for use during instantiation (XML::Simple::Sugar->new({ xml => $xml_string })). See also L</xml_read>.
347              
348             =head2 xml_autovivify (Bool DEFAULT true)
349              
350             This attribute determines on a per element basis whether new attributes or elements may be introduced. Child elements inherit this setting from their parent. Setting autovivify to false is useful when working with templates with a strict predefined XML structure. This attribute is true by default.
351              
352             my $xs = XML::Simple::Sugar->new(
353             {
354             xml => qq(
355             <strict_document>
356             <foo>bar</foo>
357             </strict_document>
358             ),
359             xml_autovivify => 0,
360             }
361             );
362              
363             $xs->strict_document->foo('baz'); # Changes bar to baz. Ok!
364             $xs->strict_document->biz('a new element'); # Error! Biz doesn't exist!
365              
366             =head2 xml_data (XML::Simple compliant Perl representation of an XML document)
367              
368             This is the Perl representation of the XML. This is ugly to work with directly (hence this module), but in lieu of methods yet unwritten there may be a use case for having direct access to this structure.
369              
370             =head2 xml_index
371              
372             The index number of an element in a collection
373              
374             =head2 xml_node
375              
376             The name of the current node
377              
378             =head2 xml_parent
379              
380             The parent XML::Simple::Sugar object to the current element
381              
382             =head2 xml_xs
383              
384             This is underlying XML::Simple object. If you need to adjust the XML declaration, you can do that by passing an an XML::Simple object with your preferred options to the C<new> constructor. Be wary of setting other XML::Simple options as this module will happily overwrite anything that conflicts with its assumptions.
385              
386             =head2 xml_root
387              
388             Returns the root element XML::Simple::Sugar object
389              
390             =head1 METHODS
391              
392             =head2 xml_read (XML Str)
393              
394             Parses an XML string and sets the data attribute
395              
396             =head2 xml_write
397              
398             Writes out an XML string
399              
400             =head2 xml_content (Str)
401              
402             Gets or sets the content of the element
403              
404             $xs->person->first_name->xml_content('Bob');
405              
406             # Which can be implicitly written
407             $xs->person->first_name('Bob');
408              
409             # Or using [ index, content, attributes ] notation
410             $xs->person->first_name([ 0, 'Bob', undef ]);
411              
412             say $xs->person->first_name->xml_content;
413             # Bob
414              
415             =head2 xml_attr (HashRef)
416              
417             Gets or sets the attributes of the element.
418              
419             $xs->person->xml_attr( { position => 'Accountant' } );
420              
421             # Which can be implicitly written as...
422             $xs->person( { position => 'Accountant' } );
423              
424             # Or using [ index, content, attributes ] notation
425             $xs->person([ 0, undef, { position => 'Accountant' } ]);
426              
427             my $attributes = $xs->person->xml_attr;
428             say $attributes->{'position'};
429             # Accountant
430              
431             =head2 xml_rmattr (Str)
432              
433             This method removes the passed scalar argument from the element's list of attributes.
434              
435             =head2 xml_nest (XML::Simple::Sugar)
436              
437             Merges another XML::Simple::Sugar object as a child of the current node.
438              
439             my $first_name = XML::Simple::Sugar->new({ xml => '<first_name>Bob</first_name>' });
440             $xs->person->xml_nest( $first_name );
441              
442             # Or using [ index, content, attributes ] notation
443             $xs->person( [ 0, $first_name, undef ] );
444              
445             =head1 Collections
446              
447             When working with a collection of same-named elements, you can access a specific element by index by passing the collection's name an ArrayRef with the index number. For example:
448              
449             my $person2 = $xs->people->person([1]); # Returns the second person element (index 1)
450              
451             You can also work with the entire collection of individual elements by passing an ArrayRef with the string 'all'.
452              
453             my @people = $xs->people->person(['all']); # Returns an array of XML::Simple::Sugar objects
454              
455             =head1 Using [ index, content, attributes ] Notation
456              
457             When authoring even simple XML documents, using [ index, content, attributes ] notation allows you to implicitly invoke L</xml_content>, L</xml_attr>, and L</xml_nest> methods on nodes deep within a collection. For example:
458              
459             # Sets the position attribute of the second person
460             $xs->people->person([ 1, undef, { position => 'Engineer' } ]);
461              
462             # Sets the third person's second favorite color to orange
463             # with a neon attribute
464             $xs->people->person([ 2 ])->favorite_colors->color([ 1, 'orange', { neon => 1 } ]);
465              
466             # Composing large documents from smaller ones
467             my $xs = XML::Simple::Sugar->new( {
468             xml_xs => XML::Simple->new( XMLDecl => '<!DOCTYPE html>' )
469             } );
470             my $xs2 = XML::Simple::Sugar->new;
471              
472             $xs2->table->tr->th([ 0, 'First Name', { style => 'text-align:left' } ]);
473             $xs2->table->tr->th([ 1, 'Last Name' ]);
474              
475             $xs->html->body->div([0])->h1('Page Title');
476             $xs->html->body->div([ 1, $xs2 ]);
477              
478             say $xs->xml_write;
479              
480             # <!DOCTYPE html>
481             # <html>
482             # <body>
483             # <div>
484             # <h1>Page Title</h1>
485             # </div>
486             # <div>
487             # <table>
488             # <tr>
489             # <th style="text-align:left">First Name</th>
490             # <th>Last Name</th>
491             # </tr>
492             # </table>
493             # </div>
494             # </body>
495             # </html>
496              
497             =head1 PLEASE BE ADVISED
498              
499             Most of the automagic happens with AUTOLOAD. Accessors/mutators and method names in this package cannot be used as element names in the XML document. XML naming rules prohibit the use of elements starting with the string "xml", so "xml_" is used as a prefix for all accessors/mutators/methods to avoid potential document conflicts.
500              
501             =head1 REPOSITORY
502              
503             L<https://github.com/Camspi/XML-Simple-Sugar>
504              
505             =head1 MINIMUM PERL VERSION SUPPORTED
506              
507             Perl 5.18.2 or later is required by this module. Lesser Perl versions struggle with deep recursion. Patches welcome.
508              
509             =head1 VERSIONING
510              
511             Semantic versioning is adopted by this module. See L<http://semver.org/>.
512              
513             =head1 SEE ALSO
514              
515             =over 4
516              
517             =item
518             * L<XML::Simple>
519              
520             =back
521              
522             =head1 CREDITS
523              
524             =over 4
525              
526             =item
527             * Jonathan Cast for excellent critique.
528              
529             =item
530             * Kyle Bolton for peeking over my shoulder and giving me pro tips.
531              
532             =item
533             * eMortgage Logic, LLC., for allowing me to publish this module to CPAN
534              
535             =back
536              
537             =head1 AUTHOR
538              
539             Chris Tijerina
540              
541             =head1 COPYRIGHT AND LICENSE
542              
543             This software is copyright (c) 2014-2017 by eMortgage Logic LLC.
544              
545             This is free software; you can redistribute it and/or modify it under
546             the same terms as the Perl 5 programming language system itself.
547              
548             =cut