File Coverage

blib/lib/Protocol/FIX/Parser.pm
Criterion Covered Total %
statement 143 144 99.3
branch 72 78 92.3
condition 11 14 78.5
subroutine 14 14 100.0
pod 1 1 100.0
total 241 251 96.0


line stmt bran cond sub pod time code
1             package Protocol::FIX::Parser;
2              
3 11     11   78 use strict;
  11         24  
  11         361  
4 11     11   57 use warnings;
  11         22  
  11         327  
5              
6 11     11   61 use List::Util qw/first/;
  11         21  
  11         1490  
7 11     11   107 use Protocol::FIX qw/humanize/;
  11         22  
  11         308  
8 11     11   4913 use Protocol::FIX::TagsAccessor;
  11         26  
  11         338  
9 11     11   4942 use Protocol::FIX::MessageInstance;
  11         28  
  11         20528  
10              
11             our $VERSION = '0.06'; ## VERSION
12              
13             =head1 NAME
14              
15             Protocol::FIX::Parser - FIX messages parser
16              
17             =cut
18              
19             sub _parse_tag_pair {
20 290     290   591 my ($protocol, $pair, $check_value) = @_;
21 290 100       578 return unless $pair;
22              
23 289 100       1893 if ($pair =~ /^(\d{1,})($Protocol::FIX::TAG_SEPARATOR)(.*)$/) {
24 286         914 my ($tag_id, $value) = ($1, $3);
25              
26 286         814 my $field = $protocol->{fields_lookup}->{by_number}->{$tag_id};
27 286 100       587 return (undef, "Protocol error: unknown tag '$tag_id' in tag pair '" . Protocol::FIX::humanize($pair) . "'")
28             unless $field;
29              
30 285 50       735 return unless defined $3;
31              
32 285 100       601 return ([$field, $value]) unless $check_value;
33              
34             return (undef,
35 279 100       813 "Protocol error: value for field '" . $field->{name} . "' does not pass validation in tag pair '" . Protocol::FIX::humanize($pair) . "'")
36             unless $field->check_raw($value);
37              
38 277         893 return ([$field, $value]);
39              
40             } else {
41 3 100       50 if ($pair =~ /[\D$Protocol::FIX::TAG_SEPARATOR]+/) {
42 2         14 return (undef, "Protocol error: sequence '" . Protocol::FIX::humanize($pair) . "' does not match tag pair");
43             }
44 1         7 return;
45             }
46             }
47              
48             =head1 METHODS (for protocol developers)
49              
50             =head3 parse
51              
52             parse($protocol, $buff_ref)
53              
54             Tries to parse FIX message in the buffer refernce. Not for end-user usage;
55             please refer to L.
56              
57             =cut
58              
59             sub parse {
60 124     124 1 305 my ($protocol, $buff_ref) = @_;
61 124         216 my $consumed_length = 0;
62              
63             # this are fatal messages, i.e. point to developer error, hence we die
64 124 50       370 die("buffer is undefined, cannot parse") unless defined $buff_ref;
65 124 50       349 die("buffer is not scalar reference") unless ref($buff_ref) eq 'SCALAR';
66              
67 124         283 my $begin_string = $protocol->{begin_string};
68 124         253 my $buff_length = length($$buff_ref);
69              
70             # no enough data
71 124 100       479 return (undef, undef) if $buff_length < length($begin_string);
72              
73 114 100       392 if (substr($$buff_ref, 0, length($begin_string)) ne $begin_string) {
74 1         8 return (undef, "Mismatch protocol introduction, expected: $begin_string");
75             }
76              
77             # no enough data
78 113 100       291 return if $buff_length == length($begin_string);
79              
80 111         2052 my @header_pairs = split $Protocol::FIX::SEPARATOR, $$buff_ref, 3;
81              
82             # minimal required fields: BeginString, MessageLength, MessageType, CheckSum
83 111 100       446 return (undef, "Protocol error: separator expecter right after $begin_string")
84             if (@header_pairs < 2);
85              
86 110         228 $consumed_length += length($begin_string) + 1;
87              
88             # extract, but do not check-for-correctness body length
89 110         183 my $body_length = do {
90 110         219 my $pair_is_complete = @header_pairs > 2;
91 110         344 my ($tag_pair, $err) = _parse_tag_pair($protocol, $header_pairs[1], $pair_is_complete);
92 110 100       327 return (undef, $err) unless $tag_pair;
93              
94 104         222 my ($field, $value) = @$tag_pair;
95 104 100       319 if ($field->{name} ne 'BodyLength') {
96 1         13 return (undef, "Protocol error: expected field 'BodyLength', but got '" . $field->{name} . "', in sequence '" . $header_pairs[1] . "'");
97             }
98 103 100       258 return unless $pair_is_complete;
99              
100 98         176 $consumed_length += length($header_pairs[1]) + 1;
101 98         260 $value;
102             };
103              
104 98 100       432 return (undef, undef) if $buff_length <= $consumed_length + $body_length;
105             # -1 is used to include separator
106 29         92 my $trailer = substr($$buff_ref, $consumed_length + $body_length);
107              
108             # -1 is included as it is terminator / end of message boundary
109 29         340 my @trailer_pairs = split $Protocol::FIX::SEPARATOR, $trailer, -1;
110              
111             # 2 tags expected: checksum - empty(or may be new message)
112             # the trailing SOH is included into BODY
113 29 100 66     223 return (undef, undef) if (@trailer_pairs < 2) || !$trailer_pairs[0];
114             # from here we assume $body_length is valid
115              
116 22         58 my $checksum_pair = $trailer_pairs[0];
117 22         45 my $body;
118 22         38 my $checksum = do {
119 22         67 my ($tag_pair, $err) = _parse_tag_pair($protocol, $checksum_pair, 1);
120 22 50       61 return (undef, $err) if $err;
121              
122 22         74 my ($field, $value) = @$tag_pair;
123 22 50       89 if ($field->{name} ne 'CheckSum') {
124 0         0 return (undef, "Protocol error: expected field 'CheckSum', but got '" . $field->{name} . "', in sequence '" . $checksum_pair . "'");
125             }
126              
127 22         98 my $is_number = ($value =~ /^\d{3}$/);
128              
129 22 100 66     213 if (!$is_number || ($is_number && ($value > 255))) {
      100        
130             return (undef,
131             "Protocol error: value for field '"
132             . $field->{name}
133 3         21 . "' does not pass validation in tag pair '"
134             . Protocol::FIX::humanize($checksum_pair)
135             . "'");
136             }
137              
138 19         80 my $header_body = substr($$buff_ref, 0, $consumed_length + $body_length);
139 19         46 my $sum = 0;
140 19         741 $sum += ord $_ for split //, $header_body;
141 19         175 $sum %= 256;
142              
143 19 100       69 if ($sum != $value) {
144             return (undef,
145 1         6 "Protocol error: Checksum mismatch; got $sum, expected $value for message '" . Protocol::FIX::humanize($header_body) . "'");
146             }
147 18         92 $body = substr($$buff_ref, $consumed_length, $body_length);
148 18         68 $value;
149             };
150 18         100 my ($message, $error) = _parse_body($protocol, \$body);
151 18 100       98 return (undef, $error) if $error;
152              
153             # checksum surrounded by separators
154 11         32 my $trailer_length = length($checksum_pair) + 1;
155 11         28 my $total_length = $consumed_length + $body_length + $trailer_length;
156              
157 11         42 $$buff_ref = substr $$buff_ref, $total_length;
158 11         72 return ($message, undef);
159             }
160              
161             sub _parse_body {
162 18     18   61 my ($protocol, $body_ref) = @_;
163 18         279 my @pairs = split $Protocol::FIX::SEPARATOR, $$body_ref;
164              
165             my @tag_pairs = map {
166 18         66 my ($tag_pair, $err) = _parse_tag_pair($protocol, $_, 1);
  158         324  
167 158 100       316 return (undef, $err) if $err;
168 157         308 $tag_pair;
169             } @pairs;
170              
171 17     17   279 my $msg_pair_idx = first { $tag_pairs[$_]->[0]->{name} eq 'MsgType' } (0 .. @tag_pairs - 1);
  17         76  
172 17 100       165 return (undef, "Protocol error: 'MsgType' was not found in body")
173             unless defined $msg_pair_idx;
174              
175 16         83 my $msg_type_pair = splice @tag_pairs, $msg_pair_idx, 1;
176              
177 16         48 my (undef, $msg_type) = @$msg_type_pair;
178 16         87 my $message = $protocol->{messages_lookup}->{by_number}->{$msg_type};
179             # we don't die, as it might be possible to use custom message types
180             # http://fixwiki.org/fixwiki/MsgType
181 16 50       59 return (undef, "Protocol error: MessageType '$msg_type' is not available")
182             unless $message;
183              
184 16         71 my ($ta, $err) = _construct_tag_accessor($protocol, $message, \@tag_pairs, 1);
185 16 100       82 return (undef, $err) if $err;
186 11         77 return (Protocol::FIX::MessageInstance->new($message, $ta));
187             }
188              
189             sub _construct_tag_accessor_component {
190 17     17   65 my ($protocol, $composite, $tag_pairs) = @_;
191 17         40 my $field = $tag_pairs->[0]->[0];
192              
193 17         42 my $sub_composite_name = $composite->{field_to_component}->{$field->{name}};
194 17         52 my $composite_desc = $composite->{composite_by_name}->{$sub_composite_name};
195             # TODO: check for required in description?
196 17         39 my ($sub_composite) = @$composite_desc;
197 17         73 my ($ta, $error) = _construct_tag_accessor($protocol, $sub_composite, $tag_pairs, 0);
198 17 100       52 return (undef, $error) if ($error);
199 16         43 return ([$sub_composite => $ta]);
200             }
201              
202             sub _construct_tag_accessor_field {
203 121     121   200 my (undef, undef, $tag_pairs) = @_;
204              
205 121         177 my ($field, $value) = @{shift(@$tag_pairs)};
  121         236  
206             my $humanized_value =
207             $field->has_mapping
208 121 100       336 ? $field->{values}->{by_id}->{$value}
209             : $value;
210 121         303 return ([$field => $humanized_value]);
211             }
212              
213             sub _construct_tag_accessor_group {
214 12     12   38 my ($protocol, $composite, $tag_pairs) = @_;
215 12         22 my ($field, $value) = @{shift(@$tag_pairs)};
  12         42  
216              
217 12         35 my $composite_desc = $composite->{composite_by_name}->{$field->{name}};
218             # todo: check that group have required field?
219 12         29 my ($sub_composite, undef) = @$composite_desc;
220              
221 12         24 my @tag_accessors;
222 12         54 for (my $idx = 1; $idx <= $value;) {
223 19         64 my ($ta) = _construct_tag_accessor($protocol, $sub_composite, $tag_pairs, 0);
224             return (undef,
225             "Protocol error: cannot construct item #${idx} for "
226             . $composite->{type} . " '"
227             . $composite->{name} . "' ("
228             . $sub_composite->{type} . " '"
229 19 100       71 . $sub_composite->{name} . "')")
230             unless $ta;
231 18         39 push @tag_accessors, $ta;
232 18         72 $idx += $ta->count;
233             }
234 11         34 return ([$sub_composite => \@tag_accessors]);
235             }
236              
237             sub _construct_tag_accessor {
238 52     52   118 my ($protocol, $composite, $tag_pairs, $fail_on_missing) = @_;
239              
240 52         84 my @direct_pairs;
241             my %parsed_subcomposites;
242 52         141 while (@$tag_pairs) {
243              
244             # non-destructive look ahead
245 171         262 my $field = $tag_pairs->[0]->[0];
246              
247             # do not look ahead too much, i.e. for group it is enough to construct just one item
248 171 100       430 last if $parsed_subcomposites{$field->{name}};
249              
250 163         434 my $owner = $composite->{field_to_component}->{$field->{name}};
251              
252             # The logic is following:
253             # 1. try to construct sub-components (if there are fields pointing to them)
254             # 2. otherwise try to construct field group
255             # 3. or simple (single) field
256             my $constructor =
257             ($owner && ($owner ne $composite->{name}))
258             ? \&_construct_tag_accessor_component
259 163 100 100     827 : ($composite->{composite_by_name}->{$field->{name}}) ? $field->{type} eq 'NUMINGROUP'
    100          
    100          
260             ? \&_construct_tag_accessor_group
261             : \&_construct_tag_accessor_field
262             : undef;
263              
264 163 100       309 if ($constructor) {
265 150         302 my ($ta_descr, $error) = $constructor->($protocol, $composite, $tag_pairs);
266 150 100       297 return (undef, $error) if ($error);
267 148         257 my ($sub_composite, $tags_accessor) = @$ta_descr;
268 148         299 push @direct_pairs, $sub_composite => $tags_accessor;
269 148         506 $parsed_subcomposites{$sub_composite->{name}} = 1;
270             } else {
271             # the error can occur only for top-level message
272             return (undef,
273 13 100       61 "Protocol error: field '" . $field->{name} . "' was not expected in " . $composite->{type} . " '" . $composite->{name} . "'")
274             if $fail_on_missing;
275 10         18 last;
276             }
277             }
278 47   50     78 for my $mandatory_composite (@{$composite->{mandatory_composites} // []}) {
  47         162  
279 82 100       188 if (!exists $parsed_subcomposites{$mandatory_composite}) {
280 1         14 return (undef, "Protocol error: '$mandatory_composite' is mandatory for " . $composite->{type} . " '" . $composite->{name} . "'");
281             }
282             }
283 46 100       228 return (@direct_pairs ? Protocol::FIX::TagsAccessor->new(\@direct_pairs) : ());
284             }
285              
286             1;