File Coverage

blib/lib/Thrift/Parser.pm
Criterion Covered Total %
statement 160 195 82.0
branch 27 62 43.5
condition 5 24 20.8
subroutine 25 26 96.1
pod 7 7 100.0
total 224 314 71.3


line stmt bran cond sub pod time code
1             package Thrift::Parser;
2              
3             =head1 NAME
4              
5             Thrift::Parser - A Thrift message (de)serialization OO representation
6              
7             =head1 SYNOPSIS
8              
9             use Thrift;
10             use Thrift::Parser;
11             use Thrift::IDL;
12              
13             my $parser = Thrift::Parser->new(
14             idl => Thrift::IDL->parse_thrift_file('tutorial.thrift'),
15             service => 'Calculator',
16             );
17              
18             ## Parse a payload
19              
20             # Obtain a Thrift::Protocol subclass somehow with a loaded buffer
21             my $buffer = ...;
22             my $protocol = Thrift::BinaryProtocol->new($buffer);
23              
24             my $message = $parser->parse_message($protocol);
25              
26             print "Received method call " . $message->method->name . "\n";
27              
28             ## Use the auto-generated classes to create request/responses
29              
30             my $request = tutorial::Calculator::add->compose_message_call(
31             num1 => 15,
32             num2 => 33,
33             );
34              
35             my $response = $request->compose_reply(48);
36              
37             =head1 DESCRIPTION
38              
39             This module provides strict typing and full object orientation of all the Thrift types. It allows you, with a L object, to create a parser which can parse any L object into a message object, and creates dynamic classes according to the IDL specification, allowing you to create method calls, objects, and responses.
40              
41             =cut
42              
43 7     7   362161 use strict;
  7         18  
  7         263  
44 7     7   38 use warnings;
  7         14  
  7         192  
45 7     7   4409 use Thrift;
  7         18865  
  7         304  
46 7     7   7005 use Params::Validate;
  7         112911  
  7         456  
47 7     7   4564 use Data::Dumper;
  7         40334  
  7         496  
48 7     7   63 use File::Path; # mkpath, for full_docs_to_dir
  7         15  
  7         450  
49 7     7   39 use base qw(Class::Accessor);
  7         16  
  7         4172  
50             __PACKAGE__->mk_accessors(qw(idl service built_classes));
51 7     7   7550 use Carp;
  7         17  
  7         368  
52              
53 7     7   4029 use Thrift::Parser::Types;
  7         26  
  7         291  
54 7     7   9438 use Thrift::Parser::Method;
  7         29  
  7         232  
55 7     7   4370 use Thrift::Parser::Exceptions;
  7         31  
  7         390  
56 7     7   4100 use Thrift::Parser::Message;
  7         31  
  7         301  
57 7     7   3891 use Thrift::Parser::Field;
  7         26  
  7         199  
58 7     7   3930 use Thrift::Parser::FieldSet;
  7         22  
  7         14149  
59              
60             our $VERSION = '0.06';
61              
62             =head1 METHODS
63              
64             =head2 new
65              
66             my $parser = Thrift::Parser->new(
67             idl => Thrift::IDL->parse_thrift_file('..'),
68             service => 'myServiceName',
69             )
70              
71             Creates the Parser object and dynamic classes from the IDL file.
72              
73             =cut
74              
75             sub new {
76 5     5 1 1003 my $class = shift;
77              
78 5         180 my %self = validate(@_, {
79             idl => 1,
80             service => 1,
81             });
82              
83 5         89 my $self = bless \%self, $class;
84              
85 4         43 $self->_load_service($self->{service});
86 3         41 $self->_build_classes();
87              
88 3         72 return $self;
89             }
90              
91             sub _load_service {
92 4     4   11 my ($self, $service_name, $basename, $service_extended) = @_;
93              
94             #print "_load_service($service_name, ".($basename || 'undef').")\n";
95              
96             ## Find the Service object that I'll be implementing
97              
98 4         9 my ($service, $found_service);
99 4         7 foreach $service (@{ $self->idl->services }) {
  4         26  
100 4 100       23 next if $service->name ne $service_name;
101 3 50       152 if ($basename) {
102 0 0       0 next if $service->{header}{basename} ne $basename;
103             }
104 3         525 $found_service = $service;
105 3         572 last;
106             }
107              
108 4 100       520 if (! $found_service) {
109 1 50       5 my $full_name = ($basename ? $basename . '.' : '') . $service_name;
110 1 50       10 die "The service named '$full_name' is not implemented in the IDL document passed ("
111 1         6 .join(', ', map { ($_->{header}{basename} ? $_->{header}{basename} . '.' : '') . $_->name } @{ $self->idl->services }).")";
  1         6  
112             }
113 3         58 $service = $found_service;
114              
115             # Copy all the methods into a lookup hash by name
116 3         9 foreach my $method (@{ $service->methods }) {
  3         16  
117 3         25 my $namespace = $service->{header}->namespace('perl');
118 3 50 33     33 my $message_class = ($namespace ? $namespace . '::' : '')
119             . ($service_extended || $service->name) . '::' . $method->name;
120              
121 3         93 $self->{methods}{ $method->name } = {
122             idl => $method,
123             class => $message_class,
124             };
125             }
126              
127             # If this service extends another service, load that too
128 3 50       42 if ($service->extends) {
129 0         0 my ($extends_namespace, $extends_service_name) = $service->extends =~ m{^([^.]+) \. ([^.]+)$}x;
130 0   0     0 $extends_service_name ||= $service->extends;
131 0   0     0 $self->_load_service($extends_service_name, $extends_namespace, ($service_extended || $service_name));
132             }
133             }
134              
135             sub _build_classes {
136 3     3   8 my $self = shift;
137              
138 3         8 my @build;
139              
140 3         5 foreach my $method_name (keys %{ $self->{methods} }) {
  3         17  
141 3         8 my $details = $self->{methods}{$method_name};
142 1         14 push @build, {
143             class => $details->{class},
144             base => 'Thrift::Parser::Method',
145             idl => $details->{idl},
146             name => $method_name,
147             accessors => {
148             return_class => $self->idl_type_class($details->{idl}->returns),
149             throw_classes => {
150 3         55 map { $_->name => $self->idl_type_class($_->type) }
151 3         23 @{ $details->{idl}->throws }
152             },
153             },
154             };
155             }
156              
157 3         49 foreach my $struct (@{ $self->idl->structs }) {
  3         17  
158 2         30 my $namespace = $struct->{header}->namespace('perl');
159 2 50       10 push @build, {
    100          
160             class => join ('::', (defined $namespace ? ($namespace) : ()), $struct->name),
161             base => $struct->isa('Thrift::IDL::Exception') ? 'Thrift::Parser::Type::Exception' : 'Thrift::Parser::Type::Struct',
162             idl => $struct,
163             name => $struct->name,
164             };
165             }
166              
167 3         26 foreach my $enum (@{ $self->idl->enums }) {
  3         12  
168 1         227 my $namespace = $enum->{header}->namespace('perl');
169 1 50       9 push @build, {
170             class => join ('::', (defined $namespace ? ($namespace) : ()), $enum->name),
171             base => 'Thrift::Parser::Type::Enum',
172             idl => $enum,
173             name => $enum->name,
174             };
175             }
176              
177 3         28 foreach my $typedef (@{ $self->idl->typedefs }) {
  3         13  
178 3         47 my $namespace = $typedef->{header}->namespace('perl');
179 3 50       16 push @build, {
180             class => join ('::', (defined $namespace ? ($namespace) : ()), $typedef->name),
181             base => 'Thrift::Parser::Type::' . lc $typedef->type->name,
182             idl => $typedef,
183             name => $typedef->name,
184             };
185             }
186              
187 3         18 foreach my $build (@build) {
188             #print STDERR "Building $$build{class} (base $$build{base})\n";
189              
190 4     4   35 eval <
  4     4   10  
  4     4   228  
  4         24  
  4         8  
  4         175  
  4         22  
  4         9  
  4         424  
  9         884  
191             package $$build{class};
192              
193             use strict;
194             use warnings;
195             use base qw($$build{base});
196             EOF
197 9 50       36 die $@ if $@;
198              
199 9         187 $build->{class}->idl($build->{idl});
200 9         352 $build->{class}->idl_doc($self->idl);
201 9         615 $build->{class}->name($build->{name});
202            
203 9   100     253 $build->{accessors} ||= {};
204 9         18 while (my ($key, $value) = each %{ $build->{accessors} }) {
  15         158  
205 6         194 $build->{class}->$key($value);
206             }
207             }
208              
209 3         21 $self->built_classes(\@build);
210             }
211              
212             =head2 parse_message
213              
214             my $message = $parser->parse_message($transport);
215              
216             Given a L object, the parser will create a L object.
217              
218             =cut
219              
220             sub parse_message {
221 1     1 1 545 my ($self, $input) = @_;
222              
223 1         3 my %meta;
224 1         8 $input->readMessageBegin(\$meta{method}, \$meta{type}, \$meta{seqid});
225              
226 1         18 my $method_details = $self->{methods}{$meta{method}};
227 1         4 my $idl = $method_details->{idl};
228 1 50       5 if (! $idl) {
229 0         0 die "No way to process unknown method '$meta{method}'"; # TODO
230             }
231              
232 1         16 my $idl_fields = [];
233 1 50 33     8 if ($meta{type} == TMessageType::CALL || $meta{type} == TMessageType::ONEWAY) {
    0          
    0          
234 1         5 $idl_fields = $idl->arguments;
235             }
236             elsif ($meta{type} == TMessageType::REPLY) {
237 0         0 $idl_fields = [
238             Thrift::IDL::Field->new({ id => 0, type => $idl->returns, name => '_return_value' }),
239 0         0 @{ $idl->throws }
240             ];
241             }
242             elsif ($meta{type} == TMessageType::EXCEPTION) {
243 0         0 $idl_fields = [
244             Thrift::IDL::Field->new({ id => 1, name => 'message', type => Thrift::IDL::Type::Base->new({ name => 'string' }) }),
245             Thrift::IDL::Field->new({ id => 2, name => 'code', type => Thrift::IDL::Type::Base->new({ name => 'i32' }) }),
246             ];
247             }
248              
249 1         15 my $arguments = $self->parse_structure($input, $idl_fields);
250              
251             # Finish reading the message
252 1         7 $input->readMessageEnd();
253              
254 1         13 my $message = Thrift::Parser::Message->new({
255             method => $method_details->{class},
256             type => $meta{type},
257             seqid => $meta{seqid},
258             arguments => $arguments,
259             });
260              
261 1         6 return $message;
262             }
263              
264             =head2 full_docs_to_dir
265              
266             $parser->full_docs_to_dir($dir, $format);
267              
268             Using the dynamically generated classes, this will create 'pod' or 'pm' files in the target directory in the following format:
269              
270             $dir/tutorial::Calculator::testVars.pod
271             (or with format 'pm')
272             $dir/tutorial/Calculator/testVars.pm
273              
274             The directory will be created if it doesn't exist.
275              
276             =cut
277              
278             sub full_docs_to_dir {
279 0     0 1 0 my ($self, $dir, $format, $ignore_existing) = @_;
280 0         0 my $class = ref $self;
281 0   0     0 $format ||= 'pod';
282              
283 0         0 foreach my $built (@{ $self->built_classes }) {
  0         0  
284 0         0 my $filename;
285              
286 0 0       0 if ($format eq 'pod') {
    0          
287 0         0 $filename = $dir . '/' . $built->{class} . '.pod';
288             }
289             elsif ($format eq 'pm') {
290 0         0 $filename = $dir . '/' . $built->{class} . '.pm';
291 0         0 $filename =~ s{::}{/}g;
292             }
293              
294 0 0 0     0 if ($ignore_existing && -f $filename) {
295 0         0 next;
296             }
297              
298 0         0 my $pod = $built->{class}->docs_as_pod( $built->{base} );
299              
300 0         0 my ($base_path) = $filename =~ m{^(.+)/[^/]+$};
301 0 0 0     0 -d $base_path || mkpath($base_path) || die "Can't mkpath $base_path: $!";
302              
303 0 0       0 open my $podfh, '>', $filename or die "Can't open '$filename' for writing: $!";
304 0         0 print $podfh $pod;
305 0         0 close $podfh;
306             }
307             }
308              
309             =head1 INTERNAL METHODS
310              
311             =head2 parse_structure
312              
313             my $fieldset = $parser->parse_structure($transport, $thrift_idl_method->arguments);
314              
315             Returns a L. Attempts to read a structure off the transport, using an array of L objects to define the specification of the structure.
316              
317             =cut
318              
319             sub parse_structure {
320 1     1 1 2 my ($self, $input, $idl_fields) = @_;
321              
322             # Preprocess the list of IDL fields
323 1         3 my %idl_fields_by_id;
324 1   50     4 $idl_fields ||= [];
325 1         3 foreach my $field (@$idl_fields) {
326 2         16 $idl_fields_by_id{ $field->id } = $field;
327             }
328              
329 1         11 my @fields;
330              
331 1         6 $input->readStructBegin();
332 1         3 while (1) {
333 3         11 my %meta;
334              
335 3         15 $input->readFieldBegin(\$meta{name}, \$meta{type}, \$meta{id});
336              
337 3 100       36 last if $meta{type} == TType::STOP;
338              
339             # Reference the Thrift::IDL::Field if present
340 2         6 $meta{idl} = $idl_fields_by_id{$meta{id}};
341              
342             # Read the value of the field from the input
343 2         8 my $value = $self->parse_type($input, \%meta);
344 2 50       10 push @fields, Thrift::Parser::Field->new({
345             id => $meta{id},
346             value => $value,
347             name => ($meta{idl} ? $meta{idl}{name} : undef),
348             });
349              
350 2         10 $input->readFieldEnd();
351             }
352 1         6 $input->readStructEnd();
353              
354 1         10 return Thrift::Parser::FieldSet->new({ fields => \@fields });
355             }
356              
357             =head2 parse_type
358              
359             my $typed_value = $parser->parse_type($transport, { idl => $thrift_idl_type_object || type => 4 });
360              
361             Reads a single value off the transport and returns it as an object in a L subclass.
362              
363             =cut
364              
365             sub parse_type {
366 2     2 1 6 my ($self, $input, $meta) = @_;
367              
368 2         2 my $type_class;
369 2 50       7 if ($meta->{idl}) {
370 2         30 $type_class = $self->idl_type_class($meta->{idl}{type});
371             }
372             else {
373             # Field didn't correspond with an expected field from the IDL
374 0         0 my $type_name = Thrift::Parser::Types->to_name($meta->{type});
375 0 0       0 if (! defined $type_name) {
376 0         0 die "Failed to find type name from type id $$meta{type}; " . Dumper($meta);
377             }
378 0         0 $type_class = 'Thrift::Parser::Type::' . lc $type_name;
379             }
380              
381 2         29 my $typed_value = $type_class->new();
382 2 50       44 if ($typed_value->can('read')) {
383 0         0 $typed_value->read($self, $input, $meta);
384 0         0 return $typed_value;
385             }
386              
387 2         19 my $read_method = Thrift::Parser::Types->read_method($meta->{type});
388 2 50       12 if ($input->can($read_method)) {
389 2         10 $input->$read_method(\$meta->{value});
390 2         23 $typed_value->value($meta->{value});
391             }
392             else {
393 0         0 my $type = Thrift::Parser::Types->to_name($meta->{type});
394 0         0 die "Don't know how to read $type; tried $read_method";
395             }
396              
397 2         232 return $typed_value;
398             }
399              
400             =head2 idl_type_class
401              
402             my $parser_type_class = $parser->idl_type_class($thrift_idl_type_object);
403              
404             Maps the given L object to one in my parser namespace. If it's a custom type, it'll map into a dynamic class.
405              
406             =cut
407              
408             sub idl_type_class {
409 8     8 1 96 my ($self, $type) = @_;
410 8 100       94 if ($type->isa('Thrift::IDL::Type::Custom')) {
411 3         15 my $referenced_type = $self->idl->object_full_named($type->full_name);
412 3 50       611 if (! $referenced_type) {
413 0         0 die "Couldn't find definition of custom type '".$type->full_name."'";#; ".Dumper($self->idl);
414             }
415 3         45 my $namespace = $referenced_type->{header}->namespace('perl');
416 3 50       38 return join '::', (defined $namespace ? ($namespace) : ()), $type->local_name;
417             }
418             else {
419 5         22 return 'Thrift::Parser::Type::' . $type->name;
420             }
421             }
422              
423             =head2 resolve_idl_type
424              
425             my $thrift_idl_type_object = $parser->resolve_idl_type($thrift_idl_custom_type_object);
426              
427             Returns the base L object from the given L object
428              
429             =cut
430              
431             # FIXME: Shouldn't this be in L?
432              
433             sub resolve_idl_type {
434 2     2 1 5 my ($self, $type) = @_;
435 2         14 while ($type->isa('Thrift::IDL::Type::Custom')) {
436 2         9 $type = $self->idl->object_named($type->name)->type;
437             }
438 2         45 return $type;
439             }
440              
441             =head1 SEE ALSO
442              
443             L, L
444              
445             =head1 DEVELOPMENT
446              
447             This module is being developed via a git repository publicly available at L. I encourage anyone who is interested to fork my code and contribute bug fixes or new features, or just have fun and be creative.
448              
449             =head1 COPYRIGHT
450              
451             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.
452              
453             The full text of the license can be found in the LICENSE file included with this module.
454              
455             =head1 AUTHOR
456              
457             Eric Waters
458              
459             =cut
460              
461             1;