File Coverage

blib/lib/PRANG/Graph/Meta/Class.pm
Criterion Covered Total %
statement 175 193 90.6
branch 81 98 82.6
condition 25 33 75.7
subroutine 18 19 94.7
pod 5 9 55.5
total 304 352 86.3


line stmt bran cond sub pod time code
1             package PRANG::Graph::Meta::Class;
2             $PRANG::Graph::Meta::Class::VERSION = '0.21';
3 11     11   38765 use 5.010;
  11         49  
4 11     11   85 use Moose::Role;
  11         29  
  11         96  
5 11     11   59738 use Moose::Util::TypeConstraints;
  11         36  
  11         95  
6 11     11   24022 use MooseX::Params::Validate;
  11         31  
  11         102  
7 11     11   5853 use XML::LibXML;
  11         48376  
  11         100  
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 41     41 0 88 my $self = shift;
81            
82 41         82 my @nodes = map { $_->graph_node } @{ $self->xml_elements };
  92         2773  
  41         1271  
83 41 100       277 if ( @nodes != 1 ) {
    50          
84 12         96 PRANG::Graph::Seq->new(
85             members => \@nodes,
86             );
87             }
88             elsif (@nodes) {
89 29         892 $nodes[0];
90             }
91             }
92              
93             sub add_to_list($$) {
94 1360 100   1360 0 2815 if ( !defined $_[0] ) {
95 1052         3587 $_[0] = $_[1];
96             }
97             else {
98 308 100 100     1054 if ( (ref($_[0])||"") ne "ARRAY" ) {
99 153         321 $_[0] = [ $_[0] ];
100             }
101 308         408 push @{ $_[0] }, $_[1];
  308         917  
102             }
103             }
104              
105             sub as_listref($) {
106 107 100   107 0 337 if ( ref($_[0]) eq "ARRAY" ) {
107 51         454 $_[0]
108             }
109             else {
110 56         524 [ $_[0] ];
111             }
112             }
113              
114             sub as_item($) {
115 211 50   211 0 562 if ( ref($_[0]) eq "ARRAY" ) {
116 0         0 die scalar(@{$_[0]})." item(s) found where 1 expected";
  0         0  
117             }
118             else {
119 211         618 $_[0];
120             }
121             }
122              
123             sub accept_attributes {
124 264     264 1 53298 my $self = shift;
125 264         1585 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 264         127876 my $attributes = $self->xml_attr;
133 264         541 my %rv;
134              
135             # process attributes
136 264         646 for my $attr (@$node_attr) {
137 100         447 my $prefix = $attr->prefix;
138 100 100       256 if ( !defined $prefix ) {
139 74         127 $prefix = "";
140             }
141 100 50 66     970 if ( length $prefix and !exists $context->xsi->{$prefix} ) {
142 0         0 $context->exception("unknown xmlns prefix '$prefix'");
143             }
144 100 100       330 my $xmlns = $context->get_xmlns($prefix)
145             if length $prefix;
146 100   100     304 $xmlns //= "";
147 100         488 my $meta_att = $attributes->{$xmlns}{$attr->localname};
148 100         164 my $xmlns_att_name;
149             my $_xmlns_att_name = sub {
150 34 50   34   1043 $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 100         460 };
157              
158 100 100 33     505 if ($meta_att) {
    100          
    100          
    100          
    50          
159              
160             # sweet, it's ok
161 57         188 my $att_name = $meta_att->name;
162              
163             # check the type constraint
164 57 100 66     2065 if ( my $tc = $meta_att->type_constraint
165             and !$meta_att->xml_isa )
166             {
167 56 100       349 if ( !$tc->check($attr->value) ) {
168 2         183 $context->exception(
169             "invalid value '" . $attr->value . "' of attribute ".$attr->nodeName,
170             $attr->parentNode,
171             );
172             }
173             }
174 55         2432 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 6         20 my $att_name = $meta_att->name;
182 6         17 $_xmlns_att_name->();
183 6         41 add_to_list($rv{$att_name}, $attr->value);
184 6         22 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 6         69 $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 28         121 my $att_name = $meta_att->name;
197 28         65 $_xmlns_att_name->();
198             add_to_list(
199 28         242 $rv{$att_name}{$attr->localname},
200             $attr->value,
201             );
202             add_to_list(
203 28         137 $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 3 100       26 $context->exception("invalid attribute '".$attr->name."'")
220             unless $lax;
221             }
222             }
223 260         940 (%rv);
224             }
225              
226 11     11   23700 use JSON;
  11         113101  
  11         130  
227              
228             sub accept_childnodes {
229 272     272 1 28192 my $self = shift;
230            
231 272         1798 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 272         117891 my $graph = $self->graph;
239              
240 272         823 my (%init_args, %init_arg_names, %init_arg_xmlns, %init_arg_nodes);
241 272         0 my @rv;
242             my @nodes = grep {
243 272   100     1032 !( $_->isa("XML::LibXML::Text")
  436         2503  
244             and $_->data =~ /\A\s*\Z/
245             )
246             }
247             @$childNodes;
248 272         1508 while ( my $input_node = shift @nodes ) {
249 410 50       3373 next if $input_node->nodeType == XML_COMMENT_NODE;
250 410         1620 my ($key, $value, $name, $xmlns) =
251             $graph->accept($input_node, $context, $lax);
252 383 100       927 if ( !$key ) {
253 5 100       51 next if $lax;
254            
255 2         15 my (@what) = $graph->expected($context);
256 2         13 $context->exception(
257             "unexpected node: expecting @what",
258             $input_node,
259             );
260             }
261 378         1301 my $meta_att = $self->find_attribute_by_name($key);
262 378 100       28242 if ( !$meta_att->_item_tc->check($value) ) {
263 4         446 $context = $context->next_ctx(
264             $input_node->namespaceURI,
265             $input_node->localname,
266             );
267 4         32 $context->exception(
268             "bad value '$value'",
269             $input_node
270             );
271             }
272 374         40318 add_to_list($init_args{$key}, $value);
273 374         1362 add_to_list($init_arg_nodes{$key}, $input_node);
274 374 100       876 if ( defined $name ) {
275             add_to_list(
276 135         344 $init_arg_names{$key},
277             $name,
278             );
279             }
280 374 100       853 if ( defined $xmlns ) {
281             add_to_list(
282 354         891 $init_arg_xmlns{$key},
283             $xmlns,
284             );
285             }
286             }
287              
288 239 100       1146 if ( !$graph->complete($context) ) {
289 6         34 my (@what) = $graph->expected($context);
290 6         47 $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 233         493 for my $element ( @{ $self->xml_elements } ) {
  233         6764  
298 805         2574 my $key = $element->name;
299 805 100       1830 next unless exists $init_args{$key};
300 273         411 my $expect;
301 273 100 100     8985 if ( $element->has_xml_max and $element->xml_max == 1 ) {
302 202         528 $expect = \&as_item;
303             }
304             else {
305 71         261 $expect = \&as_listref;
306             }
307             push @rv, eval {
308 273         840 my $val = $expect->(delete $init_args{$key});
309 273 50       8524 if ( my $t_c = $element->type_constraint) {
310 273 50       4263 if ( !$t_c->check($val) ) {
311 0 0       0 if ( ref $val ) {
312 0         0 $val = encode_json $val;
313             }
314 0         0 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 273 100 66     32982 $expect->($init_arg_xmlns{$key})
    100 66        
332             )
333             : ()
334             ),
335             $key => $val,
336             );
337 273 50       574 } or do {
338 0         0 my $err = $@;
339 0         0 my $bad = $init_arg_nodes{$key};
340 0 0       0 if ( ref $bad eq "ARRAY" ) {
341 0         0 $bad = $bad->parentNode;
342             }
343             else {
344 0         0 $context =
345             $context->next_ctx($bad->namespaceURI,
346             $bad->localname);
347             }
348 0         0 $context->exception(
349             "internal error: processing '$key' attribute: $err",
350             $bad,
351             );
352             };
353             }
354 233 50       887 if (my @leftovers = keys %init_args) {
355 0         0 $context->exception(
356             "internal error: ".@leftovers
357             ." init arg(s) left over (@leftovers)",
358             );
359             }
360 233         1178 return @rv;
361             }
362              
363             sub marshall_in_element {
364 259     259 1 2281 my $self = shift;
365            
366 259         1721 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 259         79949 my @node_attr = grep { $_->isa("XML::LibXML::Attr") }
  153         1460  
374             $node->attributes;
375 259         2411 my @ns_attr = $node->getNamespaces;
376              
377 259 100       800 if (@ns_attr) {
378             $ctx->add_xmlns($_->declaredPrefix//"" => $_->declaredURI)
379 28   100     351 for @ns_attr;
380             }
381              
382 259         1643 my $new_ctx = $ctx->next_ctx(
383             $node->namespaceURI,
384             $node->localname,
385             );
386              
387 259         1077 my @init_args = $self->accept_attributes( \@node_attr, $new_ctx, $lax );
388              
389             # now process elements
390             my @childNodes = grep {
391 257   66     949 !( $_->isa("XML::LibXML::Comment")
  789         9033  
392             or
393             $_->isa("XML::LibXML::Text") and $_->data =~ /\A\s+\Z/
394             )
395             } $node->childNodes;
396              
397 257         2032 push @init_args, $self->accept_childnodes( \@childNodes, $new_ctx, $lax );
398              
399 227         432 my $value = eval { $self->name->new(@init_args) };
  227         1249  
400 227 50       255935 if ( !$value ) {
401 0         0 my $error = $@;
402 0         0 $error =~ m|^(.+) at /|;
403 0         0 my $msg = $1;
404 0         0 $ctx->exception(
405             "Validation error from ".$self->name
406             ." constructor: $1",
407             $node,
408             );
409             }
410             else {
411 227         1090 return $value;
412             }
413             }
414              
415             sub add_xml_attr {
416 144     144 1 6420 my $self = shift;
417            
418 144         697 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 144         47774 my $attributes = $self->xml_attr;
427 144         749 while ( my ($xmlns, $att) = each %$attributes ) {
428 117         710 while ( my ($attName, $meta_att) = each %$att ) {
429 97         505 my $is_optional;
430 97         350 my $obj_att_name = $meta_att->name;
431 97 100       3443 if ( $meta_att->has_xml_required ) {
    100          
432 48         1407 $is_optional = !$meta_att->xml_required;
433             }
434             elsif ( !$meta_att->is_required ) {
435              
436             # it's optional
437 47         458 $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 97         822 my $value = $item->$obj_att_name;
444 97         643 my $xml_att_name = $attName;
445 97 100       3133 if ( $meta_att->has_xml_name ) {
446 43         1435 my $method = $meta_att->has_xmlns_attr;
447 43         104 $xml_att_name = $attName;
448             }
449 97 100       3082 if ( $meta_att->has_xmlns_attr ) {
450 24         746 my $method = $meta_att->xmlns_attr;
451 24         661 $xmlns = $item->$method;
452             }
453 97 100       395 if ( !defined $value ) {
454 43 50       139 die "could not serialize $item; slot "
455             .$meta_att->name." empty"
456             unless $is_optional;
457 43         273 next;
458             }
459              
460             my $emit_att = sub {
461 68     68   210 my ($xmlns, $name, $value) = @_;
462 68         101 my $prefix;
463 68 100       139 if ($xmlns) {
464 14         66 $prefix = $ctx->get_prefix(
465             $xmlns, $item, $node,
466             );
467 14 50       47 if ( length $prefix ) {
468 14         30 $prefix .= ":";
469             }
470             }
471             else {
472 54         89 $prefix = "";
473             }
474 68         274 $node->setAttribute(
475             $prefix.$name, $value,
476             );
477 54         311 };
478              
479             my $do_array = sub {
480 5     5   12 my $att_name = shift;
481 5         10 my $array = shift;
482 5         11 my $xmlns = shift;
483 5         18 for ( my $i = 0; $i <= $#$array; $i++ ) {
484 15   66     197 $emit_att->(
485             $xmlns&&$xmlns->[$i],
486             $att_name,
487             $array->[$i],
488             );
489             }
490 54         237 };
491              
492 54 100       197 if ( ref $value eq "HASH" ) {
    100          
493              
494             # wildcarded attribute name case
495 11         65 while ( my ($att, $val) = each %$value ) {
496 15         80 my $att_xmlns;
497 15 100       37 if ($xmlns) {
498 12         35 $att_xmlns = $xmlns->{$att};
499             }
500              
501             # now, we can *still* have arrays here..
502 15 100       51 if ( ref $val eq "ARRAY" ) {
503 4         14 $do_array->(
504             $att, $val,
505             $att_xmlns,
506             );
507             }
508             else {
509 11         30 $emit_att->(
510             $att_xmlns,
511             $att, $val,
512             );
513             }
514             }
515             }
516             elsif ( ref $value eq "ARRAY" ) {
517 1         8 $do_array->(
518             $xml_att_name,
519             $value,
520             $xmlns,
521             );
522             }
523             else {
524 42         92 $emit_att->( $xmlns, $xml_att_name, $value );
525             }
526             }
527             }
528             }
529              
530             sub to_libxml {
531 142     142 1 2074 my $self = shift;
532            
533 142         848 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 142         45843 $self->add_xml_attr($item, $node, $ctx);
542 142         4691 $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.21';
547 0     0     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