File Coverage

blib/lib/PRANG/Graph/Meta/Class.pm
Criterion Covered Total %
statement 12 14 85.7
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 17 19 89.4


line stmt bran cond sub pod time code
1             package PRANG::Graph::Meta::Class;
2             $PRANG::Graph::Meta::Class::VERSION = '0.20';
3 1     1   1433 use 5.010;
  1         4  
4 1     1   6 use Moose::Role;
  1         2  
  1         5  
5 1     1   4423 use Moose::Util::TypeConstraints;
  1         3  
  1         7  
6 1     1   2050 use MooseX::Params::Validate;
  1         2  
  1         10  
7 1     1   692 use XML::LibXML;
  0            
  0            
8              
9             has 'xml_attr' =>
10             isa => "HashRef[HashRef[PRANG::Graph::Meta::Attr]]",
11             is => "ro",
12             lazy => 1,
13             required => 1,
14             default => sub {
15             my $self = shift;
16             my @attr = grep { $_->does("PRANG::Graph::Meta::Attr") }
17             $self->get_all_attributes;
18             my $default_xmlns = ""; #eval { $self->name->xmlns };
19             my %attr_ns;
20             for my $attr (@attr) {
21             my $xmlns = $attr->has_xmlns
22             ?
23             $attr->xmlns
24             : $default_xmlns;
25             my $xml_name = $attr->has_xml_name
26             ?
27             $attr->xml_name
28             : $attr->name;
29             $attr_ns{$xmlns//""}{$xml_name} = $attr;
30             }
31             \%attr_ns;
32             };
33              
34             has 'xml_elements' =>
35             isa => "ArrayRef[PRANG::Graph::Meta::Element]",
36             is => "ro",
37             lazy => 1,
38             required => 1,
39             default => sub {
40             my $self = shift;
41             my @elements = grep {
42             $_->does("PRANG::Graph::Meta::Element")
43             } $self->get_all_attributes;
44             my @e_c = map { $_->associated_class->name } @elements;
45             my %e_c_does;
46             for my $parent (@e_c) {
47             for my $child (@e_c) {
48             if ( $parent eq $child ) {
49             $e_c_does{$parent}{$child} = 0;
50             }
51             else {
52             my $cdp = $child->does($parent) ||
53             $child->isa($parent);
54             $e_c_does{$parent}{$child} =
55             ( $cdp ? -1 : 1 );
56             }
57             }
58             }
59             [ map { $elements[$_] } sort {
60             $e_c_does{$e_c[$a]}{$e_c[$b]} or
61             (
62             $elements[$a]->insertion_order
63             <=> $elements[$b]->insertion_order
64             )
65             } 0..$#elements
66             ];
67             };
68              
69             has 'graph' =>
70             is => "rw",
71             isa => "PRANG::Graph::Node",
72             lazy => 1,
73             required => 1,
74             default => sub {
75             $_[0]->build_graph;
76             },
77             ;
78              
79             sub build_graph {
80             my $self = shift;
81            
82             my @nodes = map { $_->graph_node } @{ $self->xml_elements };
83             if ( @nodes != 1 ) {
84             PRANG::Graph::Seq->new(
85             members => \@nodes,
86             );
87             }
88             elsif (@nodes) {
89             $nodes[0];
90             }
91             }
92              
93             sub add_to_list($$) {
94             if ( !defined $_[0] ) {
95             $_[0] = $_[1];
96             }
97             else {
98             if ( (ref($_[0])||"") ne "ARRAY" ) {
99             $_[0] = [ $_[0] ];
100             }
101             push @{ $_[0] }, $_[1];
102             }
103             }
104              
105             sub as_listref($) {
106             if ( ref($_[0]) eq "ARRAY" ) {
107             $_[0]
108             }
109             else {
110             [ $_[0] ];
111             }
112             }
113              
114             sub as_item($) {
115             if ( ref($_[0]) eq "ARRAY" ) {
116             die scalar(@{$_[0]})." item(s) found where 1 expected";
117             }
118             else {
119             $_[0];
120             }
121             }
122              
123             sub accept_attributes {
124             my $self = shift;
125             my ( $node_attr, $context, $lax ) = pos_validated_list(
126             \@_,
127             { isa => 'ArrayRef[XML::LibXML::Attr]' },
128             { isa => 'PRANG::Graph::Context' },
129             { isa => 'Bool', optional => 1, default => 0 },
130             );
131              
132             my $attributes = $self->xml_attr;
133             my %rv;
134              
135             # process attributes
136             for my $attr (@$node_attr) {
137             my $prefix = $attr->prefix;
138             if ( !defined $prefix ) {
139             $prefix = "";
140             }
141             if ( length $prefix and !exists $context->xsi->{$prefix} ) {
142             $context->exception("unknown xmlns prefix '$prefix'");
143             }
144             my $xmlns = $context->get_xmlns($prefix)
145             if length $prefix;
146             $xmlns //= "";
147             my $meta_att = $attributes->{$xmlns}{$attr->localname};
148             my $xmlns_att_name;
149             my $_xmlns_att_name = sub {
150             $xmlns_att_name = $meta_att->xmlns_attr
151             or $context->exception(
152             "xmlns wildcarded, but no xmlns_attr set on "
153             .$self->name." property '"
154             .$meta_att->att_name."'",
155             );
156             };
157              
158             if ($meta_att) {
159              
160             # sweet, it's ok
161             my $att_name = $meta_att->name;
162              
163             # check the type constraint
164             if ( my $tc = $meta_att->type_constraint
165             and !$meta_att->xml_isa )
166             {
167             if ( !$tc->check($attr->value) ) {
168             $context->exception(
169             "invalid value '" . $attr->value . "' of attribute ".$attr->nodeName,
170             $attr->parentNode,
171             );
172             }
173             }
174             add_to_list($rv{$att_name}, $attr->value);
175             }
176             elsif ( $meta_att = $attributes->{"*"}{$attr->localname} ) {
177              
178             # wildcard xmlns only; need to store the xmlns
179             # in another attribute. Also, multiple values
180             # may appear with different xml namespaces.
181             my $att_name = $meta_att->name;
182             $_xmlns_att_name->();
183             add_to_list($rv{$att_name}, $attr->value);
184             add_to_list($rv{$xmlns_att_name}, $xmlns);
185             }
186             elsif ( $meta_att = $attributes->{$xmlns}{"*"} ) {
187              
188             # wildcard attribute name. This attribute gets
189             # HashRef treatment.
190             $rv{$meta_att->name}{$attr->localname} = $attr->value;
191             }
192             elsif ( $meta_att = $attributes->{"*"}{"*"} ) {
193              
194             # wildcard attribute name and namespace. Both
195             # attributes gets the joy of HashRef[ArrayRef[Str]|Str]
196             my $att_name = $meta_att->name;
197             $_xmlns_att_name->();
198             add_to_list(
199             $rv{$att_name}{$attr->localname},
200             $attr->value,
201             );
202             add_to_list(
203             $rv{$xmlns_att_name}{$attr->localname},
204             $xmlns
205             );
206             }
207             elsif (
208             $xmlns =~ m{^\w+://\w+\.w3\.org/.*schema}i
209             and
210             $attr->localname =~ m{schema}i
211             )
212             {
213              
214             # they said "schema" twice, they must be mad.
215             # ignore their craven input.
216             }
217             else {
218             # fail, unless we're in lax mode, in which case do nothing.
219             $context->exception("invalid attribute '".$attr->name."'")
220             unless $lax;
221             }
222             }
223             (%rv);
224             }
225              
226             use JSON;
227              
228             sub accept_childnodes {
229             my $self = shift;
230            
231             my ( $childNodes, $context, $lax ) = pos_validated_list(
232             \@_,
233             { isa => 'ArrayRef[XML::LibXML::Node]' },
234             { isa => 'PRANG::Graph::Context' },
235             { isa => 'Bool', optional => 1, default => 0 },
236             );
237            
238             my $graph = $self->graph;
239              
240             my (%init_args, %init_arg_names, %init_arg_xmlns, %init_arg_nodes);
241             my @rv;
242             my @nodes = grep {
243             !( $_->isa("XML::LibXML::Text")
244             and $_->data =~ /\A\s*\Z/
245             )
246             }
247             @$childNodes;
248             while ( my $input_node = shift @nodes ) {
249             next if $input_node->nodeType == XML_COMMENT_NODE;
250             my ($key, $value, $name, $xmlns) =
251             $graph->accept($input_node, $context, $lax);
252             if ( !$key ) {
253             next if $lax;
254            
255             my (@what) = $graph->expected($context);
256             $context->exception(
257             "unexpected node: expecting @what",
258             $input_node,
259             );
260             }
261             my $meta_att = $self->find_attribute_by_name($key);
262             if ( !$meta_att->_item_tc->check($value) ) {
263             $context = $context->next_ctx(
264             $input_node->namespaceURI,
265             $input_node->localname,
266             );
267             $context->exception(
268             "bad value '$value'",
269             $input_node
270             );
271             }
272             add_to_list($init_args{$key}, $value);
273             add_to_list($init_arg_nodes{$key}, $input_node);
274             if ( defined $name ) {
275             add_to_list(
276             $init_arg_names{$key},
277             $name,
278             );
279             }
280             if ( defined $xmlns ) {
281             add_to_list(
282             $init_arg_xmlns{$key},
283             $xmlns,
284             );
285             }
286             }
287              
288             if ( !$graph->complete($context) ) {
289             my (@what) = $graph->expected($context);
290             $context->exception(
291             "Node incomplete; expecting: @what",
292             );
293             }
294              
295             # now, we have to take all the values we just got and
296             # collapse them to init args
297             for my $element ( @{ $self->xml_elements } ) {
298             my $key = $element->name;
299             next unless exists $init_args{$key};
300             my $expect;
301             if ( $element->has_xml_max and $element->xml_max == 1 ) {
302             $expect = \&as_item;
303             }
304             else {
305             $expect = \&as_listref;
306             }
307             push @rv, eval {
308             my $val = $expect->(delete $init_args{$key});
309             if ( my $t_c = $element->type_constraint) {
310             if ( !$t_c->check($val) ) {
311             if ( ref $val ) {
312             $val = encode_json $val;
313             }
314             die "value '$val' failed type check";
315             }
316             }
317             ( ( ( $element->has_xml_nodeName_attr
318             and
319             exists $init_arg_names{$key}
320             )
321             ? ( $element->xml_nodeName_attr =>
322             $expect->($init_arg_names{$key})
323             )
324             : ()
325             ),
326             ( ( $element->has_xmlns_attr
327             and
328             exists $init_arg_xmlns{$key}
329             )
330             ? ( $element->xmlns_attr =>
331             $expect->($init_arg_xmlns{$key})
332             )
333             : ()
334             ),
335             $key => $val,
336             );
337             } or do {
338             my $err = $@;
339             my $bad = $init_arg_nodes{$key};
340             if ( ref $bad eq "ARRAY" ) {
341             $bad = $bad->parentNode;
342             }
343             else {
344             $context =
345             $context->next_ctx($bad->namespaceURI,
346             $bad->localname);
347             }
348             $context->exception(
349             "internal error: processing '$key' attribute: $err",
350             $bad,
351             );
352             };
353             }
354             if (my @leftovers = keys %init_args) {
355             $context->exception(
356             "internal error: ".@leftovers
357             ." init arg(s) left over (@leftovers)",
358             );
359             }
360             return @rv;
361             }
362              
363             sub marshall_in_element {
364             my $self = shift;
365            
366             my ( $node, $ctx, $lax ) = pos_validated_list(
367             \@_,
368             { isa => 'XML::LibXML::Node' },
369             { isa => 'PRANG::Graph::Context' },
370             { isa => 'Bool', optional => 1, default => 0 },
371             );
372            
373             my @node_attr = grep { $_->isa("XML::LibXML::Attr") }
374             $node->attributes;
375             my @ns_attr = $node->getNamespaces;
376              
377             if (@ns_attr) {
378             $ctx->add_xmlns($_->declaredPrefix//"" => $_->declaredURI)
379             for @ns_attr;
380             }
381              
382             my $new_ctx = $ctx->next_ctx(
383             $node->namespaceURI,
384             $node->localname,
385             );
386              
387             my @init_args = $self->accept_attributes( \@node_attr, $new_ctx, $lax );
388              
389             # now process elements
390             my @childNodes = grep {
391             !( $_->isa("XML::LibXML::Comment")
392             or
393             $_->isa("XML::LibXML::Text") and $_->data =~ /\A\s+\Z/
394             )
395             } $node->childNodes;
396              
397             push @init_args, $self->accept_childnodes( \@childNodes, $new_ctx, $lax );
398              
399             my $value = eval { $self->name->new(@init_args) };
400             if ( !$value ) {
401             my $error = $@;
402             $error =~ m|^(.+) at /|;
403             my $msg = $1;
404             $ctx->exception(
405             "Validation error from ".$self->name
406             ." constructor: $1",
407             $node,
408             );
409             }
410             else {
411             return $value;
412             }
413             }
414              
415             sub add_xml_attr {
416             my $self = shift;
417            
418             my ( $item, $node, $ctx ) = pos_validated_list(
419             \@_,
420             { isa => 'Object' },
421             { isa => 'XML::LibXML::Element' },
422             { isa => 'PRANG::Graph::Context' },
423             );
424            
425            
426             my $attributes = $self->xml_attr;
427             while ( my ($xmlns, $att) = each %$attributes ) {
428             while ( my ($attName, $meta_att) = each %$att ) {
429             my $is_optional;
430             my $obj_att_name = $meta_att->name;
431             if ( $meta_att->has_xml_required ) {
432             $is_optional = !$meta_att->xml_required;
433             }
434             elsif ( !$meta_att->is_required ) {
435              
436             # it's optional
437             $is_optional = 1;
438             }
439              
440             # we /could/ use $meta_att->get_value($item)
441             # here, but I consider that to break
442             # encapsulation
443             my $value = $item->$obj_att_name;
444             my $xml_att_name = $attName;
445             if ( $meta_att->has_xml_name ) {
446             my $method = $meta_att->has_xmlns_attr;
447             $xml_att_name = $attName;
448             }
449             if ( $meta_att->has_xmlns_attr ) {
450             my $method = $meta_att->xmlns_attr;
451             $xmlns = $item->$method;
452             }
453             if ( !defined $value ) {
454             die "could not serialize $item; slot "
455             .$meta_att->name." empty"
456             unless $is_optional;
457             next;
458             }
459              
460             my $emit_att = sub {
461             my ($xmlns, $name, $value) = @_;
462             my $prefix;
463             if ($xmlns) {
464             $prefix = $ctx->get_prefix(
465             $xmlns, $item, $node,
466             );
467             if ( length $prefix ) {
468             $prefix .= ":";
469             }
470             }
471             else {
472             $prefix = "";
473             }
474             $node->setAttribute(
475             $prefix.$name, $value,
476             );
477             };
478              
479             my $do_array = sub {
480             my $att_name = shift;
481             my $array = shift;
482             my $xmlns = shift;
483             for ( my $i = 0; $i <= $#$array; $i++ ) {
484             $emit_att->(
485             $xmlns&&$xmlns->[$i],
486             $att_name,
487             $array->[$i],
488             );
489             }
490             };
491              
492             if ( ref $value eq "HASH" ) {
493              
494             # wildcarded attribute name case
495             while ( my ($att, $val) = each %$value ) {
496             my $att_xmlns;
497             if ($xmlns) {
498             $att_xmlns = $xmlns->{$att};
499             }
500              
501             # now, we can *still* have arrays here..
502             if ( ref $val eq "ARRAY" ) {
503             $do_array->(
504             $att, $val,
505             $att_xmlns,
506             );
507             }
508             else {
509             $emit_att->(
510             $att_xmlns,
511             $att, $val,
512             );
513             }
514             }
515             }
516             elsif ( ref $value eq "ARRAY" ) {
517             $do_array->(
518             $xml_att_name,
519             $value,
520             $xmlns,
521             );
522             }
523             else {
524             $emit_att->( $xmlns, $xml_att_name, $value );
525             }
526             }
527             }
528             }
529              
530             sub to_libxml {
531             my $self = shift;
532            
533             my ( $item, $node, $ctx ) = pos_validated_list(
534             \@_,
535             { isa => 'Object' },
536             { isa => 'XML::LibXML::Element' },
537             { isa => 'PRANG::Graph::Context' },
538             );
539            
540            
541             $self->add_xml_attr($item, $node, $ctx);
542             $self->graph->output($item, $node, $ctx);
543             }
544              
545             package Moose::Meta::Class::Custom::Trait::PRANG;
546             $Moose::Meta::Class::Custom::Trait::PRANG::VERSION = '0.20';
547             sub register_implementation {"PRANG::Graph::Meta::Class"}
548              
549             1;
550              
551             __END__
552              
553             =head1 NAME
554              
555             PRANG::Graph::Meta::Class - metaclass metarole for PRANG-enabled classes
556              
557             =head1 SYNOPSIS
558              
559             package MyClass;
560             use Moose;
561             use PRANG::Graph;
562              
563             # - or -
564             package MyClass;
565             use Moose -traits => ["PRANG"];
566              
567             # - or -
568             package MyClass;
569             use Moose;
570             PRANG::Graph::Meta::Class->meta->apply(__PACKAGE__->meta);
571              
572             =head1 DESCRIPTION
573              
574             This role defines class properties and methods for PRANG classes' meta
575             objects. ie, the methods it defines are all to be found in
576             C<YourClass-E<gt>meta>, not C<YourClass>.
577              
578             The L<PRANG::Graph::Meta::Class> object is the centre of the state
579             machine which defines the parsing and emitting rules for your classes.
580             In other words, the I<XML Graph> (see L<PRANG>). Each one corresponds
581             to an XML element (though not all XML elements will require a full
582             object class), and via these objects can be found the lists of
583             elements and attributes which define the XML structure.
584              
585             =head1 ATTRIBUTES
586              
587             =over
588              
589             =item B<HashRef[HashRef[PRANG::Graph::Meta::Attr]] xml_attr>
590              
591             This read-only property maps from XML namespace and localname to a
592             L<PRANG::Graph::Meta::Attr> object, defining the type of that
593             attribute and other things described on its perldoc.
594              
595             The first time it is accessed, it is built - so be sure to carry out
596             any run-time meta magic before parsing or emitting objects of that
597             type.
598              
599             =item B<ArrayRef[PRANG::Graph::Meta::Element] xml_elements>
600              
601             This contains an ordered list of all of the XML elements which exist
602             in this class. See L<PRANG::Graph::Meta::Element>.
603              
604             Like C<xml_attr>, the first time it is accessed it is built. There
605             are currently some problems with ordering and role composition; as the
606             ordering of elements is returned from a moose accessor, but when
607             composing roles into classes, they are applied in any order.
608              
609             =item B<PRANG::Graph::Node graph>
610              
611             The C<graph> property is the acceptor and emitter for the child nodes
612             of this class. See L<PRANG::Graph::Node> for the low-down. This is
613             constructed by a transform on the B<xml_elements> property.
614              
615             =back
616              
617             =head1 METHODS
618              
619             =head2 B<accept_attributes(\@node_attr, $ctx)>
620              
621             =head2 B<accept_childnodes(\@childNodes, $ctx)>
622              
623             =head2 B<marshall_in_element($node, $ctx)>
624              
625             These methods are the parsing machinery, their API is quite subject to
626             change; the methods provided by the L<PRANG::Graph> role are what you
627             should be using, unless you are writing a PRANG extension.
628              
629             =head2 B<add_xml_attr($item, $node, $ctx)>
630              
631             =head2 B<to_libxml($item, $node, $ctx)>
632              
633             Similarly, these are the emitting methods.
634              
635             =head1 SEE ALSO
636              
637             L<PRANG::Graph::Meta::Attr>, L<PRANG::Graph::Meta::Element>,
638             L<PRANG::Graph::Node>
639              
640             =head1 AUTHOR AND LICENCE
641              
642             Development commissioned by NZ Registry Services, and carried out by
643             Catalyst IT - L<http://www.catalyst.net.nz/>
644              
645             Copyright 2009, 2010, NZ Registry Services. This module is licensed
646             under the Artistic License v2.0, which permits relicensing under other
647             Free Software licenses.
648              
649             =cut
650