fix-engine 0.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c91eff8188ecd2d2a756b520272008fe02b84eda
4
+ data.tar.gz: f8a6ca41d7b37da9f38811a869d03013a1f925f1
5
+ SHA512:
6
+ metadata.gz: a773023d43105871d71e3300ce97e9b8156e7412535f0a2867b2ec43bc826a7bd139c9e5cdb0a99c65f786ea5efced658bd1bd6b1cf590ccf03f6e8bbe72e0bd
7
+ data.tar.gz: bca29ee94ce994971a868db41c1fdb110b913923cd79d4e04b0a42633476d3c9d1b008921f669fee078d8e1edeb09b15e0adfd650313bbb22ebc5128e2ae73f1
data/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Permission is hereby granted, free of charge, to any person obtaining a copy
2
+ of this software and associated documentation files (the "Software"), to deal
3
+ in the Software without restriction, including without limitation the rights
4
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
5
+ copies of the Software, and to permit persons to whom the Software is
6
+ furnished to do so, subject to the following conditions:
7
+
8
+ The above copyright notice and this permission notice shall be included in
9
+ all copies or substantial portions of the Software.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
17
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ FIX Engine [![Build Status](https://secure.travis-ci.org/Paymium/fix-engine.png?branch=master)](http://travis-ci.org/Paymium/fix-engine)
2
+ =
3
+
4
+ This library provides an event-machine based FIX server.
data/bin/fix-engine ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fix/engine'
4
+
5
+ ip = ENV['FE_IP'] || FE::DEFAULT_IP
6
+ port = ENV['FE_PORT'] || FE::DEFAULT_PORT
7
+
8
+ Fix::Engine.run!(ip, port)
9
+
data/lib/fix/engine.rb ADDED
@@ -0,0 +1,38 @@
1
+ require 'fix/engine/logger'
2
+ require 'fix/engine/server'
3
+
4
+ #
5
+ # Main FIX namespace
6
+ #
7
+ module Fix
8
+
9
+ #
10
+ # Main Fix::Engine namespace
11
+ #
12
+ module Engine
13
+
14
+ # The default IP on which the server will listen
15
+ DEFAULT_IP = '0.0.0.0'
16
+
17
+ # The default port on which the server will listen
18
+ DEFAULT_PORT = 8359
19
+
20
+ #
21
+ # Runs a FIX server engine
22
+ #
23
+ def self.run!(ip = DEFAULT_IP, port = DEFAULT_PORT, handler = FE::Connection)
24
+ Server.new(ip, port, handler).run!
25
+ end
26
+
27
+ #
28
+ # Alias the +Fix::Engine+ namespace to +FE+ if possible, because lazy is not necessarily dirty
29
+ #
30
+ def self.alias_namespace!
31
+ Object.const_set(:FE, Engine) unless Object.const_defined?(:FE)
32
+ end
33
+
34
+ end
35
+ end
36
+
37
+ Fix::Engine.alias_namespace!
38
+
@@ -0,0 +1,56 @@
1
+ require 'fix/engine/logger'
2
+
3
+ module Fix
4
+ module Engine
5
+
6
+ #
7
+ # Represents a connected client
8
+ #
9
+ class Client
10
+
11
+ @@clients = {}
12
+
13
+ attr_accessor :ip, :port, :connection, :client_id
14
+
15
+ include Logger
16
+
17
+ def initialize(ip, port, connection)
18
+ @ip = ip
19
+ @port = port
20
+ @connection = connection
21
+ end
22
+
23
+ #
24
+ # Returns a client instance from its connection IP
25
+ #
26
+ # @param ip [String] The connection IP
27
+ # @return [Fix::Engine::Client] The client connected for this IP
28
+ #
29
+ def self.get(ip, port, connection)
30
+ @@clients[key(ip, port)] ||= Client.new(ip, port, connection)
31
+ end
32
+
33
+ def self.count
34
+ @@clients.count
35
+ end
36
+
37
+ def self.delete(ip, port)
38
+ @@clients.delete(key(ip, port))
39
+ end
40
+
41
+ def has_session?
42
+ !!client_id
43
+ end
44
+
45
+ def key
46
+ self.class.key(ip, port)
47
+ end
48
+
49
+ def self.key(ip, port)
50
+ "#{ip}:#{port}"
51
+ end
52
+
53
+ end
54
+ end
55
+ end
56
+
@@ -0,0 +1,292 @@
1
+ require 'eventmachine'
2
+
3
+ require 'fix/engine/message_buffer'
4
+ require 'fix/engine/client'
5
+
6
+ module Fix
7
+ module Engine
8
+
9
+ #
10
+ # The client connection handling logic and method overrides
11
+ #
12
+ class Connection < EM::Connection
13
+
14
+ include Logger
15
+
16
+ #
17
+ # Timespan during which a client must send a logon message after connecting
18
+ #
19
+ LOGON_TIMEOUT = 10
20
+
21
+ #
22
+ # Timespan during which we will wait for a heartbeat response from the client
23
+ #
24
+ HRTBT_TIMEOUT = 10
25
+
26
+ #
27
+ # Grace time before we disconnect a client that doesn't reply to a test request
28
+ #
29
+ TEST_REQ_GRACE_TIME = 15
30
+
31
+ attr_accessor :ip, :port, :client, :msg_buf, :hrtbt_int, :last_request_at
32
+
33
+ #
34
+ # Our own company ID
35
+ #
36
+ DEFAULT_COMP_ID = 'PYMBTC'
37
+
38
+ #
39
+ # Run after a client has connected
40
+ #
41
+ def post_init
42
+ @port, @ip = Socket.unpack_sockaddr_in(get_peername)
43
+ @client = Client.get(ip, port, self)
44
+ @expected_clt_seq_num = 1
45
+
46
+ log("Client connected from <#{@client.key}>, expecting logon message in the next #{LOGON_TIMEOUT}s")
47
+
48
+ @comp_id ||= DEFAULT_COMP_ID
49
+
50
+ # The sent messages
51
+ @messages = []
52
+
53
+ # TODO : How do we test this
54
+ # TODO : Do we cancel the periodic timeout when leaving ?
55
+ EM.add_timer(LOGON_TIMEOUT) { logon_timeout }
56
+ end
57
+
58
+ def logon_timeout
59
+ unless client.has_session?
60
+ log("Client <#{client.key}> failed to authenticate before timeout, closing connection")
61
+ close_connection_after_writing
62
+ Client.delete(ip, port)
63
+ end
64
+ end
65
+
66
+ def manage_hrtbts
67
+ @last_send_at ||= 0
68
+ @last_request_at ||= 0
69
+ @hrtbt_int ||= 0
70
+
71
+ # Send a regular heartbeat when we don't send anything down the line for a while
72
+ if @hrtbt_int > 0 && (@last_send_at < (Time.now.to_i - @hrtbt_int))
73
+ send_heartbeat
74
+ end
75
+
76
+ # Trigger a test req message when we haven't received anything for a while
77
+ if !@pending_test_req_id && (last_request_at < (Time.now.to_i - @hrtbt_int))
78
+ tr = FP::Messages::TestRequest.new
79
+ tr.test_req_id = SecureRandom.hex(6)
80
+ send_msg(tr)
81
+ @pending_test_req_id = tr.test_req_id
82
+
83
+ EM.add_timer(TEST_REQ_GRACE_TIME) do
84
+ @pending_test_req_id && kill!
85
+ end
86
+ end
87
+ end
88
+
89
+ def send_msg(msg)
90
+ @send_seq_num ||= 1
91
+
92
+ msg.msg_seq_num = @send_seq_num
93
+ msg.sender_comp_id = @comp_id
94
+ msg.target_comp_id ||= @client_comp_id
95
+
96
+ log("Sending <#{msg.class}> to <#{ip}:#{port}> with sequence number <#{msg.msg_seq_num}>")
97
+
98
+ if msg.valid?
99
+ @messages[msg.msg_seq_num] = msg
100
+ send_data(msg.dump)
101
+ @send_seq_num += 1
102
+ @last_send_at = Time.now.to_i
103
+ else
104
+ log(msg.errors.join(', '))
105
+ raise "Tried to send invalid message!"
106
+ end
107
+ end
108
+
109
+ def set_heartbeat_interval(interval)
110
+ @hrtbt_int && raise("Can't set heartbeat interval twice")
111
+ @hrtbt_int = interval
112
+
113
+ log("Heartbeat interval for <#{ip}:#{port}> : <#{hrtbt_int}s>")
114
+ @hrtbt_monitor = EM.add_periodic_timer(1) { manage_hrtbts }
115
+ end
116
+
117
+ def kill!
118
+ if @client_comp_id
119
+ log("Logging out client <#{ip}:#{port}>")
120
+ logout = FP::Messages::Logout.new
121
+ logout.text = 'Bye!'
122
+ send_msg(logout)
123
+ end
124
+
125
+ close_connection_after_writing
126
+ end
127
+
128
+ def unbind
129
+ log("Terminating client <#{ip}:#{port}>")
130
+ Client.delete(ip, port)
131
+ @hrtbt_monitor && @hrtbt_monitor.cancel
132
+ end
133
+
134
+ def client_error(error_msg, msg_seq_num, opts = {})
135
+ log("Client error: \"#{error_msg}\"")
136
+ rjct = FP::Messages::Reject.new
137
+ rjct.text = error_msg
138
+ rjct.ref_seq_num = msg_seq_num
139
+ rjct.target_comp_id = opts[:target_comp_id] if opts[:target_comp_id]
140
+ send_msg(rjct)
141
+ kill!
142
+ end
143
+
144
+ def handle_msg(msg)
145
+ @recv_seq_num = msg.msg_seq_num
146
+
147
+ log("Received a <#{msg.class}> from <#{ip}:#{port}> with sequence number <#{msg.msg_seq_num}>")
148
+
149
+ # If sequence number == expected, then process it normally
150
+ if (@expected_clt_seq_num == @recv_seq_num)
151
+
152
+ if @comp_id && msg.target_comp_id != @comp_id
153
+ @client_comp_id = msg.sender_comp_id
154
+
155
+ if (msg.target_comp_id != @comp_id)
156
+ client_error("Incorrect TARGET_COMP_ID in message, expected <#{@comp_id}>, got <#{msg.target_comp_id}>", msg.header.msg_seq_num)
157
+ end
158
+
159
+ else
160
+ if !@client_comp_id && msg.is_a?(FP::Messages::Logon)
161
+ log("Client authenticated as <#{msg.username}> with heartbeat interval of <#{msg.heart_bt_int}s> and message sequence number start <#{msg.msg_seq_num}>")
162
+ client.client_id = msg.username
163
+ @client_comp_id = msg.sender_comp_id
164
+ set_heartbeat_interval(msg.heart_bt_int)
165
+
166
+ logon = FP::Messages::Logon.new
167
+ logon.username = msg.username
168
+ logon.target_comp_id = msg.sender_comp_id
169
+ logon.sender_comp_id = msg.target_comp_id
170
+ logon.reset_seq_num_flag = true
171
+ send_msg(logon)
172
+
173
+ elsif @client_comp_id && msg.is_a?(FP::Messages::Logon)
174
+ log("Received second logon message, reset_seq_num_flag <#{msg.reset_seq_num_flag}>")
175
+ if msg.reset_seq_num_flag = 'Y'
176
+ @send_seq_num = 1
177
+ @messages = []
178
+ end
179
+
180
+ elsif !@client_comp_id
181
+ client_error("The session must be started with a logon message", msg.msg_seq_num, target_comp_id: msg.sender_comp_id)
182
+
183
+ elsif msg.is_a?(FP::Messages::Heartbeat)
184
+ # If we were expecting an answer to a test request we can sign it off and
185
+ # cancel the scheduled connection termination
186
+ if @pending_test_req_id && msg.test_req_id && (@pending_test_req_id == msg.test_req_id)
187
+ @pending_test_req_id = nil
188
+ end
189
+
190
+ elsif msg.is_a?(FP::Messages::TestRequest)
191
+ # Answer test requests with a matching heartbeat
192
+ hb = FP::Messages::Heartbeat.new
193
+ hb.test_req_id = msg.test_req_id
194
+ send_msg(hb)
195
+
196
+ elsif msg.is_a?(FP::Messages::ResendRequest)
197
+ # Re-send requested message range
198
+ @messages[msg.begin_seq_no, msg.end_seq_no.zero? ? @messages.length : msg.end_seq_no].each do |m|
199
+ log("Re-sending <#{m.class}> to <#{ip}:#{port}> with sequence number <#{m.msg_seq_num}>")
200
+ send_data(m.dump)
201
+ @last_send_at = Time.now.to_i
202
+ end
203
+
204
+ elsif msg.is_a?(FP::Message)
205
+ on_message(msg)
206
+ end
207
+ end
208
+
209
+ @expected_clt_seq_num += 1
210
+
211
+ elsif msg.is_a?(FP::Messages::Logon)
212
+ client_error("Expected logon message to have msg_seq_num = <1>, received <#{msg.msg_seq_num}>", msg.msg_seq_num, target_comp_id: msg.sender_comp_id)
213
+
214
+ elsif (@expected_clt_seq_num > @recv_seq_num)
215
+ log("Ignoring message <#{msg}> with stale sequence number <#{msg.msg_seq_num}>, expecting <#{@expected_clt_seq_num}>")
216
+
217
+ elsif (@expected_clt_seq_num < @recv_seq_num) && @client_comp_id
218
+ # Request missing range when detect a gap
219
+ rr = FP::Messages::ResendRequest.new
220
+ rr.begin_seq_no = @expected_clt_seq_num
221
+ send_msg(rr)
222
+ end
223
+
224
+ self.last_request_at = Time.now.to_i
225
+ end
226
+
227
+ def on_message(msg)
228
+ end
229
+
230
+ #
231
+ # Run when a client has sent a chunk of data, it gets appended to a buffer
232
+ # and a parsing attempt is made at the buffered data
233
+ #
234
+ # @param data [String] The received data chunk
235
+ #
236
+ def receive_data(data)
237
+ data_chunk = data.chomp
238
+ msg_buf << data_chunk
239
+
240
+ begin
241
+ parse_messages_from_buffer
242
+ rescue
243
+ log("Client <#{@client.key}> raised exception when parsing data <#{data.gsub(/\x01/, '|')}>, terminating.")
244
+ log($!.message + $!.backtrace.join("\n"))
245
+ kill!
246
+ end
247
+ end
248
+
249
+ #
250
+ # Attempts to parse fields from the message buffer, if the fields that get parsed
251
+ # complete the temporary message, it is handled
252
+ #
253
+ def parse_messages_from_buffer
254
+ while idx = msg_buf.index("\x01")
255
+ field = msg_buf.slice!(0, idx + 1).gsub(/\x01\Z/, '')
256
+ msg.append(field)
257
+
258
+ if msg.complete?
259
+ parsed = msg.parse!
260
+ if parsed.is_a?(FP::Message)
261
+ handle_msg(parsed)
262
+ elsif parsed.is_a?(FP::ParseFailure)
263
+ client_error(parsed.errors.join(", "), @expected_clt_seq_num, target_comp_id: (@client_comp_id || 'UNKNOWN'))
264
+ end
265
+ end
266
+ end
267
+ end
268
+
269
+ #
270
+ # The data buffer string
271
+ #
272
+ def msg_buf
273
+ @msg_buf ||= ''
274
+ end
275
+
276
+ #
277
+ # The temporary message to which fields get appended
278
+ #
279
+ def msg
280
+ @msg ||= MessageBuffer.new(@client)
281
+ end
282
+
283
+ def send_heartbeat(test_req_id = nil)
284
+ msg = FP::Messages::Heartbeat.new
285
+ test_req_id && msg.test_req_id = test_req_id
286
+ send_msg(msg)
287
+ end
288
+
289
+ end
290
+ end
291
+ end
292
+
@@ -0,0 +1,33 @@
1
+ require 'logger'
2
+
3
+ module Fix
4
+ module Engine
5
+
6
+ #
7
+ # Naive logger implementation used in development
8
+ #
9
+ module Logger
10
+
11
+ @@logger = nil
12
+
13
+ #
14
+ # Logs a message to the standard output
15
+ #
16
+ # @param msg [String] The message to log
17
+ #
18
+ def log(msg)
19
+ FE::Logger.log(msg)
20
+ end
21
+
22
+ #
23
+ # Class-methods are easier to stub to disable logging while
24
+ # running specs
25
+ #
26
+ def self.log(msg)
27
+ @logger ||= ::Logger.new(STDOUT)
28
+ @logger.debug(msg)
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,83 @@
1
+ require 'fix/protocol'
2
+
3
+ module Fix
4
+ module Engine
5
+
6
+ #
7
+ # A FIX message to which fields get appended, once it is completed by a
8
+ # proper terminator it is handled
9
+ #
10
+ class MessageBuffer
11
+
12
+ include Logger
13
+
14
+ attr_accessor :fields, :client
15
+
16
+ def initialize(client)
17
+ @fields = []
18
+ @client = client
19
+ end
20
+
21
+ #
22
+ # Append a single FIX field to the message
23
+ #
24
+ # @param fld [String] A FIX formatted field, such as "35=0\x01"
25
+ #
26
+ def append(fld)
27
+ raise "Cannot append to complete message" if complete?
28
+ field = fld.split('=')
29
+ field[0] = field[0].to_i
30
+ field[1] = field[1].gsub(/\x01\Z/, '')
31
+ @fields << field
32
+ end
33
+
34
+ #
35
+ # Returns true if the last field of the collection is a FIX checksum
36
+ #
37
+ # @return [Boolean] Whether the message is complete
38
+ #
39
+ def complete?
40
+ (@fields.count > 0) && (@fields.last[0] == 10)
41
+ end
42
+
43
+ #
44
+ # Parses the message into a FP::Message instance
45
+ #
46
+ def parse
47
+ msg = FP.parse(to_s)
48
+ if (msg.class == FP::ParseFailure) || !msg.errors.count.zero?
49
+ log("Failed to parse message <#{debug}>")
50
+ log_errors(msg)
51
+ end
52
+
53
+ msg
54
+ end
55
+
56
+ #
57
+ # Parses the message and empties the fields array so a new message
58
+ # can start to get buffered right away
59
+ #
60
+ def parse!
61
+ parsed = parse
62
+ @fields = []
63
+ parsed
64
+ end
65
+
66
+ def debug
67
+ to_s('|')
68
+ end
69
+
70
+ def to_s(sep = "\x01")
71
+ fields.map { |f| f.join('=') }.join(sep) + sep
72
+ end
73
+
74
+ def log_errors(msg)
75
+ log("Invalid message received <#{debug}>")
76
+ msg.errors.each { |e| log(" >>> #{e}") }
77
+ end
78
+
79
+
80
+ end
81
+ end
82
+ end
83
+
@@ -0,0 +1,51 @@
1
+ require 'eventmachine'
2
+
3
+ require 'fix/protocol'
4
+ require 'fix/engine/version'
5
+ require 'fix/engine/connection'
6
+
7
+ module Fix
8
+ module Engine
9
+
10
+ #
11
+ # Main FIX engine server class
12
+ #
13
+ class Server
14
+
15
+ include Logger
16
+
17
+ REPORT_INTERVAL = 10
18
+
19
+ attr_accessor :ip, :port
20
+
21
+ def initialize(ip, port, handler)
22
+ @ip = ip
23
+ @port = port
24
+ @handler = handler
25
+ end
26
+
27
+ #
28
+ # Starts running the server engine
29
+ #
30
+ def run!
31
+ trap('INT') { EM.stop }
32
+ log("Starting FIX engine v#{FE::VERSION}, listening on <#{ip}:#{port}>, exit with <Ctrl-C>")
33
+ EM.run { start_server }
34
+ end
35
+
36
+ #
37
+ # Starts a listener inside a running reactor
38
+ #
39
+ def start_server
40
+ raise "EventMachine must be running to start a server" unless EM.reactor_running?
41
+ EM.start_server(ip, port, @handler)
42
+ REPORT_INTERVAL && EM.add_periodic_timer(REPORT_INTERVAL) { report_status }
43
+ end
44
+
45
+ def report_status
46
+ log("#{Client.count} client(s) currently connected")
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,10 @@
1
+ module Fix
2
+ module Engine
3
+
4
+ #
5
+ # The fix-engine gem version string
6
+ #
7
+ VERSION = '0.0.24'
8
+
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,166 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fix-engine
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.24
5
+ platform: ruby
6
+ authors:
7
+ - David François
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: yard
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: redcarpet
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: fix-protocol
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: eventmachine
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: FIX engine handling connections, sessions, and message callbacks
126
+ email:
127
+ - david.francois@paymium.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - LICENSE
133
+ - README.md
134
+ - bin/fix-engine
135
+ - lib/fix/engine.rb
136
+ - lib/fix/engine/client.rb
137
+ - lib/fix/engine/connection.rb
138
+ - lib/fix/engine/logger.rb
139
+ - lib/fix/engine/message_buffer.rb
140
+ - lib/fix/engine/server.rb
141
+ - lib/fix/engine/version.rb
142
+ homepage: https://github.com/paymium/fix-engine
143
+ licenses: []
144
+ metadata: {}
145
+ post_install_message:
146
+ rdoc_options: []
147
+ require_paths:
148
+ - lib
149
+ required_ruby_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - '>='
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ required_rubygems_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - '>='
157
+ - !ruby/object:Gem::Version
158
+ version: 1.3.6
159
+ requirements: []
160
+ rubyforge_project:
161
+ rubygems_version: 2.4.2
162
+ signing_key:
163
+ specification_version: 4
164
+ summary: FIX engine handling connections, sessions, and message callbacks
165
+ test_files: []
166
+ has_rdoc: