omq-cli 0.15.1 → 0.15.3

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: '08825b7e8c6acff6b7c8847cd1966def9bfa751a06cfb0a69c36f0bdc8f71b0e'
4
- data.tar.gz: 2d060c1e4bdfea82a807f053ff5457eb392d372fd73e1c4f962b4b90cbbcfa06
3
+ metadata.gz: 5b503b668b3427210a067278ba175bbda172e98ccf19df42f4dcf4cc84cd1dd4
4
+ data.tar.gz: 9861f0dd910a0ca723b97043996a8c5e47c2222efd77321f8f1e43efdc872818
5
5
  SHA512:
6
- metadata.gz: 5784667a548da85592c289fa905c70290e8e90426815a2fafbf5b833f15159c4d0bdaf1ebb561d50dfe4ed4e801f2e90ffd44c04541a2e62a59e6d17da6f44ef
7
- data.tar.gz: ff42a632e0360a1b926d7833cbbeaa3331a2db0d10a47d0d4b5b5f7ef863d948a58cf5972643872566478c2ca0706b9394f75e49518887dc66ad54b67140d9bc
6
+ metadata.gz: 82972297897d9ea36f3a10ecad3372b476ae935e9b4d20ff312f23db5ecee438b52b0a443d814048f4dab8d5e5243e927509a994917b4926058d80c3f314188b
7
+ data.tar.gz: cbca40c66d0061a3d51782e3b1cf001aad9d44596f2b9203a7d6837c087049227565456f07646de7e2788d827ce91a1ba5290fa7c8792f103475b867ee663c28
data/CHANGELOG.md CHANGED
@@ -1,5 +1,58 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.3 — 2026-04-16
4
+
5
+ ### Changed
6
+
7
+ - **Cache decoded `--data` input.** `read_inline_data` and
8
+ `ParallelWorker#compute_reply` now memoize the decoded result
9
+ instead of re-decoding the same literal on every iteration.
10
+
11
+ ## Unreleased
12
+
13
+ ### Added
14
+
15
+ - **REQ generator mode (`-E`/`-e` with no stdin).** `omq req` now
16
+ produces each request from the send-eval alone when no `-d`/`-f`
17
+ is given and stdin is not piped, matching the existing PUSH/PUB
18
+ behaviour. Bounded by `-n` or paced by `-i` like the other
19
+ generator-capable runners.
20
+
21
+ ### Tests
22
+
23
+ - **System tests for REQ and PUB `-E` generator mode.** REQ fires
24
+ `-E'"foo"' -n 3` against a REP running `-e '|(a)| a.upcase' -n 3`
25
+ and verifies three `FOO` replies round-trip. PUB fires
26
+ `-E'"tick"' -i 0.05 -n 3` against a SUB and verifies three `tick`
27
+ frames are received.
28
+ - **System tests split into themed files under `test/system/`.** The
29
+ monolithic `test/system_test.sh` is replaced by 12 standalone files
30
+ (req_rep, push_pull, pub_sub, router_dealer, format, compression,
31
+ interval, transport, script, validation, pipe, parallel) sharing
32
+ `support.sh` helpers. `run_all.sh` chains them; each file also runs
33
+ standalone. `rake test:system` now invokes `run_all.sh`.
34
+
35
+ ## 0.15.2 — 2026-04-15
36
+
37
+ ### Fixed
38
+
39
+ - **`-vvv` trace output for REQ/REP/PAIR.** `recv_msg` now calls
40
+ `trace_recv` itself, so every runner logs received messages under
41
+ `-vvv` (previously only `run_recv_logic` / `recv_tick` runners did,
42
+ leaving REQ/REP/PAIR receive-side traces silent).
43
+
44
+ ### Tests
45
+
46
+ - **System test for REQ/REP verbose trace order.** Asserts REQ logs
47
+ `>> hi` then `<< HI`, and REP (with `-e'it.first.upcase'`) logs
48
+ `<< hi` then `>> HI`.
49
+ - **Test suite aborts on background-thread exceptions.**
50
+ `Thread.abort_on_exception = true` in `test_helper.rb` — a raising
51
+ spawned thread now re-raises in the main thread immediately instead
52
+ of leaving the test hanging on a receive from a dead peer.
53
+ - **`connect_before_bind_test`**: drop `linger: 1` from
54
+ `OMQ::PULL.new` — PULL is read-only and doesn't accept `linger`.
55
+
3
56
  ## 0.15.0 — 2026-04-14
4
57
 
5
58
  ### Changed
data/README.md CHANGED
@@ -1,33 +1,58 @@
1
- # omq — ZeroMQ CLI
1
+ # omq — Swiss army knife for ØMQ
2
2
 
3
3
  [![Gem Version](https://img.shields.io/gem/v/omq-cli?color=e9573f)](https://rubygems.org/gems/omq-cli)
4
4
  [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](LICENSE)
5
5
  [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%203.3-CC342D?logo=ruby&logoColor=white)](https://www.ruby-lang.org)
6
6
 
7
- Command-line tool for sending and receiving ZeroMQ messages on any socket type.
8
- Like `nngcat` from libnng, but with Ruby eval, Ractor parallelism, and message handlers.
9
-
10
- Built on [omq](https://github.com/zeromq/omq) — pure Ruby ZeroMQ, no C dependencies.
11
-
12
- ## Install
7
+ A command-line tool that speaks every ZeroMQ socket pattern, transforms
8
+ messages with inline Ruby, parallelizes across Ractors, encrypts with CURVE,
9
+ and reshapes stdin/stdout through three I/O formats on top of three
10
+ on-the-wire formats. Built on [ØMQ](https://github.com/zeromq/omq) — pure
11
+ Ruby, no C dependencies.
13
12
 
14
13
  ```sh
15
14
  gem install omq-cli
16
15
  ```
17
16
 
18
- ## Quick Start
17
+ Think of it as `nngcat` with a scripting engine bolted on. A few things it can do out of the box:
19
18
 
20
19
  ```sh
21
- # Echo server
20
+ # an echo server in one line
22
21
  omq rep -b tcp://:5555 --echo
23
22
 
24
- # Client
25
- echo "hello" | omq req -c tcp://localhost:5555
26
-
27
- # Upcase server — -e evals Ruby on each incoming message
23
+ # upcase every incoming message
28
24
  omq rep -b tcp://:5555 -e 'it.map(&:upcase)'
25
+
26
+ # publish a live timestamp every second
27
+ omq pub -c tcp://localhost:5556 -E 'Time.now.to_s' -i 1
28
+
29
+ # CPU-parallel PULL → transform → PUSH pipeline across all cores
30
+ omq pipe -c@work -c@sink -P0 -r./fib.rb -e 'fib(Integer(it.first)).to_s'
31
+
32
+ # JSON over the wire, filter by field
33
+ omq sub -c tcp://localhost:5556 -rjson -e 'JSON.parse(it.first)["temperature"]'
29
34
  ```
30
35
 
36
+ ## Highlights
37
+
38
+ - **Every socket pattern** — req/rep, pub/sub, push/pull, dealer/router,
39
+ xpub/xsub, pair, and draft types (client/server, radio/dish, scatter/gather,
40
+ peer, channel)
41
+ - **Inline Ruby transforms** — `-e` rewrites incoming messages, `-E` rewrites
42
+ outgoing. `next` / `break` / `nil` for flow control, `BEGIN{}` / `END{}` for
43
+ awk-style aggregation, `-r` to load helper scripts
44
+ - **Ractor-parallel `pipe`** — `-P0` spawns one worker Ractor per core, each
45
+ with its own PULL/PUSH pair. CPU-bound transforms actually scale
46
+ - **I/O formats** — ASCII, quoted, or JSONL for stdin/stdout (display only;
47
+ wire stays plain)
48
+ - **Wire formats** — raw ZMTP, MessagePack, or Ruby Marshal shape the frame
49
+ payload itself, so arbitrary Ruby objects round-trip end-to-end. Optional
50
+ Zstandard compression (`-z`) on top
51
+ - **CURVE encryption** — end-to-end encrypted sockets via libsodium (or nuckle,
52
+ pure Ruby). `omq keygen` generates a persistent keypair
53
+ - **Transient mode** — `--transient` exits cleanly when peers disconnect,
54
+ perfect for pipeline workers and one-shot sinks
55
+
31
56
  ```
32
57
  Usage: omq TYPE [options]
33
58
 
@@ -61,7 +86,8 @@ Pipe takes two positional endpoints (input, output) or uses `--in`/`--out` for m
61
86
  | `scatter` | `gather` | Pipeline (draft, single-frame only) |
62
87
  | `radio` | `dish` | Group messaging (draft, single-frame only) |
63
88
 
64
- Send-only sockets read from stdin (or `--data`/`--file`) and send. Recv-only sockets receive and write to stdout.
89
+ Send-only sockets read from stdin (or `--data`/`--file`) and send. Recv-only
90
+ sockets receive and write to stdout.
65
91
 
66
92
  ```sh
67
93
  echo "task" | omq push -c tcp://worker:5557
@@ -96,8 +122,8 @@ echo "hello" | omq req -c tcp://localhost:5555
96
122
  | `dealer` | Like `pair` but round-robin send to multiple peers |
97
123
  | `channel` | Like `pair` (draft, single-frame) |
98
124
 
99
- These spawn two concurrent tasks: a receiver (prints incoming) and a sender (reads stdin).
100
- `-e` transforms incoming, `-E` transforms outgoing.
125
+ These spawn two concurrent tasks: a receiver (prints incoming) and a sender
126
+ (reads stdin). `-e` transforms incoming, `-E` transforms outgoing.
101
127
 
102
128
  ### Routing sockets
103
129
 
@@ -212,8 +238,7 @@ echo hello | omq push -c tcp://localhost:5557 -E 'it.map(&:upcase)'
212
238
  omq pull -b tcp://:5557 -e 'it.first.include?("error") ? it : nil'
213
239
 
214
240
  # REQ: different transforms per direction
215
- echo hello | omq req -c tcp://localhost:5555 \
216
- -E 'it.map(&:upcase)' -e 'it.map(&:reverse)'
241
+ echo hello | omq req -c tcp://localhost:5555 -E 'it.map(&:upcase)' -e 'it.map(&:reverse)'
217
242
 
218
243
  # generate messages without stdin
219
244
  omq pub -c tcp://localhost:5556 -E 'Time.now.to_s' -i 1
@@ -305,17 +330,32 @@ OMQ.outgoing { |msg| [*msg, Time.now.iso8601] }
305
330
 
306
331
  ## Formats
307
332
 
333
+ Two distinct axes: **I/O formats** reshape how messages are read from stdin
334
+ and printed to stdout, while **wire formats** shape the frame payload that
335
+ actually goes over ZMTP. Pick one of each — they compose freely.
336
+
337
+ ### I/O formats (stdin/stdout only)
338
+
308
339
  | Flag | Format |
309
340
  |------|--------|
310
341
  | `-A` / `--ascii` | Tab-separated frames, non-printable → dots (default) |
311
342
  | `-Q` / `--quoted` | C-style escapes, lossless round-trip |
312
- | `--raw` | Raw ZMTP binary (pipe to `hexdump -C` for debugging) |
313
343
  | `-J` / `--jsonl` | JSON Lines — `["frame1","frame2"]` per line |
314
- | `--msgpack` | MessagePack arrays (binary stream) |
315
- | `-M` / `--marshal` | Ruby Marshal — one arbitrary Ruby object per message |
316
344
 
317
- Multipart messages: in ASCII/quoted mode, frames are tab-separated. In JSONL mode,
318
- each message is a JSON array.
345
+ Display-only: the wire carries plain ZMTP frames. Multipart messages are
346
+ tab-separated in ASCII/quoted mode and encoded as JSON arrays in JSONL.
347
+
348
+ ### Wire formats (on the ZMTP frame payload)
349
+
350
+ | Flag | Format |
351
+ |------|--------|
352
+ | `--raw` | Raw ZMTP binary (pipe to `hexdump -C` for debugging) |
353
+ | `--msgpack` | MessagePack — each frame is one packed object |
354
+ | `-M` / `--marshal` | Ruby Marshal — each frame is one arbitrary Ruby object |
355
+
356
+ Wire formats reshape the payload end-to-end: inside `-e`/`-E`, `it` is the
357
+ decoded object (not an Array of frames), so scalars, hashes, and custom
358
+ classes flow through transparently between peers speaking the same format.
319
359
 
320
360
  ```sh
321
361
  # send multipart via tabs
@@ -326,12 +366,8 @@ echo '["key","value"]' | omq push -c tcp://localhost:5557 -J
326
366
  omq pull -b tcp://:5557 -J
327
367
  ```
328
368
 
329
- Under `-M`, each wire frame is one Marshal-dumped Ruby object. Inside `-e` / `-E`,
330
- `it` is that raw object — not an Array of frames — so scalars, hashes, custom
331
- classes, or any Marshal-safe value flow through transparently:
332
-
333
369
  ```sh
334
- # send a bare String, receive a { string => encoding } Hash
370
+ # send a bare String with Marshal, receive a { string => encoding } Hash
335
371
  omq push -b tcp://:5557 -ME '"foo"'
336
372
  omq pull -c tcp://:5557 -M -e '{it => it.encoding}'
337
373
  # => {"foo" => #<Encoding:UTF-8>}
@@ -413,8 +449,7 @@ omq rep -b tcp://:5555 --echo --curve-server --crypto nuckle
413
449
  omq rep -b tcp://:5555 --echo --curve-server
414
450
 
415
451
  # client (paste the key)
416
- echo "secret" | omq req -c tcp://localhost:5555 \
417
- --curve-server-key '<key from server>'
452
+ echo "secret" | omq req -c tcp://localhost:5555 --curve-server-key '<key from server>'
418
453
  ```
419
454
 
420
455
  Persistent keys via env vars: `OMQ_SERVER_PUBLIC` + `OMQ_SERVER_SECRET` (server), `OMQ_SERVER_KEY` (client).
@@ -289,7 +289,6 @@ module OMQ
289
289
  loop do
290
290
  parts = recv_msg
291
291
  break if parts.nil?
292
- trace_recv(parts)
293
292
  parts = eval_recv_expr(parts)
294
293
  output(parts)
295
294
  i += 1
@@ -316,7 +315,6 @@ module OMQ
316
315
  @recv_tick_eof = true
317
316
  return 0
318
317
  end
319
- trace_recv(parts)
320
318
  parts = eval_recv_expr(parts)
321
319
  output(parts)
322
320
  1
@@ -405,6 +403,7 @@ module OMQ
405
403
  parts = Marshal.load(parts.first)
406
404
  end
407
405
 
406
+ trace_recv(parts)
408
407
  transient_ready!
409
408
  parts
410
409
  end
@@ -423,7 +422,7 @@ module OMQ
423
422
 
424
423
  def read_inline_data
425
424
  if config.data
426
- @fmt.decode(config.data + "\n")
425
+ @inline_data ||= @fmt.decode(config.data + "\n")
427
426
  else
428
427
  @file_data ||= (config.file == "-" ? $stdin.read : File.read(config.file)).chomp
429
428
  @fmt.decode(@file_data + "\n")
@@ -28,6 +28,7 @@ module OMQ
28
28
  expr, begin_body, end_body = extract_blocks(src)
29
29
  @begin_proc = eval("proc { #{begin_body} }") if begin_body
30
30
  @end_proc = eval("proc { #{end_body} }") if end_body
31
+
31
32
  if expr && !expr.strip.empty?
32
33
  @eval_proc = eval("proc { #{expr} }")
33
34
  end
@@ -85,6 +86,7 @@ module OMQ
85
86
  begin_proc = eval("proc { #{begin_body} }") if begin_body
86
87
  end_proc = eval("proc { #{end_body} }") if end_body
87
88
  eval_proc = nil
89
+
88
90
  if expr && !expr.strip.empty?
89
91
  eval_proc = eval("proc { #{expr} }")
90
92
  end
@@ -100,12 +102,12 @@ module OMQ
100
102
  # (Ractors cannot call back into instance state).
101
103
  #
102
104
  def self.extract_block(expr, keyword)
103
- start = expr.index(/#{keyword}\s*\{/)
104
- return [expr, nil] unless start
105
+ start = expr.index(/#{keyword}\s*\{/) or return [expr, nil]
105
106
 
106
107
  i = expr.index("{", start)
107
108
  depth = 1
108
109
  j = i + 1
110
+
109
111
  while j < expr.length && depth > 0
110
112
  case expr[j]
111
113
  when "{"
@@ -113,6 +115,7 @@ module OMQ
113
115
  when "}"
114
116
  depth -= 1
115
117
  end
118
+
116
119
  j += 1
117
120
  end
118
121
 
@@ -157,7 +157,7 @@ module OMQ
157
157
  elsif @config.echo
158
158
  parts
159
159
  elsif @config.data
160
- @fmt.decode(@config.data + "\n")
160
+ @inline_data ||= @fmt.decode(@config.data + "\n")
161
161
  else
162
162
  parts
163
163
  end
@@ -10,19 +10,27 @@ module OMQ
10
10
  def run_loop(task)
11
11
  n = config.count
12
12
  i = 0
13
- sleep(config.delay) if config.delay
13
+
14
+ sleep config.delay if config.delay
15
+ generator = @send_eval_proc && !config.data && !config.file && !stdin_ready?
16
+
14
17
  loop do
15
- parts = read_next
16
- break unless parts
17
- parts = eval_send_expr(parts)
18
+ if generator
19
+ parts = eval_send_expr(nil)
20
+ else
21
+ parts = read_next
22
+ break unless parts
23
+ parts = eval_send_expr(parts)
24
+ end
25
+
18
26
  next unless parts
27
+
19
28
  send_msg(parts)
20
- reply = recv_msg
21
- break if reply.nil?
29
+ reply = recv_msg or break
22
30
  output(eval_recv_expr(reply))
23
31
  i += 1
24
32
  break if n && n > 0 && i >= n
25
- break if !config.interval && (config.data || config.file)
33
+ break if !config.interval && (generator || config.data || config.file) && !(n && n > 0)
26
34
  wait_for_interval if config.interval
27
35
  end
28
36
  end
@@ -38,7 +46,11 @@ module OMQ
38
46
  # Runner for REP sockets (synchronous request-reply server).
39
47
  class RepRunner < BaseRunner
40
48
  def call(task)
41
- config.parallel ? run_parallel_workers(:REP) : super
49
+ if config.parallel
50
+ run_parallel_workers(:REP)
51
+ else
52
+ super
53
+ end
42
54
  end
43
55
 
44
56
 
@@ -48,9 +60,9 @@ module OMQ
48
60
  def run_loop(task)
49
61
  n = config.count
50
62
  i = 0
63
+
51
64
  loop do
52
- msg = recv_msg
53
- break if msg.nil?
65
+ msg = recv_msg or break
54
66
  break unless handle_rep_request(msg)
55
67
  i += 1
56
68
  break if n && n > 0 && i >= n
@@ -61,6 +73,7 @@ module OMQ
61
73
  def handle_rep_request(msg)
62
74
  if config.recv_expr || @recv_eval_proc
63
75
  reply = eval_recv_expr(msg)
76
+
64
77
  unless reply.equal?(SENT)
65
78
  output(reply)
66
79
  send_msg(reply || [""])
@@ -76,6 +89,7 @@ module OMQ
76
89
  else
77
90
  abort "REP needs a reply source: --echo, --data, --file, -e, or stdin pipe"
78
91
  end
92
+
79
93
  true
80
94
  end
81
95
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OMQ
4
4
  module CLI
5
- VERSION = "0.15.1"
5
+ VERSION = "0.15.3"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.1
4
+ version: 0.15.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -15,20 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.19'
19
- - - ">="
20
- - !ruby/object:Gem::Version
21
- version: 0.19.3
18
+ version: '0.22'
22
19
  type: :runtime
23
20
  prerelease: false
24
21
  version_requirements: !ruby/object:Gem::Requirement
25
22
  requirements:
26
23
  - - "~>"
27
24
  - !ruby/object:Gem::Version
28
- version: '0.19'
29
- - - ">="
30
- - !ruby/object:Gem::Version
31
- version: 0.19.3
25
+ version: '0.22'
32
26
  - !ruby/object:Gem::Dependency
33
27
  name: omq-ffi
34
28
  requirement: !ruby/object:Gem::Requirement
@@ -49,84 +43,84 @@ dependencies:
49
43
  requirements:
50
44
  - - "~>"
51
45
  - !ruby/object:Gem::Version
52
- version: '0.1'
46
+ version: '0.2'
53
47
  type: :runtime
54
48
  prerelease: false
55
49
  version_requirements: !ruby/object:Gem::Requirement
56
50
  requirements:
57
51
  - - "~>"
58
52
  - !ruby/object:Gem::Version
59
- version: '0.1'
53
+ version: '0.2'
60
54
  - !ruby/object:Gem::Dependency
61
55
  name: omq-rfc-radiodish
62
56
  requirement: !ruby/object:Gem::Requirement
63
57
  requirements:
64
58
  - - "~>"
65
59
  - !ruby/object:Gem::Version
66
- version: '0.1'
60
+ version: '0.2'
67
61
  type: :runtime
68
62
  prerelease: false
69
63
  version_requirements: !ruby/object:Gem::Requirement
70
64
  requirements:
71
65
  - - "~>"
72
66
  - !ruby/object:Gem::Version
73
- version: '0.1'
67
+ version: '0.2'
74
68
  - !ruby/object:Gem::Dependency
75
69
  name: omq-rfc-scattergather
76
70
  requirement: !ruby/object:Gem::Requirement
77
71
  requirements:
78
72
  - - "~>"
79
73
  - !ruby/object:Gem::Version
80
- version: '0.1'
74
+ version: '0.2'
81
75
  type: :runtime
82
76
  prerelease: false
83
77
  version_requirements: !ruby/object:Gem::Requirement
84
78
  requirements:
85
79
  - - "~>"
86
80
  - !ruby/object:Gem::Version
87
- version: '0.1'
81
+ version: '0.2'
88
82
  - !ruby/object:Gem::Dependency
89
83
  name: omq-rfc-channel
90
84
  requirement: !ruby/object:Gem::Requirement
91
85
  requirements:
92
86
  - - "~>"
93
87
  - !ruby/object:Gem::Version
94
- version: '0.1'
88
+ version: '0.2'
95
89
  type: :runtime
96
90
  prerelease: false
97
91
  version_requirements: !ruby/object:Gem::Requirement
98
92
  requirements:
99
93
  - - "~>"
100
94
  - !ruby/object:Gem::Version
101
- version: '0.1'
95
+ version: '0.2'
102
96
  - !ruby/object:Gem::Dependency
103
97
  name: omq-rfc-p2p
104
98
  requirement: !ruby/object:Gem::Requirement
105
99
  requirements:
106
100
  - - "~>"
107
101
  - !ruby/object:Gem::Version
108
- version: '0.1'
102
+ version: '0.2'
109
103
  type: :runtime
110
104
  prerelease: false
111
105
  version_requirements: !ruby/object:Gem::Requirement
112
106
  requirements:
113
107
  - - "~>"
114
108
  - !ruby/object:Gem::Version
115
- version: '0.1'
109
+ version: '0.2'
116
110
  - !ruby/object:Gem::Dependency
117
111
  name: omq-rfc-zstd
118
112
  requirement: !ruby/object:Gem::Requirement
119
113
  requirements:
120
114
  - - "~>"
121
115
  - !ruby/object:Gem::Version
122
- version: '0.2'
116
+ version: '0.3'
123
117
  type: :runtime
124
118
  prerelease: false
125
119
  version_requirements: !ruby/object:Gem::Requirement
126
120
  requirements:
127
121
  - - "~>"
128
122
  - !ruby/object:Gem::Version
129
- version: '0.2'
123
+ version: '0.3'
130
124
  - !ruby/object:Gem::Dependency
131
125
  name: msgpack
132
126
  requirement: !ruby/object:Gem::Requirement