http://qs321.pair.com?node_id=581313

This is a continuation to my previous (RFC) XML::TransformRules node. The first version of the module, for now without support for namespaces and processing instructions (and comments - you might want to process even those in some cases) may be found at my site.

A few weeks ago someone compared XML to Lisp. I can't find the node now, but it was something about how you could just transform the XML to Lisp and let it execute to produce the result you need. This forced me to think ... if we can transform the XML to Lisp, we can just as well transform it to Perl:

<root> <foo x="5"> <bar>hello</bar> <baz>world</baz> </foo> </root> => root( foo( x => 5, bar("hello"), baz("world") ) )
but the question is whether we would gain anything. Of course it would be silly to convert the whole XML to Perl code and then eval("") it, we can instead execute the subroutines as we parse the closing tag and just remember the results so that we could pass them to the subroutine for the parent tag.

I tried to come up with a few examples of things I might want to do with a XML and tried to implement them in this style and I like the results. Maybe it's my functional programming affected brain, but I find this style convenient.

$xml = <<'*END*'; <doc> <person> <fname>Jane</fname> <lname>Luser</lname> <email>JLuser@bogus.com</email> <address> <street>Washington st.</street> <city>Old Creek</city> <country>The US</country> <bogus>bleargh</bogus> </address> <phones> <phone type="home">123-456-7890</phone> <phone type="office">663-486-7890</phone> <phone type="fax">663-486-7000</phone> </phones> </person> <person> <fname>John</fname> <lname>Other</lname> <email>JOther@silly.com</email> <address> <street>Grant's st.</street> <city>New Creek</city> <country>Canada</country> <bogus>sdrysdfgtyh degtrhy <foo>degtrhy werthy</foo>werthy drthyu</ +bogus> </address> <phones> <phone type="office">663-486-7891</phone> </phones> </person> </doc> *END* %rules = ( _default => 'content', bogus => undef, # means "ignore" address => sub {address => "$_[1]->{street}, $_[1]->{city} ($_[1]- +>{country})"}, person => sub { '@person' => "$_[1]->{lname}, $_[1]->{fname}\n<$_[1]->{email}> +\n$_[1]->{address}" }, doc => sub { join "\n\n", @{$_[1]->{person}} }, ); # $parser->parse() will return a single string containin the addresses + # in plain text format %rules = ( _default => 'content', # bogus => sub {}, # means "returns no value. The subtags ARE proce +ssed. bogus => undef, # means "ignore". The subtags ARE NOT processed. address => 'no content', person => 'no content array', doc => sub {$_[1]->{person}}, #'pass no content', foo => sub {print "FOOOOOOOO\n"}, ); # returns a simplified data structure kinda similar to XML::Simple my $parser = new XML::Rules ( rules => [ _default => sub {$_[0] => $_[1]->{_content}}, 'fname,lname' => sub {$_[0] => $_[1]->{_content}}, bogus => undef, address => sub {address => "$_[1]->{street}, $_[1]->{city} ($_ +[1]->{country})"}, phone => sub {$_[1]->{type} => $_[1]->{_content}}, # let's use the "type" attribute as the key and the conten +t as the value phones => sub {delete $_[1]->{_content}; %{$_[1]}}, # remove the text content and pass along the type => conte +nt from the child nodes person => sub { # lets print the values, all the data is readi +ly available in the attributes print "$_[1]->{lname}, $_[1]->{fname} <$_[1]->{email}>\n"; print "Home phone: $_[1]->{home}\n" if $_[1]->{home}; print "Office phone: $_[1]->{office}\n" if $_[1]->{office} +; print "Fax: $_[1]->{fax}\n" if $_[1]->{fax}; print "$_[1]->{address}\n\n"; return; # the <person> tag is processed, no need to rememb +er what it contained }, ] ); # prints the addresses, returns nothing
As you can see the rules applied to the parsed tags are basicaly of two types. Either they specify what data gets passed to the parent tag's rule and how or they do something with the attributes of the tag and the data returned by the rules of subtags. You can of course do both in your rules. For example if the XML looked like this:
<doc> <person> <fname>Jane</fname> <lname>Luser</lname> <email>JLuser@bogus.com</email> <address> <street>Washington st.</street> <city>Old Creek</city> <country>The US</country> <bogus>bleargh</bogus> </address> <phones> <phone type="home">123-456-7890</phone> <phone type="office">663-486-7890</phone> <phone type="fax">663-486-7000</phone> </phones> </person> <person> <fname>John</fname> <lname>Other</lname> <email>JOther@silly.com</email> <address id="12345"/> <phones> <phone type="office">663-486-7891</phone> </phones> </person> </doc>
You might use a subroutine like this for the <address> tag:
sub { if (exists $_[1]->{id} and $_[1]->{id}+0 > 0) { $get_addr->execute($_[1]->{id}); my $result = $get_addr->fetchall_arrayref(); my ($street, $sity, $country) = ( $result->[0][0], $result->[0][1], +$result->[0][2]); return address => "$_[1]->{street}, $_[1]->{city} ($_[1]->{country}) +" } else { return address => "$_[1]->{street}, $_[1]->{city} ($_[1]->{country}) +" } }
and proceed as if the data was directly in the XML in all cases.

Let me know please what you think. I'd also be grateful for any suggestions regarding the support for XML namespaces.

Update 2006-11-07: I just uploaded an updated version of the module, with more tests and "start tag" rules allowing you to skip branches of XML if you can decide based on the tag's attribute that you do not need them. I also uploaded the module to CPAN.