prosody 0.3.0 → 0.5.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.cargo/config.toml +3 -0
  3. data/.release-please-manifest.json +1 -1
  4. data/AGENTS.md +395 -0
  5. data/ARCHITECTURE.md +14 -4
  6. data/CHANGELOG.md +28 -0
  7. data/CLAUDE.md +1 -0
  8. data/CONFIGURATION.md +167 -0
  9. data/Cargo.lock +1115 -645
  10. data/Cargo.toml +7 -6
  11. data/README.md +436 -146
  12. data/Rakefile +11 -1
  13. data/examples/keyed_state.rb +70 -0
  14. data/examples/keyed_state.rbs +18 -0
  15. data/examples/keyed_state_windowing.rb +55 -0
  16. data/examples/keyed_state_windowing.rbs +16 -0
  17. data/ext/prosody/Cargo.toml +1 -0
  18. data/ext/prosody/src/admin.rs +1 -5
  19. data/ext/prosody/src/bridge/mod.rs +17 -32
  20. data/ext/prosody/src/client/config.rs +501 -28
  21. data/ext/prosody/src/client/mod.rs +167 -74
  22. data/ext/prosody/src/client/request.rs +132 -0
  23. data/ext/prosody/src/client/support.rs +122 -0
  24. data/ext/prosody/src/handler/context.rs +150 -5
  25. data/ext/prosody/src/handler/message.rs +67 -0
  26. data/ext/prosody/src/handler/mod.rs +115 -85
  27. data/ext/prosody/src/handler/state/mod.rs +488 -0
  28. data/ext/prosody/src/handler/state/registration.rs +104 -0
  29. data/ext/prosody/src/handler/state/scan.rs +218 -0
  30. data/ext/prosody/src/lib.rs +15 -3
  31. data/ext/prosody/src/published.rs +273 -0
  32. data/ext/prosody/src/scheduler/mod.rs +2 -2
  33. data/ext/prosody/src/scheduler/processor.rs +2 -2
  34. data/ext/prosody/src/scheduler/result.rs +7 -4
  35. data/ext/prosody/src/util.rs +86 -5
  36. data/lib/prosody/configuration.rb +71 -11
  37. data/lib/prosody/handler.rb +65 -8
  38. data/lib/prosody/native_stubs.rb +550 -9
  39. data/lib/prosody/request.rb +45 -0
  40. data/lib/prosody/state.rb +816 -0
  41. data/lib/prosody/version.rb +1 -1
  42. data/lib/prosody.rb +6 -0
  43. data/release-please-config.json +4 -0
  44. data/sig/configuration.rbs +70 -11
  45. data/sig/handler.rbs +17 -5
  46. data/sig/processor.rbs +28 -12
  47. data/sig/prosody.rbs +53 -7
  48. data/sig/request.rbs +66 -0
  49. data/sig/sentry.rbs +6 -0
  50. data/sig/state.rbs +390 -0
  51. data/steep_expectations.yml +57 -0
  52. data/typecheck/payload_types.rb +54 -0
  53. data/typecheck/payload_types.rbs +22 -0
  54. data/typecheck_negative/payload_types.rb +20 -0
  55. data/typecheck_negative/payload_types.rbs +9 -0
  56. metadata +32 -9
@@ -13,25 +13,45 @@
13
13
  use crate::bridge::Bridge;
14
14
  use crate::client::config::NativeConfiguration;
15
15
  use crate::handler::RubyHandler;
16
+ use crate::published::{NativePublishedDeque, NativePublishedMap, NativePublishedValue};
16
17
  use crate::tracing_util::extract_opentelemetry_context;
17
18
  use crate::util::ensure_runtime_context;
18
19
  use crate::{BRIDGE, ROOT_MOD, id};
19
20
  use educe::Educe;
21
+ use futures::FutureExt;
22
+ use futures::future::{BoxFuture, Shared};
20
23
  use magnus::value::ReprValue;
21
- use magnus::{Error, Module, Object, RClass, Ruby, StaticSymbol, Value, function, method};
24
+ use magnus::{
25
+ Class, Error, Module, Object, RClass, RModule, Ruby, StaticSymbol, Value, function, kwargs,
26
+ method,
27
+ };
22
28
  use opentelemetry::propagation::TextMapCompositePropagator;
29
+ use prosody::cassandra::config::CassandraConfigurationBuilder;
23
30
  use prosody::high_level::ConsumerBuilders;
24
- use prosody::high_level::HighLevelClient;
31
+ use prosody::high_level::erased::{
32
+ ErasedConsumerState, ErasedReadCache, SharedHighLevelClient, new_erased,
33
+ };
25
34
  use prosody::high_level::mode::Mode;
26
- use prosody::high_level::state::ConsumerState;
27
35
  use prosody::propagator::new_propagator;
36
+ use prosody::requester::ResponseError;
37
+ use prosody::subsystem::SubsystemName;
38
+ use serde::Deserialize;
28
39
  use serde_magnus::deserialize;
40
+ use serde_magnus::serialize;
29
41
  use std::sync::Arc;
42
+ use std::time::Duration;
30
43
  use tracing::{Span, debug, info_span};
31
44
  use tracing_opentelemetry::OpenTelemetrySpanExt;
32
45
 
33
46
  /// Configuration types and conversion between Ruby and Rust representations
34
47
  mod config;
48
+ mod request;
49
+ mod support;
50
+
51
+ pub use support::init;
52
+ use support::{read_cache, response_error, shutdown, validate_handler};
53
+
54
+ type Shutdown = Shared<BoxFuture<'static, Result<(), Arc<str>>>>;
35
55
 
36
56
  /// A Ruby-compatible wrapper around the Prosody high-level client.
37
57
  ///
@@ -44,11 +64,14 @@ mod config;
44
64
  pub struct Client {
45
65
  /// The underlying Prosody client
46
66
  #[educe(Debug(ignore))]
47
- inner: Arc<HighLevelClient<RubyHandler>>,
67
+ inner: SharedHighLevelClient<RubyHandler>,
68
+ /// One shutdown operation shared by all callers
69
+ #[educe(Debug(ignore))]
70
+ shutdown: Shutdown,
48
71
  /// Bridge for communicating between Rust and Ruby
49
72
  bridge: Bridge,
50
73
  /// OpenTelemetry propagator for distributed tracing
51
- propagator: TextMapCompositePropagator,
74
+ propagator: Arc<TextMapCompositePropagator>,
52
75
  /// PID at construction time, used to detect post-fork usage
53
76
  pid: u32,
54
77
  }
@@ -96,14 +119,6 @@ impl Client {
96
119
  .try_into()
97
120
  .map_err(|error: String| Error::new(ruby.exception_arg_error(), error))?;
98
121
 
99
- let client = HighLevelClient::new(
100
- mode,
101
- &mut config_ref.into(),
102
- &consumer_builders,
103
- &config_ref.into(),
104
- )
105
- .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
106
-
107
122
  let bridge = BRIDGE
108
123
  .get()
109
124
  .ok_or(Error::new(
@@ -111,11 +126,23 @@ impl Client {
111
126
  "Bridge not initialized",
112
127
  ))?
113
128
  .clone();
129
+ let cassandra = Into::<CassandraConfigurationBuilder>::into(config_ref);
130
+ let mut producer = config_ref.into();
131
+ let client = bridge
132
+ .wait_for(
133
+ ruby,
134
+ async move {
135
+ new_erased(mode, &mut producer, &consumer_builders, &cassandra).await
136
+ },
137
+ Span::current(),
138
+ )?
139
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
114
140
 
115
141
  Ok(Self {
116
- inner: Arc::new(client),
117
- bridge: bridge.clone(),
118
- propagator: new_propagator(),
142
+ shutdown: shutdown(&client),
143
+ inner: client,
144
+ bridge,
145
+ propagator: Arc::new(new_propagator()),
119
146
  pid: std::process::id(),
120
147
  })
121
148
  }
@@ -133,7 +160,8 @@ impl Client {
133
160
 
134
161
  /// Returns the current state of the consumer.
135
162
  ///
136
- /// The consumer can be in one of three states:
163
+ /// The consumer can be in one of four states:
164
+ /// - `:shut_down` - The client is shut down
137
165
  /// - `:unconfigured` - The consumer has not been configured yet
138
166
  /// - `:configured` - The consumer is configured but not running
139
167
  /// - `:running` - The consumer is actively consuming messages
@@ -158,14 +186,14 @@ impl Client {
158
186
  let state: Result<&'static str, String> = this.bridge.wait_for(
159
187
  ruby,
160
188
  async move {
161
- let view = inner.consumer_state().await;
162
- match &*view {
163
- ConsumerState::Unconfigured => Ok("unconfigured"),
164
- ConsumerState::ConfigurationFailed(err) => {
165
- Err(format!("consumer configuration failed: {err:#}"))
189
+ match inner.consumer_state().await {
190
+ ErasedConsumerState::Shutdown => Ok("shut_down"),
191
+ ErasedConsumerState::Unconfigured => Ok("unconfigured"),
192
+ ErasedConsumerState::ConfigurationFailed(error) => {
193
+ Err(format!("consumer configuration failed: {error}"))
166
194
  }
167
- ConsumerState::Configured(_) => Ok("configured"),
168
- ConsumerState::Running { .. } => Ok("running"),
195
+ ErasedConsumerState::Configured(_) => Ok("configured"),
196
+ ErasedConsumerState::Running { .. } => Ok("running"),
169
197
  }
170
198
  },
171
199
  Span::current(),
@@ -215,30 +243,39 @@ impl Client {
215
243
  this.bridge
216
244
  .wait_for(
217
245
  ruby,
218
- async move { client.send(topic.as_str().into(), &key, value).await },
246
+ async move { client.send(topic.as_str().into(), key, value).await },
219
247
  span,
220
248
  )?
221
249
  .map_err(|error| Error::new(ruby.exception_runtime_error(), format!("{error:#}")))
222
250
  }
223
251
 
224
- /// Subscribes to events using the provided Ruby handler.
225
- ///
226
- /// The handler must implement an `on_message(context, message)` method
227
- /// that will be called for each received message.
228
- ///
229
- /// # Arguments
230
- ///
231
- /// * `ruby` - The Ruby VM context
232
- /// * `this` - The client instance
233
- /// * `handler` - A Ruby object that will handle incoming messages
252
+ /// Sends an excise record for a key.
253
+ fn excise(ruby: &Ruby, this: &Self, topic: String, key: String) -> Result<(), Error> {
254
+ Self::check_fork(ruby, this)?;
255
+ let _guard = ensure_runtime_context(ruby);
256
+ let client = this.inner.clone();
257
+ let context = extract_opentelemetry_context(ruby, &this.propagator)?;
258
+ let span = info_span!("ruby-excise", %topic, %key);
259
+ if let Err(err) = span.set_parent(context) {
260
+ debug!("failed to set parent span: {err:#}");
261
+ }
262
+ this.bridge
263
+ .wait_for(
264
+ ruby,
265
+ async move { client.excise(topic.as_str().into(), key).await },
266
+ span,
267
+ )?
268
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), format!("{error:#}")))
269
+ }
270
+
271
+ /// Subscribes with a complete Ruby event handler.
234
272
  ///
235
273
  /// # Errors
236
274
  ///
237
- /// Returns an error if:
238
- /// - The handler cannot be wrapped
239
- /// - The client cannot subscribe with the handler
275
+ /// Returns an error if the handler is incomplete or subscription fails.
240
276
  fn subscribe(ruby: &Ruby, this: &Self, handler: Value) -> Result<(), Error> {
241
277
  Self::check_fork(ruby, this)?;
278
+ validate_handler(ruby, handler)?;
242
279
  let _guard = ensure_runtime_context(ruby);
243
280
  let wrapper = RubyHandler::new(this.bridge.clone(), ruby, handler)?;
244
281
  let inner = this.inner.clone();
@@ -327,6 +364,22 @@ impl Client {
327
364
  .map_err(|error| Error::new(ruby.exception_runtime_error(), format!("{error:#}")))
328
365
  }
329
366
 
367
+ /// Shuts down the client and all its services.
368
+ /// Concurrent and repeated calls wait for the same operation.
369
+ ///
370
+ /// # Errors
371
+ ///
372
+ /// Returns an error if shutdown fails.
373
+ fn shutdown(ruby: &Ruby, this: &Self) -> Result<(), Error> {
374
+ Self::check_fork(ruby, this)?;
375
+ let _guard = ensure_runtime_context(ruby);
376
+ let shutdown = this.shutdown.clone();
377
+
378
+ this.bridge
379
+ .wait_for(ruby, shutdown, Span::current())?
380
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), format!("{error:#}")))
381
+ }
382
+
330
383
  /// Returns the configured source system identifier.
331
384
  ///
332
385
  /// The source system is used to identify the originating service or
@@ -342,41 +395,81 @@ impl Client {
342
395
  fn source_system(this: &Self) -> &str {
343
396
  this.inner.source_system()
344
397
  }
345
- }
346
398
 
347
- /// Initializes the client module in Ruby.
348
- ///
349
- /// Defines the `Prosody::Client` class and its methods, making the client
350
- /// functionality available to Ruby code.
351
- ///
352
- /// # Arguments
353
- ///
354
- /// * `ruby` - The Ruby VM context
355
- ///
356
- /// # Errors
357
- ///
358
- /// Returns an error if Ruby class or method definition fails.
359
- pub fn init(ruby: &Ruby) -> Result<(), Error> {
360
- let module = ruby.get_inner(&ROOT_MOD);
361
- let class = module.define_class(id!(ruby, "Client"), ruby.class_object())?;
362
-
363
- class.define_singleton_method("new", function!(Client::new, 1))?;
364
- class.define_method(
365
- id!(ruby, "consumer_state"),
366
- method!(Client::consumer_state, 0),
367
- )?;
368
- class.define_method(id!(ruby, "send_message"), method!(Client::send, 3))?;
369
- class.define_method(id!(ruby, "subscribe"), method!(Client::subscribe, 1))?;
370
- class.define_method(
371
- id!(ruby, "assigned_partitions"),
372
- method!(Client::assigned_partitions, 0),
373
- )?;
374
- class.define_method(id!(ruby, "is_stalled?"), method!(Client::is_stalled, 0))?;
375
- class.define_method(id!(ruby, "unsubscribe"), method!(Client::unsubscribe, 0))?;
376
- class.define_method(
377
- id!(ruby, "source_system"),
378
- method!(Client::source_system, 0),
379
- )?;
380
-
381
- Ok(())
399
+ fn published_value(
400
+ ruby: &Ruby,
401
+ this: &Self,
402
+ subsystem: String,
403
+ name: String,
404
+ cache_seconds: Option<f64>,
405
+ cache_disabled: bool,
406
+ ) -> Result<NativePublishedValue, Error> {
407
+ Self::check_fork(ruby, this)?;
408
+ let cache = read_cache(ruby, cache_seconds, cache_disabled)?;
409
+ let inner = this.inner.clone();
410
+ let reader = this
411
+ .bridge
412
+ .wait_for(
413
+ ruby,
414
+ async move { inner.value_state(subsystem, name, cache).await },
415
+ Span::current(),
416
+ )?
417
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
418
+ Ok(NativePublishedValue {
419
+ inner: reader,
420
+ bridge: this.bridge.clone(),
421
+ })
422
+ }
423
+
424
+ fn published_map(
425
+ ruby: &Ruby,
426
+ this: &Self,
427
+ subsystem: String,
428
+ name: String,
429
+ cache_seconds: Option<f64>,
430
+ cache_disabled: bool,
431
+ ) -> Result<NativePublishedMap, Error> {
432
+ Self::check_fork(ruby, this)?;
433
+ let cache = read_cache(ruby, cache_seconds, cache_disabled)?;
434
+ let inner = this.inner.clone();
435
+ let reader = this
436
+ .bridge
437
+ .wait_for(
438
+ ruby,
439
+ async move { inner.map_state(subsystem, name, cache).await },
440
+ Span::current(),
441
+ )?
442
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
443
+ Ok(NativePublishedMap {
444
+ inner: reader,
445
+ bridge: this.bridge.clone(),
446
+ propagator: Arc::clone(&this.propagator),
447
+ })
448
+ }
449
+
450
+ fn published_deque(
451
+ ruby: &Ruby,
452
+ this: &Self,
453
+ subsystem: String,
454
+ name: String,
455
+ cache_seconds: Option<f64>,
456
+ cache_disabled: bool,
457
+ ) -> Result<NativePublishedDeque, Error> {
458
+ Self::check_fork(ruby, this)?;
459
+ let cache = read_cache(ruby, cache_seconds, cache_disabled)?;
460
+ let inner = this.inner.clone();
461
+ let reader = this
462
+ .bridge
463
+ .wait_for(
464
+ ruby,
465
+ async move { inner.deque_state(subsystem, name, cache).await },
466
+ Span::current(),
467
+ )?
468
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
469
+ Ok(NativePublishedDeque {
470
+ inner: reader,
471
+ bridge: this.bridge.clone(),
472
+ propagator: Arc::clone(&this.propagator),
473
+ })
474
+ }
382
475
  }
@@ -0,0 +1,132 @@
1
+ use super::{
2
+ Class, Client, Deserialize, Duration, Error, Module, OpenTelemetrySpanExt, RClass, ROOT_MOD,
3
+ ReprValue, Ruby, SubsystemName, Value, debug, deserialize, ensure_runtime_context,
4
+ extract_opentelemetry_context, id, info_span, kwargs, response_error, serialize,
5
+ };
6
+
7
+ #[derive(Deserialize)]
8
+ struct NativeRequest {
9
+ topic: String,
10
+ key: String,
11
+ payload: serde_json::Value,
12
+ subsystems: Vec<String>,
13
+ timeout: f64,
14
+ }
15
+
16
+ #[derive(Deserialize)]
17
+ struct NativeExciseRequest {
18
+ topic: String,
19
+ key: String,
20
+ subsystems: Vec<String>,
21
+ timeout: f64,
22
+ }
23
+
24
+ pub(super) fn request(ruby: &Ruby, this: &Client, request: Value) -> Result<Value, Error> {
25
+ Client::check_fork(ruby, this)?;
26
+ let _guard = ensure_runtime_context(ruby);
27
+ let request: NativeRequest = deserialize(ruby, request)?;
28
+ let (subsystems, timeout) = request_parameters(ruby, request.subsystems, request.timeout)?;
29
+ let topic = prosody::Topic::from(request.topic.as_str());
30
+ let context = extract_opentelemetry_context(ruby, &this.propagator)?;
31
+ let span = info_span!("ruby-request", topic = %request.topic, key = %request.key);
32
+ if let Err(error) = span.set_parent(context) {
33
+ debug!("failed to set parent span: {error:#}");
34
+ }
35
+ let inner = this.inner.clone();
36
+ let results = this
37
+ .bridge
38
+ .wait_for(
39
+ ruby,
40
+ async move {
41
+ inner
42
+ .request(
43
+ Vec::new(),
44
+ topic,
45
+ request.key,
46
+ request.payload,
47
+ subsystems,
48
+ timeout,
49
+ )
50
+ .await
51
+ },
52
+ span,
53
+ )?
54
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
55
+
56
+ request_outcomes(ruby, results)
57
+ }
58
+
59
+ pub(super) fn request_excise(ruby: &Ruby, this: &Client, request: Value) -> Result<Value, Error> {
60
+ Client::check_fork(ruby, this)?;
61
+ let _guard = ensure_runtime_context(ruby);
62
+ let request: NativeExciseRequest = deserialize(ruby, request)?;
63
+ let (subsystems, timeout) = request_parameters(ruby, request.subsystems, request.timeout)?;
64
+ let topic = prosody::Topic::from(request.topic.as_str());
65
+ let context = extract_opentelemetry_context(ruby, &this.propagator)?;
66
+ let span = info_span!("ruby-request-excise", topic = %request.topic, key = %request.key);
67
+ if let Err(error) = span.set_parent(context) {
68
+ debug!("failed to set parent span: {error:#}");
69
+ }
70
+ let inner = this.inner.clone();
71
+ let results = this
72
+ .bridge
73
+ .wait_for(
74
+ ruby,
75
+ async move {
76
+ inner
77
+ .request_excise(Vec::new(), topic, request.key, subsystems, timeout)
78
+ .await
79
+ },
80
+ span,
81
+ )?
82
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
83
+
84
+ request_outcomes(ruby, results)
85
+ }
86
+
87
+ fn request_parameters(
88
+ ruby: &Ruby,
89
+ subsystems: Vec<String>,
90
+ timeout: f64,
91
+ ) -> Result<(Vec<SubsystemName>, Duration), Error> {
92
+ let subsystems = subsystems
93
+ .into_iter()
94
+ .map(SubsystemName::try_new)
95
+ .collect::<Result<Vec<_>, _>>()
96
+ .map_err(|error| Error::new(ruby.exception_arg_error(), error.to_string()))?;
97
+ let timeout = Duration::try_from_secs_f64(timeout).map_err(|_| {
98
+ Error::new(
99
+ ruby.exception_arg_error(),
100
+ "timeout must be a finite, non-negative duration",
101
+ )
102
+ })?;
103
+ Ok((subsystems, timeout))
104
+ }
105
+
106
+ fn request_outcomes<I>(ruby: &Ruby, results: I) -> Result<Value, Error>
107
+ where
108
+ I: IntoIterator<
109
+ Item = (
110
+ SubsystemName,
111
+ Result<serde_json::Value, prosody::requester::ResponseError>,
112
+ ),
113
+ >,
114
+ {
115
+ let module = ruby.get_inner(&ROOT_MOD);
116
+ let outcomes = ruby.hash_new();
117
+ let success: RClass = module.const_get(id!(ruby, "Success"))?;
118
+ let failure: RClass = module.const_get(id!(ruby, "Failure"))?;
119
+ for (subsystem, result) in results {
120
+ let outcome = match result {
121
+ Ok(value) => {
122
+ let value: Value = serialize(ruby, &value)?;
123
+ success.new_instance((kwargs!(ruby, "value" => value),))?
124
+ }
125
+ Err(error) => failure.new_instance((kwargs!(
126
+ ruby, "error" => response_error(ruby, module, error)?
127
+ ),))?,
128
+ };
129
+ outcomes.aset(subsystem.as_str(), outcome)?;
130
+ }
131
+ Ok(outcomes.as_value())
132
+ }
@@ -0,0 +1,122 @@
1
+ use super::{
2
+ Arc, Class, Client, Duration, ErasedReadCache, Error, FutureExt, Module, Object, RClass,
3
+ RModule, ROOT_MOD, ReprValue, ResponseError, Ruby, RubyHandler, SharedHighLevelClient,
4
+ Shutdown, Value, function, id, kwargs, method, request,
5
+ };
6
+
7
+ pub(super) fn validate_handler(ruby: &Ruby, handler: Value) -> Result<(), Error> {
8
+ let event_handler: RClass = ruby
9
+ .get_inner(&ROOT_MOD)
10
+ .const_get(id!(ruby, "EventHandler"))?;
11
+ let _: Value = event_handler.funcall(id!(ruby, "validate_handler!"), (handler,))?;
12
+ Ok(())
13
+ }
14
+
15
+ pub(super) fn shutdown(client: &SharedHighLevelClient<RubyHandler>) -> Shutdown {
16
+ let client = client.clone();
17
+ async move {
18
+ client
19
+ .shutdown()
20
+ .await
21
+ .map_err(|error| Arc::from(error.to_string()))
22
+ }
23
+ .boxed()
24
+ .shared()
25
+ }
26
+
27
+ pub(super) fn read_cache(
28
+ ruby: &Ruby,
29
+ seconds: Option<f64>,
30
+ disabled: bool,
31
+ ) -> Result<ErasedReadCache, Error> {
32
+ match (seconds, disabled) {
33
+ (None, false) => Ok(ErasedReadCache::Inherit),
34
+ (None, true) => Ok(ErasedReadCache::Disabled),
35
+ (Some(seconds), false) => Duration::try_from_secs_f64(seconds)
36
+ .map(ErasedReadCache::Ttl)
37
+ .map_err(|_| {
38
+ Error::new(
39
+ ruby.exception_arg_error(),
40
+ "read_cache must be finite and non-negative",
41
+ )
42
+ }),
43
+ (Some(_), true) => Err(Error::new(
44
+ ruby.exception_arg_error(),
45
+ "read_cache cannot specify a TTL and be disabled",
46
+ )),
47
+ }
48
+ }
49
+
50
+ /// Initializes the client module in Ruby.
51
+ ///
52
+ /// Defines the `Prosody::Client` class and its methods, making the client
53
+ /// functionality available to Ruby code.
54
+ ///
55
+ /// # Arguments
56
+ ///
57
+ /// * `ruby` - The Ruby VM context
58
+ ///
59
+ /// # Errors
60
+ ///
61
+ /// Returns an error if Ruby class or method definition fails.
62
+ pub fn init(ruby: &Ruby) -> Result<(), Error> {
63
+ let module = ruby.get_inner(&ROOT_MOD);
64
+ let class = module.define_class(id!(ruby, "Client"), ruby.class_object())?;
65
+
66
+ class.define_singleton_method("new", function!(Client::new, 1))?;
67
+ class.define_method(
68
+ id!(ruby, "consumer_state"),
69
+ method!(Client::consumer_state, 0),
70
+ )?;
71
+ class.define_method(id!(ruby, "send_message"), method!(Client::send, 3))?;
72
+ class.define_method(id!(ruby, "excise"), method!(Client::excise, 2))?;
73
+ class.define_method(id!(ruby, "native_request"), method!(request::request, 1))?;
74
+ class.define_method(
75
+ id!(ruby, "native_request_excise"),
76
+ method!(request::request_excise, 1),
77
+ )?;
78
+ class.define_method(id!(ruby, "subscribe"), method!(Client::subscribe, 1))?;
79
+ class.define_method(
80
+ id!(ruby, "assigned_partitions"),
81
+ method!(Client::assigned_partitions, 0),
82
+ )?;
83
+ class.define_method(id!(ruby, "is_stalled?"), method!(Client::is_stalled, 0))?;
84
+ class.define_method(id!(ruby, "unsubscribe"), method!(Client::unsubscribe, 0))?;
85
+ class.define_method(id!(ruby, "shutdown"), method!(Client::shutdown, 0))?;
86
+ class.define_method(
87
+ id!(ruby, "source_system"),
88
+ method!(Client::source_system, 0),
89
+ )?;
90
+ class.define_method(
91
+ id!(ruby, "published_value"),
92
+ method!(Client::published_value, 4),
93
+ )?;
94
+ class.define_method(
95
+ id!(ruby, "published_map"),
96
+ method!(Client::published_map, 4),
97
+ )?;
98
+ class.define_method(
99
+ id!(ruby, "published_deque"),
100
+ method!(Client::published_deque, 4),
101
+ )?;
102
+
103
+ Ok(())
104
+ }
105
+
106
+ pub(super) fn response_error(
107
+ ruby: &Ruby,
108
+ module: RModule,
109
+ error: ResponseError,
110
+ ) -> Result<Value, Error> {
111
+ let (name, message) = match error {
112
+ ResponseError::Handler { message } => ("HandlerError", Some(message)),
113
+ ResponseError::Timeout => ("Timeout", None),
114
+ ResponseError::FormatMismatch => ("FormatMismatch", None),
115
+ ResponseError::Malformed => ("MalformedResponse", None),
116
+ };
117
+ let class: RClass = module.const_get(name)?;
118
+ match message {
119
+ Some(message) => class.new_instance((kwargs!(ruby, "message" => message),)),
120
+ None => class.new_instance(()),
121
+ }
122
+ }