omq-cli 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a68efaffe40870b21e4fa8d94fb99efb40dddc96cc68fd0ebaa232373073c35b
4
+ data.tar.gz: 0b817664632dbd6a83844562cf79b296493da2438526675f65801e85305add2a
5
+ SHA512:
6
+ metadata.gz: a4c3df6862c4820561ac8f7c8c1f60d89caac4c538a9bce7cbce7daeff2abdf0e33c63520e46a36cad291a8d1255eb934516041a1a723e00893c22a2926ba0f7
7
+ data.tar.gz: ee47b7dc9f0becaf518532a70a724dc998a2df1a59d80bb525bf9cc597e61df2688577c2cf748b0949121e60f5544b4f0ec066f40fdb01e24eccd423a21121d4
data/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025-2026, Patrik Wenger
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,464 @@
1
+ # omq — ZeroMQ CLI
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/omq-cli?color=e9573f)](https://rubygems.org/gems/omq-cli)
4
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](LICENSE)
5
+ [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%203.3-CC342D?logo=ruby&logoColor=white)](https://www.ruby-lang.org)
6
+
7
+ Command-line tool for sending and receiving ZeroMQ messages on any socket type. Like `nngcat` from libnng, but with Ruby eval, Ractor parallelism, and message handlers.
8
+
9
+ Built on [omq](https://github.com/zeromq/omq) — pure Ruby ZeroMQ, no C dependencies.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ gem install omq-cli
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```sh
20
+ # Echo server
21
+ omq rep -b tcp://:5555 --echo
22
+
23
+ # Client
24
+ echo "hello" | omq req -c tcp://localhost:5555
25
+
26
+ # Upcase server — -e evals Ruby on each incoming message
27
+ omq rep -b tcp://:5555 -e '$F.map(&:upcase)'
28
+ ```
29
+
30
+ ```
31
+ Usage: omq TYPE [options]
32
+
33
+ Types: req, rep, pub, sub, push, pull, pair, dealer, router
34
+ Draft: client, server, radio, dish, scatter, gather, channel, peer
35
+ Virtual: pipe (PULL → eval → PUSH)
36
+ ```
37
+
38
+ ## Connection
39
+
40
+ Every socket needs at least one `--bind` or `--connect`:
41
+
42
+ ```sh
43
+ omq pull --bind tcp://:5557 # listen on port 5557
44
+ omq push --connect tcp://host:5557 # connect to host
45
+ omq pull -b ipc:///tmp/feed.sock # IPC (unix socket)
46
+ omq push -c ipc://@abstract # IPC (abstract namespace, Linux)
47
+ ```
48
+
49
+ Multiple endpoints are allowed — `omq pull -b tcp://:5557 -b tcp://:5558` binds both. Pipe requires exactly two (`-c` for pull-side, `-c` for push-side).
50
+
51
+ ## Socket types
52
+
53
+ ### Unidirectional (send-only / recv-only)
54
+
55
+ | Send | Recv | Pattern |
56
+ |------|------|---------|
57
+ | `push` | `pull` | Pipeline — round-robin to workers |
58
+ | `pub` | `sub` | Publish/subscribe — fan-out with topic filtering |
59
+ | `scatter` | `gather` | Pipeline (draft, single-frame only) |
60
+ | `radio` | `dish` | Group messaging (draft, single-frame only) |
61
+
62
+ Send-only sockets read from stdin (or `--data`/`--file`) and send. Recv-only sockets receive and write to stdout.
63
+
64
+ ```sh
65
+ echo "task" | omq push -c tcp://worker:5557
66
+ omq pull -b tcp://:5557
67
+ ```
68
+
69
+ ### Bidirectional (request-reply)
70
+
71
+ | Type | Behavior |
72
+ |------|----------|
73
+ | `req` | Sends a request, waits for reply, prints reply |
74
+ | `rep` | Receives request, sends reply (from `--echo`, `-e`, `--data`, `--file`, or stdin) |
75
+ | `client` | Like `req` (draft, single-frame) |
76
+ | `server` | Like `rep` (draft, single-frame, routing-ID aware) |
77
+
78
+ ```sh
79
+ # echo server
80
+ omq rep -b tcp://:5555 --echo
81
+
82
+ # upcase server
83
+ omq rep -b tcp://:5555 -e '$F.map(&:upcase)'
84
+
85
+ # client
86
+ echo "hello" | omq req -c tcp://localhost:5555
87
+ ```
88
+
89
+ ### Bidirectional (concurrent send + recv)
90
+
91
+ | Type | Behavior |
92
+ |------|----------|
93
+ | `pair` | Exclusive 1-to-1 — concurrent send and recv tasks |
94
+ | `dealer` | Like `pair` but round-robin send to multiple peers |
95
+ | `channel` | Like `pair` (draft, single-frame) |
96
+
97
+ These spawn two concurrent tasks: a receiver (prints incoming) and a sender (reads stdin). `-e` transforms incoming, `-E` transforms outgoing.
98
+
99
+ ### Routing sockets
100
+
101
+ | Type | Behavior |
102
+ |------|----------|
103
+ | `router` | Receives with peer identity prepended; sends to peer by identity |
104
+ | `server` | Like `router` but draft, single-frame, uses routing IDs |
105
+ | `peer` | Like `server` (draft, single-frame) |
106
+
107
+ ```sh
108
+ # monitor mode — just print what arrives
109
+ omq router -b tcp://:5555
110
+
111
+ # reply to specific peer
112
+ omq router -b tcp://:5555 --target worker-1 -D "reply"
113
+
114
+ # dynamic routing via send-eval (first element = identity)
115
+ omq router -b tcp://:5555 -E '["worker-1", $_.upcase]'
116
+ ```
117
+
118
+ `--target` and `--send-eval` are mutually exclusive on routing sockets.
119
+
120
+ ### Pipe (virtual)
121
+
122
+ Pipe creates an internal PULL → eval → PUSH pipeline:
123
+
124
+ ```sh
125
+ omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
126
+
127
+ # with Ractor workers for CPU parallelism
128
+ omq pipe -c ipc://@work -c ipc://@sink -P 4 -r./fib.rb -e 'fib(Integer($_)).to_s'
129
+ ```
130
+
131
+ The first endpoint is the pull-side (input), the second is the push-side (output). Both must use `-c`.
132
+
133
+ ## Eval: -e and -E
134
+
135
+ `-e` (alias `--recv-eval`) runs a Ruby expression for each **incoming** message.
136
+ `-E` (alias `--send-eval`) runs a Ruby expression for each **outgoing** message.
137
+
138
+ ### Globals
139
+
140
+ | Variable | Value |
141
+ |----------|-------|
142
+ | `$F` | Message parts (`Array<String>`) |
143
+ | `$_` | First part (`$F.first`) — works in inline expressions |
144
+
145
+ ### Return value
146
+
147
+ | Return | Effect |
148
+ |--------|--------|
149
+ | `Array` | Used as the message parts |
150
+ | `String` | Wrapped in `[result]` |
151
+ | `nil` | Message is skipped (filtered) |
152
+ | `self` (the socket) | Signals "I already sent" (REP only) |
153
+
154
+ ### Control flow
155
+
156
+ ```sh
157
+ # skip messages matching a pattern
158
+ omq pull -b tcp://:5557 -e 'next if /^#/; $F'
159
+
160
+ # stop on "quit"
161
+ omq pull -b tcp://:5557 -e 'break if /quit/; $F'
162
+ ```
163
+
164
+ ### BEGIN/END blocks
165
+
166
+ Like awk — `BEGIN{}` runs once before the message loop, `END{}` runs after:
167
+
168
+ ```sh
169
+ omq pull -b tcp://:5557 -e 'BEGIN{ @sum = 0 } @sum += Integer($_); next END{ puts @sum }'
170
+ ```
171
+
172
+ Local variables won't work to share state between the blocks. Use `@ivars` instead.
173
+
174
+ ### Which sockets accept which flag
175
+
176
+ | Socket | `-E` (send) | `-e` (recv) |
177
+ |--------|-------------|-------------|
178
+ | push, pub, scatter, radio | transforms outgoing | error |
179
+ | pull, sub, gather, dish | error | transforms incoming |
180
+ | req, client | transforms request | transforms reply |
181
+ | rep, server (reply mode) | error | transforms request → return = reply |
182
+ | pair, dealer, channel | transforms outgoing | transforms incoming |
183
+ | router, server, peer (monitor) | routes outgoing (first element = identity) | transforms incoming |
184
+ | pipe | error | transforms in pipeline |
185
+
186
+ ### Examples
187
+
188
+ ```sh
189
+ # upcase echo server
190
+ omq rep -b tcp://:5555 -e '$F.map(&:upcase)'
191
+
192
+ # transform before sending
193
+ echo hello | omq push -c tcp://localhost:5557 -E '$F.map(&:upcase)'
194
+
195
+ # filter incoming
196
+ omq pull -b tcp://:5557 -e '$F.first.include?("error") ? $F : nil'
197
+
198
+ # REQ: different transforms per direction
199
+ echo hello | omq req -c tcp://localhost:5555 \
200
+ -E '$F.map(&:upcase)' -e '$F.map(&:reverse)'
201
+
202
+ # generate messages without stdin
203
+ omq pub -c tcp://localhost:5556 -E 'Time.now.to_s' -i 1
204
+
205
+ # use gems
206
+ omq sub -c tcp://localhost:5556 -s "" -rjson -e 'JSON.parse($F.first)["temperature"]'
207
+ ```
208
+
209
+ ## Script handlers (-r)
210
+
211
+ For non-trivial transforms, put the logic in a Ruby file and load it with `-r`:
212
+
213
+ ```ruby
214
+ # handler.rb
215
+ db = PG.connect("dbname=app")
216
+
217
+ OMQ.outgoing { |msg| msg.map(&:upcase) }
218
+ OMQ.incoming { |msg| db.exec(msg.first).values.flatten }
219
+
220
+ at_exit { db.close }
221
+ ```
222
+
223
+ ```sh
224
+ omq req -c tcp://localhost:5555 -r./handler.rb
225
+ ```
226
+
227
+ ### Registration API
228
+
229
+ | Method | Effect |
230
+ |--------|--------|
231
+ | `OMQ.outgoing { |msg| ... }` | Register outgoing message transform |
232
+ | `OMQ.incoming { |msg| ... }` | Register incoming message transform |
233
+
234
+ - use explicit block variable (like `msg`) instead of `$F`/`$_`
235
+ - Setup: use local variables and closures at the top of the script
236
+ - Teardown: use Ruby's `at_exit { ... }`
237
+ - CLI flags (`-e`/`-E`) override script-registered handlers for the same direction
238
+ - A script can register one direction while the CLI handles the other:
239
+
240
+ ```sh
241
+ # handler.rb registers recv_eval, CLI adds send_eval
242
+ omq req -c tcp://localhost:5555 -r./handler.rb -E '$F.map(&:upcase)'
243
+ ```
244
+
245
+ ### Script handler examples
246
+
247
+ ```ruby
248
+ # count.rb — count messages, print total on exit
249
+ count = 0
250
+ OMQ.incoming { |msg| count += 1; msg }
251
+ at_exit { $stderr.puts "processed #{count} messages" }
252
+ ```
253
+
254
+ ```ruby
255
+ # json_transform.rb — parse JSON, extract field
256
+ require "json"
257
+ OMQ.incoming { |first_part, _| [JSON.parse(first_part)["value"]] }
258
+ ```
259
+
260
+ ```ruby
261
+ # rate_limit.rb — skip messages arriving too fast
262
+ last = 0
263
+
264
+ OMQ.incoming do |msg|
265
+ now = Async::Clock.now # monotonic clock
266
+
267
+ if now - last >= 0.1
268
+ last = now
269
+ msg
270
+ end
271
+ end
272
+ ```
273
+
274
+ ```ruby
275
+ # enrich.rb — add timestamp to outgoing messages
276
+ OMQ.outgoing { |msg| [*msg, Time.now.iso8601] }
277
+ ```
278
+
279
+ ## Data sources
280
+
281
+ | Flag | Behavior |
282
+ |------|----------|
283
+ | (stdin) | Read lines from stdin, one message per line |
284
+ | `-D "text"` | Send literal string (one-shot or repeated with `-i`) |
285
+ | `-F file` | Read message from file (`-F -` reads stdin as blob) |
286
+ | `--echo` | Echo received messages back (REP only) |
287
+
288
+ `-D` and `-F` are mutually exclusive.
289
+
290
+ ## Formats
291
+
292
+ | Flag | Format |
293
+ |------|--------|
294
+ | `-A` / `--ascii` | Tab-separated frames, non-printable → dots (default) |
295
+ | `-Q` / `--quoted` | C-style escapes, lossless round-trip |
296
+ | `--raw` | Raw ZMTP binary (pipe to `hexdump -C` for debugging) |
297
+ | `-J` / `--jsonl` | JSON Lines — `["frame1","frame2"]` per line |
298
+ | `--msgpack` | MessagePack arrays (binary stream) |
299
+ | `-M` / `--marshal` | Ruby Marshal (binary stream of `Array<String>` objects) |
300
+
301
+ Multipart messages: in ASCII/quoted mode, frames are tab-separated. In JSONL mode, each message is a JSON array.
302
+
303
+ ```sh
304
+ # send multipart via tabs
305
+ printf "key\tvalue" | omq push -c tcp://localhost:5557
306
+
307
+ # JSONL
308
+ echo '["key","value"]' | omq push -c tcp://localhost:5557 -J
309
+ omq pull -b tcp://:5557 -J
310
+ ```
311
+
312
+ ## Timing
313
+
314
+ | Flag | Effect |
315
+ |------|--------|
316
+ | `-i SECS` | Repeat send every N seconds (wall-clock aligned) |
317
+ | `-n COUNT` | Max messages to send/receive (0 = unlimited) |
318
+ | `-d SECS` | Delay before first send |
319
+ | `-t SECS` | Send/receive timeout |
320
+ | `-l SECS` | Linger time on close (default 5s) |
321
+ | `--reconnect-ivl` | Reconnect interval: `SECS` or `MIN..MAX` (default 0.1) |
322
+ | `--heartbeat-ivl SECS` | ZMTP heartbeat interval (detects dead peers) |
323
+
324
+ ```sh
325
+ # publish a tick every second, 10 times
326
+ omq pub -c tcp://localhost:5556 -D "tick" -i 1 -n 10 -d 1
327
+
328
+ # receive with 5s timeout
329
+ omq pull -b tcp://:5557 -t 5
330
+ ```
331
+
332
+ ## Compression
333
+
334
+ Both sides must use `--compress` (`-z`). Requires the `zstd-ruby` gem.
335
+
336
+ ```sh
337
+ omq push -c tcp://remote:5557 -z < data.txt
338
+ omq pull -b tcp://:5557 -z
339
+ ```
340
+
341
+ ## CURVE encryption
342
+
343
+ End-to-end encryption using CurveZMQ. Requires a crypto backend:
344
+ - **rbnacl** (recommended) — wraps libsodium, fast and audited. `gem install rbnacl`
345
+ - **nuckle** — pure Ruby, no system dependencies, not audited. `gem install nuckle`
346
+
347
+ By default, `rbnacl` is used if installed. To use `nuckle` explicitly:
348
+
349
+ ```sh
350
+ omq rep -b tcp://:5555 --echo --curve-server --curve-crypto nuckle
351
+ # or: OMQ_CURVE_CRYPTO=nuckle omq rep -b tcp://:5555 --echo --curve-server
352
+ ```
353
+
354
+ ```sh
355
+ # server (prints OMQ_SERVER_KEY=...)
356
+ omq rep -b tcp://:5555 --echo --curve-server
357
+
358
+ # client (paste the key)
359
+ echo "secret" | omq req -c tcp://localhost:5555 \
360
+ --curve-server-key '<key from server>'
361
+ ```
362
+
363
+ Persistent keys via env vars: `OMQ_SERVER_PUBLIC` + `OMQ_SERVER_SECRET` (server), `OMQ_SERVER_KEY` (client).
364
+
365
+ ## Subscription and groups
366
+
367
+ ```sh
368
+ # subscribe to topic prefix
369
+ omq sub -b tcp://:5556 -s "weather."
370
+
371
+ # subscribe to all (default)
372
+ omq sub -b tcp://:5556
373
+
374
+ # multiple subscriptions
375
+ omq sub -b tcp://:5556 -s "weather." -s "sports."
376
+
377
+ # RADIO/DISH groups
378
+ omq dish -b tcp://:5557 -j "weather" -j "sports"
379
+ omq radio -c tcp://localhost:5557 -g "weather" -D "72F"
380
+ ```
381
+
382
+ ## Identity and routing
383
+
384
+ ```sh
385
+ # DEALER with identity
386
+ echo "hello" | omq dealer -c tcp://localhost:5555 --identity worker-1
387
+
388
+ # ROUTER receives identity + message as tab-separated
389
+ omq router -b tcp://:5555
390
+
391
+ # ROUTER sends to specific peer
392
+ omq router -b tcp://:5555 --target worker-1 -D "reply"
393
+
394
+ # ROUTER dynamic routing via -E (first element = routing identity)
395
+ omq router -b tcp://:5555 -E '["worker-1", $_.upcase]'
396
+
397
+ # binary routing IDs (0x prefix)
398
+ omq router -b tcp://:5555 --target 0xdeadbeef -D "reply"
399
+ ```
400
+
401
+ ## Pipe
402
+
403
+ Pipe creates an in-process PULL → eval → PUSH pipeline:
404
+
405
+ ```sh
406
+ # basic pipe (positional: first = input, second = output)
407
+ omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
408
+
409
+ # parallel Ractor workers (default: all CPUs)
410
+ omq pipe -c ipc://@work -c ipc://@sink -P -r./fib.rb -e 'fib(Integer($_)).to_s'
411
+
412
+ # fixed number of workers
413
+ omq pipe -c ipc://@work -c ipc://@sink -P 4 -e '$F.map(&:upcase)'
414
+
415
+ # exit when producer disconnects
416
+ omq pipe -c ipc://@work -c ipc://@sink --transient -e '$F.map(&:upcase)'
417
+ ```
418
+
419
+ ### Multi-peer pipe with `--in`/`--out`
420
+
421
+ Use `--in` and `--out` to attach multiple endpoints per side. These are modal switches — subsequent `-b`/`-c` flags attach to the current side:
422
+
423
+ ```sh
424
+ # fan-in: 2 producers → 1 consumer
425
+ omq pipe --in -c ipc://@work1 -c ipc://@work2 --out -c ipc://@sink -e '$F'
426
+
427
+ # fan-out: 1 producer → 2 consumers (round-robin)
428
+ omq pipe --in -b tcp://:5555 --out -c ipc://@sink1 -c ipc://@sink2 -e '$F'
429
+
430
+ # bind on input, connect on output
431
+ omq pipe --in -b tcp://:5555 -b tcp://:5556 --out -c tcp://sink:5557 -e '$F'
432
+
433
+ # parallel workers with fan-in (all must be -c)
434
+ omq pipe --in -c ipc://@a -c ipc://@b --out -c ipc://@sink -P 4 -e '$F'
435
+ ```
436
+
437
+ `-P`/`--parallel` requires all endpoints to be `--connect`. In parallel mode, each Ractor worker gets its own PULL/PUSH pair connecting to all endpoints.
438
+
439
+ Note: in Ractor workers, use `__F` instead of `$F` (global variables aren't shared across Ractors).
440
+
441
+ ## Transient mode
442
+
443
+ `--transient` makes the socket exit when all peers disconnect. Useful for pipeline workers and sinks:
444
+
445
+ ```sh
446
+ # worker exits when producer is done
447
+ omq pipe -c ipc://@work -c ipc://@sink --transient -e '$F.map(&:upcase)'
448
+
449
+ # sink exits when all workers disconnect
450
+ omq pull -b tcp://:5557 --transient
451
+ ```
452
+
453
+ ## Exit codes
454
+
455
+ | Code | Meaning |
456
+ |------|---------|
457
+ | 0 | Success |
458
+ | 1 | Error (connection, argument, runtime) |
459
+ | 2 | Timeout |
460
+ | 3 | Eval error (`-e`/`-E` expression raised) |
461
+
462
+ ## License
463
+
464
+ [ISC](LICENSE)
data/exe/omq ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ Warning[:experimental] = false
4
+
5
+ require "omq/cli"
6
+ OMQ::CLI.run