omq 0.7.0 → 0.9.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.
data/lib/omq/cli.rb DELETED
@@ -1,540 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "optparse"
4
- require_relative "cli/config"
5
- require_relative "cli/formatter"
6
- require_relative "cli/base_runner"
7
- require_relative "cli/push_pull"
8
- require_relative "cli/pub_sub"
9
- require_relative "cli/scatter_gather"
10
- require_relative "cli/radio_dish"
11
- require_relative "cli/req_rep"
12
- require_relative "cli/pair"
13
- require_relative "cli/router_dealer"
14
- require_relative "cli/channel"
15
- require_relative "cli/client_server"
16
- require_relative "cli/peer"
17
- require_relative "cli/pipe"
18
-
19
- module OMQ
20
-
21
- class << self
22
- attr_reader :outgoing_proc, :incoming_proc
23
-
24
- def outgoing(&block) = @outgoing_proc = block
25
- def incoming(&block) = @incoming_proc = block
26
- end
27
-
28
-
29
- module CLI
30
- SOCKET_TYPE_NAMES = %w[
31
- req rep pub sub push pull pair dealer router
32
- client server radio dish scatter gather channel peer
33
- pipe
34
- ].freeze
35
-
36
-
37
- RUNNER_MAP = {
38
- "push" => [PushRunner, :PUSH],
39
- "pull" => [PullRunner, :PULL],
40
- "pub" => [PubRunner, :PUB],
41
- "sub" => [SubRunner, :SUB],
42
- "req" => [ReqRunner, :REQ],
43
- "rep" => [RepRunner, :REP],
44
- "dealer" => [DealerRunner, :DEALER],
45
- "router" => [RouterRunner, :ROUTER],
46
- "pair" => [PairRunner, :PAIR],
47
- "client" => [ClientRunner, :CLIENT],
48
- "server" => [ServerRunner, :SERVER],
49
- "radio" => [RadioRunner, :RADIO],
50
- "dish" => [DishRunner, :DISH],
51
- "scatter" => [ScatterRunner, :SCATTER],
52
- "gather" => [GatherRunner, :GATHER],
53
- "channel" => [ChannelRunner, :CHANNEL],
54
- "peer" => [PeerRunner, :PEER],
55
- "pipe" => [PipeRunner, nil],
56
- }.freeze
57
-
58
-
59
- EXAMPLES = <<~'TEXT'
60
- ── Request / Reply ──────────────────────────────────────────
61
-
62
- ┌─────┐ "hello" ┌─────┐
63
- │ REQ │────────────→│ REP │
64
- │ │←────────────│ │
65
- └─────┘ "HELLO" └─────┘
66
-
67
- # terminal 1: echo server
68
- omq rep --bind tcp://:5555 --recv-eval '$F.map(&:upcase)'
69
-
70
- # terminal 2: send a request
71
- echo "hello" | omq req --connect tcp://localhost:5555
72
-
73
- # or over IPC (unix socket, single machine)
74
- omq rep --bind ipc:///tmp/echo.sock --echo &
75
- echo "hello" | omq req --connect ipc:///tmp/echo.sock
76
-
77
- ── Publish / Subscribe ──────────────────────────────────────
78
-
79
- ┌─────┐ "weather.nyc 72F" ┌─────┐
80
- │ PUB │────────────────────→│ SUB │ --subscribe "weather."
81
- └─────┘ └─────┘
82
-
83
- # terminal 1: subscriber (all topics by default)
84
- omq sub --bind tcp://:5556
85
-
86
- # terminal 2: publisher (needs --delay for subscription to propagate)
87
- echo "weather.nyc 72F" | omq pub --connect tcp://localhost:5556 --delay 1
88
-
89
- ── Periodic Publish ───────────────────────────────────────────
90
-
91
- ┌─────┐ "tick 1" ┌─────┐
92
- │ PUB │──(every 1s)─→│ SUB │
93
- └─────┘ └─────┘
94
-
95
- # terminal 1: subscriber
96
- omq sub --bind tcp://:5556
97
-
98
- # terminal 2: publish a tick every second (wall-clock aligned)
99
- omq pub --connect tcp://localhost:5556 --delay 1 \
100
- --data "tick" --interval 1
101
-
102
- # 5 ticks, then exit
103
- omq pub --connect tcp://localhost:5556 --delay 1 \
104
- --data "tick" --interval 1 --count 5
105
-
106
- ── Pipeline ─────────────────────────────────────────────────
107
-
108
- ┌──────┐ ┌──────┐
109
- │ PUSH │──────────→│ PULL │
110
- └──────┘ └──────┘
111
-
112
- # terminal 1: worker
113
- omq pull --bind tcp://:5557
114
-
115
- # terminal 2: send tasks
116
- echo "task 1" | omq push --connect tcp://localhost:5557
117
-
118
- # or over IPC (unix socket)
119
- omq pull --bind ipc:///tmp/pipeline.sock &
120
- echo "task 1" | omq push --connect ipc:///tmp/pipeline.sock
121
-
122
- ── Pipe (PULL → eval → PUSH) ────────────────────────────────
123
-
124
- ┌──────┐ ┌──────┐ ┌──────┐
125
- │ PUSH │────────→│ pipe │────────→│ PULL │
126
- └──────┘ └──────┘ └──────┘
127
-
128
- # terminal 1: producer
129
- echo -e "hello\nworld" | omq push --bind ipc://@work
130
-
131
- # terminal 2: worker — uppercase each message
132
- omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
133
- # terminal 3: collector
134
- omq pull --bind ipc://@sink
135
-
136
- # 4 Ractor workers in a single process (-P)
137
- omq pipe -c ipc://@work -c ipc://@sink -P4 -r./fib -e 'fib(Integer($_)).to_s'
138
-
139
- # exit when producer disconnects (--transient)
140
- omq pipe -c ipc://@work -c ipc://@sink --transient -e '$F.map(&:upcase)'
141
-
142
- # fan-in: multiple sources → one sink
143
- omq pipe --in -c ipc://@work1 -c ipc://@work2 \
144
- --out -c ipc://@sink -e '$F.map(&:upcase)'
145
-
146
- # fan-out: one source → multiple sinks (round-robin)
147
- omq pipe --in -b tcp://:5555 --out -c ipc://@sink1 -c ipc://@sink2 -e '$F'
148
-
149
- ── CLIENT / SERVER (draft) ──────────────────────────────────
150
-
151
- ┌────────┐ "hello" ┌────────┐
152
- │ CLIENT │───────────→│ SERVER │ --recv-eval '$F.map(&:upcase)'
153
- │ │←───────────│ │
154
- └────────┘ "HELLO" └────────┘
155
-
156
- # terminal 1: upcasing server
157
- omq server --bind tcp://:5555 --recv-eval '$F.map(&:upcase)'
158
-
159
- # terminal 2: client
160
- echo "hello" | omq client --connect tcp://localhost:5555
161
-
162
- ── Formats ──────────────────────────────────────────────────
163
-
164
- # ascii (default) — non-printable replaced with dots
165
- omq pull --bind tcp://:5557 --ascii
166
-
167
- # quoted — lossless, round-trippable (uses String#dump escaping)
168
- omq pull --bind tcp://:5557 --quoted
169
-
170
- # JSON Lines — structured, multipart as arrays
171
- echo '["key","value"]' | omq push --connect tcp://localhost:5557 --jsonl
172
- omq pull --bind tcp://:5557 --jsonl
173
-
174
- # multipart via tabs
175
- printf "routing-key\tpayload" | omq push --connect tcp://localhost:5557
176
-
177
- ── Compression ──────────────────────────────────────────────
178
-
179
- # both sides must use --compress
180
- omq pull --bind tcp://:5557 --compress &
181
- echo "compressible data" | omq push --connect tcp://localhost:5557 --compress
182
-
183
- ── CURVE Encryption ─────────────────────────────────────────
184
-
185
- # server (prints OMQ_SERVER_KEY=...)
186
- omq rep --bind tcp://:5555 --echo --curve-server
187
-
188
- # client (paste the server's key)
189
- echo "secret" | omq req --connect tcp://localhost:5555 \
190
- --curve-server-key '<key from server>'
191
-
192
- ── ROUTER / DEALER ──────────────────────────────────────────
193
-
194
- ┌────────┐ ┌────────┐
195
- │ DEALER │─────────→│ ROUTER │
196
- │ id=w1 │ │ │
197
- └────────┘ └────────┘
198
-
199
- # terminal 1: router shows identity + message
200
- omq router --bind tcp://:5555
201
-
202
- # terminal 2: dealer with identity
203
- echo "hello" | omq dealer --connect tcp://localhost:5555 --identity worker-1
204
-
205
- ── Ruby Eval ────────────────────────────────────────────────
206
-
207
- # filter incoming: only pass messages containing "error"
208
- omq pull -b tcp://:5557 --recv-eval '$F.first.include?("error") ? $F : nil'
209
-
210
- # transform incoming with gems
211
- omq sub -c tcp://localhost:5556 -rjson -e 'JSON.parse($F.first)["temperature"]'
212
-
213
- # require a local file, use its methods
214
- omq rep --bind tcp://:5555 --require ./transform.rb -e 'upcase_all($F)'
215
-
216
- # next skips, break stops — regexps match against $_
217
- omq pull -b tcp://:5557 -e 'next if /^#/; break if /quit/; $F'
218
-
219
- # BEGIN/END blocks (like awk) — accumulate and summarize
220
- omq pull -b tcp://:5557 -e 'BEGIN{@sum = 0} @sum += Integer($_); nil END{puts @sum}'
221
-
222
- # transform outgoing messages
223
- echo hello | omq push -c tcp://localhost:5557 --send-eval '$F.map(&:upcase)'
224
-
225
- # REQ: transform request and reply independently
226
- echo hello | omq req -c tcp://localhost:5555 -E '$F.map(&:upcase)' -e '$_'
227
-
228
- ── Script Handlers (-r) ────────────────────────────────────
229
-
230
- # handler.rb — register transforms from a file
231
- # db = PG.connect("dbname=app")
232
- # OMQ.incoming { |first_part, _| db.exec(first_part).values.flatten }
233
- # at_exit { db.close }
234
- omq pull --bind tcp://:5557 -r./handler.rb
235
-
236
- # combine script handlers with inline eval
237
- omq req -c tcp://localhost:5555 -r./handler.rb -E '$F.map(&:upcase)'
238
-
239
- # OMQ.outgoing { |msg| ... } — registered outgoing transform
240
- # OMQ.incoming { |msg| ... } — registered incoming transform
241
- # CLI flags (-e/-E) override registered handlers
242
- TEXT
243
-
244
- module_function
245
-
246
-
247
- # Displays text through the system pager, or prints directly
248
- # when stdout is not a terminal.
249
- #
250
- def page(text)
251
- if $stdout.tty?
252
- if ENV["PAGER"]
253
- pager = ENV["PAGER"]
254
- else
255
- ENV["LESS"] ||= "-FR"
256
- pager = "less"
257
- end
258
- IO.popen(pager, "w") { |io| io.puts text }
259
- else
260
- puts text
261
- end
262
- rescue Errno::ENOENT
263
- puts text
264
- rescue Errno::EPIPE
265
- # user quit pager early
266
- end
267
-
268
-
269
- # Parses CLI arguments, validates options, and runs the main
270
- # event loop inside an Async reactor.
271
- #
272
- def run(argv = ARGV)
273
- config = build_config(argv)
274
-
275
- require_relative "../omq"
276
- require "async"
277
- require "json"
278
- require "console"
279
-
280
- validate_gems!(config)
281
-
282
- trap("INT") { Process.exit!(0) }
283
- trap("TERM") { Process.exit!(0) }
284
-
285
- Console.logger = Console::Logger.new(Console::Output::Null.new) unless config.verbose
286
-
287
- runner_class, socket_sym = RUNNER_MAP.fetch(config.type_name)
288
-
289
- Async do |task|
290
- runner = if socket_sym
291
- runner_class.new(config, OMQ.const_get(socket_sym))
292
- else
293
- runner_class.new(config)
294
- end
295
- runner.call(task)
296
- rescue IO::TimeoutError, Async::TimeoutError
297
- $stderr.puts "omq: timeout" unless config.quiet
298
- exit 2
299
- rescue OMQ::SocketDeadError => e
300
- $stderr.puts "omq: #{e.cause.class}: #{e.cause.message}"
301
- exit 1
302
- rescue ::Socket::ResolutionError => e
303
- $stderr.puts "omq: #{e.message}"
304
- exit 1
305
- end
306
- end
307
-
308
-
309
- # Builds a frozen Config from command-line arguments.
310
- #
311
- def build_config(argv)
312
- opts = parse_options(argv)
313
- validate!(opts)
314
-
315
- opts[:has_msgpack] = begin; require "msgpack"; true; rescue LoadError; false; end
316
- opts[:has_zstd] = begin; require "zstd-ruby"; true; rescue LoadError; false; end
317
- opts[:stdin_is_tty] = $stdin.tty?
318
-
319
- Ractor.make_shareable(Config.new(**opts))
320
- end
321
-
322
-
323
- # Parses command-line arguments into a mutable options hash.
324
- #
325
- def parse_options(argv)
326
- opts = {
327
- endpoints: [],
328
- connects: [],
329
- binds: [],
330
- in_endpoints: [],
331
- out_endpoints: [],
332
- data: nil,
333
- file: nil,
334
- format: :ascii,
335
- subscribes: [],
336
- joins: [],
337
- group: nil,
338
- identity: nil,
339
- target: nil,
340
- interval: nil,
341
- count: nil,
342
- delay: nil,
343
- timeout: nil,
344
- linger: 5,
345
- reconnect_ivl: nil,
346
- heartbeat_ivl: nil,
347
- conflate: false,
348
- compress: false,
349
- send_expr: nil,
350
- recv_expr: nil,
351
- parallel: nil,
352
- transient: false,
353
- verbose: false,
354
- quiet: false,
355
- echo: false,
356
- curve_server: false,
357
- curve_server_key: nil,
358
- }
359
-
360
- pipe_side = nil # nil = legacy positional mode; :in/:out = modal
361
-
362
- parser = OptionParser.new do |o|
363
- o.banner = "Usage: omq TYPE [options]\n\n" \
364
- "Types: req, rep, pub, sub, push, pull, pair, dealer, router\n" \
365
- "Draft: client, server, radio, dish, scatter, gather, channel, peer\n" \
366
- "Virtual: pipe (PULL → eval → PUSH)\n\n"
367
-
368
- o.separator "Connection:"
369
- o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v|
370
- ep = Endpoint.new(v, false)
371
- case pipe_side
372
- when :in then opts[:in_endpoints] << ep
373
- when :out then opts[:out_endpoints] << ep
374
- else opts[:endpoints] << ep; opts[:connects] << v
375
- end
376
- }
377
- o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v|
378
- ep = Endpoint.new(v, true)
379
- case pipe_side
380
- when :in then opts[:in_endpoints] << ep
381
- when :out then opts[:out_endpoints] << ep
382
- else opts[:endpoints] << ep; opts[:binds] << v
383
- end
384
- }
385
- o.on("--in", "Pipe: subsequent -b/-c attach to input (PULL) side") { pipe_side = :in }
386
- o.on("--out", "Pipe: subsequent -b/-c attach to output (PUSH) side") { pipe_side = :out }
387
-
388
- o.separator "\nData source (REP: reply source):"
389
- o.on( "--echo", "Echo received messages back (REP)") { opts[:echo] = true }
390
- o.on("-D", "--data DATA", "Message data (literal string)") { |v| opts[:data] = v }
391
- o.on("-F", "--file FILE", "Read message from file (- = stdin)") { |v| opts[:file] = v }
392
-
393
- o.separator "\nFormat (input + output):"
394
- o.on("-A", "--ascii", "Tab-separated frames, safe ASCII (default)") { opts[:format] = :ascii }
395
- o.on("-Q", "--quoted", "C-style quoted with escapes") { opts[:format] = :quoted }
396
- o.on( "--raw", "Raw binary, no framing") { opts[:format] = :raw }
397
- o.on("-J", "--jsonl", "JSON Lines (array of strings per line)") { opts[:format] = :jsonl }
398
- o.on( "--msgpack", "MessagePack arrays (binary stream)") { opts[:format] = :msgpack }
399
- o.on("-M", "--marshal", "Ruby Marshal stream (binary, Array<String>)") { opts[:format] = :marshal }
400
-
401
- o.separator "\nSubscription/groups:"
402
- o.on("-s", "--subscribe PREFIX", "Subscribe prefix (SUB, default all)") { |v| opts[:subscribes] << v }
403
- o.on("-j", "--join GROUP", "Join group (repeatable, DISH only)") { |v| opts[:joins] << v }
404
- o.on("-g", "--group GROUP", "Publish group (RADIO only)") { |v| opts[:group] = v }
405
-
406
- o.separator "\nIdentity/routing:"
407
- o.on("--identity ID", "Set socket identity (DEALER/ROUTER)") { |v| opts[:identity] = v }
408
- o.on("--target ID", "Target peer (ROUTER/SERVER/PEER, 0x prefix for binary)") { |v| opts[:target] = v }
409
-
410
- o.separator "\nTiming:"
411
- o.on("-i", "--interval SECS", Float, "Repeat interval") { |v| opts[:interval] = v }
412
- o.on("-n", "--count COUNT", Integer, "Max iterations (0=inf)") { |v| opts[:count] = v }
413
- o.on("-d", "--delay SECS", Float, "Delay before first send") { |v| opts[:delay] = v }
414
- o.on("-t", "--timeout SECS", Float, "Send/receive timeout") { |v| opts[:timeout] = v }
415
- o.on("-l", "--linger SECS", Float, "Drain time on close (default 5)") { |v| opts[:linger] = v }
416
- o.on("--reconnect-ivl IVL", "Reconnect interval: SECS or MIN..MAX (default 0.1)") { |v|
417
- opts[:reconnect_ivl] = if v.include?("..")
418
- lo, hi = v.split("..", 2)
419
- Float(lo)..Float(hi)
420
- else
421
- Float(v)
422
- end
423
- }
424
- o.on("--heartbeat-ivl SECS", Float, "ZMTP heartbeat interval (detects dead peers)") { |v| opts[:heartbeat_ivl] = v }
425
-
426
- o.separator "\nDelivery:"
427
- o.on("--conflate", "Keep only last message per subscriber (PUB/RADIO)") { opts[:conflate] = true }
428
-
429
- o.separator "\nCompression:"
430
- o.on("-z", "--compress", "Zstandard compression per frame") { opts[:compress] = true }
431
-
432
- o.separator "\nProcessing (-e = incoming, -E = outgoing):"
433
- o.on("-e", "--recv-eval EXPR", "Eval Ruby for each incoming message ($F = parts)") { |v| opts[:recv_expr] = v }
434
- o.on("-E", "--send-eval EXPR", "Eval Ruby for each outgoing message ($F = parts)") { |v| opts[:send_expr] = v }
435
- o.on("-r", "--require LIB", "Require lib/file; scripts can register OMQ.outgoing/incoming") { |v|
436
- require_relative "../omq" unless defined?(OMQ::VERSION)
437
- v.start_with?("./", "../") ? require(File.expand_path(v)) : require(v)
438
- }
439
- o.on("-P", "--parallel [N]", Integer, "Parallel Ractor workers (pipe only, default: nproc)") { |v|
440
- require "etc"
441
- opts[:parallel] = v || Etc.nprocessors
442
- }
443
-
444
- o.separator "\nCURVE encryption (requires omq-curve gem):"
445
- o.on("--curve-server", "Enable CURVE as server (generates keypair)") { opts[:curve_server] = true }
446
- o.on("--curve-server-key KEY", "Enable CURVE as client (server's Z85 public key)") { |v| opts[:curve_server_key] = v }
447
- o.separator " Env vars: OMQ_SERVER_KEY (client), OMQ_SERVER_PUBLIC + OMQ_SERVER_SECRET (server)"
448
-
449
- o.separator "\nOther:"
450
- o.on("-v", "--verbose", "Print connection events to stderr") { opts[:verbose] = true }
451
- o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
452
- o.on( "--transient", "Exit when all peers disconnect") { opts[:transient] = true }
453
- o.on("-V", "--version") { require_relative "../omq"; puts "omq #{OMQ::VERSION}"; exit }
454
- o.on("-h") { puts o; exit }
455
- o.on( "--help") { page "#{o}\n#{EXAMPLES}"; exit }
456
- o.on( "--examples") { page EXAMPLES; exit }
457
-
458
- o.separator "\nExit codes: 0 = success, 1 = error, 2 = timeout"
459
- end
460
-
461
- begin
462
- parser.parse!(argv)
463
- rescue OptionParser::ParseError => e
464
- abort e.message
465
- end
466
-
467
- type_name = argv.shift
468
- abort parser.to_s unless type_name
469
- unless SOCKET_TYPE_NAMES.include?(type_name.downcase)
470
- abort "Unknown socket type: #{type_name}. Known: #{SOCKET_TYPE_NAMES.join(', ')}"
471
- end
472
-
473
- opts[:type_name] = type_name.downcase
474
-
475
- normalize = ->(url) { url.sub(%r{\Atcp://\*:}, "tcp://0.0.0.0:").sub(%r{\Atcp://:}, "tcp://localhost:") }
476
- normalize_ep = ->(ep) { Endpoint.new(normalize.call(ep.url), ep.bind?) }
477
- opts[:binds].map!(&normalize)
478
- opts[:connects].map!(&normalize)
479
- opts[:endpoints].map!(&normalize_ep)
480
- opts[:in_endpoints].map!(&normalize_ep)
481
- opts[:out_endpoints].map!(&normalize_ep)
482
-
483
- opts
484
- end
485
-
486
-
487
- # Validates option combinations.
488
- #
489
- def validate!(opts)
490
- type_name = opts[:type_name]
491
-
492
- if type_name == "pipe"
493
- has_in_out = opts[:in_endpoints].any? || opts[:out_endpoints].any?
494
- if has_in_out
495
- abort "pipe --in requires at least one endpoint" if opts[:in_endpoints].empty?
496
- abort "pipe --out requires at least one endpoint" if opts[:out_endpoints].empty?
497
- abort "pipe: don't mix --in/--out with bare -b/-c endpoints" unless opts[:endpoints].empty?
498
- else
499
- abort "pipe requires exactly 2 endpoints (pull-side and push-side), or use --in/--out" if opts[:endpoints].size != 2
500
- end
501
- else
502
- abort "--in/--out are only valid for pipe" if opts[:in_endpoints].any? || opts[:out_endpoints].any?
503
- abort "At least one --connect or --bind is required" if opts[:connects].empty? && opts[:binds].empty?
504
- end
505
- abort "--data and --file are mutually exclusive" if opts[:data] && opts[:file]
506
- abort "--subscribe is only valid for SUB" if !opts[:subscribes].empty? && type_name != "sub"
507
- abort "--join is only valid for DISH" if !opts[:joins].empty? && type_name != "dish"
508
- abort "--group is only valid for RADIO" if opts[:group] && type_name != "radio"
509
- abort "--identity is only valid for DEALER/ROUTER" if opts[:identity] && !%w[dealer router].include?(type_name)
510
- abort "--target is only valid for ROUTER/SERVER/PEER" if opts[:target] && !%w[router server peer].include?(type_name)
511
- abort "--conflate is only valid for PUB/RADIO" if opts[:conflate] && !%w[pub radio].include?(type_name)
512
- abort "--recv-eval is not valid for send-only sockets (use --send-eval / -E)" if opts[:recv_expr] && SEND_ONLY.include?(type_name)
513
- abort "--send-eval is not valid for recv-only sockets (use --recv-eval / -e)" if opts[:send_expr] && RECV_ONLY.include?(type_name)
514
- abort "--send-eval and --target are mutually exclusive" if opts[:send_expr] && opts[:target]
515
-
516
- if opts[:parallel]
517
- abort "-P/--parallel is only valid for pipe" unless type_name == "pipe"
518
- abort "-P/--parallel must be >= 2" if opts[:parallel] < 2
519
- all_pipe_eps = opts[:in_endpoints] + opts[:out_endpoints] + opts[:endpoints]
520
- abort "-P/--parallel requires all endpoints to use --connect (not --bind)" if all_pipe_eps.any?(&:bind?)
521
- end
522
-
523
- (opts[:connects] + opts[:binds]).each do |url|
524
- abort "inproc not supported, use tcp:// or ipc://" if url.include?("inproc://")
525
- end
526
- end
527
-
528
-
529
- # Validates that required gems are available.
530
- #
531
- def validate_gems!(config)
532
- abort "--msgpack requires the msgpack gem" if config.format == :msgpack && !config.has_msgpack
533
- abort "--compress requires the zstd-ruby gem" if config.compress && !config.has_zstd
534
-
535
- if config.recv_only? && (config.data || config.file)
536
- abort "--data/--file not valid for #{config.type_name} (receive-only)"
537
- end
538
- end
539
- end
540
- end