omq 0.3.1 → 0.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d77750001a2c45808301e5a005f83c8a6f4df44ffa033aa121cc9229b597347
4
- data.tar.gz: b009c94d5ea30d5e0983831fb443e1ceb31e8980725b1b9c75b9403814a7a469
3
+ metadata.gz: d49a1c7caa7a1548615285ae294341b31833fd02bb60a7fa7cbfc0ed5c707d79
4
+ data.tar.gz: db36537c5dab13faa9cbdbe611c282cbf9dd96726e88145f80c3aaa7bf3df8a5
5
5
  SHA512:
6
- metadata.gz: e6612b16ae23874f1539154f108359190ac70eeaf9691c326deb4b2545151590e4458071912a987258b9d59cf80c5015024fbfa546d1f5bbcd979564be5cdffe
7
- data.tar.gz: 7706e73997b1a96446e6c5e6053756e57e4b33d6b084a82c49f78e40b9454aa26aed1d5e3ca14207c450be75864707618c3a87cf27c750020064393dbe85c71f
6
+ metadata.gz: 3e3a9e5a0b9ee3857a2946e7468ea2ca30c87544fa75965c79b1ea905c3a8d969aeb94b49282acd502d2ffbb6a0db5b2952f7a18a17e90363f1c5ed13cfbecd4
7
+ data.tar.gz: 1cb5f13312278d55684f22c420f17287a922633eebdfd143336415aafdc725dd7a38e23b5f9387cc55bf28acc0b125840c5770ef9112556b97b7c3cca8578ee6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 — 2026-03-27
4
+
5
+ ### Added (omqcat)
6
+
7
+ - `--curve-server` flag — generates ephemeral keypair, prints
8
+ `OMQ_SERVER_KEY=...` to stderr for easy copy-paste
9
+ - `--curve-server-key KEY` flag — CURVE client mode from the CLI
10
+ - `--echo` flag for REP — explicit echo mode
11
+ - REP reads stdin/`-F` as reply source (one line per reply, exits at EOF)
12
+ - REP without a reply source now aborts with a helpful error message
13
+
14
+ ### Changed
15
+
16
+ - CURVE env vars renamed: `OMQ_SERVER_KEY`, `OMQ_SERVER_PUBLIC`,
17
+ `OMQ_SERVER_SECRET` (was `SERVER_KEY`, `SERVER_PUBLIC`, `SERVER_SECRET`)
18
+ - REP with `--echo`/`-D`/`-e` serves forever by default (like a server).
19
+ Use `-n 1` for one-shot, `-n` to limit exchanges. Stdin/`-F` replies
20
+ naturally terminate at EOF.
21
+
22
+ ## 0.3.2 — 2026-03-26
23
+
24
+ ### Improved
25
+
26
+ - Hide the warning about the experimental `IO::Buffer` (used by io-stream)
27
+
3
28
  ## 0.3.1 — 2026-03-26
4
29
 
5
30
  ### Improved
data/README.md CHANGED
@@ -137,6 +137,52 @@ Benchmarked with benchmark-ips on Linux x86_64 (Ruby 4.0.1 +YJIT):
137
137
 
138
138
  See [`bench/`](bench/) for full results and scripts.
139
139
 
140
+ ## omqcat — CLI tool
141
+
142
+ `omqcat` is a command-line tool for sending and receiving messages on any OMQ socket. Like `nngcat` from libnng, but with Ruby superpowers.
143
+
144
+ ```sh
145
+ # Echo server in one line
146
+ omqcat rep -b tcp://:5555 -e '$F.map(&:upcase)'
147
+
148
+ # Client
149
+ echo "hello" | omqcat req -c tcp://localhost:5555
150
+ # => HELLO
151
+
152
+ # PUB/SUB
153
+ omqcat sub -b tcp://:5556 -s "weather." &
154
+ echo "weather.nyc 72F" | omqcat pub -c tcp://localhost:5556 -d 0.3
155
+
156
+ # Pipeline with filtering
157
+ tail -f /var/log/syslog | omqcat push -c tcp://collector:5557
158
+ omqcat pull -b tcp://:5557 -e '$F.first.include?("error") ? $F : nil'
159
+
160
+ # Multipart messages via tabs
161
+ printf "routing-key\tpayload data" | omqcat push -c tcp://localhost:5557
162
+ omqcat pull -b tcp://:5557
163
+ # => routing-key payload data
164
+
165
+ # JSONL for structured data
166
+ echo '["key","value"]' | omqcat push -c tcp://localhost:5557 -J
167
+ omqcat pull -b tcp://:5557 -J
168
+
169
+ # Zstandard compression
170
+ omqcat push -c tcp://remote:5557 -z < data.txt
171
+ omqcat pull -b tcp://:5557 -z
172
+
173
+ # CURVE encryption (auto-detected from env vars)
174
+ SERVER_KEY=... omqcat req -c tcp://secure:5555 -D "secret"
175
+ ```
176
+
177
+ The `-e` flag runs Ruby inside the socket instance — the full socket API (`self <<`, `send`, `subscribe`, ...) is available. Use `-r` to require gems:
178
+
179
+ ```sh
180
+ omqcat sub -c tcp://localhost:5556 -s "" -r json \
181
+ -e 'JSON.parse($F.first)["temperature"]'
182
+ ```
183
+
184
+ Formats: `--ascii` (default, tab-separated), `--quoted`, `--raw`, `--jsonl`, `--msgpack`. See `omqcat --help` for all options.
185
+
140
186
  ## Interop with native ZMQ
141
187
 
142
188
  OMQ speaks ZMTP 3.1 on the wire and interoperates with libzmq, CZMQ, pyzmq, etc. over **tcp** and **ipc**. The `inproc://` transport is OMQ-internal (in-process Ruby queues) and is not visible to native ZMQ running in the same process — use `ipc://` to talk across library boundaries.
data/exe/omqcat CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
+ Warning[:experimental] = false
3
4
 
4
5
  #
5
6
  # omqcat — command-line access to OMQ (ZeroMQ) sockets.
@@ -29,7 +30,7 @@ opts = {
29
30
  identity: nil,
30
31
  target: nil,
31
32
  interval: nil,
32
- count: 0,
33
+ count: nil,
33
34
  delay: nil,
34
35
  recv_timeout: nil,
35
36
  send_timeout: nil,
@@ -47,7 +48,8 @@ parser = OptionParser.new do |o|
47
48
  o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v| opts[:connects] << v }
48
49
  o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v| opts[:binds] << v }
49
50
 
50
- o.separator "\nData source:"
51
+ o.separator "\nData source (REP: reply source):"
52
+ o.on( "--echo", "Echo received messages back (REP)") { opts[:echo] = true }
51
53
  o.on("-D", "--data DATA", "Message data (literal string)") { |v| opts[:data] = v }
52
54
  o.on("-F", "--file FILE", "Read message from file (- = stdin)") { |v| opts[:file] = v }
53
55
 
@@ -79,6 +81,11 @@ parser = OptionParser.new do |o|
79
81
  o.on("-e", "--eval EXPR", "Eval Ruby for each message ($F = parts)") { |v| opts[:expr] = v }
80
82
  o.on("-r", "--require LIB", "Require a Ruby library (repeatable)") { |v| require v }
81
83
 
84
+ o.separator "\nCURVE encryption (requires omq-curve gem):"
85
+ o.on("--curve-server", "Enable CURVE as server (generates keypair)") { opts[:curve_server] = true }
86
+ o.on("--curve-server-key KEY", "Enable CURVE as client (server's Z85 public key)") { |v| opts[:curve_server_key] = v }
87
+ o.separator " Env vars: OMQ_SERVER_KEY (client), OMQ_SERVER_PUBLIC + OMQ_SERVER_SECRET (server)"
88
+
82
89
  o.separator "\nOther:"
83
90
  o.on("-v", "--verbose", "Print connection events to stderr") { opts[:verbose] = true }
84
91
  o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
@@ -215,21 +222,31 @@ end
215
222
 
216
223
  # ── CURVE setup ─────────────────────────────────────────────────────
217
224
 
218
- def setup_curve(sock)
219
- if ENV["SERVER_KEY"]
225
+ def setup_curve(sock, opts)
226
+ server_key_z85 = opts[:curve_server_key] || ENV["OMQ_SERVER_KEY"]
227
+ server_mode = opts[:curve_server] || (ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"])
228
+
229
+ if server_key_z85
230
+ # Client mode
220
231
  require "omq/curve"
221
- server_key = OMQ::Z85.decode(ENV["SERVER_KEY"])
232
+ server_key = OMQ::Z85.decode(server_key_z85)
222
233
  client_key = RbNaCl::PrivateKey.generate
223
234
  sock.mechanism = OMQ::Curve.client(
224
235
  client_key.public_key.to_s, client_key.to_s, server_key: server_key
225
236
  )
226
- $stderr.puts "CURVE client mode" if $VERBOSE
227
- elsif ENV["SERVER_PUBLIC"] && ENV["SERVER_SECRET"]
237
+ elsif server_mode
238
+ # Server mode
228
239
  require "omq/curve"
229
- server_pub = OMQ::Z85.decode(ENV["SERVER_PUBLIC"])
230
- server_sec = OMQ::Z85.decode(ENV["SERVER_SECRET"])
240
+ if ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"]
241
+ server_pub = OMQ::Z85.decode(ENV["OMQ_SERVER_PUBLIC"])
242
+ server_sec = OMQ::Z85.decode(ENV["OMQ_SERVER_SECRET"])
243
+ else
244
+ key = RbNaCl::PrivateKey.generate
245
+ server_pub = key.public_key.to_s
246
+ server_sec = key.to_s
247
+ end
231
248
  sock.mechanism = OMQ::Curve.server(server_pub, server_sec)
232
- $stderr.puts "SERVER_KEY=#{ENV["SERVER_PUBLIC"]}"
249
+ $stderr.puts "OMQ_SERVER_KEY='#{OMQ::Z85.encode(server_pub)}'"
233
250
  end
234
251
  rescue LoadError
235
252
  abort "omq-curve gem required for CURVE encryption: gem install omq-curve"
@@ -280,6 +297,10 @@ def eval_expr(parts, sock, opts)
280
297
  end
281
298
  end
282
299
 
300
+ def count_reached?(i, opts)
301
+ opts[:count] && opts[:count] > 0 && i >= opts[:count]
302
+ end
303
+
283
304
  def output(parts, opts)
284
305
  return if opts[:quiet]
285
306
  parts = [""] if parts.nil?
@@ -303,7 +324,7 @@ def send_loop(sock, opts)
303
324
  sleep(opts[:delay]) if opts[:delay] && i == 0
304
325
  send_msg(sock, parts, opts) if parts
305
326
  i += 1
306
- break if opts[:count] > 0 && i >= opts[:count]
327
+ break if count_reached?(i, opts)
307
328
  if opts[:interval]
308
329
  sleep(opts[:interval])
309
330
  else
@@ -318,7 +339,7 @@ def send_loop(sock, opts)
318
339
  sleep(opts[:delay]) if opts[:delay] && i == 0
319
340
  send_msg(sock, parts, opts) if parts
320
341
  i += 1
321
- break if opts[:count] > 0 && i >= opts[:count]
342
+ break if count_reached?(i, opts)
322
343
  sleep(opts[:interval]) if opts[:interval]
323
344
  end
324
345
  end
@@ -331,7 +352,7 @@ def recv_loop(sock, opts)
331
352
  parts = eval_expr(parts, sock, opts)
332
353
  output(parts, opts)
333
354
  i += 1
334
- break if opts[:count] > 0 && i >= opts[:count]
355
+ break if count_reached?(i, opts)
335
356
  end
336
357
  end
337
358
 
@@ -346,7 +367,7 @@ def req_loop(sock, opts)
346
367
  reply = eval_expr(reply, sock, opts)
347
368
  output(reply, opts)
348
369
  i += 1
349
- break if opts[:count] > 0 && i >= opts[:count]
370
+ break if count_reached?(i, opts)
350
371
  if opts[:interval]
351
372
  sleep(opts[:interval])
352
373
  elsif !opts[:data] && !opts[:file]
@@ -365,13 +386,19 @@ def rep_loop(sock, opts)
365
386
  reply = eval_expr(msg, sock, opts)
366
387
  output(reply, opts)
367
388
  send_msg(sock, reply || [""], opts)
368
- else
389
+ elsif opts[:echo]
390
+ output(msg, opts)
391
+ send_msg(sock, msg, opts)
392
+ elsif opts[:data] || opts[:file] || !$stdin.tty?
393
+ reply = read_next(opts)
394
+ break unless reply # EOF on stdin/-F
369
395
  output(msg, opts)
370
- reply = opts[:data] ? parse_input(opts[:data] + "\n", opts[:format]) : msg
371
396
  send_msg(sock, reply, opts)
397
+ else
398
+ abort "REP needs a reply source: --echo, --data, --file, -e, or stdin pipe"
372
399
  end
373
400
  i += 1
374
- break if opts[:count] > 0 && i >= opts[:count]
401
+ break if count_reached?(i, opts)
375
402
  end
376
403
  end
377
404
 
@@ -383,7 +410,7 @@ def pair_loop(sock, opts, task)
383
410
  parts = eval_expr(parts, sock, opts)
384
411
  output(parts, opts)
385
412
  i += 1
386
- break if opts[:count] > 0 && i >= opts[:count]
413
+ break if count_reached?(i, opts)
387
414
  end
388
415
  end
389
416
 
@@ -396,13 +423,13 @@ def pair_loop(sock, opts, task)
396
423
  sleep(opts[:delay]) if opts[:delay] && i == 0
397
424
  send_msg(sock, parts, opts) if parts
398
425
  i += 1
399
- break if opts[:count] > 0 && i >= opts[:count]
426
+ break if count_reached?(i, opts)
400
427
  break if (opts[:data] || opts[:file]) && !opts[:interval]
401
428
  sleep(opts[:interval]) if opts[:interval]
402
429
  end
403
430
  end
404
431
 
405
- if opts[:count] > 0
432
+ if opts[:count] && opts[:count] > 0
406
433
  receiver.wait
407
434
  sender.stop
408
435
  else
@@ -424,7 +451,7 @@ def router_loop(sock, opts, task)
424
451
  result = eval_expr([id_display, *parts], sock, opts)
425
452
  output(result, opts)
426
453
  i += 1
427
- break if opts[:count] > 0 && i >= opts[:count]
454
+ break if count_reached?(i, opts)
428
455
  end
429
456
  end
430
457
 
@@ -439,7 +466,7 @@ def router_loop(sock, opts, task)
439
466
  end
440
467
  send_msg(sock, parts, opts)
441
468
  i += 1
442
- break if opts[:count] > 0 && i >= opts[:count]
469
+ break if count_reached?(i, opts)
443
470
  break if (opts[:data] || opts[:file]) && !opts[:interval]
444
471
  sleep(opts[:interval]) if opts[:interval]
445
472
  end
@@ -447,7 +474,7 @@ def router_loop(sock, opts, task)
447
474
 
448
475
  # If count is set, the receiver will exit when count is reached.
449
476
  # Otherwise, wait for Ctrl-C.
450
- if opts[:count] > 0
477
+ if opts[:count] && opts[:count] > 0
451
478
  receiver.wait
452
479
  sender.stop
453
480
  else
@@ -472,7 +499,7 @@ Async do |task|
472
499
  sock.send_timeout = opts[:send_timeout] if opts[:send_timeout]
473
500
  sock.identity = opts[:identity] if opts[:identity]
474
501
 
475
- setup_curve(sock)
502
+ setup_curve(sock, opts)
476
503
 
477
504
  opts[:binds].each do |url|
478
505
  sock.bind(url)
data/lib/omq/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OMQ
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -35,7 +35,7 @@ module OMQ
35
35
  engine.handle_accepted(IO::Stream::Buffered.wrap(client, minimum_write_size: 0), endpoint: endpoint)
36
36
  rescue ProtocolError, *ZMTP::CONNECTION_LOST
37
37
  # peer disconnected during handshake
38
- rescue => e
38
+ rescue
39
39
  client&.close rescue nil
40
40
  raise
41
41
  end
@@ -32,7 +32,7 @@ module OMQ
32
32
  engine.handle_accepted(IO::Stream::Buffered.wrap(client, minimum_write_size: 0), endpoint: resolved)
33
33
  rescue ProtocolError, *ZMTP::CONNECTION_LOST
34
34
  # peer disconnected during handshake
35
- rescue => e
35
+ rescue
36
36
  client&.close rescue nil
37
37
  raise
38
38
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger