XML::C...:Reader(3pm)

πŸ“¦
version 1.64 Β· 3 versions
, in debian trixie, ubuntu resolute, ubuntu stonking

πŸ’– NAME πŸ’–

So this is the XML::Compile::Translate::Reader(3pm) page, straight outta the User Contributed Perl Documentation, and bestie, it's XML::Compile::Translate::Reader β€” the thing that translates XML to HASH. XML goes in, a nested Perl hash comes out. That's the whole vibe. πŸ’…


πŸ¦‹ INHERITANCE πŸ¦‹

Like, its family tree is:

XML::Compile::Translate::Reader
  is a XML::Compile::Translate

✨ SYNOPSIS ✨

The little taste of the API:

my $schema = XML::Compile::Schema->new(...);
my $code   = $schema->compile(READER => ...);

πŸ‘‘ DESCRIPTION πŸ‘‘

Okay so here's the tea: the translator understands schemas, but it does not encode that into actions. It's not that girl. THIS module is the one that implements those actions, so it can translate from XML into a (nested) Perl HASH structure. Iconic behavior, honestly.

Extends "DESCRIPTION" in XML::Compile::Translate. 🫢


🌸 METHODS 🌸

Extends "METHODS" in XML::Compile::Translate.


πŸŽ€ DETAILS πŸŽ€

Extends "DETAILS" in XML::Compile::Translate.


πŸ’œ Translator options

Extends "Translator options" in XML::Compile::Translate.


🫢 Processing Wildcards

So, if you wanna collect information from the XML structure that's permitted by any and anyAttribute specifications in the schema, you have to implement that yourself. No cap. The problem is that XML::Compile has less knowledge than you about the possible data. It's giving "you know more than the module does." 😭

option any_attribute

By default, the anyAttribute specification is ignored. When TAKE_ALL is given, all attributes which are fulfilling the name-space requirement added to the returned data-structure. As key, the absolute element name will be used, with as value the related unparsed XML element.

In the current implementation, if an explicit attribute is also covered by the name-spaces permitted by the anyAttribute definition, then it will also appear in that list (and hence the handler will be called as well). Yeah, double duty. 🀷

Use XML::Compile::Schema::compile(any_attribute) to write your own handler, to influence the behavior. The handler will be called for each attribute, and you must return list of pairs of derived information. When the returned is empty, the attribute data is lost. The value may be a complex structure. πŸ‘‘

πŸ’… Example: anyAttribute in a READER

Say your schema looks like this:

<schema targetNamespace="http://mine"
   xmlns:me="http://mine" ...>
  <element name="el">
    <complexType>
      <attribute name="a" type="xs:int" />
      <anyAttribute namespace="##targetNamespace"
         processContents="lax">
    </complexType>
  </element>
  <simpleType name="non-empty">
    <restriction base="NCName" />
  </simpleType>
</schema>

Then, in an application, you write:

my $r = $schema->compile
 ( READER => pack_type('http://mine', 'el')
 , anyAttribute => 'ALL'
 );
# or lazy: READER => '{http://mine}el'

my $h = $r->( <<'__XML' );
  <el xmlns:me="http://mine">
    <a>42</a>
    <b type="me:non-empty">
       everything
    </b>
  </el>
__XML

use Data::Dumper 'Dumper';
print Dumper $h;
__XML__

The output is something like

$VAR1 =
 { a => 42
 , '{http://mine}a' => ... # XML::LibXML::Node with <a>42</a>
 , '{http://mine}b' => ... # XML::LibXML::Node with <b>everything</b>
 };

You can improve the reader with a callback. When you know that the extra attribute is always of type non-empty, then you can do

my $read = $schema->compile
 ( READER => '{http://mine}el'
 , anyAttribute => \&filter
 );

my $anyAttRead = $schema->compile
 ( READER => '{http://mine}non-empty'
 );

sub filter($$$$)
{   my ($fqn, $xml, $path, $translator) = @_;
    return () if $fqn ne '{http://mine}b';
    (b => $anyAttRead->($xml));
}

my $h = $r->( see above );
print Dumper $h;

Which will result in

$VAR1 =
 { a => 42
 , b => 'everything'
 };

The filter will be called twice, but return nothing in the first case. You can implement any kind of complex processing in the filter. Go off. πŸ”₯

option any_element

By default, the any definition in a schema will ignore all elements from the container which are not used. Also in this case TAKE_ALL is required to produce any results. SKIP_ALL will ignore all results, although this are being processed for validation needs.

option any_type CODE

By default, the elements which have type xsd:anyType will return an XML::LibXML::Element when there are sub-elements. Otherwise, it will return the textual content.

If you pass your own CODE reference, you can change this behavior. It will get called with the path, the node, and the default handler. Be awayre the $node may actually be a string already. πŸ™ˆ

  $schema->compile(READER => ..., any_type => \&handle_any_type);
  sub handle_any_type($$$)
  { my ($path, $node, $handler) = @_;
    ref $node or return $node;
    $node;
  }

🧚 Mixed elements

[available since 0.86] ComplexType and ComplexContent components can be declared with the <mixed="true"> attribute. This implies that text is not limited to the content of containers, but may also be used inbetween elements. Usually, you will only find ignorable white-space between elements. Lowkey sneaky.

In this example, the a container is marked to be mixed:

<a id="5"> before <b>2</b> after </a>

Often the mixed option is bending one of both ways: either the element is needed as text, or the element should be parsed and the text ignored. The reader has various options to avoid the need of processing raw XML::LibXML nodes. 🫢

[1.00] When the return is a HASH, that HASH will also contain the _MIXED_ELEMENT_MODE key, to help people understand what happens. This is not possible for all modes, only for some.

With XML::Compile::Schema::compile(mixed_elements) set to:

  • ATTRIBUTES (the default) β€” a HASH is returned, the attributes are processed. The node is found as XML::LibXML::Element with the key _. Above example will produce
  $r = { id => 5, _ => $xmlnode };
  • TEXTUAL β€” Like the previous, but now the textual representation of the content is returned with key _. Above example will produce
  $r = { id => 5, _ => ' before 2 after '};
  • STRUCTURAL β€” will remove all mixed-in text, and treat the element as normal element. The example will be transformed into
  $r = { id => 5, b => 2 };
  • XML_NODE β€” return the XML::LibXML::Node itself. The example:
  $r = $xmlnode;
  • XML_STRING β€” return the mixed node as XML string, just as in the source.
Be warned that it is rather expensive: the string was parsed and then stringified again, which is costly for large nodes. 😭

Result:

  $r = '<a id="5"> before <b>2</b> after </a>';
  • CODE reference β€” the reference is called with the XML::LibXML::Node as first argument. When a value is returned (even undef), then the right tag with the value will be included in the translators result. When an empty list is returned by the code reference, then nothing is returned (which may result in an error if the element is required according to the schema)

When some of your mixed elements need different behavior from other elements, then you have to go play with the normal hooks in specific cases. Main character energy only. πŸ’…


🌈 Schema hooks

hooks executed before the XML is being processed

The before hooks receives an XML::LibXML::Node object and the path string. It must return a new (or same) XML node which will be used from then on. You probably can best modify a node clone, not the original as provided by the user. When undef is returned, the whole node will disappear. Poof. πŸ’€

This hook offers a predefined PRINT_PATH.

πŸ”₯ Example: to trace the paths

$schema->addHook
  ( action => 'READER'
  , path   => qr/./
  , before => 'PRINT_PATH'
  );

hooks executed as replacement

Your replace hook should return a list of key-value pairs. To produce it, it will get the XML::LibXML::Element, the translator settings as HASH, the path, and the localname.

This hook has a predefined SKIP, which will not process the found element, but simply return the string SKIPPED as value. This way, a whole tree of unneeded translations can be avoided. Slay, efficiency. ✨

[1.51] The predefined hook XML_NODE will not attempt to parse the selected element, but returns the XML::LibXML::Element node instead.

This may break on some schema-contained validations. πŸ™ˆ

Sometimes, the Schema spec is such a mess, that XML::Compile cannot automatically translate it. I have seen cases where confusion over name-spaces is created: a choice between three elements with the same name but different types. Well, in such case you may use XML::LibXML::Simple to translate a part of your tree. Simply

use XML::LibXML::Simple  qw/XMLin/;
$schema->addHook
  ( action  => 'READER'
  , type    => 'tns:xyz'     # or pack_type($tns,'xyz')
 #  path    => qr!/company$! # by element name
  , replace =>
      sub { my ($xml, $args, $path, $type, $r) = @_;
            ($type => XMLin($xml, ...));
          }
  );

hooks for post-processing, after the data is collected

Your code reference gets called with three parameters: the XML node, the data collected and the path. Be careful that the collected data might be a SCALAR (for simpleType). Return a HASH or a SCALAR. undef may work, unless it is the value of a required element you throw awy. 😬

This hook also offers a predefined PRINT_PATH. Besides, it has INCLUDE_PATH, XML_NODE, NODE_TYPE, ELEMENT_ORDER, and ATTRIBUTE_ORDER, which will result in additional fields in the HASH, respectively containing the NODE which was processed (an XML::LibXML::Element), the type_of_node, the element names, and the attribute names. The keys start with an underscore _.


🌟 Typemaps

In a typemap, a relation between an XML element type and a Perl class (or object) is made. Each translator back-end will implement this a little differently. This section is about how the reader handles typemaps. πŸ¦‹

Typemap to Class

Usually, an XML type will be mapped on a Perl class. The Perl class implements the fromXML method as constructor.

$schema->addTypemaps($sometype => 'My::Perl::Class');

package My::Perl::Class;
...
sub fromXML
{   my ($class, $data, $xmltype) = @_;
    my $self = $class->new($data);
    ...
    $self;
}

Your method returns the data which will be included in the result tree of the reader. You may return an object, the unmodified $data, or undef. When undef is returned, this may fail the schema parser when the data element is required.

In the simpelest implementation, the class stores its data exactly as the XML structure:

package My::Perl::Class;
sub fromXML
{   my ($class, $data, $xmltype) = @_;
    bless $data, $class;
}

# The same, even shorter:
sub fromXML { bless $_[1], $_[0] }

πŸ’… Typemap to Object

Another glow-up option? Implementing an object factory β€” like, one single object whose whole job is birthing other objects. And this is where the $xmltype parameter really earns its keep, bestie, because now one object can spawn a whole multitude of different objects. It's giving mother of dragons, honestly.

 my $object = My::Perl::Class->new(...);
 $schema->typemap($sometype => $object);

 package My::Perl::Class;
 sub fromXML
 {   my ($object, $xmltype, $data) = @_;
     return Some::Other::Class->new($data);
 }

This object factory thing is lowkey the perfect solution when you're mapping XML onto objects that are not under your control β€” you know, the tragic case where there is not way to add the fromXML method. 😭 Can't touch the source? No problem, the factory's got you. 🫢


🧚 Typemap to CODE

The light, diet-coke version of an object factory runs on CODE references. ✨

 $schema->typemap($t1 => \&myhandler);
 sub myhandler
 {   my ($backend, $data, $type) = @_;
     return My::Perl::Class->new($data)
         if $backend eq 'READER';
     $data;
 }

 # shorter
 $schema->typemap($t1 => sub {My::Perl::Class->new($_[1])} );

Yes, that second one is the shorter flex and it ate, no notes. πŸ’…


πŸ”₯ Typemap implementation

So here's the tea on how it works under the hood: internally, the typemap is simply translated into an after hook for that specific type. Once the data has been processed via the usual mechanism, that hook will call the method fromXML on the class or object you specified, handing it the data which was read. And guess what β€” you can still use before and replace hooks if you need them. Best of all worlds, fr fr. 🌈

Syntactic sugar, because we love a shortcut:

 $schema->typemap($t1 => 'My::Package');
 $schema->typemap($t2 => $object);

is comparible to

 $schema->typemap($t1 => sub {My::Package->fromXML(@_)});
 $schema->typemap($t2 => sub {$object->fromXML(@_)} );

with some extra checks. πŸ‘‘


πŸ¦‹ SEE ALSO

This module is part of XML-Compile distribution version 1.64, built on October 21, 2024. Website: http://perl.overmeer.net/xml-compile/


πŸ‘‘ LICENSE

Copyrights 2006-2024 by [Mark Overmeer <markov@cpan.org>]. For other contributors see ChangeLog.

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See http://dev.perl.org/licenses/

Heads up bestie β€” that's the whole licensing situation, don't @ me, just read it. πŸ’€

And that's a wrap on the page, my love!! πŸŽ€ This was pulled from perl v5.40.0 with the date stamp 2024-10-27, and the page itself is XML::C...:Reader(3pm). Go forth and parse, you absolute icon. πŸ’–βœ¨πŸ¦‹

πŸ“¦ where this page comes from