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   73 use strict;
  11         20  
  11         304  
4 11     11   56 use warnings;
  11         24  
  11         298  
5              
6 11     11   55 use List::Util qw/first/;
  11         20  
  11         1204  
7 11     11   76 use Protocol::FIX qw/humanize/;
  11         31  
  11         275  
8 11     11   4406 use Protocol::FIX::TagsAccessor;
  11         33  
  11         335  
9 11     11   4490 use Protocol::FIX::MessageInstance;
  11         29  
  11         19751  
10              
11             our $VERSION = '0.08'; ## 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   548 my ($protocol, $pair, $check_value) = @_;
21 290 100       508 return unless $pair;
22              
23 289 100       1761 if ($pair =~ /^(\d{1,})($Protocol::FIX::TAG_SEPARATOR)(.*)$/) {
24 286         850 my ($tag_id, $value) = ($1, $3);
25              
26 286         821 my $field = $protocol->{fields_lookup}->{by_number}->{$tag_id};
27 286 100       577 return (undef, "Protocol error: unknown tag '$tag_id' in tag pair '" . Protocol::FIX::humanize($pair) . "'")
28             unless $field;
29              
30 285 50       648 return unless defined $3;
31              
32 285 100       486 return ([$field, $value]) unless $check_value;
33              
34             return (undef,
35 279 100       707 "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         906 return ([$field, $value]);
39              
40             } else {
41 3 100       80 if ($pair =~ /[\D$Protocol::FIX::TAG_SEPARATOR]+/) {
42 2         10 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 266 my ($protocol, $buff_ref) = @_;
61 124         209 my $consumed_length = 0;
62              
63             # this are fatal messages, i.e. point to developer error, hence we die
64 124 50       305 die("buffer is undefined, cannot parse") unless defined $buff_ref;
65 124 50       301 die("buffer is not scalar reference") unless ref($buff_ref) eq 'SCALAR';
66              
67 124         238 my $begin_string = $protocol->{begin_string};
68 124         207 my $buff_length = length($$buff_ref);
69              
70             # no enough data
71 124 100       362 return (undef, undef) if $buff_length < length($begin_string);
72              
73 114 100       328 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       246 return if $buff_length == length($begin_string);
79              
80 111         1614 my @header_pairs = split $Protocol::FIX::SEPARATOR, $$buff_ref, 3;
81              
82             # minimal required fields: BeginString, MessageLength, MessageType, CheckSum
83 111 100       362 return (undef, "Protocol error: separator expecter right after $begin_string")
84             if (@header_pairs < 2);
85              
86 110         192 $consumed_length += length($begin_string) + 1;
87              
88             # extract, but do not check-for-correctness body length
89 110         191 my $body_length = do {
90 110         186 my $pair_is_complete = @header_pairs > 2;
91 110         263 my ($tag_pair, $err) = _parse_tag_pair($protocol, $header_pairs[1], $pair_is_complete);
92 110 100       287 return (undef, $err) unless $tag_pair;
93              
94 104         230 my ($field, $value) = @$tag_pair;
95 104 100       257 if ($field->{name} ne 'BodyLength') {
96 1         15 return (undef, "Protocol error: expected field 'BodyLength', but got '" . $field->{name} . "', in sequence '" . $header_pairs[1] . "'");
97             }
98 103 100       224 return unless $pair_is_complete;
99              
100 98         166 $consumed_length += length($header_pairs[1]) + 1;
101 98         253 $value;
102             };
103              
104 98 100       415 return (undef, undef) if $buff_length <= $consumed_length + $body_length;
105             # -1 is used to include separator
106 29         73 my $trailer = substr($$buff_ref, $consumed_length + $body_length);
107              
108             # -1 is included as it is terminator / end of message boundary
109 29         279 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     171 return (undef, undef) if (@trailer_pairs < 2) || !$trailer_pairs[0];
114             # from here we assume $body_length is valid
115              
116 22         49 my $checksum_pair = $trailer_pairs[0];
117 22         36 my $body;
118 22         33 my $checksum = do {
119 22         49 my ($tag_pair, $err) = _parse_tag_pair($protocol, $checksum_pair, 1);
120 22 50       68 return (undef, $err) if $err;
121              
122 22         46 my ($field, $value) = @$tag_pair;
123 22 50       63 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         71 my $is_number = ($value =~ /^\d{3}$/);
128              
129 22 100 66     119 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         73 my $header_body = substr($$buff_ref, 0, $consumed_length + $body_length);
139 19         31 my $sum = 0;
140 19         577 $sum += ord $_ for split //, $header_body;
141 19         112 $sum %= 256;
142              
143 19 100       61 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         44 $body = substr($$buff_ref, $consumed_length, $body_length);
148 18         49 $value;
149             };
150 18         65 my ($message, $error) = _parse_body($protocol, \$body);
151 18 100       95 return (undef, $error) if $error;
152              
153             # checksum surrounded by separators
154 11         29 my $trailer_length = length($checksum_pair) + 1;
155 11         20 my $total_length = $consumed_length + $body_length + $trailer_length;
156              
157 11         32 $$buff_ref = substr $$buff_ref, $total_length;
158 11         63 return ($message, undef);
159             }
160              
161             sub _parse_body {
162 18     18   40 my ($protocol, $body_ref) = @_;
163 18         201 my @pairs = split $Protocol::FIX::SEPARATOR, $$body_ref;
164              
165             my @tag_pairs = map {
166 18         57 my ($tag_pair, $err) = _parse_tag_pair($protocol, $_, 1);
  158         267  
167 158 100       323 return (undef, $err) if $err;
168 157         280 $tag_pair;
169             } @pairs;
170              
171 17     17   199 my $msg_pair_idx = first { $tag_pairs[$_]->[0]->{name} eq 'MsgType' } (0 .. @tag_pairs - 1);
  17         54  
172 17 100       82 return (undef, "Protocol error: 'MsgType' was not found in body")
173             unless defined $msg_pair_idx;
174              
175 16         43 my $msg_type_pair = splice @tag_pairs, $msg_pair_idx, 1;
176              
177 16         40 my (undef, $msg_type) = @$msg_type_pair;
178 16         45 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       48 return (undef, "Protocol error: MessageType '$msg_type' is not available")
182             unless $message;
183              
184 16         48 my ($ta, $err) = _construct_tag_accessor($protocol, $message, \@tag_pairs, 1);
185 16 100       77 return (undef, $err) if $err;
186 11         65 return (Protocol::FIX::MessageInstance->new($message, $ta));
187             }
188              
189             sub _construct_tag_accessor_component {
190 17     17   38 my ($protocol, $composite, $tag_pairs) = @_;
191 17         27 my $field = $tag_pairs->[0]->[0];
192              
193 17         34 my $sub_composite_name = $composite->{field_to_component}->{$field->{name}};
194 17         37 my $composite_desc = $composite->{composite_by_name}->{$sub_composite_name};
195             # TODO: check for required in description?
196 17         34 my ($sub_composite) = @$composite_desc;
197 17         49 my ($ta, $error) = _construct_tag_accessor($protocol, $sub_composite, $tag_pairs, 0);
198 17 100       42 return (undef, $error) if ($error);
199 16         39 return ([$sub_composite => $ta]);
200             }
201              
202             sub _construct_tag_accessor_field {
203 121     121   206 my (undef, undef, $tag_pairs) = @_;
204              
205 121         148 my ($field, $value) = @{shift(@$tag_pairs)};
  121         231  
206             my $humanized_value =
207             $field->has_mapping
208 121 100       283 ? $field->{values}->{by_id}->{$value}
209             : $value;
210 121         358 return ([$field => $humanized_value]);
211             }
212              
213             sub _construct_tag_accessor_group {
214 12     12   39 my ($protocol, $composite, $tag_pairs) = @_;
215 12         22 my ($field, $value) = @{shift(@$tag_pairs)};
  12         27  
216              
217 12         27 my $composite_desc = $composite->{composite_by_name}->{$field->{name}};
218             # todo: check that group have required field?
219 12         28 my ($sub_composite, undef) = @$composite_desc;
220              
221 12         19 my @tag_accessors;
222 12         53 for my $idx (1 .. $value) {
223 20         52 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       82 . $sub_composite->{name} . "')")
230             unless $ta;
231 19         46 push @tag_accessors, $ta;
232             }
233 11         54 return ([$sub_composite => \@tag_accessors]);
234             }
235              
236             sub _construct_tag_accessor {
237 53     53   96 my ($protocol, $composite, $tag_pairs, $fail_on_missing) = @_;
238              
239 53         82 my @direct_pairs;
240             my %parsed_subcomposites;
241 53         112 while (@$tag_pairs) {
242              
243             # non-destructive look ahead
244 172         248 my $field = $tag_pairs->[0]->[0];
245              
246 172         603 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     713 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     729 : ($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       291 if ($constructor) {
263 150         276 my ($ta_descr, $error) = $constructor->($protocol, $composite, $tag_pairs);
264 150 100       314 return (undef, $error) if ($error);
265 148         266 my ($sub_composite, $tags_accessor) = @$ta_descr;
266 148         286 push @direct_pairs, $sub_composite => $tags_accessor;
267 148         499 $parsed_subcomposites{$sub_composite->{name}} = 1;
268             } else {
269             # the error can occur only for top-level message
270             return (undef,
271 13 100       59 "Protocol error: field '" . $field->{name} . "' was not expected in " . $composite->{type} . " '" . $composite->{name} . "'")
272             if $fail_on_missing;
273 10         28 last;
274             }
275             }
276 48   50     76 for my $mandatory_composite (@{$composite->{mandatory_composites} // []}) {
  48         172  
277 83 100       196 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       179 return (@direct_pairs ? Protocol::FIX::TagsAccessor->new(\@direct_pairs) : ());
282             }
283              
284             1;