rstest_bdd/context/mod.rs
1//! Step execution context, fixture access, and step return overrides.
2//! `StepContext` stores named fixture references plus a map of last-seen step
3//! results keyed by fixture name. Returned values must be `'static` so they can
4//! be boxed. When exactly one fixture matches a returned type, its name records
5//! the override (last write wins); ambiguous matches leave fixtures untouched.
6
7use std::any::{Any, TypeId};
8use std::cell::{Ref, RefCell, RefMut};
9use std::collections::HashMap;
10
11/// Context passed to step functions containing references to requested fixtures.
12///
13/// This is constructed by the `#[scenario]` macro for each step invocation. Use
14/// [`insert_owned`](Self::insert_owned) when a fixture should be shared
15/// mutably across steps; step functions may then request `&mut T` and mutate
16/// world state without resorting to interior mutability wrappers.
17///
18/// # Examples
19///
20/// ```
21/// use rstest_bdd::StepContext;
22///
23/// let mut ctx = StepContext::default();
24/// let value = 42;
25/// ctx.insert("my_fixture", &value);
26/// let owned = StepContext::owned_cell(String::from("hi"));
27/// ctx.insert_owned::<String>("owned", &owned);
28///
29/// let retrieved: Option<&i32> = ctx.get("my_fixture");
30/// assert_eq!(retrieved, Some(&42));
31/// {
32/// let mut suffix = ctx.borrow_mut::<String>("owned").expect("owned fixture");
33/// suffix.value_mut().push('!');
34/// }
35/// drop(ctx);
36/// let owned = owned.into_inner();
37/// let owned: String = *owned
38/// .downcast::<String>()
39/// .expect("fixture should downcast to String");
40/// assert_eq!(owned, "hi!");
41/// ```
42#[derive(Default)]
43pub struct StepContext<'a> {
44 fixtures: HashMap<&'static str, FixtureEntry<'a>>,
45 values: HashMap<&'static str, Box<dyn Any>>,
46}
47
48struct FixtureEntry<'a> {
49 kind: FixtureKind<'a>,
50 type_id: TypeId,
51}
52
53enum FixtureKind<'a> {
54 Shared(&'a dyn Any),
55 Mutable(&'a RefCell<Box<dyn Any>>),
56}
57
58impl<'a> StepContext<'a> {
59 /// Create an owned fixture cell for use with [`insert_owned`](Self::insert_owned).
60 ///
61 /// This helper boxes the provided value and erases its concrete type so it
62 /// can back a mutable fixture. Callers must retain the returned cell for as
63 /// long as the context references it.
64 #[must_use]
65 pub fn owned_cell<T: Any>(value: T) -> RefCell<Box<dyn Any>> {
66 RefCell::new(Box::new(value))
67 }
68
69 /// Insert a fixture reference by name.
70 pub fn insert<T: Any>(&mut self, name: &'static str, value: &'a T) {
71 self.fixtures.insert(name, FixtureEntry::shared(value));
72 }
73
74 /// Insert a fixture backed by a `RefCell<Box<dyn Any>>`, enabling mutable borrows.
75 ///
76 /// A runtime type check ensures the stored value matches the requested `T`
77 /// so mismatches are surfaced immediately instead of silently failing at
78 /// borrow time.
79 ///
80 /// # Panics
81 ///
82 /// Panics when the provided cell does not currently store a value of type
83 /// `T`, because continuing would render the fixture un-borrowable at run
84 /// time.
85 pub fn insert_owned<T: Any>(&mut self, name: &'static str, cell: &'a RefCell<Box<dyn Any>>) {
86 let guard = cell.borrow();
87 let actual = guard.as_ref().type_id();
88 assert!(
89 actual == TypeId::of::<T>(),
90 "insert_owned: stored value type ({actual:?}) does not match requested {:?} for fixture '{name}'",
91 TypeId::of::<T>()
92 );
93 self.fixtures.insert(name, FixtureEntry::owned::<T>(cell));
94 }
95
96 /// Retrieve a fixture reference by name and type.
97 ///
98 /// Values returned from prior `#[when]` steps override fixtures of the same
99 /// type when that type is unique among fixtures. This enables a functional
100 /// style where step return values feed into later assertions without having
101 /// to define ad-hoc fixtures.
102 #[must_use]
103 pub fn get<T: Any>(&'a self, name: &str) -> Option<&'a T> {
104 if let Some(val) = self.values.get(name) {
105 return val.downcast_ref::<T>();
106 }
107 match self.fixtures.get(name)?.kind {
108 FixtureKind::Shared(value) => value.downcast_ref::<T>(),
109 FixtureKind::Mutable(_) => None,
110 }
111 }
112
113 /// Borrow a fixture by name, keeping the guard alive until dropped.
114 pub fn borrow_ref<'b, T: Any>(&'b self, name: &str) -> Option<FixtureRef<'b, T>>
115 where
116 'a: 'b,
117 {
118 if let Some(val) = self.values.get(name) {
119 return val.downcast_ref::<T>().map(FixtureRef::Shared);
120 }
121 self.fixtures.get(name)?.borrow_ref::<T>()
122 }
123
124 /// Borrow a fixture mutably by name.
125 ///
126 /// # Panics
127 ///
128 /// The underlying fixtures use `RefCell` for interior mutability. Attempting
129 /// to borrow the same fixture mutably while an existing mutable guard is
130 /// alive will panic via `RefCell::borrow_mut`. Callers must drop guards
131 /// before requesting another mutable borrow of the same fixture.
132 pub fn borrow_mut<'b, T: Any>(&'b mut self, name: &str) -> Option<FixtureRefMut<'b, T>>
133 where
134 'a: 'b,
135 {
136 if let Some(val) = self.values.get_mut(name) {
137 return val.downcast_mut::<T>().map(FixtureRefMut::Override);
138 }
139 self.fixtures.get(name)?.borrow_mut::<T>()
140 }
141
142 /// Returns an iterator over the names of all available fixtures.
143 ///
144 /// This method is useful for diagnostic purposes, such as generating error
145 /// messages that list which fixtures are available when a required fixture
146 /// is missing.
147 ///
148 /// # Examples
149 ///
150 /// ```
151 /// use rstest_bdd::StepContext;
152 ///
153 /// let mut ctx = StepContext::default();
154 /// let value = 42;
155 /// ctx.insert("my_fixture", &value);
156 ///
157 /// let names: Vec<_> = ctx.available_fixtures().collect();
158 /// assert!(names.contains(&"my_fixture"));
159 /// ```
160 pub fn available_fixtures(&self) -> impl Iterator<Item = &'static str> + '_ {
161 self.fixtures.keys().copied()
162 }
163
164 /// Insert a value produced by a prior step.
165 /// The value overrides a fixture only if exactly one fixture has the same
166 /// type; otherwise it is ignored to avoid ambiguity.
167 ///
168 /// Returns the previous override for that fixture when one existed.
169 pub fn insert_value(&mut self, value: Box<dyn Any>) -> Option<Box<dyn Any>> {
170 let ty = value.as_ref().type_id();
171 let mut matches = self
172 .fixtures
173 .iter()
174 .filter_map(|(&name, entry)| (entry.type_id == ty).then_some(name));
175 let name = matches.next()?;
176 if matches.next().is_some() {
177 let message =
178 crate::localization::message_with_args("step-context-ambiguous-override", |args| {
179 args.set("type_id", format!("{ty:?}"));
180 });
181 log::warn!("{message}");
182 #[expect(
183 clippy::print_stderr,
184 reason = "surface ambiguous overrides when logging is disabled"
185 )]
186 if !log::log_enabled!(log::Level::Warn) {
187 eprintln!("{message}");
188 }
189 return None;
190 }
191 self.values.insert(name, value)
192 }
193}
194
195impl<'a> FixtureEntry<'a> {
196 fn shared<T: Any>(value: &'a T) -> Self {
197 Self {
198 kind: FixtureKind::Shared(value),
199 type_id: TypeId::of::<T>(),
200 }
201 }
202
203 fn owned<T: Any>(cell: &'a RefCell<Box<dyn Any>>) -> Self {
204 Self {
205 kind: FixtureKind::Mutable(cell),
206 type_id: TypeId::of::<T>(),
207 }
208 }
209
210 fn borrow_ref<T: Any>(&self) -> Option<FixtureRef<'_, T>> {
211 match self.kind {
212 FixtureKind::Shared(value) => {
213 if self.type_id != TypeId::of::<T>() {
214 return None;
215 }
216 value.downcast_ref::<T>().map(FixtureRef::Shared)
217 }
218 FixtureKind::Mutable(cell) => {
219 if self.type_id != TypeId::of::<T>() {
220 return None;
221 }
222 let guard = cell.borrow();
223 let mapped = Ref::filter_map(guard, |b| b.downcast_ref::<T>()).ok()?;
224 Some(FixtureRef::Borrowed(mapped))
225 }
226 }
227 }
228
229 fn borrow_mut<T: Any>(&self) -> Option<FixtureRefMut<'_, T>> {
230 if self.type_id != TypeId::of::<T>() {
231 return None;
232 }
233 match self.kind {
234 FixtureKind::Shared(_) => None,
235 FixtureKind::Mutable(cell) => {
236 let guard = cell.borrow_mut();
237 let mapped = RefMut::filter_map(guard, |b| b.downcast_mut::<T>()).ok()?;
238 Some(FixtureRefMut::Borrowed(mapped))
239 }
240 }
241 }
242}
243/// Borrowed fixture reference that keeps any underlying `RefCell` borrow alive
244/// for the duration of a step.
245pub enum FixtureRef<'a, T> {
246 /// Reference bound directly to a shared fixture.
247 Shared(&'a T),
248 /// Borrow guard taken from a backing `RefCell`.
249 Borrowed(Ref<'a, T>),
250}
251
252impl<T> FixtureRef<'_, T> {
253 /// Access the borrowed value as an immutable reference.
254 #[must_use]
255 pub fn value(&self) -> &T {
256 match self {
257 Self::Shared(value) => value,
258 Self::Borrowed(guard) => guard,
259 }
260 }
261}
262
263impl<T> AsRef<T> for FixtureRef<'_, T> {
264 fn as_ref(&self) -> &T {
265 self.value()
266 }
267}
268
269/// Borrowed mutable fixture reference tied to the lifetime of the step borrow.
270pub enum FixtureRefMut<'a, T> {
271 /// Mutable reference produced by a prior step override.
272 Override(&'a mut T),
273 /// Borrow guard obtained from the underlying `RefCell`.
274 Borrowed(RefMut<'a, T>),
275}
276
277impl<T> FixtureRefMut<'_, T> {
278 /// Access the borrowed value mutably.
279 #[must_use]
280 pub fn value_mut(&mut self) -> &mut T {
281 match self {
282 Self::Override(value) => value,
283 Self::Borrowed(guard) => guard,
284 }
285 }
286}
287
288impl<T> AsMut<T> for FixtureRefMut<'_, T> {
289 fn as_mut(&mut self) -> &mut T {
290 self.value_mut()
291 }
292}
293
294#[cfg(test)]
295mod tests;