File Coverage

blib/lib/XML/Simple/Sugar.pm
Criterion Covered Total %
statement 317 356 89.0
branch 111 230 48.2
condition 9 15 60.0
subroutine 62 62 100.0
pod n/a
total 499 663 75.2


line stmt bran cond sub pod time code
1 1     1   827 use 5.18.0;
  2         7285  
2 2     1   1572 use Modern::Perl;
  2         13747  
  2         825  
3 2     1   1204 use Moops;
  2         61052  
  2         152  
4              
5 2     1   185709 class XML::Simple::Sugar 1.1.0 {
  2     1   255  
  2     1   163  
  2     1   154  
  2     1   206  
  2     1   945  
  2     1   2558  
  2     1   191  
  2     1   2202  
  2     1   174  
  1     1   9  
  1     1   63  
  1     1   2  
  1     1   55  
  1         6  
  1         2  
  1         117  
  1         34  
  1         6  
  1         2  
  1         11  
  1         4183  
  1         2  
  1         8  
  1         1265  
  1         3862  
  1         4  
  1         159  
  1         2  
  1         10  
  1         876  
  1         9643  
  1         12  
  1         1036  
  1         3228  
  1         7  
  1         1822  
  1         4371  
  1         12  
  1         133080  
  1         3  
  1         5  
  1         2  
  1         26  
  1         4  
  1         2  
  1         42  
  1         5  
  1         3  
  1         138  
  1         4021  
  3         1868  
6 1         12 our $AUTOLOAD;
7 1     1   1340 use XML::Simple;
  1         13236  
  1         6  
8 1     1   889 use UNIVERSAL::isa;
  1         1325  
  1         5  
9 1     1   45 use overload '""' => 'xml_write';
  1         1  
  1         9  
10              
11 1         6 has 'xml_index' => ( 'is' => 'ro', 'isa' => 'Int', default => 0 );
12 1         2512 has 'xml_node' => ( 'is' => 'ro', 'isa' => Maybe[Str] );
13             has 'xml_xs' => (
14             'is' => 'rw',
15             'isa' => 'XML::Simple',
16 90         12827 'default' => sub { XML::Simple->new( XMLDecl => '<?xml version="1.0"?>' ); }
17 1         2158 );
18             has 'xml_data' => (
19             'is' => 'rw',
20             'isa' => Maybe[HashRef|ArrayRef],
21 1         2184 'default' => method { $self->xml_data ? $self->xml_data : {}; }
22             );
23 6         1105 has 'xml_parent' => ( 'is' => 'ro', 'isa' => InstanceOf['XML::Simple::Sugar'] );
24 6         12 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         23 }
39             );
40              
41 1 50   1   7018 method xml_write {
  1     6   23  
  1         121  
  3         378  
  3         6  
  3         10  
42 3         9 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   20852 method xml_read (Str $xml) {
  1 50   1   2  
  1 50   3   167  
  1 50       4  
  1 50       2  
  1         117  
  3         13  
  3         8  
  3         8  
  3         9  
  0         0  
  0         0  
  0         0  
  3         4  
50 3         10 $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         165 return $self;
60             }
61              
62 1 50   1   1236 method xml_root {
  1     14   2  
  1         164  
  3         9  
  0         0  
  7         233  
63 0 100       0 if ( defined( $self->xml_parent ) ) {
64 3         40 return $self->xml_parent->xml_root;
65             }
66             else {
67 3         1104 return $self;
68             }
69             }
70              
71 1 50   1   2631 multi method xml_attr (HashRef $attr) {
  1 50   1   2  
  1 50   3   321  
  1 50       6  
  1 50       2  
  1 0       329  
  3 0       14  
  3 50       7  
  3         8  
  3         10  
  3         9  
  3         5  
  3         5  
  3         4  
  3         65  
  5         202  
  3         95  
  1         137  
72 1         4 foreach my $attribute (keys %$attr) {
73 1 50 33     5 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   1498 multi method xml_attr () {
  1 50   3   2  
  1 50       307  
  3 50       3  
  1         25  
  1         12  
  6         325  
  6         19  
  6         16  
  6         18  
94 2         3 my %attr;
95 2         8 foreach ( keys %{ $self->xml_data } ) {
  6         9  
96             $attr{$_} = $self->xml_data->{$_}
97 6 100       15 if ( !( UNIVERSAL::isa( $self->xml_data->{$_}, 'ARRAY' ) ) );
98             }
99 2         46 return \%attr;
100             }
101              
102 1 50   1   2131 method xml_rmattr (Str $attr) {
  1 50   1   2  
  1 50   1   165  
  1 50       5  
  1 50       2  
  1         117  
  3         12  
  2         16  
  4         84  
  2         110  
  2         6  
  2         8  
  2         7  
  2         2  
103             delete $self->xml_parent->xml_data->{ $self->xml_node }
104 2         9 ->[ $self->xml_index ]->{$attr};
105 2         3 return $self;
106             }
107              
108 1 50   1   5987 method xml_content (Str $content?) {
  1 50   1   3  
  1 100   6   173  
  1 50       5  
  1 100       1  
  1         186  
  3         4  
  2         43  
  2         27  
  1         754  
  1         3  
  1         5  
  1         4  
  1         5  
109 1 100       5 if ($content) {
110 1         12 $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   7251 method xml_nest (InstanceOf['XML::Simple::Sugar'] $xs) {
  1 50   1   2  
  1 50   2   177  
  1 50       6  
  1 50       1  
  1         127  
  3         48  
  0         0  
  1         5  
  1         4  
  0         0  
  0         0  
  0         0  
  1         1  
119 1         24 $self->xml_parent->xml_data->{ $self->xml_node }->[ $self->xml_index ]
120             = $xs->xml_data;
121 3         5466 return $self;
122             }
123              
124 1 50   1   4281 multi method xml_subnode (Str $node, InstanceOf['XML::Simple::Sugar'] $content) {
  1 50   1   2  
  1 50   1   301  
  1 50   1   6  
  1 50       1  
  1 0       178  
  1 0       5  
  1 50       2  
  1 50       108  
  3 0       112659  
  3 0       7  
  3 50       11  
  3         10  
  3         9  
  3         11  
  3         10  
  0         0  
  0         0  
  0         0  
  3         9  
  3         10  
  0         0  
  0         0  
  0         0  
  3         4  
  3         10  
125 3         69 $self->xml_data->{$node}->[ $self->xml_index ] = $content->xml_data;
126             }
127              
128 1 50   1   3463 multi method xml_subnode (Str $node, HashRef $content) {
  1 50   1   2  
  1 50   1   319  
  1 50   3   5  
  1 50       2  
  1 0       178  
  1 0       5  
  1 50       2  
  1 50       578  
  14 0       62  
  2 0       77  
  1 50       30  
  1         28  
  1         40  
  1         41  
  0         0  
  1         31  
  0         0  
  2         32  
  19         9866  
  19         35  
  19         45  
  19         50  
  19         42  
  19         40  
  19         40  
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         45 $self->xml_data->{$node}->[ $self->xml_index ]
136             } )
137             )
138             {
139             $self->xml_data->{$node}->[ $self->xml_index ]
140 19         36 ->{$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         21 { 'value' => $content->{$attribute} };
155             }
156             else {
157 19         102 die qq|$attribute is not an attribute of $node|;
158             }
159             }
160             }
161 17         360 return $self;
162             }
163              
164 1 50   1   3415 multi method xml_subnode (Str $node, ArrayRef $content) {
  1 50   1   3  
  1 50   1   284  
  1 50   19   10  
  1 50       2  
  1 0       209  
  1 0       5  
  1 50       2  
  1 50       948  
  14 0       15  
  17 0       452  
  6 50       151  
  17         436  
  8         273  
  0         0  
  0         0  
  0         0  
  0         0  
  17         536  
  17         1873  
  1         28  
  2         71  
  17         54  
  2         58  
  17         149  
  2         44  
165 1 100       23 if ( $content->[0] =~ m/^[0-9]+$/ )
    50          
166             {
167 2 50       152 if ( $self->xml_autovivify ) {
168 1 100       23 unless ( $self->xml_data->{$node} ) {
169 1         28 $self->xml_data->{$node} = [];
170             }
171 12 100       6913 unless (
172             UNIVERSAL::isa(
173             $self->xml_data->{$node}->[ $content->[0] ], 'HASH'
174             )
175             )
176             {
177 12         24 $self->xml_data->{$node}->[ $content->[0] ] = {};
178             }
179             }
180             else {
181 12 0       33 unless ( $self->xml_data->{$node} ) {
182 12         28 die qq|$node is not a subnode of |
183             . $self->xml_parent->xml_node;
184             }
185 12 0       40 unless (
186             UNIVERSAL::isa(
187             $self->xml_data->{$node}->[ $content->[0] ], 'HASH'
188             )
189             )
190             {
191 12         33 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         29 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     27 if ( defined( $content->[2] )
213             && UNIVERSAL::isa( $content->[2], 'HASH' ) )
214             {
215 12         29 $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         15 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         134 return;
236             }
237            
238 1 50   1   3468 multi method xml_subnode (Str $node, Str $content) {
  1 50   1   2  
  1 50   1   304  
  1 50   12   5  
  1 50       2  
  1 0       177  
  1 0       5  
  1 50       2  
  1 50       123  
  14 0       43  
  51 0       74423  
  51 50       92  
  51         157  
  51         122  
  51         113  
  51         108  
  51         108  
  0         0  
  0         0  
  0         0  
  51         63  
  51         1070  
  22         589  
  21         1393  
  1         20  
  50         1293  
239 50         1861 $self->xml_data->{$node}->[0]->{xml_content} = $content;
240 0         0 return $self;
241             }
242              
243 1 50   1   2490 multi method xml_subnode (Str $node) {
  1 50   1   2  
  1 50   51   285  
  1 50       5  
  1 50       2  
  1 0       427  
  2 0       10  
  86 50       9377  
  86         232  
  86         205  
  86         117  
  86         463  
  86         2085  
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   2052 method AUTOLOAD ($content?) {
  1 50   86   2  
  1 100       196  
  12         264  
279             my ( $node ) = $AUTOLOAD =~ m/.*::(.+)$/;
280   100         $content ? $self->xml_subnode($node, $content) : $self->xml_subnode($node);
281             }
282             }
283              
284             1;
285              
286             # ABSTRACT: Sugar sprinkled on XML::Simple
287             # PODNAME: XML::Simple::Sugar
288              
289             __END__
290              
291             =pod
292              
293             =encoding UTF-8
294              
295             =head1 NAME
296              
297             XML::Simple::Sugar - Sugar sprinkled on XML::Simple
298              
299             =head1 VERSION
300              
301             version v1.1.0
302              
303             =head1 SYNOPSIS
304              
305             use Modern::Perl;
306             use XML::Simple::Sugar;
307            
308             my $xs = XML::Simple::Sugar->new;
309            
310             # Autovivify some XML elements
311             my $person = $xs->company->departments->department->person;
312            
313             # Set some content and attributes
314             $person->first_name('John')
315             ->last_name('Smith')
316             ->email('jsmith@example.com')
317             ->salary(60000);
318            
319             $person->xml_attr( { position => 'Engineer' } );
320            
321             say $xs->xml_write;
322            
323             # <?xml version="1.0"?>
324             # <company>
325             # <departments>
326             # <department>
327             # <person position="Engineer">
328             # <email>jsmith@example.com</email>
329             # <first_name>John</first_name>
330             # <last_name>Smith</last_name>
331             # <salary>60000</salary>
332             # </person>
333             # </department>
334             # </departments>
335             # </company>
336              
337             =head1 DESCRIPTION
338              
339             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.
340              
341             =head1 ATTRIBUTES
342              
343             =head2 xml (XML Str)
344              
345             This readonly attribute is for use during instantiation (XML::Simple::Sugar->new({ xml => $xml_string })). See also L</xml_read>.
346              
347             =head2 xml_autovivify (Bool DEFAULT true)
348              
349             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.
350              
351             my $xs = XML::Simple::Sugar->new(
352             {
353             xml => qq(
354             <strict_document>
355             <foo>bar</foo>
356             </strict_document>
357             ),
358             xml_autovivify => 0,
359             }
360             );
361              
362             $xs->strict_document->foo('baz'); # Changes bar to baz. Ok!
363             $xs->strict_document->biz('a new element'); # Error! Biz doesn't exist!
364              
365             =head2 xml_data (XML::Simple compliant Perl representation of an XML document)
366              
367             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.
368              
369             =head2 xml_index
370              
371             The index number of an element in a collection
372              
373             =head2 xml_node
374              
375             The name of the current node
376              
377             =head2 xml_parent
378              
379             The parent XML::Simple::Sugar object to the current element
380              
381             =head2 xml_xs
382              
383             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.
384              
385             =head2 xml_root
386              
387             Returns the root element XML::Simple::Sugar object
388              
389             =head1 METHODS
390              
391             =head2 xml_read (XML Str)
392              
393             Parses an XML string and sets the data attribute
394              
395             =head2 xml_write
396              
397             Writes out an XML string
398              
399             =head2 xml_content (Str)
400              
401             Gets or sets the content of the element
402              
403             $xs->person->first_name->xml_content('Bob');
404              
405             # Which can be implicitly written
406             $xs->person->first_name('Bob');
407              
408             # Or using [ index, content, attributes ] notation
409             $xs->person->first_name([ 0, 'Bob', undef ]);
410              
411             say $xs->person->first_name->xml_content;
412             # Bob
413              
414             =head2 xml_attr (HashRef)
415              
416             Gets or sets the attributes of the element.
417              
418             $xs->person->xml_attr( { position => 'Accountant' } );
419              
420             # Which can be implicitly written as...
421             $xs->person( { position => 'Accountant' } );
422              
423             # Or using [ index, content, attributes ] notation
424             $xs->person([ 0, undef, { position => 'Accountant' } ]);
425              
426             my $attributes = $xs->person->xml_attr;
427             say $attributes->{'position'};
428             # Accountant
429              
430             =head2 xml_rmattr (Str)
431              
432             This method removes the passed scalar argument from the element's list of attributes.
433              
434             =head2 xml_nest (XML::Simple::Sugar)
435              
436             Merges another XML::Simple::Sugar object as a child of the current node.
437              
438             my $first_name = XML::Simple::Sugar->new({ xml => '<first_name>Bob</first_name>' });
439             $xs->person->xml_nest( $first_name );
440              
441             # Or using [ index, content, attributes ] notation
442             $xs->person( [ 0, $first_name, undef ] );
443              
444             =head1 Collections
445              
446             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:
447              
448             my $person2 = $xs->people->person([1]); # Returns the second person element (index 1)
449              
450             You can also work with the entire collection of individual elements by passing an ArrayRef with the string 'all'.
451              
452             my @people = $xs->people->person(['all']); # Returns an array of XML::Simple::Sugar objects
453              
454             =head1 Using [ index, content, attributes ] Notation
455              
456             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:
457              
458             # Sets the position attribute of the second person
459             $xs->people->person([ 1, undef, { position => 'Engineer' } ]);
460              
461             # Sets the third person's second favorite color to orange
462             # with a neon attribute
463             $xs->people->person([ 2 ])->favorite_colors->color([ 1, 'orange', { neon => 1 } ]);
464              
465             # Composing large documents from smaller ones
466             my $xs = XML::Simple::Sugar->new( {
467             xml_xs => XML::Simple->new( XMLDecl => '<!DOCTYPE html>' )
468             } );
469             my $xs2 = XML::Simple::Sugar->new;
470              
471             $xs2->table->tr->th([ 0, 'First Name', { style => 'text-align:left' } ]);
472             $xs2->table->tr->th([ 1, 'Last Name' ]);
473              
474             $xs->html->body->div([0])->h1('Page Title');
475             $xs->html->body->div([ 1, $xs2 ]);
476              
477             say $xs->xml_write;
478              
479             # <!DOCTYPE html>
480             # <html>
481             # <body>
482             # <div>
483             # <h1>Page Title</h1>
484             # </div>
485             # <div>
486             # <table>
487             # <tr>
488             # <th style="text-align:left">First Name</th>
489             # <th>Last Name</th>
490             # </tr>
491             # </table>
492             # </div>
493             # </body>
494             # </html>
495              
496             =head1 PLEASE BE ADVISED
497              
498             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.
499              
500             =head1 REPOSITORY
501              
502             L<https://github.com/Camspi/XML-Simple-Sugar>
503              
504             =head1 MINIMUM PERL VERSION SUPPORTED
505              
506             Perl 5.18.2 or later is required by this module. Lesser Perl versions struggle with deep recursion. Patches welcome.
507              
508             =head1 VERSIONING
509              
510             Semantic versioning is adopted by this module. See L<http://semver.org/>.
511              
512             =head1 SEE ALSO
513              
514             =over 4
515              
516             =item
517             * L<XML::Simple>
518              
519             =back
520              
521             =head1 CREDITS
522              
523             =over 4
524              
525             =item
526             * Jonathan Cast for excellent critique.
527              
528             =item
529             * Kyle Bolton for peeking over my shoulder and giving me pro tips.
530              
531             =item
532             * eMortgage Logic, LLC., for allowing me to publish this module to CPAN
533              
534             =back
535              
536             =head1 AUTHOR
537              
538             Chris Tijerina
539              
540             =head1 COPYRIGHT AND LICENSE
541              
542             This software is copyright (c) 2014 by eMortgage Logic LLC.
543              
544             This is free software; you can redistribute it and/or modify it under
545             the same terms as the Perl 5 programming language system itself.
546              
547             =cut