File Coverage

blib/lib/Thrift/IDL.pm
Criterion Covered Total %
statement 162 175 92.5
branch 33 48 68.7
condition 10 12 83.3
subroutine 34 34 100.0
pod 2 3 66.6
total 241 272 88.6


line stmt bran cond sub pod time code
1             package Thrift::IDL;
2              
3             =head1 NAME
4              
5             Thrift::IDL - Parser and OO representation of a Thrift interface definintion language (IDL)
6              
7             =head1 SYNOPSIS
8              
9             my $idl = Thrift::IDL->parse_thrift_file('tutorial.thrift');
10              
11             foreach my $service ($idl->services) {
12             printf "Offers service '%s':\n", $service->name;
13             }
14              
15              
16             =head1 DESCRIPTION
17              
18             The Thrift interface definition language (IDL) file is a structured file describing all the data types, services, methods, etc. of a Thrift interface. This is necessary if you need an in-memory representation of the Thrift schema.
19              
20             =cut
21              
22 6     6   198481 use strict;
  6         16  
  6         241  
23 6     6   33 use warnings;
  6         12  
  6         208  
24 6     6   12114 use Parse::RecDescent;
  6         409568  
  6         56  
25 6     6   416 use Cwd qw(abs_path);
  6         16  
  6         418  
26 6     6   4676 use Thrift::IDL::Comment;
  6         32  
  6         79  
27 6     6   4999 use Thrift::IDL::Constant;
  6         17  
  6         59  
28 6     6   3698 use Thrift::IDL::CppInclude;
  6         20  
  6         52  
29 6     6   252 use Thrift::IDL::Definition;
  6         12  
  6         107  
30 6     6   10484 use Thrift::IDL::DocumentHeader;
  6         16  
  6         67  
31 6     6   3818 use Thrift::IDL::Document;
  6         19  
  6         1484  
32 6     6   7782 use Thrift::IDL::Enum;
  6         19  
  6         73  
33 6     6   4316 use Thrift::IDL::Exception;
  6         19  
  6         72  
34 6     6   3675 use Thrift::IDL::Field;
  6         18  
  6         49  
35 6     6   234 use Thrift::IDL::Header;
  6         11  
  6         67  
36 6     6   3584 use Thrift::IDL::Include;
  6         57  
  6         53  
37 6     6   3727 use Thrift::IDL::Method;
  6         23  
  6         67  
38 6     6   4492 use Thrift::IDL::Namespace;
  6         20  
  6         58  
39 6     6   4159 use Thrift::IDL::Senum;
  6         20  
  6         73  
40 6     6   3601 use Thrift::IDL::Service;
  6         18  
  6         52  
41 6     6   237 use Thrift::IDL::Struct;
  6         13  
  6         77  
42 6     6   3632 use Thrift::IDL::Type;
  6         21  
  6         59  
43 6     6   3709 use Thrift::IDL::Type::Base;
  6         17  
  6         62  
44 6     6   3832 use Thrift::IDL::Type::Custom;
  6         20  
  6         56  
45 6     6   3995 use Thrift::IDL::Type::List;
  6         20  
  6         59  
46 6     6   4323 use Thrift::IDL::Type::Map;
  6         22  
  6         68  
47 6     6   3831 use Thrift::IDL::Type::Set;
  6         18  
  6         58  
48 6     6   3966 use Thrift::IDL::TypeDef;
  6         17  
  6         88  
49              
50             our ($wrap, $g_current_header);
51              
52             our %reserved_keywords = map { $_ => 1 } qw(
53             abstract and args as assert break case class continue declare def default
54             del delete do elif else elseif except exec false finally float for
55             foreach function global goto if implements import in inline instanceof interface is
56             lambda native new not or pass public print private protected raise return sizeof
57             static switch synchronized this throw transient true try unsigned var virtual volatile
58             while with union yield register
59             );
60              
61             =head1 METHODS
62              
63             =head2 parse_thrift_file ($input_filename, $debug)
64              
65             my $document = Thrift::IDL->parse_thrift_file('tutorial.thrift');
66              
67             Given a filename of a Thrift IDL file and an optional boolean for debug output, parse the input file into a L object and return it. The debug flag will cause verbose output on STDERR.
68              
69             =cut
70              
71             sub parse_thrift_file {
72 1     1 1 13 my ($class, $input_fn, $debug) = @_;
73 1         9 return $class->_new_from_parsed({ file => $input_fn }, $debug);
74             }
75              
76             =head2 parse_thrift ($data, $debug)
77              
78             my $document = Thrift::IDL->parse_thrift(...);
79              
80             Given a scalar of a Thrift IDL file and an optional boolean for debug output, parse the input into a L object and return it. The debug flag will cause verbose output on STDERR.
81              
82             NOTE: If the thrift references other documents via an B statement, you'll need to use absolute paths
83              
84             =cut
85              
86             sub parse_thrift {
87 5     5 1 3787 my ($class, $data, $debug) = @_;
88 5         41 return $class->_new_from_parsed({ buffer => $data }, $debug);
89             }
90              
91             sub parser {
92 7     7 0 19 my ($class, $debug) = @_;
93              
94 7         30 $::RD_ERRORS = 1; # Make sure the parser dies when it encounters an error
95 7         17 $::RD_WARN = 1; # Enable warnings. This will warn on unused rules &c.
96 7         15 $::RD_HINT = 1; # Give out hints to help fix problems.
97             #$::RD_AUTOACTION = q { [@item[0..$#item]] };
98              
99             $wrap = sub {
100 217     217   629466 my $self = shift;
101 217         715 my $class = delete $self->{class};
102 217 50       670 die "No class!" unless $class;
103 217         625 $class = 'Thrift::IDL::' . $class;
104 217         983 my $return = bless $self, $class;
105 217 100       2325 if ($return->can('setup')) {
106 19         132 $return->setup;
107             }
108              
109             # Store a reference to the current global DocumentHeader
110 217         9098 $self->{header} = $g_current_header;
111              
112 217 50       569 print "Created $class $return\n" if $debug;
113 217         7618 return $return;
114 7         63 };
115              
116 7 50       409 my $parser = Parse::RecDescent->new(<<'EOF') or die "Invalid grammer!";
117             document: (comment | header | definition)(s)
118              
119             comment: (
120             /\/[*] .*? [*]\//sx
121             | /^\/\/ .*/x
122             | /^[#] .*/x
123             )
124             { $Thrift::IDL::wrap->({ class => 'Comment', value => $item[1] }) }
125              
126             header: ( include | cpp_include | namespace )
127              
128             definition: (
129             const
130             | typedef
131             | enum
132             | senum
133             | struct
134             | service
135             )
136             { $item[1] }
137              
138             include:
139             'include' quoted_string
140             { $Thrift::IDL::wrap->({ class => 'Include', value => $item[2] }) }
141              
142             cpp_include:
143             'cpp_include' quoted_string
144             { $Thrift::IDL::wrap->({ class => 'CppInclude', value => $item[2] }) }
145              
146             namespace:
147             'namespace' (word | '*') word
148             { $Thrift::IDL::wrap->({ class => 'Namespace', scope => $item[2], value => $item[3] }) }
149              
150             const:
151             'const' type name '=' value
152             { $Thrift::IDL::wrap->({ class => 'Constant', type => $item[2], name => $item[3], value => $item[5] }) }
153              
154             typedef:
155             'typedef' type name
156             { $Thrift::IDL::wrap->({ class => 'TypeDef', type => $item[2], name => $item[3] }) }
157              
158             enum:
159             'enum' name '{' enum_pair(s /,/) '}'
160             { $Thrift::IDL::wrap->({ class => 'Enum', name => $item[2], values => $item[4] }) }
161              
162             senum:
163             'senum' name '{' quoted_string(s /,/) '}'
164             { $Thrift::IDL::wrap->({ class => 'Senum', name => $item[2], values => $item[4] }) }
165              
166             enum_pair:
167             name ('=' number)(?)
168             { [ $item[1] => $item[2][0] ] }
169              
170             struct:
171             ('struct' | 'exception') name '{' field_statement(s /,?/) (',' | '') '}'
172             { $Thrift::IDL::wrap->({ class => $item[1] eq 'struct' ? 'Struct' : 'Exception', name => $item[2], children => $item[4] }) }
173              
174             field_statement:
175             comment | field
176              
177             field:
178             number ':' ('optional')(?) type name ('=' value)(?)
179             { $Thrift::IDL::wrap->({ class => 'Field', id => $item[1], optional => $item[3][0] ? 1 : undef, type => $item[4], name => $item[5], default_value => $item[6][0] }) }
180              
181             service:
182             'service' name ('extends' word)(?) '{' service_statement(s) '}'
183             { $Thrift::IDL::wrap->({ class => 'Service', name => $item[2], extends => $item[3][0], children => $item[5] }) }
184              
185             service_statement:
186             comment | method_statement
187              
188             method_statement:
189             ('oneway')(?) type name '(' field_statement(s? /,?/) (',')(?) ')' (throws)(?) (',')(?)
190             #{ print "$item[0]: ".join(', ', map { "'$_'" } @item[1 .. $#item]) . "\n" }
191             { $Thrift::IDL::wrap->({
192             class => 'Method',
193             oneway => $item[1][0] ? 1 : undef,
194             returns => $item[2],
195             name => $item[3],
196             arguments => $item[5],
197             throws => $item[8][0] ? $item[8][0] : [],
198             }) }
199              
200             throws:
201             'throws' '(' field_statement(s? /,/) ')'
202             #{ print "$item[0]: ".join(', ', map { "'$_'" } @item[1 .. $#item]) . "\n" }
203             { $item[3] }
204              
205             type:
206             base_type | container_type | custom_type
207            
208             base_type:
209             ('bool' | 'byte' | 'i16' | 'i32' | 'i64' | 'double' | 'string' | 'binary' | 'slist' | 'void')
210             { $Thrift::IDL::wrap->({ class => 'Type::Base', name => $item[1] }) }
211              
212             custom_type:
213             name
214             { $Thrift::IDL::wrap->({ class => 'Type::Custom', name => $item[1] }) }
215              
216             container_type:
217             map_type | set_type | list_type
218              
219             map_type:
220             'map' cpp_type(?) '<' type ',' type '>'
221             { $Thrift::IDL::wrap->({ class => 'Type::Map', key_type => $item[4], val_type => $item[6], cpp_type => $item[2][0] }) }
222              
223             set_type:
224             'set' cpp_type(?) '<' type '>'
225             { $Thrift::IDL::wrap->({ class => 'Type::Set', val_type => $item[4], cpp_type => $item[2][0] }) }
226              
227             list_type:
228             'list' '<' type '>' cpp_type(?)
229             { $Thrift::IDL::wrap->({ class => 'Type::List', val_type => $item[3], cpp_type => $item[5][0] }) }
230              
231             cpp_type:
232             'cpp_type' quoted_string
233             { $item[2] }
234              
235             name:
236             /[A-Za-z_][A-Za-z0-9_.]*/
237             { if ($Thrift::IDL::reserved_keywords{$item[1]}) { warn "Cannot use reserved language keyword: $item[1]\n"; return undef } $item[1] }
238             #{ print "$item[0]: ".join(', ', map { "'$_'" } @item[1 .. $#item]) . "\n"; $return = $item[1]; }
239              
240             word:
241             /(\w|[.])+/
242             #{ print "$item[0]: ".join(', ', map { "'$_'" } @item[1 .. $#item]) . "\n"; $return = $item[1]; }
243              
244             value:
245             number | value_map
246              
247             value_map:
248             '{' value_map_pair(s /,/) '}'
249             { { map { @$_ } @{ $item[2] } } }
250              
251             value_map_pair:
252             quoted_string ':' quoted_string
253             { [ $item[1] => $item[3] ] }
254              
255             number:
256             /[0-9]+/
257              
258             quoted_string:
259             single_quoted_string | double_quoted_string
260              
261             double_quoted_string:
262             '"' /[^"]+/ '"'
263             { $item[3] }
264              
265             single_quoted_string:
266             "'" /[^']+/ "'"
267             { $item[3] }
268             EOF
269              
270 7         3222064 return $parser;
271             }
272              
273             sub _new_from_parsed {
274 6     6   16 my ($class, $source, $debug) = @_;
275              
276 6         40 my ($children, $headers) = $class->_parse_thrift($source, $debug, {});
277              
278 6         136 my $document = Thrift::IDL::Document->new({ children => $children, headers => $headers });
279 6         113 _set_associations($document);
280              
281 6         46 return $document;
282             }
283              
284             sub _parse_thrift {
285 7     7   21 my ($class, $source, $debug, $state) = @_;
286              
287 7   100     59 my $input_fn = $source->{file} || undef;
288              
289 7 50 66     83 if (defined $input_fn && $state->{_parse_input_fn_processed}{$input_fn}++) {
290 0         0 print "Skipping repeat processing of $input_fn\n";
291 0         0 return ([], []);
292             }
293              
294 7   100     39 my $data = $source->{buffer} || undef;
295              
296 7 100       37 if ($input_fn) {
297             # Slurp the file in
298 2         10 local $/ = undef;
299 2 50       165 open my $in, '<', $input_fn or die "Can't read from $input_fn: $!";
300 2         600 $data = <$in>;
301 2         76 close $in;
302             }
303              
304             # Create a DocumentHeader object
305 7         177 my $header = Thrift::IDL::DocumentHeader->new({ includes => [], namespaces => [] });
306              
307 7 100       123 if ($input_fn) {
308 2         25 my ($basename) = $input_fn =~ m{([^/]+)\.thrift$};
309 2 50       18 $header->basename($basename) if defined $basename;
310             }
311              
312             # Set the global $g_current_header to this document header so all blessed objects
313             # created upon parsing will have a reference to $header
314 7         281 $g_current_header = $header;
315              
316 7 50 66     49 print STDERR "Parsing '$input_fn'\n" if $input_fn && $debug;
317              
318 7 50       42 my $parsed = $class->parser($debug)->document(\$data) or die "Bad input";
319              
320 7 50       33664 if ($data !~ m{^\s*$}s) {
321 0         0 my $error = "Parsing failed to consume all of the input; stopped at:\n";
322 0         0 my @lines = grep { $_ !~ /^\s*$/ } split /\n/, $data;
  0         0  
323 0 0       0 my $cropped = int @lines > 20 ? 1 : 0;
324 0         0 foreach my $line (@lines[0 .. 19]) {
325             #next unless $line;
326 0         0 chomp $line;
327 0         0 $error .= "> $line\n";
328             }
329 0 0       0 if ($cropped) {
330 0         0 $error .= " (more cropped)\n";
331             }
332 0         0 die $error;
333             }
334              
335             # Setup return of header list
336 7         63224 my @headers;
337 7         32 push @headers, $header;
338              
339 7         33 my $include_base = '/'; # Somewhat insane default value
340 7 100       36 if ($input_fn) {
341             # Check for any Include headers and include those elements as well
342 2         276 $input_fn = abs_path($input_fn);
343 2         26 ($include_base) = $input_fn =~ m{^(.+)/[^/]+$};
344             }
345              
346             # Extract header objects and associate each non-header child object with
347             # the document header (for namespacing mainly)
348 7         19 my (@parsed, @comments);
349 7         34 foreach my $child (@$parsed) {
350             # Collect comments that come before Include and Namespace objects; dispose of them when we dispose of the object
351             # Otherwise, we'll have combined comments that don't make sense (comment for now gone Namespace with comment
352             # for a typedef)
353 68 100       859 if ($child->isa('Thrift::IDL::Comment')) {
    100          
354 28         52 push @comments, $child;
355             }
356             elsif (! $child->isa('Thrift::IDL::Header')) {
357             #$child->{header} = $header;
358 27         63 push @parsed, @comments, $child;
359 27         65 @comments = ();
360             }
361             else {
362 13 100       137 if ($child->isa('Thrift::IDL::Include')) {
    50          
363 1         2 push @{ $header->{includes} }, $child;
  1         4  
364             # Insert the included document at the same point as the 'include' declaration
365 1         9 my $include_fn = abs_path($include_base . '/' . $child->value);
366 1 50       131 if (! -f $include_fn) {
367 0         0 die "include file '".$child->value."' not found in $include_base";
368             }
369 1         17 my ($include_parsed, $include_headers) = $class->_parse_thrift({ file => $include_fn }, $debug, $state);
370 1         7 push @parsed, @$include_parsed;
371 1         3 push @headers, @$include_headers;
372 1         7 @comments = ();
373             }
374             elsif ($child->isa('Thrift::IDL::Namespace')) {
375 12         23 push @{ $header->{namespaces} }, $child;
  12         672  
376 12         39 @comments = ();
377             }
378             }
379             }
380 7         91 return (\@parsed, \@headers);
381             }
382              
383             sub _set_associations {
384 21     21   137 my $element = shift;
385              
386 21         31 my @comments;
387 21         41 for (my $i = 0; $i <= $#{ $element->{children} }; $i++) {
  95         1220  
388 74         154 my $child = $element->{children}[$i];
389              
390             # Reference myself in the child object
391             #$child->{parent} = $element;
392              
393 74 100       479 $child->{service} = $element if $child->isa('Thrift::IDL::Method');
394              
395             # Associate comments with the child elements that follow the comments
396 74 100       432 if ($child->isa('Thrift::IDL::Comment')) {
    100          
397 12         22 push @comments, $child;
398             }
399             elsif (@comments) {
400 9         30 $child->{comments} = [ @comments ];
401 9         16 @comments = ();
402             }
403              
404 74   100     444 $child->{comments} ||= [];
405              
406 74 100       228 if ($child->{children}) {
407 15         59 _set_associations($child);
408             }
409             }
410             }
411             1;