File Coverage

blib/lib/Protocol/FIX.pm
Criterion Covered Total %
statement 216 224 96.4
branch 29 40 72.5
condition 4 10 40.0
subroutine 33 33 100.0
pod 14 14 100.0
total 296 321 92.2


line stmt bran cond sub pod time code
1             package Protocol::FIX;
2             # ABSTRACT: Financial Information eXchange (FIX) messages parser/serializer
3              
4 11     11   722417 use strict;
  11         73  
  11         332  
5 11     11   60 use warnings;
  11         19  
  11         288  
6              
7 11     11   5551 use XML::Fast;
  11         141841  
  11         654  
8 11     11   6404 use File::ShareDir qw/dist_dir/;
  11         344009  
  11         733  
9 11     11   10341 use Path::Tiny;
  11         133267  
  11         742  
10              
11 11     11   6233 use Protocol::FIX::Component;
  11         32  
  11         376  
12 11     11   4276 use Protocol::FIX::Field;
  11         30  
  11         401  
13 11     11   5345 use Protocol::FIX::Group;
  11         28  
  11         322  
14 11     11   73 use Protocol::FIX::BaseComposite;
  11         19  
  11         206  
15 11     11   4719 use Protocol::FIX::Message;
  11         28  
  11         446  
16 11     11   5470 use Protocol::FIX::Parser;
  11         36  
  11         457  
17 11     11   98 use Exporter qw/import/;
  11         25  
  11         29890  
18              
19             our @EXPORT_OK = qw/humanize/;
20             our $VERSION = '0.06';
21              
22             =head1 NAME
23              
24             Protocol::FIX - Financial Information eXchange (FIX) messages parser/serializer
25              
26             =head1 SYNOPSIS
27              
28             use Protocol::FIX;
29              
30             my $proto = Protocol::FIX->new('FIX44')->extension('t/data/extension-sample.xml');
31              
32             my $serialized = $proto->serialize_message('IOI', [
33             SenderCompID => 'me',
34             TargetCompID => 'you',
35             MsgSeqNum => 1,
36             SendingTime => '20090107-18:15:16',
37             IOIID => 'abc',
38             IOITransType => 'CANCEL',
39             IOIQty => 'LARGE',
40             Side => 'BORROW',
41             Instrument => [
42             Symbol => 'EURUSD',
43             EvntGrp => [ NoEvents => [ [EventType => 'PUT'], [EventType => 'CALL'], [EventType => 'OTHER'] ] ],
44             ],
45             OrderQtyData => [
46             OrderQty => '499',
47             ],
48             ]);
49             # managed fields (BeginString, MsgType, and CheckSum) are handled automatically,
50             # no need to provide them
51              
52             my ($message_instance, $error) = $proto->parse_message(\$serialized);
53             print("No error on parsing message");
54             print "Message, ", $message_instance->name, " / ", $message_instance->category, "\n";
55              
56             print "Field 'SenderCompID' value: ", $message_instance->value('SenderCompID'), "\n";
57              
58             print "Component 'OrderQtyData' access: ",
59             $message_instance->value('OrderQtyData')->value('OrderQty'), "\n";
60              
61             my $group = $message_instance->value('Instrument')->value('EvntGrp')->value('NoEvents');
62             print "0th group 'NoEvents' of component 'Instrument/EvntGrp' access: ",
63             $group->[0]->value('EventType'), "\n";
64              
65             my $buff = '';
66             ($message_instance, $error) = $proto->parse_message(\$buff);
67             # no error nor message_instance, as there is no enough data.
68              
69             See also the "eg" folder for sample of FIX-server.
70              
71             =head1 DESCRIPTION
72              
73             With this module you can easily create new FIX messages in human-readable way, i.e. use
74             names like OrderQty => '499', instead of directly wring string like '39=499'; and vise
75             versa, you can parse the gibberish FIX messages to access fields in human-readable way
76             too.
77              
78             The module checks that mandatory fields are present, and that field values bypass
79             the validation.
80              
81             =cut
82              
83             my $distribution = 'Protocol-FIX';
84              
85             my %MANAGED_COMPOSITES = map { $_ => 1 } qw/BeginString BodyLength MsgType CheckSum/;
86              
87             my %specification_for = (fix44 => 'FIX44.xml');
88              
89             our $SEPARATOR = "\x{01}";
90             our $TAG_SEPARATOR = "=";
91              
92             =head1 METHODS
93              
94             =head3 new
95              
96             new($class, $version)
97              
98             Creates new protocol instance for the specified FIX protocol version. Currently
99             shipped version is 'FIX44'.
100              
101             The xml with protocol definition was taken at L.
102              
103             =cut
104              
105             sub new {
106 7     7 1 683 my ($class, $version) = @_;
107 7 50       30 die("FIX protocol version should be specified")
108             unless $version;
109              
110 7         37 my $file = $specification_for{lc $version};
111 7 50       28 die("Unsupported FIX protocol version: $version. Supported versions are: " . join(", ", sort { $a cmp $b } keys %specification_for))
  0         0  
112             unless $file;
113              
114 7   33     70 my $dir = $ENV{PROTOCOL_FIX_SHARE_DIR} // dist_dir($distribution);
115 7         1161 my $xml = path("$dir/$file")->slurp;
116 7         6586 my $protocol_definition = xml2hash $xml;
117 7         119327 my $obj = {
118             version => lc $version,
119             };
120 7         63 bless $obj, $class;
121 7         556 $obj->_construct_from_definition($protocol_definition);
122 7         17214 return $obj;
123             }
124              
125             =head3 extension
126              
127             extension($self, $extension_path)
128              
129             Modifies the protocol, by loading XML extension.
130              
131             The extension might contain additional B or B. The
132             extension XML should conform the format as the protocol definition itself,
133             i.e.:
134              
135            
136            
137            
138            
139            
140            
141            
142            
143            
144            
145            
146            
147            
148            
149            
150              
151             =cut
152              
153             sub extension {
154 2     2 1 21 my ($self, $extension_path) = @_;
155              
156 2         20 my $xml = path($extension_path)->slurp;
157 2         2441 my $definition = xml2hash $xml;
158              
159 2         132 my ($type, $major, $minor) = @{$definition->{fix}}{qw/-type -major -minor/};
  2         13  
160 2         10 my $extension_id = join('.', $type, $major, $minor);
161 2         7 my $protocol_id = $self->{id};
162 2 50       12 die("Extension ID ($extension_id) does not match Protocol ID ($protocol_id)")
163             unless $extension_id eq $protocol_id;
164              
165 2         28 my $new_fields_lookup = $self->_construct_fields($definition);
166 2         13 _merge_lookups($self->{fields_lookup}->{by_name}, $new_fields_lookup->{by_name});
167 2         8 _merge_lookups($self->{fields_lookup}->{by_number}, $new_fields_lookup->{by_number});
168              
169 2         10 my $new_messsages_lookup = $self->_construct_messages($definition);
170 2         12 _merge_lookups($self->{messages_lookup}->{by_name}, $new_messsages_lookup->{by_name});
171 2         7 _merge_lookups($self->{messages_lookup}->{by_number}, $new_messsages_lookup->{by_number});
172              
173 2         30 return $self;
174             }
175              
176             =head3 serialize_message
177              
178             serialize_message($self, $message_name, $payload)
179              
180             Returns serialized string for the supplied C<$message_name> and C<$payload>.
181             Dies in case of end-user (developer) error, e.g. if mandatory field is
182             absent.
183              
184             =cut
185              
186             sub serialize_message {
187 11     11 1 22910 my ($self, $message_name, $payload) = @_;
188 11         50 my $message = $self->message_by_name($message_name);
189 11         65 return $message->serialize($payload);
190             }
191              
192             =head3 parse_message
193              
194             parse_message($self, $buff_ref)
195              
196             my ($message_instance, $error) = $protocol->parse($buff_ref);
197              
198             Tries to parse FIX message in the buffer refernce.
199              
200             In the case of success it returns C and C<$error> is undef.
201             The string in C<$buff_ref> will be consumed.
202              
203             In the case of B, the C<$message_instance> will be undef,
204             and C<$error> will contain the error description. The string in C<$buff_ref>
205             will be kept untouched.
206              
207             In the case, when there is no enough data in C<$buff_ref> both C<$error>
208             and C<$message_instance> will be undef. The string in C<$buff_ref>
209             will be kept untouched, i.e. waiting futher accumulation of bytes from
210             network.
211              
212             In other cases it dies; that indicates either end-user (developer) error
213             or bug in the module.
214              
215             =cut
216              
217             sub parse_message {
218 124     124 1 190184 return Protocol::FIX::Parser::parse(@_);
219             }
220              
221             sub _construct_fields {
222 9     9   31 my ($self, $definition) = @_;
223              
224 9         48 my $fields_lookup = {
225             by_number => {},
226             by_name => {},
227             };
228              
229 9         36 my $fields_arr = $definition->{fix}->{fields}->{field};
230 9 100       73 $fields_arr = [$fields_arr] if ref($fields_arr) ne 'ARRAY';
231              
232 9         40 for my $field_descr (@$fields_arr) {
233 6386         9182 my ($name, $number, $type) = map { $field_descr->{$_} } qw/-name -number -type/;
  19158         39032  
234 6386         8716 my $values;
235 6386         8404 my $values_arr = $field_descr->{value};
236 6386 100       10623 if ($values_arr) {
237 1715         3013 for my $value_desc (@$values_arr) {
238 11956         15420 my ($key, $description) = map { $value_desc->{$_} } qw/-enum -description/;
  23912         45643  
239 11956         26993 $values->{$key} = $description;
240             }
241             }
242 6386         12858 my $field = Protocol::FIX::Field->new($number, $name, $type, $values);
243 6386         20594 $fields_lookup->{by_number}->{$number} = $field;
244 6386         21759 $fields_lookup->{by_name}->{$name} = $field;
245             }
246              
247 9         56 return $fields_lookup;
248             }
249              
250             sub _get_composites {
251 4918     4918   9641 my ($values, $lookup) = @_;
252 4918 100       9392 return () unless $values;
253              
254 2683 100       6206 my $array = ref($values) ne 'ARRAY' ? [$values] : $values;
255             my @composites = map {
256 2683         5382 my $ref = $_;
  22587         28783  
257 22587         38252 my $name = $ref->{-name};
258 22587         38109 my $required = $ref->{-required} eq 'Y';
259 22587         51372 my $composite = $lookup->{by_name}->{$name};
260              
261 22587 100       40641 die($name) unless $composite;
262              
263 22146         41193 ($composite, $required);
264             } @$array;
265 2242         14033 return @composites;
266             }
267              
268             sub _construct_components {
269 7     7   28 my ($self, $definition, $fields_lookup) = @_;
270              
271 7         29 my $components_lookup = {
272             by_name => {},
273             };
274              
275 7         18 my @components_queue = map { $_->{-type} = 'component'; $_; } @{$definition->{fix}->{components}->{component}};
  728         1289  
  728         1110  
  7         44  
276             OUTER:
277 7         57 while (my $component_descr = shift @components_queue) {
278 1169         1566 my @composites;
279 1169         2270 my $name = $component_descr->{-name};
280              
281 1169         1493 my $fatal = 0;
282 1169         1660 my $eval_result = eval {
283 1169         2864 push @composites, _get_composites($component_descr->{component}, $components_lookup);
284              
285 1127         2350 my $group_descr = $component_descr->{group};
286 1127 100       2066 if ($group_descr) {
287 1036         1310 my @group_composites;
288              
289             # we might fail to construct group as dependent components might not be
290             # constructed yet
291 1036         2154 push @group_composites, _get_composites($group_descr->{component}, $components_lookup);
292              
293             # now we should be able to construct group
294 637         1101 $fatal = 1;
295 637         1283 push @group_composites, _get_composites($group_descr->{field}, $fields_lookup);
296              
297 637         1348 my $group_name = $group_descr->{-name};
298 637   50     1974 my $base_field = $fields_lookup->{by_name}->{$group_name}
299             // die("${group_name} refers field '${group_name}', which is not available");
300 637         2077 my $group = Protocol::FIX::Group->new($base_field, \@group_composites);
301              
302 637         1420 my $group_required = $group_descr->{-required} eq 'Y';
303 637         1948 push @composites, $group => $group_required;
304             }
305 728         1277 1;
306             };
307 1169 100       2476 if (!$eval_result) {
308 441 50       845 die("$@") if ($fatal);
309             # not constructed yet, postpone current component construction
310 441         672 push @components_queue, $component_descr;
311 441         1166 next OUTER;
312             }
313              
314 728         1058 $eval_result = eval { push @composites, _get_composites($component_descr->{field}, $fields_lookup); 1 };
  728         2071  
  728         1455  
315 728 50       1409 if (!$eval_result) {
316             # make it human friendly
317 0         0 die("Cannot find field '$@' referred by '$name'");
318             }
319              
320 728         2127 my $component = Protocol::FIX::Component->new($name, \@composites);
321 728         3750 $components_lookup->{by_name}->{$name} = $component;
322             }
323              
324 7         28 return $components_lookup;
325             }
326              
327             sub _construct_composite {
328 14     14   52 my ($self, $name, $descr, $fields_lookup, $components_lookup) = @_;
329              
330 14         44 my @composites;
331 14         29 my $eval_result = eval {
332 14         54 push @composites, _get_composites($descr->{field}, $fields_lookup);
333 14         75 push @composites, _get_composites($descr->{component}, $components_lookup);
334 14         46 1;
335             };
336 14 50       71 if (!$eval_result) {
337 0         0 die("Cannot find composite '$@', referred in '$name'");
338             }
339              
340 14         66 return Protocol::FIX::BaseComposite->new($name, $name, \@composites);
341             }
342              
343             sub _construct_messages {
344 9     9   29 my ($self, $definition) = @_;
345              
346 9         72 my $messages_lookup = {
347             by_name => {},
348             by_number => {},
349             };
350 9         29 my $fields_lookup = $self->{fields_lookup};
351 9         21 my $components_lookup = $self->{components_lookup};
352              
353 9         38 my $messages_arr = $definition->{fix}->{messages}->{message};
354 9 100       46 $messages_arr = [$messages_arr] unless ref($messages_arr) eq 'ARRAY';
355              
356 9         168 my @messages_queue = @$messages_arr;
357 9         53 while (my $message_descr = shift @messages_queue) {
358 653         1115 my @composites;
359 653         1607 my ($name, $category, $message_type) = map { $message_descr->{$_} } qw/-name -msgcat -msgtype/;
  1959         5946  
360              
361 653         1606 my $eval_result = eval {
362 653         2040 push @composites, _get_composites($message_descr->{field}, $fields_lookup);
363 653         2533 push @composites, _get_composites($message_descr->{component}, $components_lookup);
364 653         1559 1;
365             };
366 653 50       1487 if (!$eval_result) {
367             # make it human friendly
368 0         0 die("Cannot find field '$@' referred by '$name'");
369             }
370              
371 653         1241 my $group_descr = $message_descr->{group};
372             # no need to protect with eval, as all fields/components should be availble.
373             # if something is missing this is fatal
374 653 100       1431 if ($group_descr) {
375 7         18 my @group_composites;
376              
377 7         39 push @group_composites, _get_composites($group_descr->{component}, $components_lookup);
378 7         35 push @group_composites, _get_composites($group_descr->{field}, $fields_lookup);
379              
380 7         25 my $group_name = $group_descr->{-name};
381 7   50     41 my $base_field = $fields_lookup->{by_name}->{$group_name} // die("${group_name} refers field '${group_name}', which is not available");
382 7         63 my $group = Protocol::FIX::Group->new($base_field, \@group_composites);
383              
384 7         97 my $group_required = $group_descr->{-required} eq 'Y';
385 7         30 push @composites, $group => $group_required;
386             }
387              
388 653         2655 my $message = Protocol::FIX::Message->new($name, $category, $message_type, \@composites, $self);
389 653         2711 $messages_lookup->{by_name}->{$name} = $message;
390 653         5546 $messages_lookup->{by_number}->{$message_type} = $message;
391             }
392              
393 9         62 return $messages_lookup;
394             }
395              
396             sub _construct_from_definition {
397 7     7   164 my ($self, $definition) = @_;
398              
399 7         637 my ($type, $major, $minor) = @{$definition->{fix}}{qw/-type -major -minor/};
  7         59  
400 7         48 my $protocol_id = join('.', $type, $major, $minor);
401              
402 7         40 my $fields_lookup = $self->_construct_fields($definition);
403 7         59 my $components_lookup = $self->_construct_components($definition, $fields_lookup);
404              
405 7         34 my $header_descr = $definition->{fix}->{header};
406 7         24 my $trailer_descr = $definition->{fix}->{trailer};
407 7         41 my $header = $self->_construct_composite('header', $header_descr, $fields_lookup, $components_lookup);
408 7         51 my $trailer = $self->_construct_composite('trailer', $trailer_descr, $fields_lookup, $components_lookup);
409              
410 7         60 my $serialized_begin_string = $fields_lookup->{by_name}->{BeginString}->serialize($protocol_id);
411              
412 7         142 $self->{id} = $protocol_id;
413 7         30 $self->{header} = $header;
414 7         34 $self->{trailer} = $trailer;
415 7         25 $self->{fields_lookup} = $fields_lookup;
416 7         20 $self->{components_lookup} = $components_lookup;
417 7         23 $self->{begin_string} = $serialized_begin_string;
418              
419 7         37 my $messages_lookup = $self->_construct_messages($definition);
420 7         51 $self->{messages_lookup} = $messages_lookup;
421              
422 7         33 return;
423             }
424              
425             sub _merge_lookups {
426 8     8   15 my ($old, $new) = @_;
427 8         25 @{$old}{keys %$new} = values %$new;
  8         121  
428 8         14 return;
429             }
430              
431             =head1 METHODS (for protocol developers)
432              
433             =head3 humanize
434              
435             humanize ($buffer)
436              
437             Returns human-readable string for the buffer. I.e. is just substitutes
438             L to " | ".
439              
440             This might be usable during development of own FIX-client/server.
441              
442             =cut
443              
444             sub humanize {
445 25     25 1 186 my $s = shift;
446 25         293 return $s =~ s/\x{01}/ | /gr;
447             }
448              
449             =head3 is_composite
450              
451             is_composite($object)
452              
453             Checks whether the supplied C<$object> conforms "composte" concept.
454             I.e. is it is L, L, L or L.
455              
456             =cut
457              
458             sub is_composite {
459 39730     39730 1 52087 my $obj = shift;
460             return
461             defined($obj)
462             && UNIVERSAL::can($obj, 'serialize')
463             && exists $obj->{name}
464 39730   33     256617 && exists $obj->{type};
465             }
466              
467             =head3 field_by_name
468              
469             field_by_name($self, $field_name)
470              
471             Returns Field object by it's name or dies with error.
472              
473             =cut
474              
475             sub field_by_name {
476 1988     1988 1 10637 my ($self, $field_name) = @_;
477 1988         4599 my $field = $self->{fields_lookup}->{by_name}->{$field_name};
478 1988 50       3868 if (!$field) {
479 0         0 die("Field '$field_name' is not available in protocol " . $self->{version});
480             }
481 1988         5415 return $field;
482             }
483              
484             =head3 field_by_number
485              
486             field_by_number($self, $field_number)
487              
488             Returns Field object by it's number or dies with error.
489              
490             =cut
491              
492             sub field_by_number {
493 2     2 1 716 my ($self, $field_number) = @_;
494 2         7 my $field = $self->{fields_lookup}->{by_number}->{$field_number};
495 2 50       7 if (!$field) {
496 0         0 die("Field $field_number is not available in protocol " . $self->{version});
497             }
498 2         5 return $field;
499             }
500              
501             =head3 component_by_name
502              
503             component_by_name($self, $name)
504              
505             Returns Component object by it's name or dies with error.
506              
507             =cut
508              
509             sub component_by_name {
510 11     11 1 4010 my ($self, $name) = @_;
511 11         27 my $component = $self->{components_lookup}->{by_name}->{$name};
512 11 50       33 if (!$component) {
513 0         0 die("Component '$name' is not available in protocol " . $self->{version});
514             }
515 11         28 return $component;
516             }
517              
518             =head3 message_by_name
519              
520             message_by_name($self, $name)
521              
522             Returns Message object by it's name or dies with error.
523              
524             =cut
525              
526             sub message_by_name {
527 20     20 1 12328 my ($self, $name) = @_;
528 20         94 my $message = $self->{messages_lookup}->{by_name}->{$name};
529 20 50       80 if (!$message) {
530 0         0 die("Message '$name' is not available in protocol " . $self->{version});
531             }
532 20         63 return $message;
533             }
534              
535             =head3 header
536              
537             header($self)
538              
539             Returns Message's header
540              
541             =cut
542              
543             sub header {
544 656     656 1 5766 return shift->{header};
545             }
546              
547             =head3 trailer
548              
549             trailer($self)
550              
551             Returns Message's trailer
552              
553             =cut
554              
555             sub trailer {
556 656     656 1 8126 return shift->{trailer};
557             }
558              
559             =head3 id
560              
561             id($self)
562              
563             Returns Protocol's ID string, as it appears in FIX message (BeginString field).
564              
565             =cut
566              
567             sub id {
568 1     1 1 1365 return shift->{id};
569             }
570              
571             =head3 managed_composites
572              
573             managed_composites()
574              
575             Returns list of fields, managed by protocol. Currently the list consists of
576             fields: BeginString, MsgType, and CheckSum
577              
578             =cut
579              
580             sub managed_composites {
581 34589     34589 1 98428 return \%MANAGED_COMPOSITES;
582             }
583              
584             1;