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