0

I have got perl data something like below:

$data = {
   id => 1,
   name => "A",
   users  => [ { id => 1, name => "u1" }, { id => 2, name => "u2" } ],
   groups => [ { id => 1, name => "g1" } ]
};

I would like to convert this into an xml something like below:

<map>
  <item id="1" name="A">
     <users>
        <user id="1" name="u1"/>
        <user id="2" name="u2"/> 
     </users>
     <groups>
        <group id="1" name="g1"/>
     </groups>         
  </item>
</map>

I could do that manually creating each line explicitly. However I am looking for any CPAN Module base solution.

I tried XML::Twig but didn't go anywhere. I have used XML::Simple in the past for such thing but this time wanted to try something else as XML::Simple has been getting bad reviews.

  • 1
    How did you create this data structure in the first place? It's far from ideal for creating XML data, and looks like something that `XML::Simple` would create. For instance, there's no way of knowing the names of the elements inside `groups` and `users`. And where do the names `map` and `item` come from? – Borodin Mar 16 '15 at 16:36
  • I think you'll get something like that from parsing JSON - JSON has arrays in it, where XML doesn't. – Sobrique Mar 16 '15 at 16:40
  • @Sobrique: But `XML::Simple` creates stuff like that :) – Borodin Mar 16 '15 at 16:43
  • Another reason not to like `XML::Simple` then :) – Sobrique Mar 16 '15 at 16:46

3 Answers3

1

You can do it similarly to Sobrique's method but with less hardcoded strings, like this:

#!/usr/bin/env perl
use strict; use warnings;

use XML::Twig;

my $data = {
    id => 1,
    name => "A",
    users  => [ { id => 1, name => "u1" }, { id => 2, name => "u2" } ],
    groups => [ { id => 1, name => "g1" } ]
};

sub array_to_elts {
    my ( $root, $name, $arrayref ) = @_;
    map { $root->insert_new_elt($name, $_) } @{ $arrayref };
}

my $twig  = XML::Twig
    ->new()
    ->set_xml_version("1.0")
    ->set_encoding('utf-8');

my $map = XML::Twig::Elt->new('map');
$twig->set_root($map);

my $item  = $map->insert_new_elt(
    'item',
    { id => $data->{'id'}, name => $data->{'name'} },
);

my $lines = $item->insert_new_elt('groups');
my $links = $item->insert_new_elt('users' );

array_to_elts($lines, 'group', $data->{'groups'});
array_to_elts($links, 'user',  $data->{'users' });

$twig->set_pretty_print('indented');
$twig->print;

You could go to extreme lengths to reduce the hardcoded vals and base more off the raw data, but it quickly gets harder to read..

David K-J
  • 930
  • 7
  • 14
1

"Generic" way using XML::LibXML. You might need to add new code to the "else" part to handle other types of structures.

#!/usr/bin/perl
use warnings;
use strict;

use XML::LibXML;

my $data = {
            id     => 1,
            name   => "A",
            users  => [ { id => 1, name => "u1" },
                        { id => 2, name => "u2" } ],
            groups => [ { id => 1, name => "g1" } ],
           };

sub to_xml {
    my ($data, $xml) = @_;
    for my $entry (keys %$data) {
        my $ref = ref $data->{$entry};
        if (not $ref) {
            $xml->setAttribute($entry, $data->{$entry});

        } elsif ('ARRAY' eq $ref) {
            (my $name = $entry) =~ s/s$// or die "Can't guess the element name.\n";
            my $list = $xml->addNewChild(q(), $entry);
            for my $inner (@{ $data->{$entry} }) {
                to_xml($inner, $list->addNewChild(q(), $name));
            }

        } else {
            die "Unhandled structure $ref.\n";
        }
    }
}

my $xml = 'XML::LibXML::Document'->createDocument;
my $root = $xml->createElement('map');
$xml->setDocumentElement($root);
for my $entry ($data) {
    my $item = $root->addNewChild(q(), 'item');
    to_xml($entry, $item);
}

print $xml;
choroba
  • 231,213
  • 25
  • 204
  • 289
0

Yes, wise choice. XML::Simple ... isn't. It's for simple XML.

As noted in the comments though - your data is a little ambiguous - specifically, how do you tell what the elements should be called within 'groups' or 'users'.

This looks like you might have parsed some JSON. (Indeed, you can turn it straight back into JSON:

print to_json ( $data, { pretty => 1 } );

The core problem is - where JSON supports arrays, XML does not. So there's really very little you could do that will directly turn your data structure into XML.

However if you don't mind doing a bit of work yourself:

Here's how you assemble some XML using XML::Twig

Assembling XML in Perl

use strict;
use warnings;

use XML::Twig;

my $twig = XML::Twig->new( 'pretty_print' => 'indented' );
$twig->set_root( 
    XML::Twig::Elt->new(
        'map',
    )
);
my $item = $twig->root->insert_new_elt('item', { 'id' => 1, 'name' => 'A' } );
my $users = $item ->insert_new_elt( 'users' );
   $users -> insert_new_elt ( 'user', { 'id' => 1, 'name' => 'u1' } );
   $users -> insert_new_elt ( 'user', { 'id' => 2, 'name' => 'u2' } );

my $groups = $item -> insert_new_elt ('last_child', 'groups');
   $groups -> insert_new_elt ( 'group', { 'id' => 1, 'name' => 'g1' } );

$twig->set_xml_version("1.0");
$twig->set_encoding('utf-8');

$twig->print;

Which prints:

<?xml version="1.0" encoding="utf-8"?>
<map>
  <item id="1" name="A">
    <users>
      <user id="2" name="u2"/>
      <user id="1" name="u1"/>
    </users>
    <groups>
      <group id="1" name="g1"/>
    </groups>
  </item>
</map>

Iterating your data structure is left as an exercise for the reader.

As Borodin correctly notes - you have no way to infer map item group or user from your data structure. The latter two you can perhaps infer based on plurals, but given your data set, the best I can come up with is something like this:

use strict;
use warnings;

use XML::Twig;

my $data = {
    id    => 1,
    name  => "A",
    users => [ { id => 1, name => "u1" }, { id => 2, name => "u2" } ],
    groups => [ { id => 1, name => "g1" } ]
};


my $twig = XML::Twig->new( 'pretty_print' => 'indented' );
$twig->set_root( XML::Twig::Elt->new( 'map', ) );

my $item = $twig->root->insert_new_elt('item');
foreach my $key ( keys %$data ) {
    if ( not ref $data->{$key} ) {
        $item->set_att( $key, $data->{$key} );
        next;
    }
    if ( ref( $data->{$key} ) eq "ARRAY" ) {
        my $fakearray = $item->insert_new_elt($key);
        foreach my $element ( @{ $data->{$key} } ) {
            my $name = $key;
               $name =~ s/s$//g;
            $fakearray->insert_new_elt( $name, $element );
        }
        next;
    }
    if ( ref ( $data -> {$key} ) eq "HASH" ) { 
        $item -> insert_new_elt( $key, $data -> {$key} );
        next;
    }
}

$twig->set_xml_version("1.0");
$twig->set_encoding('utf-8');

$twig->print;

This isn't ideal because - map is hardcoded, as is item. And I take the very simplistic approach of assuming the array has an s on the end, to pluralise it.

Community
  • 1
  • 1
Sobrique
  • 52,974
  • 7
  • 60
  • 101
  • That would create the correct XML, sure, but I think the OP is looking for a *generic* way of converting a Perl data structure to XML. You may as well just type in the XML instead of typing code to create it. – Borodin Mar 16 '15 at 16:38
  • I'd read it as a fairly large data structure, which'd need to be iterated to make some XML, rather than typing it all by hand. If it's just smooshing ... something from JSON (maybe?) into XML, that's altogether more complicated. – Sobrique Mar 16 '15 at 16:40
  • 2
    Then we agree. But your code doesn't make any use of the OP's data structure. As I commented above, there's no way to get the tag names `map`, `item`, `user` or `group`. – Borodin Mar 16 '15 at 16:41
  • Hmm, yes. You have to make certain assumptions. 'user' and 'group' you can perhaps fudge because of plurals, but ... – Sobrique Mar 16 '15 at 16:55
  • Thanks Sobrique for your solution, much appreciated. – Mohammad Sajid Anwar Mar 16 '15 at 17:06
  • It's not particularly complete, but hopefully it gives you a suitable start point? – Sobrique Mar 16 '15 at 17:07