File Coverage

blib/lib/Protocol/FIX/Parser.pm
Criterion Covered Total %
statement 142 143 99.3
branch 72 78 92.3
condition 17 20 85.0
subroutine 14 14 100.0
pod 1 1 100.0
total 246 256 96.0


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