prosody 0.5.1 → 0.6.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.config/rail.toml +26 -0
  3. data/.release-please-manifest.json +1 -1
  4. data/.ruby-version +1 -1
  5. data/.taplo.toml +1 -1
  6. data/AGENTS.md +29 -15
  7. data/CHANGELOG.md +7 -0
  8. data/CONFIGURATION.md +19 -16
  9. data/Cargo.lock +254 -235
  10. data/Cargo.toml +11 -8
  11. data/README.md +100 -23
  12. data/examples/keyed_state.rb +10 -2
  13. data/examples/keyed_state.rbs +1 -0
  14. data/ext/prosody/Cargo.toml +1 -1
  15. data/ext/prosody/src/admin.rs +36 -36
  16. data/ext/prosody/src/bridge/mod.rs +5 -10
  17. data/ext/prosody/src/client/config/connections.rs +170 -0
  18. data/ext/prosody/src/client/config/middleware.rs +184 -0
  19. data/ext/prosody/src/client/config/mod.rs +396 -0
  20. data/ext/prosody/src/client/config/state.rs +323 -0
  21. data/ext/prosody/src/client/mod.rs +31 -117
  22. data/ext/prosody/src/client/readers.rs +106 -0
  23. data/ext/prosody/src/client/request.rs +2 -2
  24. data/ext/prosody/src/client/support.rs +13 -32
  25. data/ext/prosody/src/gvl.rs +8 -6
  26. data/ext/prosody/src/handler/{context.rs → context/mod.rs} +36 -150
  27. data/ext/prosody/src/handler/context/vending.rs +138 -0
  28. data/ext/prosody/src/handler/message.rs +36 -0
  29. data/ext/prosody/src/handler/mod.rs +18 -8
  30. data/ext/prosody/src/handler/state/deque.rs +144 -0
  31. data/ext/prosody/src/handler/state/mod.rs +163 -268
  32. data/ext/prosody/src/handler/state/query.rs +264 -0
  33. data/ext/prosody/src/handler/state/registration.rs +15 -86
  34. data/ext/prosody/src/handler/state/scan.rs +33 -141
  35. data/ext/prosody/src/handler/state/set.rs +98 -0
  36. data/ext/prosody/src/lib.rs +54 -36
  37. data/ext/prosody/src/logging.rs +6 -6
  38. data/ext/prosody/src/published.rs +171 -152
  39. data/ext/prosody/src/scheduler/result.rs +2 -1
  40. data/ext/prosody/src/util.rs +65 -3
  41. data/lib/prosody/client.rb +32 -0
  42. data/lib/prosody/configuration.rb +20 -12
  43. data/lib/prosody/demand.rb +27 -0
  44. data/lib/prosody/native_stubs/client.rb +157 -0
  45. data/lib/prosody/native_stubs/context.rb +178 -0
  46. data/lib/prosody/native_stubs/message.rb +133 -0
  47. data/lib/prosody/native_stubs.rb +11 -956
  48. data/lib/prosody/state/deque.rb +253 -0
  49. data/lib/prosody/state/map.rb +313 -0
  50. data/lib/prosody/state/set.rb +121 -0
  51. data/lib/prosody/state/value.rb +58 -0
  52. data/lib/prosody/state.rb +157 -677
  53. data/lib/prosody/version.rb +1 -1
  54. data/lib/prosody.rb +2 -1
  55. data/sig/configuration.rbs +19 -10
  56. data/sig/prosody.rbs +37 -4
  57. data/sig/published.rbs +102 -0
  58. data/sig/state.rbs +109 -122
  59. data/typecheck/payload_types.rb +12 -0
  60. data/typecheck/payload_types.rbs +1 -0
  61. metadata +30 -11
  62. data/ext/prosody/src/client/config.rs +0 -1300
@@ -22,7 +22,7 @@ struct NativeExciseRequest {
22
22
  }
23
23
 
24
24
  pub(super) fn request(ruby: &Ruby, this: &Client, request: Value) -> Result<Value, Error> {
25
- Client::check_fork(ruby, this)?;
25
+ this.fork.check(ruby)?;
26
26
  let _guard = ensure_runtime_context(ruby);
27
27
  let request: NativeRequest = deserialize(ruby, request)?;
28
28
  let (subsystems, timeout) = request_parameters(ruby, request.subsystems, request.timeout)?;
@@ -57,7 +57,7 @@ pub(super) fn request(ruby: &Ruby, this: &Client, request: Value) -> Result<Valu
57
57
  }
58
58
 
59
59
  pub(super) fn request_excise(ruby: &Ruby, this: &Client, request: Value) -> Result<Value, Error> {
60
- Client::check_fork(ruby, this)?;
60
+ this.fork.check(ruby)?;
61
61
  let _guard = ensure_runtime_context(ruby);
62
62
  let request: NativeExciseRequest = deserialize(ruby, request)?;
63
63
  let (subsystems, timeout) = request_parameters(ruby, request.subsystems, request.timeout)?;
@@ -1,7 +1,7 @@
1
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,
2
+ Arc, Class, Client, Error, FutureExt, Module, Object, RClass, RModule, ROOT_MOD, ReprValue,
3
+ ResponseError, Ruby, RubyHandler, SharedHighLevelClient, Shutdown, Value, function, id, kwargs,
4
+ method, readers, request,
5
5
  };
6
6
 
7
7
  pub(super) fn validate_handler(ruby: &Ruby, handler: Value) -> Result<(), Error> {
@@ -24,29 +24,6 @@ pub(super) fn shutdown(client: &SharedHighLevelClient<RubyHandler>) -> Shutdown
24
24
  .shared()
25
25
  }
26
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
27
  /// Initializes the client module in Ruby.
51
28
  ///
52
29
  /// Defines the `Prosody::Client` class and its methods, making the client
@@ -77,10 +54,10 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
77
54
  )?;
78
55
  class.define_method(id!(ruby, "subscribe"), method!(Client::subscribe, 1))?;
79
56
  class.define_method(
80
- id!(ruby, "assigned_partitions"),
81
- method!(Client::assigned_partitions, 0),
57
+ id!(ruby, "assigned_partition_count"),
58
+ method!(Client::assigned_partition_count, 0),
82
59
  )?;
83
- class.define_method(id!(ruby, "is_stalled?"), method!(Client::is_stalled, 0))?;
60
+ class.define_method(id!(ruby, "stalled?"), method!(Client::is_stalled, 0))?;
84
61
  class.define_method(id!(ruby, "unsubscribe"), method!(Client::unsubscribe, 0))?;
85
62
  class.define_method(id!(ruby, "shutdown"), method!(Client::shutdown, 0))?;
86
63
  class.define_method(
@@ -89,15 +66,19 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
89
66
  )?;
90
67
  class.define_method(
91
68
  id!(ruby, "published_value"),
92
- method!(Client::published_value, 4),
69
+ method!(readers::published_value, 3),
93
70
  )?;
94
71
  class.define_method(
95
72
  id!(ruby, "published_map"),
96
- method!(Client::published_map, 4),
73
+ method!(readers::published_map, 3),
74
+ )?;
75
+ class.define_method(
76
+ id!(ruby, "published_set"),
77
+ method!(readers::published_set, 3),
97
78
  )?;
98
79
  class.define_method(
99
80
  id!(ruby, "published_deque"),
100
- method!(Client::published_deque, 4),
81
+ method!(readers::published_deque, 3),
101
82
  )?;
102
83
 
103
84
  Ok(())
@@ -54,7 +54,6 @@ use tracing::error;
54
54
  ///
55
55
  /// Adapted from: <https://github.com/temporalio/sdk-ruby/blob/main/temporalio/ext/src/util.rs>,
56
56
  /// plus various examples in the Rust/Ruby ecosystem.
57
- #[allow(unsafe_code)]
58
57
  pub(crate) fn without_gvl<F, R, U>(func: F, unblock: U) -> Result<R, GvlError>
59
58
  where
60
59
  F: FnOnce() -> R,
@@ -76,20 +75,22 @@ where
76
75
  let result = catch_unwind(AssertUnwindSafe(|| (*func)())).map_err(|_| GvlError::Panicked);
77
76
 
78
77
  // SAFETY: We box the result, returning a raw pointer. The caller
79
- // will Box::from_raw it exactly once after `rb_thread_call_without_gvl`.
78
+ // will Box::from_raw it exactly once after
79
+ // `rb_thread_call_without_gvl`.
80
80
  Box::into_raw(Box::new(result)).cast::<c_void>()
81
81
  }
82
82
 
83
83
  // SAFETY: Another FFI callback. Ruby may call this multiple times, so we do
84
84
  // NOT call `Box::from_raw` here. Instead, we just dereference the pointer
85
- // each time. We also catch any panic and abort to avoid unwinding across FFI.
85
+ // each time. We also catch any panic and abort to avoid unwinding across
86
+ // FFI.
86
87
  unsafe extern "C" fn anon_unblock<U>(data: *mut c_void)
87
88
  where
88
89
  U: FnMut() + Send,
89
90
  {
90
91
  // We take a pointer to `U`; we do NOT consume the Box here.
91
- // Note that `&mut *ptr` is not automatically UnwindSafe, so we wrap the call
92
- // in `AssertUnwindSafe`.
92
+ // Note that `&mut *ptr` is not automatically UnwindSafe, so we wrap the
93
+ // call in `AssertUnwindSafe`.
93
94
  let closure_ptr = data.cast::<U>();
94
95
 
95
96
  if catch_unwind(AssertUnwindSafe(|| {
@@ -117,7 +118,8 @@ where
117
118
  // - Call `anon_func` once, passing `func_ptr`.
118
119
  // - Potentially call `anon_unblock` multiple times with `unblock_ptr`.
119
120
  // After `rb_thread_call_without_gvl` returns, we are guaranteed that Ruby
120
- // won't call `anon_unblock` again, so we can safely free the unblock closure.
121
+ // won't call `anon_unblock` again, so we can safely free the unblock
122
+ // closure.
121
123
  let raw_result = unsafe {
122
124
  rb_thread_call_without_gvl(
123
125
  Some(anon_func::<F, R>),
@@ -3,18 +3,16 @@
3
3
  //! This module provides a Ruby-compatible wrapper around the Prosody library's
4
4
  //! `MessageContext`, allowing Ruby code to interact with message context
5
5
  //! information from Kafka messages and schedule timer events.
6
+ //! The `vending` submodule owns the keyed-state vend methods.
6
7
 
7
8
  use crate::bridge::Bridge;
8
- use crate::handler::state::{
9
- NativeJsonDequeState, NativeJsonMapState, NativeJsonValueState, NativeMessageDequeState,
10
- NativeMessageMapState, NativeMessageValueState, state_error,
11
- };
12
9
  use crate::tracing_util::extract_opentelemetry_context;
13
10
  use crate::{ROOT_MOD, id};
14
11
  use educe::Educe;
15
12
  use magnus::value::ReprValue;
16
13
  use magnus::{Error, Module, RClass, Ruby, Value, method};
17
14
  use opentelemetry::propagation::TextMapCompositePropagator;
15
+ use prosody::consumer::DemandType;
18
16
  use prosody::consumer::event_context::BoxEventContext;
19
17
  use prosody::timers::TimerType;
20
18
  use prosody::timers::datetime::CompactDateTime;
@@ -22,6 +20,8 @@ use std::sync::Arc;
22
20
  use tracing::{Span, debug, info_span};
23
21
  use tracing_opentelemetry::OpenTelemetrySpanExt;
24
22
 
23
+ mod vending;
24
+
25
25
  /// Nanosecond threshold for rounding Ruby Time objects to the nearest second.
26
26
  /// At 0.5 seconds, times round up; below 0.5 seconds, they round down.
27
27
  const NANOSECOND_ROUNDING_THRESHOLD: u32 = 500_000_000;
@@ -49,6 +49,9 @@ pub struct Context {
49
49
  /// OpenTelemetry propagator for distributed tracing
50
50
  #[educe(Debug(ignore))]
51
51
  propagator: Arc<TextMapCompositePropagator>,
52
+
53
+ /// Whether this dispatch is a normal delivery or a retry after a failure.
54
+ demand: DemandType,
52
55
  }
53
56
 
54
57
  impl Context {
@@ -59,18 +62,41 @@ impl Context {
59
62
  /// * `inner` - The Prosody event context to wrap
60
63
  /// * `bridge` - The bridge for handling async operations
61
64
  /// * `propagator` - Shared OpenTelemetry propagator for distributed tracing
65
+ /// * `demand` - The demand this dispatch serves
62
66
  pub fn new(
63
67
  inner: BoxEventContext<serde_json::Value>,
64
68
  bridge: Bridge,
65
69
  propagator: Arc<TextMapCompositePropagator>,
70
+ demand: DemandType,
66
71
  ) -> Self {
67
72
  Self {
68
73
  inner,
69
74
  bridge,
70
75
  propagator,
76
+ demand,
71
77
  }
72
78
  }
73
79
 
80
+ /// Returns the dispatch demand as a `Prosody::Demand`.
81
+ ///
82
+ /// A normal delivery has kind `:normal` and 0 retries. A retry after a
83
+ /// failure has kind `:failure` and a retry count that starts at 1. The
84
+ /// count is an estimate.
85
+ ///
86
+ /// # Errors
87
+ ///
88
+ /// Returns a Ruby error if `Prosody::Demand` cannot be built.
89
+ fn demand(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
90
+ let (kind, retries) = match this.demand {
91
+ DemandType::Normal => ("normal", 0),
92
+ DemandType::Failure { retry: retries } => ("failure", retries),
93
+ };
94
+ let class: RClass = ruby.get_inner(&ROOT_MOD).const_get(id!(ruby, "Demand"))?;
95
+ // `Data.new` maps positional members to keywords; `new_instance` would
96
+ // bypass it and call `initialize` with positional arguments.
97
+ class.funcall(id!(ruby, "new"), (ruby.sym_new(kind), retries))
98
+ }
99
+
74
100
  /// Check if cancellation has been requested.
75
101
  ///
76
102
  /// Cancellation includes both message-level cancellation (e.g., timeout)
@@ -312,8 +338,8 @@ impl Context {
312
338
  )
313
339
  })?;
314
340
 
315
- // Convert CompactDateTime objects to Ruby Time objects using idiomatic iterator
316
- // pattern
341
+ // Convert CompactDateTime objects to Ruby Time objects using idiomatic
342
+ // iterator pattern
317
343
  let ruby_array = ruby.ary_try_from_iter(
318
344
  scheduled_times
319
345
  .into_iter()
@@ -322,131 +348,6 @@ impl Context {
322
348
 
323
349
  Ok(ruby_array.as_value())
324
350
  }
325
-
326
- /// Vends the handle for the named JSON value collection.
327
- ///
328
- /// Vending verifies the collection's registration (core-side); no span is
329
- /// opened here — vended handles outlive the call, and every operation opens
330
- /// its own span.
331
- ///
332
- /// # Errors
333
- ///
334
- /// Returns a permanent state error if the name is unregistered or its
335
- /// registered identity mismatches.
336
- #[allow(clippy::needless_pass_by_value, reason = "Magnus method argument type")]
337
- fn value_state(ruby: &Ruby, this: &Self, name: String) -> Result<NativeJsonValueState, Error> {
338
- let handle = this
339
- .inner
340
- .value_state(&name)
341
- .map_err(|error| state_error(ruby, &error))?;
342
- Ok(NativeJsonValueState::new(
343
- Arc::from(handle),
344
- this.bridge.clone(),
345
- Arc::clone(&this.propagator),
346
- ))
347
- }
348
-
349
- /// Vends the handle for the named JSON map collection.
350
- ///
351
- /// # Errors
352
- ///
353
- /// See [`value_state`](Self::value_state).
354
- #[allow(clippy::needless_pass_by_value, reason = "Magnus method argument type")]
355
- fn map_state(ruby: &Ruby, this: &Self, name: String) -> Result<NativeJsonMapState, Error> {
356
- let handle = this
357
- .inner
358
- .map_state(&name)
359
- .map_err(|error| state_error(ruby, &error))?;
360
- Ok(NativeJsonMapState::new(
361
- Arc::from(handle),
362
- this.bridge.clone(),
363
- Arc::clone(&this.propagator),
364
- ))
365
- }
366
-
367
- /// Vends the handle for the named JSON deque collection.
368
- ///
369
- /// # Errors
370
- ///
371
- /// See [`value_state`](Self::value_state).
372
- #[allow(clippy::needless_pass_by_value, reason = "Magnus method argument type")]
373
- fn deque_state(ruby: &Ruby, this: &Self, name: String) -> Result<NativeJsonDequeState, Error> {
374
- let handle = this
375
- .inner
376
- .deque_state(&name)
377
- .map_err(|error| state_error(ruby, &error))?;
378
- Ok(NativeJsonDequeState::new(
379
- Arc::from(handle),
380
- this.bridge.clone(),
381
- Arc::clone(&this.propagator),
382
- ))
383
- }
384
-
385
- /// Vends the handle for the named Kafka-message value collection.
386
- ///
387
- /// # Errors
388
- ///
389
- /// See [`value_state`](Self::value_state).
390
- #[allow(clippy::needless_pass_by_value, reason = "Magnus method argument type")]
391
- fn message_value_state(
392
- ruby: &Ruby,
393
- this: &Self,
394
- name: String,
395
- ) -> Result<NativeMessageValueState, Error> {
396
- let handle = this
397
- .inner
398
- .message_value_state(&name)
399
- .map_err(|error| state_error(ruby, &error))?;
400
- Ok(NativeMessageValueState::new(
401
- Arc::from(handle),
402
- this.bridge.clone(),
403
- Arc::clone(&this.propagator),
404
- ))
405
- }
406
-
407
- /// Vends the handle for the named Kafka-message map collection.
408
- ///
409
- /// # Errors
410
- ///
411
- /// See [`value_state`](Self::value_state).
412
- #[allow(clippy::needless_pass_by_value, reason = "Magnus method argument type")]
413
- fn message_map_state(
414
- ruby: &Ruby,
415
- this: &Self,
416
- name: String,
417
- ) -> Result<NativeMessageMapState, Error> {
418
- let handle = this
419
- .inner
420
- .message_map_state(&name)
421
- .map_err(|error| state_error(ruby, &error))?;
422
- Ok(NativeMessageMapState::new(
423
- Arc::from(handle),
424
- this.bridge.clone(),
425
- Arc::clone(&this.propagator),
426
- ))
427
- }
428
-
429
- /// Vends the handle for the named Kafka-message deque collection.
430
- ///
431
- /// # Errors
432
- ///
433
- /// See [`value_state`](Self::value_state).
434
- #[allow(clippy::needless_pass_by_value, reason = "Magnus method argument type")]
435
- fn message_deque_state(
436
- ruby: &Ruby,
437
- this: &Self,
438
- name: String,
439
- ) -> Result<NativeMessageDequeState, Error> {
440
- let handle = this
441
- .inner
442
- .message_deque_state(&name)
443
- .map_err(|error| state_error(ruby, &error))?;
444
- Ok(NativeMessageDequeState::new(
445
- Arc::from(handle),
446
- this.bridge.clone(),
447
- Arc::clone(&this.propagator),
448
- ))
449
- }
450
351
  }
451
352
 
452
353
  /// Initializes the Context class in Ruby.
@@ -471,6 +372,7 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
471
372
  method!(Context::should_cancel, 0),
472
373
  )?;
473
374
  class.define_method(id!(ruby, "on_cancel"), method!(Context::on_cancel, 0))?;
375
+ class.define_method(id!(ruby, "demand"), method!(Context::demand, 0))?;
474
376
 
475
377
  // Timer scheduling methods
476
378
  class.define_method(id!(ruby, "schedule"), method!(Context::schedule, 1))?;
@@ -485,24 +387,7 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
485
387
  )?;
486
388
  class.define_method(id!(ruby, "scheduled"), method!(Context::scheduled, 0))?;
487
389
 
488
- // Keyed-state vend methods
489
- class.define_method(id!(ruby, "value_state"), method!(Context::value_state, 1))?;
490
- class.define_method(id!(ruby, "map_state"), method!(Context::map_state, 1))?;
491
- class.define_method(id!(ruby, "deque_state"), method!(Context::deque_state, 1))?;
492
- class.define_method(
493
- id!(ruby, "message_value_state"),
494
- method!(Context::message_value_state, 1),
495
- )?;
496
- class.define_method(
497
- id!(ruby, "message_map_state"),
498
- method!(Context::message_map_state, 1),
499
- )?;
500
- class.define_method(
501
- id!(ruby, "message_deque_state"),
502
- method!(Context::message_deque_state, 1),
503
- )?;
504
-
505
- Ok(())
390
+ vending::define_methods(ruby, class)
506
391
  }
507
392
 
508
393
  /// Converts a Ruby Time object to `CompactDateTime`.
@@ -571,7 +456,8 @@ fn time_to_compact_datetime(ruby: &Ruby, ruby_time: Value) -> Result<CompactDate
571
456
  /// Returns an error if the Ruby Time class cannot be accessed or if
572
457
  /// creating the Time object fails.
573
458
  fn compact_datetime_to_time(ruby: &Ruby, compact_time: CompactDateTime) -> Result<Value, Error> {
574
- // Direct access to epoch seconds - CompactDateTime has no sub-second precision
459
+ // Direct access to epoch seconds - CompactDateTime has no sub-second
460
+ // precision
575
461
  let epoch_seconds = i64::from(compact_time.epoch_seconds());
576
462
 
577
463
  // Create Ruby Time with zero nanoseconds (CompactDateTime precision limit)
@@ -0,0 +1,138 @@
1
+ //! Keyed-state vending for `Prosody::Context`.
2
+ //!
3
+ //! Each vend method opens the handle for one registered collection in the
4
+ //! current event and wraps it in its native handle class. Vending verifies the
5
+ //! collection's registration in core. It opens no span: vended handles outlive
6
+ //! the call, and every handle operation opens its own span.
7
+
8
+ use super::Context;
9
+ use crate::bridge::Bridge;
10
+ use crate::handler::state::{
11
+ NativeJsonDequeState, NativeJsonMapState, NativeJsonValueState, NativeMessageDequeState,
12
+ NativeMessageMapState, NativeMessageValueState, NativeSetState, state_error,
13
+ };
14
+ use crate::id;
15
+ use magnus::{Error, Module, RClass, RString, Ruby, method};
16
+ use opentelemetry::propagation::TextMapCompositePropagator;
17
+ use prosody::consumer::event_context::ErasedStateError;
18
+ use std::sync::Arc;
19
+
20
+ /// Wraps a core handle in its native handle class.
21
+ ///
22
+ /// # Errors
23
+ ///
24
+ /// Returns a permanent state error if the name is unregistered or its
25
+ /// registered identity mismatches.
26
+ fn vend<T: ?Sized, N>(
27
+ ruby: &Ruby,
28
+ this: &Context,
29
+ handle: Result<Box<T>, ErasedStateError>,
30
+ new: fn(Arc<T>, Bridge, Arc<TextMapCompositePropagator>) -> N,
31
+ ) -> Result<N, Error> {
32
+ let handle = handle.map_err(|error| state_error(ruby, &error))?;
33
+ Ok(new(
34
+ Arc::from(handle),
35
+ this.bridge.clone(),
36
+ Arc::clone(&this.propagator),
37
+ ))
38
+ }
39
+
40
+ fn value_state(ruby: &Ruby, this: &Context, name: RString) -> Result<NativeJsonValueState, Error> {
41
+ vend(
42
+ ruby,
43
+ this,
44
+ this.inner.value_state(&name.to_string()?),
45
+ NativeJsonValueState::new,
46
+ )
47
+ }
48
+
49
+ fn map_state(ruby: &Ruby, this: &Context, name: RString) -> Result<NativeJsonMapState, Error> {
50
+ vend(
51
+ ruby,
52
+ this,
53
+ this.inner.map_state(&name.to_string()?),
54
+ NativeJsonMapState::new,
55
+ )
56
+ }
57
+
58
+ fn set_state(ruby: &Ruby, this: &Context, name: RString) -> Result<NativeSetState, Error> {
59
+ vend(
60
+ ruby,
61
+ this,
62
+ this.inner.set_state(&name.to_string()?),
63
+ NativeSetState::new,
64
+ )
65
+ }
66
+
67
+ fn deque_state(ruby: &Ruby, this: &Context, name: RString) -> Result<NativeJsonDequeState, Error> {
68
+ vend(
69
+ ruby,
70
+ this,
71
+ this.inner.deque_state(&name.to_string()?),
72
+ NativeJsonDequeState::new,
73
+ )
74
+ }
75
+
76
+ fn message_value_state(
77
+ ruby: &Ruby,
78
+ this: &Context,
79
+ name: RString,
80
+ ) -> Result<NativeMessageValueState, Error> {
81
+ vend(
82
+ ruby,
83
+ this,
84
+ this.inner.message_value_state(&name.to_string()?),
85
+ NativeMessageValueState::new,
86
+ )
87
+ }
88
+
89
+ fn message_map_state(
90
+ ruby: &Ruby,
91
+ this: &Context,
92
+ name: RString,
93
+ ) -> Result<NativeMessageMapState, Error> {
94
+ vend(
95
+ ruby,
96
+ this,
97
+ this.inner.message_map_state(&name.to_string()?),
98
+ NativeMessageMapState::new,
99
+ )
100
+ }
101
+
102
+ fn message_deque_state(
103
+ ruby: &Ruby,
104
+ this: &Context,
105
+ name: RString,
106
+ ) -> Result<NativeMessageDequeState, Error> {
107
+ vend(
108
+ ruby,
109
+ this,
110
+ this.inner.message_deque_state(&name.to_string()?),
111
+ NativeMessageDequeState::new,
112
+ )
113
+ }
114
+
115
+ /// Registers the keyed-state vend methods on the `Prosody::Context` class.
116
+ ///
117
+ /// # Errors
118
+ ///
119
+ /// Returns a Magnus error if a method definition fails.
120
+ pub(super) fn define_methods(ruby: &Ruby, class: RClass) -> Result<(), Error> {
121
+ class.define_method(id!(ruby, "value_state"), method!(value_state, 1))?;
122
+ class.define_method(id!(ruby, "map_state"), method!(map_state, 1))?;
123
+ class.define_method(id!(ruby, "set_state"), method!(set_state, 1))?;
124
+ class.define_method(id!(ruby, "deque_state"), method!(deque_state, 1))?;
125
+ class.define_method(
126
+ id!(ruby, "message_value_state"),
127
+ method!(message_value_state, 1),
128
+ )?;
129
+ class.define_method(
130
+ id!(ruby, "message_map_state"),
131
+ method!(message_map_state, 1),
132
+ )?;
133
+ class.define_method(
134
+ id!(ruby, "message_deque_state"),
135
+ method!(message_deque_state, 1),
136
+ )?;
137
+ Ok(())
138
+ }
@@ -7,6 +7,7 @@ use crate::{ROOT_MOD, id};
7
7
  use educe::Educe;
8
8
  use magnus::value::ReprValue;
9
9
  use magnus::{Error, Module, RClass, Ruby, Value, method};
10
+ use prosody::SourceSystem;
10
11
  use prosody::consumer::Keyed;
11
12
  use prosody::consumer::message::ConsumerMessage;
12
13
  use serde_magnus::serialize;
@@ -118,6 +119,17 @@ impl Message {
118
119
  serialize(ruby, this.inner.payload())
119
120
  }
120
121
 
122
+ /// Returns the producer's source system, or `None` when the record has
123
+ /// no source system header.
124
+ fn source_system(&self) -> Option<&str> {
125
+ self.inner.source_system().map(SourceSystem::as_str)
126
+ }
127
+
128
+ /// Reports whether the producer requested a response to this record.
129
+ fn response_requested(&self) -> bool {
130
+ self.inner.response_requested()
131
+ }
132
+
121
133
  /// Clones the wrapped `ConsumerMessage` for a message-collection write.
122
134
  ///
123
135
  /// The wrapper holds the real `ConsumerMessage`, so a message write clones
@@ -157,6 +169,14 @@ impl ExciseMessage {
157
169
  (epoch_micros, ruby.to_symbol("microsecond")),
158
170
  )
159
171
  }
172
+
173
+ fn source_system(&self) -> Option<&str> {
174
+ self.inner.source_system().map(SourceSystem::as_str)
175
+ }
176
+
177
+ fn response_requested(&self) -> bool {
178
+ self.inner.response_requested()
179
+ }
160
180
  }
161
181
 
162
182
  impl From<ConsumerMessage<serde_json::Value>> for Message {
@@ -199,6 +219,14 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
199
219
  class.define_method(id!(ruby, "key"), method!(Message::key, 0))?;
200
220
  class.define_method(id!(ruby, "timestamp"), method!(Message::timestamp, 0))?;
201
221
  class.define_method(id!(ruby, "payload"), method!(Message::payload, 0))?;
222
+ class.define_method(
223
+ id!(ruby, "source_system"),
224
+ method!(Message::source_system, 0),
225
+ )?;
226
+ class.define_method(
227
+ id!(ruby, "response_requested?"),
228
+ method!(Message::response_requested, 0),
229
+ )?;
202
230
 
203
231
  let class = module.define_class(id!(ruby, "ExciseMessage"), ruby.class_object())?;
204
232
  class.define_method(id!(ruby, "topic"), method!(ExciseMessage::topic, 0))?;
@@ -206,6 +234,14 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
206
234
  class.define_method(id!(ruby, "offset"), method!(ExciseMessage::offset, 0))?;
207
235
  class.define_method(id!(ruby, "key"), method!(ExciseMessage::key, 0))?;
208
236
  class.define_method(id!(ruby, "timestamp"), method!(ExciseMessage::timestamp, 0))?;
237
+ class.define_method(
238
+ id!(ruby, "source_system"),
239
+ method!(ExciseMessage::source_system, 0),
240
+ )?;
241
+ class.define_method(
242
+ id!(ruby, "response_requested?"),
243
+ method!(ExciseMessage::response_requested, 0),
244
+ )?;
209
245
 
210
246
  Ok(())
211
247
  }