XML Generation in Perl - how it should have always been
At first big thanks to Mark Overmeer for XML::Compile. I had the pleasure to meet Mark in .nl once - cheers and carry on the great work.
Whenever I had to create XML (either with perl or php) I did it one way or the other with some sort of templating toolkit. In php I use(d) http://www.tinybutstrong.com/ for creating xml . In perl several templating systems come into mind like
Feeling Wrong
this always felt wrong and awkward for several reasons:
The Rescue: XML::Compile
This nifty Module comes to the rescue. Using XML::Compile make xml+xsd behave like I always wanted to.
I want to demonstrate this using epp as an example. I want to create a valid epp frame for creating a contact object.
The epp schemas may be obtained by a simple internet search eg here.
You may create a valid epp frame by reading the both the contact-1.0.xsd and the epp-1.0.xsd each approx. 400 lines of thrilling xml. Or use some code like:
1. Convert xsd to perl hash
use strict;
use warnings;
use XML::Compile::Schema;
my $schema = XML::Compile::Schema->new([
'epp-xsd/epp-1.0.xsd',
'epp-xsd/eppcom-1.0.xsd',
'epp-xsd/contact-1.0.xsd',
]);
my $s = $schema->template('PERL' => '{urn:ietf:params:xml:ns:contact-1.0}create');
print $s;
which gives you a more readable idea what your data should look like:
# is a x0:createType
{ # sequence of id, postalInfo, voice, fax, email, authInfo, disclose
# is a xs:token
# length <= 16
# length >= 3
id => "token",
# is a x0:postalInfoType
# occurs 1 <= # <= 2 times
postalInfo =>
[ { # sequence of name, org, addr
# is a xs:normalizedString
# length <= 255
# length >= 1
name => "example",
....
Using this perl hash template you create the first part of your epp-xml
2. Use perl hash to xml conversion
use strict;
use warnings;
use XML::Compile::Schema;
my $schema = XML::Compile::Schema->new([
'epp-xsd/epp-1.0.xsd',
'epp-xsd/eppcom-1.0.xsd',
'epp-xsd/contact-1.0.xsd'
]);
my $write = $schema->compile(WRITER => '{urn:ietf:params:xml:ns:contact-1.0}create');
my $doc = XML::LibXML::Document->new('1.0', 'UTF-8');
my $hash = {
id => 'idid',
postalInfo => {
'name' => 'name',
'addr' => {
'street' => ['street', 'street2'],
'city' => 'city',
'cc' => 'cc',
},
type => 'int',
},
email => 'mymail',
"authInfo" => {pw => "PWauthInfo"},
};
my $xml = $write->($doc, $hash);
$doc->setDocumentElement($xml);
print $doc->toString(1);
this leads to the following xml
<?xml version="1.0" encoding="UTF-8"?>
<x0:create xmlns:x0="urn:ietf:params:xml:ns:contact-1.0">
<x0:id>idid</x0:id>
<x0:postalInfo type="int">
<x0:name>name</x0:name>
<x0:addr>
<x0:street>street</x0:street>
<x0:street>street2</x0:street>
<x0:city>city</x0:city>
<x0:cc>cc</x0:cc>
</x0:addr>
</x0:postalInfo>
<x0:email>mymail</x0:email>
<x0:authInfo>
<x0:pw>PWauthInfo</x0:pw>
</x0:authInfo>
</x0:create>
this xml has to be wrapped into an epp frame. As the epp frame uses xml any elements some “manual” work is necessary for creating the complete epp frame. In principle you start with {urn:ietf:params:xml:ns:epp-1.0}epp at step one.
3. Generate epp-xml-frame and wrap contact-create command into it
use strict;
use warnings;
use XML::Compile::Cache;
use XML::Compile::Schema;
my $cache = XML::Compile::Cache->new([
'epp-xsd/eppcom-1.0.xsd',
'epp-xsd/epp-1.0.xsd',
'epp-xsd/extensions.xsd',
'epp-xsd/contact-1.0.xsd',
]);
my $create_contact_ns = '{urn:ietf:params:xml:ns:contact-1.0}create';
my $epp_frame_ns = '{urn:ietf:params:xml:ns:epp-1.0}epp';
my $prefixes = {'urn:ietf:params:xml:ns:contact-1.0' => 'contact'};
$cache->declare(WRITER => [$create_contact_ns, $epp_frame_ns, ],
(
prefixes => $prefixes,
use_default_namespace => 1,
include_namespaces => 1,
)
);
$cache->compileAll;
my $doc = XML::LibXML::Document->new('1.0', 'UTF-8');
$doc->setStandalone(0);
my $contact_data = {
id => 'idid',
postalInfo => {
'name' => 'name',
'addr' => {
'street' => ['street', 'street2'],
'city' => 'city',
'cc' => 'cc',
},
type => 'int',
},
email => 'mymail',
"authInfo" => {pw => "PWauthInfo"},
};
my $xml = $cache->writer($create_contact_ns)->($doc, $contact_data);
my $epp_frame_data = {
command => {
create => {
'{urn:ietf:params:xml:ns:contact-1.0}create' => $xml,
},
clTRID => "token",
},
};
my $eppxml = $cache->writer($epp_frame_ns)->($doc, $epp_frame_data);
$eppxml->setNamespace( 'http://www.w3.org/2001/XMLSchema-instance', 'xsi', 0 ); ## append additonal ns for the feelinx
print $doc->toString(1) .
$eppxml->toString(1) ."n";
Voila, we have a valid epp frame without even touching the xml! Again Kudos to Mark Overmeer for making this possible!
4. The result
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<command>
<create>
<contact:create xmlns:contact="urn:ietf:params:xml:ns:contact-1.0">
<contact:id>idid</contact:id>
<contact:postalInfo type="int">
<contact:name>name</contact:name>
<contact:addr>
<contact:street>street</contact:street>
<contact:street>street2</contact:street>
<contact:city>city</contact:city>
<contact:cc>cc</contact:cc>
</contact:addr>
</contact:postalInfo>
<contact:email>mymail</contact:email>
<contact:authInfo>
<contact:pw>PWauthInfo</contact:pw>
</contact:authInfo>
</contact:create>
</create>
<clTRID>token</clTRID>
</command>
</epp>
Posted: April 13th, 2012 under Perl.
Comments: none