fix-engine 0.0.24

Sign up to get free protection for your applications and to get access to all the features.
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: