File Coverage

blib/lib/Net/AMQP/Protocol.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Net::AMQP::Protocol;
2              
3             =head1 NAME
4              
5             Net::AMQP::Protocol - Loading code of the AMQP spec
6              
7             =head1 DESCRIPTION
8              
9             This class serves as a loader for the auto-generated classes of the protocol.
10              
11             =cut
12              
13 5     5   28 use strict;
  5         12  
  5         136  
14 5     5   23 use warnings;
  5         9  
  5         114  
15 5     5   2767 use Net::AMQP::Common qw(:all);
  5         11  
  5         1588  
16 5     5   3081 use Net::AMQP::Protocol::Base;
  5         16  
  5         86  
17 5     5   2701 use XML::LibXML;
  0            
  0            
18             use File::Path;
19             use File::Spec;
20              
21             our ($VERSION_MAJOR, $VERSION_MINOR, $VERSION_REVISION, %spec);
22              
23             =head1 CLASS METHODS
24              
25             =head2 header
26              
27             Returns a binary string representing the header of any AMQP communications
28              
29             =cut
30              
31             sub header {
32             'AMQP' . pack 'C*', 1, 1, $VERSION_MAJOR, $VERSION_MINOR;
33             }
34              
35             =head2 load_xml_spec
36              
37             Pass in the XML filename. Reads in the AMQP XML specifications file, XML document node , and generates subclasses of L for each frame type.
38              
39             Names are normalized, as demonstrated by this example:
40              
41            
42            
43            
44            
45            
46              
47             creates the class L with the field accessor C, allowing you to create a new object as such:
48              
49             my $method = Net::AMQP::Protocol::Basic::ConsumeOk->new(
50             consumer_tag => 'blah'
51             );
52              
53             print $method->consumer_tag() . "\n";
54             if ($method->class_id == 60 && $method->method_name == 21) {
55             # do something
56             }
57              
58             =cut
59              
60             sub load_xml_spec {
61             my ($class, $xml_fn, $xml_str_ref) = @_;
62              
63             my $parser = XML::LibXML->new();
64             my $doc = defined $xml_fn ? $parser->parse_file($xml_fn) : $parser->parse_string($$xml_str_ref);
65             my $root = $doc->documentElement;
66              
67             # Header
68              
69             if ($root->nodeName ne 'amqp') {
70             die "Invalid document node name ".$root->nodeName;
71             }
72             #print "Using spec from '" . $root->getAttribute('comment') . "'\n";
73              
74             $VERSION_MAJOR = $root->getAttribute('major');
75             $VERSION_MINOR = $root->getAttribute('minor');
76             $VERSION_REVISION = $root->getAttribute('revision');
77              
78             foreach my $child ($root->childNodes) {
79             my $nodeName = $child->nodeName;
80             my %attr = map { $_->name => $_->getValue } grep { defined $_ } $child->attributes;
81             if ($nodeName =~ m{^(constant|domain)$}) {
82             $spec{$nodeName}{ $attr{name} } = {
83             map { $_ => $attr{$_} }
84             grep { $_ ne 'name' }
85             keys %attr
86             };
87             }
88             elsif ($nodeName eq 'class') {
89             my %class = (
90             name => _normalize_name($attr{name}),
91             class_id => $attr{index},
92             handler => $attr{handler},
93             );
94             foreach my $child_method ($child->getChildrenByTagName('method')) {
95             my %method = (
96             name => _normalize_name($child_method->getAttribute('name')),
97             method_id => $child_method->getAttribute('index'),
98             synchronous => $child_method->getAttribute('synchronous'),
99             content => $child_method->getAttribute('content'),
100             responses => {},
101             );
102            
103             foreach my $child_field ($child_method->getChildrenByTagName('field')) {
104             my $field = {
105             map { $_->name => $_->getValue }
106             grep { defined $_ }
107             $child_field->attributes
108             };
109              
110             my @doc;
111             if ($child_field->firstChild && $child_field->firstChild->nodeType == 3) {
112             @doc = ( $child_field->firstChild->textContent );
113             }
114             foreach my $doc ($child_field->getChildrenByTagName('doc')) {
115             next if $doc->hasAttribute('name');
116             push @doc, $doc->textContent;
117             }
118             foreach my $i (0 .. $#doc) {
119             $doc[$i] =~ s{[\n\t]}{ }g;
120             $doc[$i] =~ s{\s{2,}}{ }g;
121             $doc[$i] =~ s{^\s*}{};
122             }
123             $field->{doc} = join "\n\n", @doc;
124              
125             push @{ $method{fields} }, $field;
126             }
127              
128             foreach my $child_response ($child_method->getChildrenByTagName('response')) {
129             my $name = _normalize_name($child_response->getAttribute('name'));
130             $method{responses}{$name} = 1;
131             }
132              
133             push @{ $class{methods} }, \%method;
134             }
135              
136             # Parse class-level fields (for ContentHeader)
137             my @class_fields = $child->getChildrenByTagName('field');
138             if (@class_fields) {
139             my @fields;
140             foreach my $child_field (@class_fields) {
141             push @fields, {
142             map { $_->name => $_->getValue }
143             grep { defined $_ }
144             $child_field->attributes
145             };
146             }
147              
148             # Create a virtual class method
149             push @{ $class{methods} }, {
150             name => 'ContentHeader',
151             method_id => 0, # FIXME: Will this conflict? This is for internal use only. Make constant maybe?
152             synchronous => undef,
153             responses => {},
154             fields => \@fields,
155             };
156             }
157              
158             $spec{class}{$class{name}} = \%class;
159             _build_class(\%class);
160             }
161             }
162             }
163              
164             sub _normalize_name {
165             my $name = shift;
166              
167             # Uppercase the first letter of each word
168             $name =~ s{\b(.+?)\b}{\u$1}g;
169            
170             # Remove hyphens
171             $name =~ s{-}{}g;
172              
173             return $name;
174             }
175              
176             sub _build_class {
177             my $class_spec = shift;
178              
179             my $base_class_name = 'Net::AMQP::Protocol::' . $class_spec->{name};
180              
181             foreach my $method_spec (@{ $class_spec->{methods} }) {
182             my $method_class_name = $base_class_name . '::' . $method_spec->{name};
183              
184             my @frame_arguments;
185             foreach my $field_spec (@{ $method_spec->{fields} }) {
186             my $type = $field_spec->{type}; # may be 'undef'
187             if ($field_spec->{domain}) {
188             $type = $spec{domain}{ $field_spec->{domain} }{type};
189             }
190             if (! $type) {
191             die "No type found for $method_class_name field $$field_spec{name}";
192             }
193             my $local_type = $data_type_map{$type};
194             if (! $local_type) {
195             die "Couldn't map spec type '$type' to a local name";
196             }
197              
198             my $local_name = $field_spec->{name};
199             $local_name =~ tr{ -}{_};
200             $local_name =~ tr{_}{}d if $local_name eq 'no_wait'; # AMQP spec is inconsistent
201              
202             push @frame_arguments, $local_name, $local_type;
203             }
204              
205             # Prefix the keys of the 'responses' hash with my base class name so I
206             # have a quick lookup table for checking if a class of message is a response
207             # to this method (synchronous methods only)
208             foreach my $key (keys %{ $method_spec->{responses} }) {
209             $method_spec->{responses}{ $base_class_name . '::' . $key } = delete $method_spec->{responses}{$key};
210             }
211              
212             eval <
213             package $method_class_name;
214              
215             use strict;
216             use warnings;
217             use base qw(Net::AMQP::Protocol::Base);
218              
219             sub class_id { return $class_spec->{class_id} }
220             sub method_id { return $method_spec->{method_id} }
221              
222             EOF
223             die $@ if $@;
224              
225             $method_class_name->class_spec($class_spec);
226             $method_class_name->method_spec($method_spec);
227             $method_class_name->frame_arguments(\@frame_arguments);
228             $method_class_name->register();
229             }
230             }
231              
232             =head2 full_docs_to_dir
233              
234             Net::AMQP::Protocol->full_docs_to_dir($dir, $format);
235              
236             Using the dynamically generated classes, this will create 'pod' or 'pm' files in the target directory in the following format:
237              
238             $dir/Net::AMQP::Protocol::Basic::Publish.pod
239             (or with format 'pm')
240             $dir/Net/AMQP/Protocol/Basic/Publish.pm
241              
242             The directory will be created if it doesn't exist.
243              
244             =cut
245              
246             sub full_docs_to_dir {
247             my ($class, $dir, $format) = @_;
248             $class = ref $class if ref $class;
249             $format ||= 'pod';
250              
251             foreach my $service_name (sort keys %{ $spec{class} }) {
252             foreach my $method (sort { $a->{name} cmp $b->{name} } @{ $spec{class}{$service_name}{methods} }) {
253             my $method_class = 'Net::AMQP::Protocol::' . $service_name . '::' . $method->{name};
254              
255             my $pod = $method_class->docs_as_pod;
256             my $filename;
257              
258             if ($format eq 'pod') {
259             $filename = File::Spec->catfile($dir, $method_class . '.pod');
260             }
261             elsif ($format eq 'pm') {
262             $filename = File::Spec->catfile($dir, $method_class . '.pm');
263             $filename =~ s{::}{/}g;
264             }
265              
266             my ($volume, $directories, undef) = File::Spec->splitpath($filename);
267             my $base_path = File::Spec->catfile($volume, $directories);
268             -d $base_path || mkpath($base_path) || die "Can't mkpath $base_path: $!";
269              
270             open my $podfn, '>', $filename or die "Can't open '$filename' for writing: $!";
271             print $podfn $pod;
272             close $podfn;
273             }
274             }
275             }
276              
277             =head1 SEE ALSO
278              
279             L
280              
281             =head1 COPYRIGHT
282              
283             Copyright (c) 2009 Eric Waters and XMission LLC (http://www.xmission.com/). All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
284              
285             The full text of the license can be found in the LICENSE file included with this module.
286              
287             =head1 AUTHOR
288              
289             Eric Waters
290              
291             =cut
292              
293             1;