firemq 0.1.0-x86_64-linux

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.
Binary file
data/lib/firemq.rb ADDED
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ # FireMQ client for Ruby, built on the Rust client with UniFFI.
4
+ #
5
+ # require "firemq"
6
+ #
7
+ # FireMQ.connect("fire://guest:guest@localhost:5673") do |client|
8
+ # client.declare_queue("jobs")
9
+ # client.publish("", "jobs", "hello", headers: { "attempt" => 1 }, content_type: "text/plain")
10
+ # client.subscribe("jobs", prefetch: 10).each do |msg|
11
+ # puts msg.text
12
+ # msg.ack
13
+ # break
14
+ # end
15
+ # end
16
+ #
17
+ # The native library is found through FIREMQ_LIB (a path to the file), next to
18
+ # lib/firemq/firemq_client_ffi.rb, or on the system library path.
19
+
20
+ require "json"
21
+ require_relative "firemq/firemq_client_ffi"
22
+
23
+ module FireMQ
24
+ VERSION = "0.1.0"
25
+ DEFAULT_URL = "fire://guest:guest@localhost:5673"
26
+
27
+ # Base class of every FireMQ error. +code+ is the AMQP reply code, or 0.
28
+ class Error < StandardError
29
+ attr_reader :code
30
+
31
+ def initialize(message = nil, code = 0)
32
+ super(message)
33
+ @code = code
34
+ end
35
+ end
36
+
37
+ # The broker refused a request; +code+ is 403, 404, 406 and so on.
38
+ class ServerError < Error; end
39
+ # A mandatory publish matched no queue.
40
+ class UnroutableError < Error; end
41
+ # The broker rejected a publish, for example because a queue was full.
42
+ class NackedError < Error; end
43
+ # The connection or subscription is closed.
44
+ class ClosedError < Error; end
45
+ # Anything else: I/O failures, invalid URLs or JSON, protocol errors.
46
+ class OtherError < Error; end
47
+
48
+ Native = ::FiremqClientFfi
49
+ NativeError = Native::FireMqError
50
+
51
+ # Property names as the protocol spells them, and as the native record spells them.
52
+ PROPERTY_FIELDS = {
53
+ content_type: :content_type, content_encoding: :content_encoding,
54
+ correlation_id: :correlation_id, reply_to: :reply_to, expiration: :expiration,
55
+ message_id: :message_id, timestamp: :timestamp, type: :message_type,
56
+ user_id: :user_id, app_id: :app_id, priority: :priority
57
+ }.freeze
58
+
59
+ # Runs the block and converts native errors into FireMQ::Error subclasses.
60
+ def self.translate
61
+ yield
62
+ rescue NativeError::Server => e
63
+ raise ServerError.new(e.message, e.code)
64
+ rescue NativeError::Unroutable => e
65
+ raise UnroutableError.new(e.message, 312)
66
+ rescue NativeError::Nacked => e
67
+ raise NackedError.new(e.message, e.code)
68
+ rescue NativeError::Closed => e
69
+ raise ClosedError.new(e.message, 0)
70
+ rescue NativeError::Other => e
71
+ raise OtherError.new(e.message, 0)
72
+ end
73
+
74
+ def self.json_or_nil(value)
75
+ value.nil? ? nil : JSON.generate(value)
76
+ end
77
+
78
+ # Connects with a URL such as fire://user:pass@host:5673/vhost. With a block, yields the
79
+ # client and closes it afterwards.
80
+ def self.connect(url = DEFAULT_URL)
81
+ client = Client.new(url)
82
+ return client unless block_given?
83
+
84
+ begin
85
+ yield client
86
+ ensure
87
+ client.close unless client.closed?
88
+ end
89
+ end
90
+
91
+ # A connection to FireMQ.
92
+ class Client
93
+ def initialize(url = DEFAULT_URL)
94
+ @inner = FireMQ.translate { Native::Client.connect(url) }
95
+ end
96
+
97
+ # Closes the connection. Unacknowledged messages return to their queues.
98
+ def close
99
+ FireMQ.translate { @inner.close }
100
+ end
101
+
102
+ def closed?
103
+ @inner.is_closed
104
+ end
105
+
106
+ # Declares a queue; an empty name asks the broker to generate one. Returns an object with
107
+ # +name+, +messages+ and +consumers+.
108
+ def declare_queue(name = "", durable: true, exclusive: false, auto_delete: false, arguments: nil)
109
+ FireMQ.translate do
110
+ @inner.declare_queue(name, durable, exclusive, auto_delete, FireMQ.json_or_nil(arguments))
111
+ end
112
+ end
113
+
114
+ # Deletes a queue and returns how many messages it held.
115
+ def delete_queue(name)
116
+ FireMQ.translate { @inner.delete_queue(name) }
117
+ end
118
+
119
+ # Removes every ready message and returns how many were removed.
120
+ def purge_queue(name)
121
+ FireMQ.translate { @inner.purge_queue(name) }
122
+ end
123
+
124
+ def declare_exchange(name, type = "direct", durable: true, auto_delete: false, arguments: nil)
125
+ FireMQ.translate do
126
+ @inner.declare_exchange(name, type, durable, auto_delete, FireMQ.json_or_nil(arguments))
127
+ end
128
+ end
129
+
130
+ def delete_exchange(name, if_unused: false)
131
+ FireMQ.translate { @inner.delete_exchange(name, if_unused) }
132
+ end
133
+
134
+ def bind_queue(queue, exchange, routing_key = "", arguments: nil)
135
+ FireMQ.translate do
136
+ @inner.bind_queue(queue, exchange, routing_key, FireMQ.json_or_nil(arguments))
137
+ end
138
+ end
139
+
140
+ def unbind_queue(queue, exchange, routing_key = "")
141
+ FireMQ.translate { @inner.unbind_queue(queue, exchange, routing_key) }
142
+ end
143
+
144
+ def bind_exchange(destination, source, routing_key = "")
145
+ FireMQ.translate { @inner.bind_exchange(destination, source, routing_key) }
146
+ end
147
+
148
+ # Publishes a message and, by default, waits for the broker's confirm. Properties use the
149
+ # protocol's names: content_type, correlation_id, type, priority, expiration and so on.
150
+ def publish(exchange, routing_key, body, headers: nil, persistent: true, mandatory: false,
151
+ confirm: true, **properties)
152
+ unknown = properties.keys - PROPERTY_FIELDS.keys
153
+ raise ArgumentError, "unknown message properties: #{unknown.join(', ')}" unless unknown.empty?
154
+
155
+ props = Native::MessageProperties.new(
156
+ **properties.compact.to_h { |k, v| [PROPERTY_FIELDS.fetch(k), v] }
157
+ )
158
+ options = Native::PublishOptions.new(
159
+ persistent: persistent, mandatory: mandatory, confirm: confirm,
160
+ properties: props, headers_json: FireMQ.json_or_nil(headers)
161
+ )
162
+ FireMQ.translate { @inner.publish(exchange, routing_key, body.to_s.b, options) }
163
+ end
164
+
165
+ # Starts consuming. +prefetch+ caps unacknowledged deliveries.
166
+ def subscribe(queue, prefetch: 100, no_ack: false)
167
+ Subscription.new(FireMQ.translate { @inner.subscribe(queue, prefetch, no_ack) })
168
+ end
169
+
170
+ # Takes one message from a queue, or returns nil when it is empty.
171
+ def get(queue, no_ack: false)
172
+ d = FireMQ.translate { @inner.get(queue, no_ack) }
173
+ d && Message.new(d)
174
+ end
175
+ end
176
+
177
+ # Messages from a queue.
178
+ class Subscription
179
+ include Enumerable
180
+
181
+ def initialize(inner)
182
+ @inner = inner
183
+ end
184
+
185
+ # Waits up to +timeout+ seconds for a message and returns nil on timeout. Raises
186
+ # ClosedError once the subscription has ended.
187
+ def next(timeout: 1.0)
188
+ d = FireMQ.translate { @inner.next((timeout * 1000).to_i) }
189
+ d && Message.new(d)
190
+ end
191
+
192
+ # Yields messages until the subscription ends.
193
+ def each
194
+ return enum_for(:each) unless block_given?
195
+
196
+ loop do
197
+ msg = self.next(timeout: 0.5)
198
+ yield msg if msg
199
+ rescue ClosedError
200
+ break
201
+ end
202
+ end
203
+
204
+ # Stops the subscription. Messages already received can still be acknowledged.
205
+ def cancel
206
+ FireMQ.translate { @inner.cancel }
207
+ end
208
+ end
209
+
210
+ # A delivered message.
211
+ class Message
212
+ def initialize(delivery)
213
+ @d = delivery
214
+ end
215
+
216
+ # The body as a binary String.
217
+ def body
218
+ @body ||= @d.body.b
219
+ end
220
+
221
+ # The body as UTF-8 text.
222
+ def text
223
+ body.dup.force_encoding(Encoding::UTF_8)
224
+ end
225
+
226
+ def json
227
+ JSON.parse(body)
228
+ end
229
+
230
+ def exchange = @d.exchange
231
+ def routing_key = @d.routing_key
232
+ def redelivered? = @d.redelivered
233
+ def delivery_tag = @d.delivery_tag
234
+
235
+ # Application headers. Binary values arrive base64-encoded.
236
+ def headers
237
+ @headers ||= JSON.parse(@d.headers_json)
238
+ end
239
+
240
+ # The properties that are set, keyed by their protocol names as strings.
241
+ def properties
242
+ @properties ||= begin
243
+ p = @d.properties
244
+ PROPERTY_FIELDS.each_with_object({}) do |(name, field), h|
245
+ v = p.public_send(field)
246
+ h[name.to_s] = v unless v.nil?
247
+ end
248
+ end
249
+ end
250
+
251
+ def ack
252
+ FireMQ.translate { @d.ack }
253
+ end
254
+
255
+ # Rejects the message. With +requeue+ it is delivered again; otherwise it is
256
+ # dead-lettered when the queue has a dead-letter exchange, and dropped if not.
257
+ def nack(requeue: true)
258
+ FireMQ.translate { @d.nack(requeue) }
259
+ end
260
+ end
261
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: firemq
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: x86_64-linux
6
+ authors:
7
+ - The FireMQ Authors
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-11-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ description: UniFFI bindings over firemq-client, the Rust client for the FireMQ message
28
+ broker.
29
+ email:
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - README.md
35
+ - lib/firemq.rb
36
+ - lib/firemq/firemq_client_ffi.rb
37
+ - lib/firemq/libfiremq_client_ffi.so
38
+ homepage: https://github.com/FireMQ/firemq
39
+ licenses:
40
+ - Apache-2.0
41
+ metadata:
42
+ source_code_uri: https://github.com/FireMQ/firemq/tree/main/bindings/ruby
43
+ changelog_uri: https://github.com/FireMQ/firemq/blob/main/CHANGELOG.md
44
+ bug_tracker_uri: https://github.com/FireMQ/firemq/issues
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.1'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.5.22
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: Ruby client for FireMQ, built on the Rust client
64
+ test_files: []