omq-backend-rust 0.1.1
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 +7 -0
- data/CHANGELOG.md +29 -0
- data/Cargo.toml +3 -0
- data/LICENSE +15 -0
- data/README.md +70 -0
- data/ext/omq_backend_rust/Cargo.toml +33 -0
- data/ext/omq_backend_rust/extconf.rb +8 -0
- data/ext/omq_backend_rust/src/error.rs +15 -0
- data/ext/omq_backend_rust/src/lib.rs +24 -0
- data/ext/omq_backend_rust/src/notify.rs +69 -0
- data/ext/omq_backend_rust/src/options.rs +185 -0
- data/ext/omq_backend_rust/src/runtime.rs +455 -0
- data/ext/omq_backend_rust/src/socket.rs +470 -0
- data/lib/omq/rust/engine.rb +359 -0
- data/lib/omq/rust/version.rb +7 -0
- data/lib/omq/rust.rb +18 -0
- metadata +87 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d3204e074915dc804b56b3b5b713cbd08c11b84dec67732f3f0f7d12e02b5b87
|
|
4
|
+
data.tar.gz: c46c4dbebce74f3b164c3d8d4c5eb10a92ab4cc730c25198ef66edee7e50330b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 47c78d4a47f5f3494a90325065ac4e7adf8361d30b37ea0d8ff46cb7b96d47709d8021e5448fd3fef8fa85c0e4f1516e8ce6c7fa5465d435ae5c669499c0427e
|
|
7
|
+
data.tar.gz: 66a1f72298ea9e30cec1c729a76f77cd6551abdff58edfbd31cdc826d54bf66764dd01889d59da9cd098b1cef01367d50c6eac94aca0c39559689ff0621c7b6b
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## 0.1.1 - 2026-07-23
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Updated to `omq-tokio` 0.19.3 and `omq-proto` 0.23.2.
|
|
10
|
+
- Removed obsolete Blake3ZMQ support.
|
|
11
|
+
- Moved release source to the `zeromq/omq.rb` monorepo.
|
|
12
|
+
|
|
13
|
+
## v0.1.0 — 2026-06-24
|
|
14
|
+
|
|
15
|
+
Initial release.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`OMQ::Rust::Engine`** — drop-in OMQ engine backed by omq-tokio. Pass
|
|
20
|
+
`backend: :rust` to any OMQ socket constructor.
|
|
21
|
+
- All standard socket types: REQ/REP, PUB/SUB, PUSH/PULL, DEALER/ROUTER,
|
|
22
|
+
XPUB/XSUB, PAIR.
|
|
23
|
+
- All draft socket types: CLIENT/SERVER, RADIO/DISH, SCATTER/GATHER, CHANNEL.
|
|
24
|
+
- TCP and IPC transports.
|
|
25
|
+
- CURVE (CurveZMQ) and BLAKE3ZMQ security mechanisms.
|
|
26
|
+
- Full cross-backend interop with the default Ruby engine.
|
|
27
|
+
- Lifecycle promises: `peer_connected`, `all_peers_gone`, `subscriber_joined`.
|
|
28
|
+
- Monitor event forwarding from the Tokio runtime.
|
|
29
|
+
- Configurable IO thread count via `OMQ::Rust.io_threads`.
|
data/Cargo.toml
ADDED
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,70 @@
|
|
|
1
|
+
# omq-backend-rust
|
|
2
|
+
|
|
3
|
+
[](https://github.com/zeromq/omq.rb/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/omq-backend-rust)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://www.ruby-lang.org)
|
|
7
|
+
|
|
8
|
+
Rust-backed engine for [OMQ](https://github.com/zeromq/omq.rb). Same socket API,
|
|
9
|
+
but networking runs on a [Tokio](https://tokio.rs/) runtime inside a native
|
|
10
|
+
extension compiled via [rb_sys](https://github.com/oxidize-rb/rb-sys).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
Requires a Rust toolchain (stable) at gem install time.
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
# Gemfile
|
|
18
|
+
gem "omq-backend-rust"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
gem install omq-backend-rust
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require "omq"
|
|
29
|
+
require "omq/rust"
|
|
30
|
+
|
|
31
|
+
Async do
|
|
32
|
+
push = OMQ::PUSH.new(backend: :rust)
|
|
33
|
+
pull = OMQ::PULL.new(backend: :rust)
|
|
34
|
+
|
|
35
|
+
port = pull.bind("tcp://127.0.0.1:0").port
|
|
36
|
+
push.connect("tcp://127.0.0.1:#{port}")
|
|
37
|
+
push.peer_connected.wait
|
|
38
|
+
|
|
39
|
+
push << "hello from Rust"
|
|
40
|
+
p pull.receive # => ["hello from Rust"]
|
|
41
|
+
ensure
|
|
42
|
+
push&.close
|
|
43
|
+
pull&.close
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The `:rust` backend is fully interoperable with the default `:ruby` backend.
|
|
48
|
+
Mix backends freely within the same process.
|
|
49
|
+
|
|
50
|
+
## Supported socket types
|
|
51
|
+
|
|
52
|
+
All standard and draft ZMTP socket types: REQ/REP, PUB/SUB, PUSH/PULL,
|
|
53
|
+
DEALER/ROUTER, XPUB/XSUB, PAIR, CLIENT/SERVER, RADIO/DISH,
|
|
54
|
+
SCATTER/GATHER, CHANNEL.
|
|
55
|
+
|
|
56
|
+
## Security mechanisms
|
|
57
|
+
|
|
58
|
+
- **NULL** (default)
|
|
59
|
+
- **CURVE** (CurveZMQ, via [Nuckle](https://github.com/paddor/nuckle))
|
|
60
|
+
|
|
61
|
+
## Development
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
OMQ_DEV=1 bundle install
|
|
65
|
+
OMQ_DEV=1 bundle exec rake
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
[ISC](LICENSE)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "omq_backend_rust"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
license = "ISC"
|
|
6
|
+
authors = ["Patrik Wenger <paddor@gmail.com>"]
|
|
7
|
+
publish = false
|
|
8
|
+
|
|
9
|
+
[lib]
|
|
10
|
+
name = "omq_backend_rust"
|
|
11
|
+
crate-type = ["cdylib"]
|
|
12
|
+
|
|
13
|
+
[features]
|
|
14
|
+
default = ["plain", "curve", "lz4"]
|
|
15
|
+
plain = ["omq-tokio/plain"]
|
|
16
|
+
curve = ["omq-tokio/curve"]
|
|
17
|
+
lz4 = ["omq-tokio/lz4"]
|
|
18
|
+
|
|
19
|
+
[dependencies]
|
|
20
|
+
omq-proto = { version = "=0.23.2", default-features = false }
|
|
21
|
+
omq-tokio = { version = "=0.19.3", default-features = false }
|
|
22
|
+
tokio = { version = "1.52.0", features = ["rt", "rt-multi-thread", "time", "sync", "io-util", "net"] }
|
|
23
|
+
yring = { version = "0.3.8", features = ["async"] }
|
|
24
|
+
|
|
25
|
+
bytes = "1.12.0"
|
|
26
|
+
flume = { version = "0.12", default-features = false, features = ["async"] }
|
|
27
|
+
futures = { version = "0.3", default-features = false, features = ["std", "async-await"] }
|
|
28
|
+
magnus = "0.8"
|
|
29
|
+
rb-sys = "0.9"
|
|
30
|
+
libc = "0.2"
|
|
31
|
+
|
|
32
|
+
[build-dependencies]
|
|
33
|
+
rb-sys = "0.9"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
use magnus::{Error, Ruby};
|
|
2
|
+
use omq_proto::error::Error as OmqError;
|
|
3
|
+
|
|
4
|
+
pub fn map_err(ruby: &Ruby, e: OmqError) -> Error {
|
|
5
|
+
match e {
|
|
6
|
+
OmqError::Closed => Error::new(ruby.exception_io_error(), "socket closed"),
|
|
7
|
+
OmqError::Timeout => Error::new(ruby.exception_runtime_error(), "operation timed out"),
|
|
8
|
+
OmqError::Unroutable => Error::new(ruby.exception_runtime_error(), "no route to peer"),
|
|
9
|
+
OmqError::InvalidEndpoint(msg) => Error::new(ruby.exception_arg_error(), msg),
|
|
10
|
+
OmqError::Protocol(msg) => Error::new(ruby.exception_runtime_error(), msg),
|
|
11
|
+
OmqError::Io(e) => Error::new(ruby.exception_runtime_error(), e.to_string()),
|
|
12
|
+
OmqError::HandshakeFailed(msg) => Error::new(ruby.exception_runtime_error(), msg),
|
|
13
|
+
_ => Error::new(ruby.exception_runtime_error(), format!("{e}")),
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
mod error;
|
|
2
|
+
mod notify;
|
|
3
|
+
mod options;
|
|
4
|
+
mod runtime;
|
|
5
|
+
mod socket;
|
|
6
|
+
|
|
7
|
+
use magnus::{Error, Ruby, function, prelude::*};
|
|
8
|
+
|
|
9
|
+
fn set_io_threads(n: usize) {
|
|
10
|
+
socket::set_io_threads(n);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#[magnus::init]
|
|
14
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
15
|
+
let omq = ruby.define_module("OMQ")?;
|
|
16
|
+
let rust = omq.define_module("Rust")?;
|
|
17
|
+
let native = rust.define_module("Native")?;
|
|
18
|
+
|
|
19
|
+
native.define_module_function("io_threads=", function!(set_io_threads, 1))?;
|
|
20
|
+
|
|
21
|
+
socket::register(ruby)?;
|
|
22
|
+
|
|
23
|
+
Ok(())
|
|
24
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
use std::os::fd::RawFd;
|
|
2
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
3
|
+
|
|
4
|
+
pub struct PipeNotify {
|
|
5
|
+
read_fd: RawFd,
|
|
6
|
+
write_fd: RawFd,
|
|
7
|
+
parking: AtomicBool,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
unsafe impl Send for PipeNotify {}
|
|
11
|
+
unsafe impl Sync for PipeNotify {}
|
|
12
|
+
|
|
13
|
+
impl PipeNotify {
|
|
14
|
+
pub fn new() -> Self {
|
|
15
|
+
let mut fds = [0i32; 2];
|
|
16
|
+
let ret = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_NONBLOCK | libc::O_CLOEXEC) };
|
|
17
|
+
assert!(
|
|
18
|
+
ret == 0,
|
|
19
|
+
"pipe2 failed: {}",
|
|
20
|
+
std::io::Error::last_os_error()
|
|
21
|
+
);
|
|
22
|
+
Self {
|
|
23
|
+
read_fd: fds[0],
|
|
24
|
+
write_fd: fds[1],
|
|
25
|
+
parking: AtomicBool::new(false),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub fn notify(&self) {
|
|
30
|
+
if self.parking.load(Ordering::Acquire) {
|
|
31
|
+
self.write_byte();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pub fn force_wake(&self) {
|
|
36
|
+
self.write_byte();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pub fn read_fd(&self) -> RawFd {
|
|
40
|
+
self.read_fd
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pub fn park_begin(&self) {
|
|
44
|
+
self.parking.store(true, Ordering::Release);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn write_byte(&self) {
|
|
48
|
+
let val: u8 = 1;
|
|
49
|
+
loop {
|
|
50
|
+
let ret =
|
|
51
|
+
unsafe { libc::write(self.write_fd, &val as *const u8 as *const libc::c_void, 1) };
|
|
52
|
+
if ret >= 0 {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
if std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
impl Drop for PipeNotify {
|
|
63
|
+
fn drop(&mut self) {
|
|
64
|
+
unsafe {
|
|
65
|
+
libc::close(self.read_fd);
|
|
66
|
+
libc::close(self.write_fd);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
use std::time::Duration;
|
|
2
|
+
|
|
3
|
+
use bytes::Bytes;
|
|
4
|
+
use magnus::{Error, Ruby, TryConvert, r_hash::RHash, value::ReprValue};
|
|
5
|
+
|
|
6
|
+
pub fn build_options(ruby: &Ruby, hash: RHash) -> Result<omq_tokio::Options, Error> {
|
|
7
|
+
let mut opts = omq_tokio::Options::default();
|
|
8
|
+
|
|
9
|
+
if let Some(v) = get_opt::<i64>(ruby, hash, "send_hwm")? {
|
|
10
|
+
opts.send_hwm = v.max(0) as u32;
|
|
11
|
+
}
|
|
12
|
+
if let Some(v) = get_opt::<i64>(ruby, hash, "recv_hwm")? {
|
|
13
|
+
opts.recv_hwm = v.max(0) as u32;
|
|
14
|
+
}
|
|
15
|
+
if let Some(v) = get_opt::<f64>(ruby, hash, "linger")? {
|
|
16
|
+
opts.linger = if v.is_infinite() {
|
|
17
|
+
None
|
|
18
|
+
} else {
|
|
19
|
+
Some(Duration::from_secs_f64(v))
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if let Some(v) = get_opt::<Vec<u8>>(ruby, hash, "identity")? {
|
|
23
|
+
if !v.is_empty() {
|
|
24
|
+
opts.identity = Bytes::from(v);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if let Some(v) = get_opt::<bool>(ruby, hash, "router_mandatory")? {
|
|
28
|
+
opts.router_mandatory = v;
|
|
29
|
+
}
|
|
30
|
+
if let Some(v) = get_opt::<bool>(ruby, hash, "conflate")? {
|
|
31
|
+
opts.conflate = v;
|
|
32
|
+
}
|
|
33
|
+
if let Some(v) = get_opt_duration(ruby, hash, "heartbeat_interval")? {
|
|
34
|
+
opts.heartbeat_interval = Some(v);
|
|
35
|
+
}
|
|
36
|
+
if let Some(v) = get_opt_duration(ruby, hash, "heartbeat_ttl")? {
|
|
37
|
+
opts.heartbeat_ttl = Some(v);
|
|
38
|
+
}
|
|
39
|
+
if let Some(v) = get_opt_duration(ruby, hash, "heartbeat_timeout")? {
|
|
40
|
+
opts.heartbeat_timeout = Some(v);
|
|
41
|
+
}
|
|
42
|
+
if let Some(v) = get_opt::<i64>(ruby, hash, "max_message_size")? {
|
|
43
|
+
opts.max_message_size = Some(v as usize);
|
|
44
|
+
}
|
|
45
|
+
if let Some(v) = get_opt::<i64>(ruby, hash, "sndbuf")? {
|
|
46
|
+
opts.send_buffer_size = Some(v as usize);
|
|
47
|
+
}
|
|
48
|
+
if let Some(v) = get_opt::<i64>(ruby, hash, "rcvbuf")? {
|
|
49
|
+
opts.recv_buffer_size = Some(v as usize);
|
|
50
|
+
}
|
|
51
|
+
if let Some(v) = get_opt::<String>(ruby, hash, "on_mute")? {
|
|
52
|
+
opts.on_mute = match v.as_str() {
|
|
53
|
+
"drop_newest" | "drop" => omq_tokio::OnMute::DropNewest,
|
|
54
|
+
_ => omq_tokio::OnMute::Block,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if let Some(v) = get_opt::<f64>(ruby, hash, "reconnect_interval")? {
|
|
59
|
+
opts.reconnect = omq_proto::options::ReconnectPolicy::Fixed(Duration::from_secs_f64(v));
|
|
60
|
+
}
|
|
61
|
+
if let Some(min) = get_opt::<f64>(ruby, hash, "reconnect_interval_min")? {
|
|
62
|
+
let max = get_opt::<f64>(ruby, hash, "reconnect_interval_max")?.unwrap_or(min * 16.0);
|
|
63
|
+
opts.reconnect = omq_proto::options::ReconnectPolicy::Exponential {
|
|
64
|
+
min: Duration::from_secs_f64(min),
|
|
65
|
+
max: Duration::from_secs_f64(max),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if let Some(mech_type) = get_opt::<String>(ruby, hash, "mechanism_type")? {
|
|
70
|
+
apply_mechanism(ruby, hash, &mech_type, &mut opts)?;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Ok(opts)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fn apply_mechanism(
|
|
77
|
+
ruby: &Ruby,
|
|
78
|
+
hash: RHash,
|
|
79
|
+
mech_type: &str,
|
|
80
|
+
opts: &mut omq_tokio::Options,
|
|
81
|
+
) -> Result<(), Error> {
|
|
82
|
+
match mech_type {
|
|
83
|
+
"null" => {}
|
|
84
|
+
|
|
85
|
+
#[cfg(feature = "curve")]
|
|
86
|
+
"curve" => {
|
|
87
|
+
let is_server = get_opt::<bool>(ruby, hash, "mechanism_server")?.unwrap_or(false);
|
|
88
|
+
let pub_key = get_opt_bytes(ruby, hash, "mechanism_public_key")?;
|
|
89
|
+
let sec_key = get_opt_bytes(ruby, hash, "mechanism_secret_key")?;
|
|
90
|
+
|
|
91
|
+
if is_server {
|
|
92
|
+
if let (Some(pk), Some(sk)) = (pub_key, sec_key) {
|
|
93
|
+
let keypair = omq_proto::CurveKeypair {
|
|
94
|
+
public: omq_proto::CurvePublicKey::from_bytes(to_32(
|
|
95
|
+
ruby,
|
|
96
|
+
&pk,
|
|
97
|
+
"public key",
|
|
98
|
+
)?),
|
|
99
|
+
secret: omq_proto::CurveSecretKey::from_bytes(to_32(
|
|
100
|
+
ruby,
|
|
101
|
+
&sk,
|
|
102
|
+
"secret key",
|
|
103
|
+
)?),
|
|
104
|
+
};
|
|
105
|
+
opts.mechanism = omq_proto::MechanismSetup::CurveServer {
|
|
106
|
+
our_keypair: keypair,
|
|
107
|
+
cookie_keyring: std::sync::Arc::new(omq_proto::CurveCookieKeyring::new()),
|
|
108
|
+
authenticator: None,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
let srv_key = get_opt_bytes(ruby, hash, "mechanism_server_key")?;
|
|
113
|
+
if let (Some(pk), Some(sk), Some(svk)) = (pub_key, sec_key, srv_key) {
|
|
114
|
+
let keypair = omq_proto::CurveKeypair {
|
|
115
|
+
public: omq_proto::CurvePublicKey::from_bytes(to_32(
|
|
116
|
+
ruby,
|
|
117
|
+
&pk,
|
|
118
|
+
"public key",
|
|
119
|
+
)?),
|
|
120
|
+
secret: omq_proto::CurveSecretKey::from_bytes(to_32(
|
|
121
|
+
ruby,
|
|
122
|
+
&sk,
|
|
123
|
+
"secret key",
|
|
124
|
+
)?),
|
|
125
|
+
};
|
|
126
|
+
opts.mechanism = omq_proto::MechanismSetup::CurveClient {
|
|
127
|
+
our_keypair: keypair,
|
|
128
|
+
server_public: omq_proto::CurvePublicKey::from_bytes(to_32(
|
|
129
|
+
ruby,
|
|
130
|
+
&svk,
|
|
131
|
+
"server key",
|
|
132
|
+
)?),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_ => {}
|
|
139
|
+
}
|
|
140
|
+
Ok(())
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn to_32(ruby: &Ruby, bytes: &[u8], label: &str) -> Result<[u8; 32], Error> {
|
|
144
|
+
bytes.try_into().map_err(|_| {
|
|
145
|
+
Error::new(
|
|
146
|
+
ruby.exception_arg_error(),
|
|
147
|
+
format!("{label} must be exactly 32 bytes, got {}", bytes.len()),
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fn get_opt_bytes(ruby: &Ruby, hash: RHash, key: &str) -> Result<Option<Vec<u8>>, Error> {
|
|
153
|
+
let k = ruby.str_new(key);
|
|
154
|
+
match hash.get(k) {
|
|
155
|
+
Some(v) => {
|
|
156
|
+
if v.is_nil() {
|
|
157
|
+
return Ok(None);
|
|
158
|
+
}
|
|
159
|
+
let s = magnus::r_string::RString::try_convert(v)?;
|
|
160
|
+
Ok(Some(unsafe { s.as_slice() }.to_vec()))
|
|
161
|
+
}
|
|
162
|
+
None => Ok(None),
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fn get_opt<T: TryConvert>(ruby: &Ruby, hash: RHash, key: &str) -> Result<Option<T>, Error> {
|
|
167
|
+
let k = ruby.str_new(key);
|
|
168
|
+
match hash.get(k) {
|
|
169
|
+
Some(v) => {
|
|
170
|
+
if v.is_nil() {
|
|
171
|
+
Ok(None)
|
|
172
|
+
} else {
|
|
173
|
+
Ok(Some(T::try_convert(v)?))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
None => Ok(None),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn get_opt_duration(ruby: &Ruby, hash: RHash, key: &str) -> Result<Option<Duration>, Error> {
|
|
181
|
+
match get_opt::<f64>(ruby, hash, key)? {
|
|
182
|
+
Some(v) => Ok(Some(Duration::from_secs_f64(v))),
|
|
183
|
+
None => Ok(None),
|
|
184
|
+
}
|
|
185
|
+
}
|