(This is the XML variant of JSONizing nested Perl objects)
Creating an XML presentation of my object, I deferred creating XML for the sub-objects to those. After an initial version was working, but created an unnecessarily deeply nested structure, I tried to flatten the structure somewhat, but ran into a problem I was able to solve via some ugly work-around only.
Let's view this code snippet (executed in a foreach
loop):
my $child = XML::Twig::Elt->new('sample', { 'name' => $_ });
my $F = $val->{$_}->XML_string($child);
$F->print;
$child = $elt->insert_new_elt(last_child => 'dummy');
$child->replace_with($F);
$F
(assigned to a temporary variable for debugging purposes) contains the XML sub-tree for the object $val->{$_}
($val
is the current object slot being processed, and in this case it's a collection (hash) of sub-objects.
So $val->{$_}
is the sub-object to process).
The sub-tree is added as children of the extra parameter ($child
, i.e.: sample
).
What I wanted to do was $elt->insert_new_elt(last_child => $F);
, but that never worked.
So I insert a dummy
element at the right position, just to replace it afterwards.
Here is some debugging output for these lines:
### The parent node where the new 'sample' children should be added
DB<4> p $elt->print
<perf_data/>
### The new child (it's still too complex...)
DB<5> p $F->print
<sample name="max"><label>max</label><value>2.48584</value><unit/><warn><range part="end">0.5</range><range part="inverted">0</range><range part="start">0</range></warn><crit><range part="end">1</range><range part="inverted">0</range><range part="start">0</range></crit><min>0</min><max/></sample>
### the dummy child
DB<6> p $child->print
<dummy/>
### The parent after adding the dummy child
DB<7> p $elt->print
<perf_data><dummy/></perf_data>
### the parent after having replaced the dummy child with the real one
DB<8> p $elt->print
<perf_data><sample name="max"><label>max</label><value>2.48584</value><unit/><warn><range part="end">0.5</range><range part="inverted">0</range><range part="start">0</range></warn><crit><range part="end">1</range><range part="inverted">0</range><range part="start">0</range></crit><min>0</min><max/></sample></perf_data>
### more 'sample' elements to follow...
How can I avoid the temporary dummy
element?
I tried to find pout from the docs, but failed.
Maybe to add the children in-order, I should use something other than insert_new_elt
(maybe an append_new_elt
?), but most of the docs is about parsing existing XML, not constructing XML.
As I was asked to provide a simplified version, here is one. However it's only a little bit similar to the original data structures. At least I tried to preserve the essence of the problem.
So here is the code:
#!/usr/bin/perl
use strict;
use warnings;
use 5.018;
package FOO;
use constant ATTRIBUTES => (
['a', 0],
['b', 1],
['c', 2],
['d', 3],
);
sub new($)
{
my $class = shift;
my $self = [];
$#$self = 4;
bless $self, $class;
foreach (@$self) {
$_ = int(rand(10))
}
return $self;
}
use XML::Twig;
sub XML_string($;$)
{
my ($self, $root) = @_;
$root //= XML::Twig::Elt->new(__PACKAGE__);
foreach (ATTRIBUTES) {
my ($name, $i) = @$_[0, 1];
if (defined(my $val = $self->[$i])) {
my $elt = $root->insert_new_elt(last_child => $name);
if ($i == 1 || $i == 2) {
my $range = $elt->insert_new_elt(last_child => 'range');
$range->set_att('baz' => $val);
}
} # else leave out
}
return $root;
}
package BAR;
use constant ATTRIBUTES => (
['e', 0],
['f', 1],
['g', 2],
['h', 3],
);
sub new($)
{
my $class = shift;
my $self = [];
$#$self = 4;
bless $self, $class;
foreach (@$self) {
$_ = int(rand(10))
}
return $self;
}
use XML::Twig;
sub XML_string($;$)
{
my $self = shift;
my $xml = XML::Twig->new()->set_xml_version('1.0')->set_encoding('utf-8');
my $b = XML::Twig::Elt->new(__PACKAGE__, {'version' => '1.0'});
$xml->set_root($b);
foreach (ATTRIBUTES) {
my ($name, $i) = @$_[0, 1];
if (defined(my $val = $self->[$i])) {
my $elt = $b->insert_new_elt(last_child => $name);
if ($i == 1) {
my $e = $elt->insert_new_elt(last_child => 'sample');
my $F = $val->XML_string($e);
#print $F->print,"\n";;
$e = $elt->insert_new_elt(last_child => 'dummy');
$e->replace_with($F);
}
} # else leave out
}
if ($#_ >= 0 && $_[0]) {
$xml->set_pretty_print('indented');
}
return $xml->sprint();
}
package main;
my $f1 = FOO->new();
my $f2 = FOO->new();
my $b = BAR->new();
$b->[1] = $f1;
$b->[2] = $f2;
print $b->XML_string(1), "\n";
And a sample output might look like this:
DB<3> x $b
0 BAR=ARRAY(0x1b83860)
0 1
1 FOO=ARRAY(0xe2ad40)
0 2
1 4
2 0
3 4
4 7
2 FOO=ARRAY(0x1633788)
0 3
1 8
2 0
3 1
4 0
3 3
4 1
DB<4> n
<?xml version="1.0" encoding="utf-8"?>
<BAR version="1.0">
<e/>
<f>
<sample>
<a/>
<b>
<range baz="4"/>
</b>
<c>
<range baz="0"/>
</c>
<d/>
</sample>
</f>
<g/>
<h/>
</BAR>
I hope it's similar enough to the original problem.
Don't use the constructor for the new element. Use the constructor parameters directly for insert_new_elt
:
my $child = $elt->insert_new_elt(last_child => 'sample', {name => $_});
$val->{$_}->XML_string($child);
or, use paste
:
my $child = XML::Twig::Elt->new('sample', {name => $_});
$child->paste(last_child => $elt);