Skip to main content

reinhardt_testkit/fixtures/
loader.rs

1use rstest::*;
2use serde::Deserialize;
3use std::collections::HashMap;
4use std::sync::Arc;
5use tokio::sync::RwLock;
6
7/// Errors that can occur during fixture loading.
8#[derive(Debug, thiserror::Error)]
9pub enum FixtureError {
10	/// The requested fixture file was not found.
11	#[error("Fixture not found: {0}")]
12	NotFound(String),
13	/// An error occurred while reading the fixture file.
14	#[error("Load error: {0}")]
15	Load(String),
16	/// An error occurred while parsing the fixture content.
17	#[error("Parse error: {0}")]
18	Parse(String),
19}
20
21/// Result type for fixture loading operations.
22pub type FixtureResult<T> = Result<T, FixtureError>;
23
24/// Fixture data loader
25pub struct FixtureLoader {
26	fixtures: Arc<RwLock<HashMap<String, serde_json::Value>>>,
27}
28
29impl FixtureLoader {
30	/// Create a new fixture loader
31	///
32	/// # Examples
33	///
34	/// ```
35	/// use reinhardt_testkit::fixtures::FixtureLoader;
36	///
37	/// let loader = FixtureLoader::new();
38	/// // Loader is ready to load fixtures
39	/// ```
40	pub fn new() -> Self {
41		Self {
42			fixtures: Arc::new(RwLock::new(HashMap::new())),
43		}
44	}
45	/// Load fixture from JSON string
46	///
47	/// # Examples
48	///
49	/// ```
50	/// use reinhardt_testkit::fixtures::FixtureLoader;
51	///
52	/// # tokio_test::block_on(async {
53	/// let loader = FixtureLoader::new();
54	/// let json = r#"{"id": 1, "name": "Test"}"#;
55	/// loader.load_from_json("test".to_string(), json).await.unwrap();
56	/// assert!(loader.exists("test").await);
57	/// # });
58	/// ```
59	pub async fn load_from_json(&self, name: String, json: &str) -> FixtureResult<()> {
60		let value: serde_json::Value =
61			serde_json::from_str(json).map_err(|e| FixtureError::Parse(e.to_string()))?;
62
63		self.fixtures.write().await.insert(name, value);
64		Ok(())
65	}
66	/// Load fixture data
67	///
68	/// # Examples
69	///
70	/// ```
71	/// use reinhardt_testkit::fixtures::FixtureLoader;
72	/// use serde::Deserialize;
73	///
74	/// #[derive(Deserialize)]
75	/// struct User {
76	///     id: i32,
77	///     name: String,
78	/// }
79	///
80	/// # tokio_test::block_on(async {
81	/// let loader = FixtureLoader::new();
82	/// let json = r#"{"id": 1, "name": "Alice"}"#;
83	/// loader.load_from_json("user".to_string(), json).await.unwrap();
84	/// let user: User = loader.load("user").await.unwrap();
85	/// assert_eq!(user.id, 1);
86	/// assert_eq!(user.name, "Alice");
87	/// # });
88	/// ```
89	pub async fn load<T: for<'de> Deserialize<'de>>(&self, name: &str) -> FixtureResult<T> {
90		let fixtures = self.fixtures.read().await;
91		let value = fixtures
92			.get(name)
93			.ok_or_else(|| FixtureError::NotFound(name.to_string()))?;
94
95		serde_json::from_value(value.clone()).map_err(|e| FixtureError::Parse(e.to_string()))
96	}
97	/// Get raw fixture value
98	///
99	/// # Examples
100	///
101	/// ```
102	/// use reinhardt_testkit::fixtures::FixtureLoader;
103	///
104	/// # tokio_test::block_on(async {
105	/// let loader = FixtureLoader::new();
106	/// let json = r#"{"status": "active"}"#;
107	/// loader.load_from_json("config".to_string(), json).await.unwrap();
108	/// let value = loader.get("config").await.unwrap();
109	/// assert!(value.is_object());
110	/// # });
111	/// ```
112	pub async fn get(&self, name: &str) -> FixtureResult<serde_json::Value> {
113		let fixtures = self.fixtures.read().await;
114		fixtures
115			.get(name)
116			.cloned()
117			.ok_or_else(|| FixtureError::NotFound(name.to_string()))
118	}
119	/// Check if fixture exists
120	///
121	/// # Examples
122	///
123	/// ```
124	/// use reinhardt_testkit::fixtures::FixtureLoader;
125	///
126	/// # tokio_test::block_on(async {
127	/// let loader = FixtureLoader::new();
128	/// assert!(!loader.exists("missing").await);
129	/// loader.load_from_json("test".to_string(), "{}").await.unwrap();
130	/// assert!(loader.exists("test").await);
131	/// # });
132	/// ```
133	pub async fn exists(&self, name: &str) -> bool {
134		self.fixtures.read().await.contains_key(name)
135	}
136	/// Clear all fixtures
137	///
138	/// # Examples
139	///
140	/// ```
141	/// use reinhardt_testkit::fixtures::FixtureLoader;
142	///
143	/// # tokio_test::block_on(async {
144	/// let loader = FixtureLoader::new();
145	/// loader.load_from_json("test".to_string(), "{}").await.unwrap();
146	/// assert_eq!(loader.list().await.len(), 1);
147	/// loader.clear().await;
148	/// assert_eq!(loader.list().await.len(), 0);
149	/// # });
150	/// ```
151	pub async fn clear(&self) {
152		self.fixtures.write().await.clear();
153	}
154	/// List all fixture names
155	///
156	/// # Examples
157	///
158	/// ```
159	/// use reinhardt_testkit::fixtures::FixtureLoader;
160	///
161	/// # tokio_test::block_on(async {
162	/// let loader = FixtureLoader::new();
163	/// loader.load_from_json("test1".to_string(), "{}").await.unwrap();
164	/// loader.load_from_json("test2".to_string(), "{}").await.unwrap();
165	/// let names = loader.list().await;
166	/// assert_eq!(names.len(), 2);
167	/// assert!(names.contains(&"test1".to_string()));
168	/// # });
169	/// ```
170	pub async fn list(&self) -> Vec<String> {
171		self.fixtures.read().await.keys().cloned().collect()
172	}
173}
174
175impl Default for FixtureLoader {
176	fn default() -> Self {
177		Self::new()
178	}
179}
180
181/// Factory trait for creating test data
182pub trait Factory<T>: Send + Sync {
183	/// Build a single instance of the test data type.
184	fn build(&self) -> T;
185	/// Build multiple instances of the test data type.
186	fn build_batch(&self, count: usize) -> Vec<T> {
187		(0..count).map(|_| self.build()).collect()
188	}
189}
190
191/// Simple factory builder
192pub struct FactoryBuilder<T, F>
193where
194	F: Fn() -> T + Send + Sync,
195{
196	builder: F,
197	_phantom: std::marker::PhantomData<T>,
198}
199
200impl<T, F> FactoryBuilder<T, F>
201where
202	F: Fn() -> T + Send + Sync,
203{
204	/// Create a new factory builder
205	///
206	/// # Examples
207	///
208	/// ```
209	/// use reinhardt_testkit::fixtures::{FactoryBuilder, Factory};
210	///
211	/// #[derive(Debug, PartialEq)]
212	/// struct TestData { id: i32 }
213	///
214	/// let factory = FactoryBuilder::new(|| TestData { id: 42 });
215	/// let item = factory.build();
216	/// assert_eq!(item.id, 42);
217	/// ```
218	pub fn new(builder: F) -> Self {
219		Self {
220			builder,
221			_phantom: std::marker::PhantomData,
222		}
223	}
224}
225
226impl<T, F> Factory<T> for FactoryBuilder<T, F>
227where
228	F: Fn() -> T + Send + Sync,
229	T: Send + Sync,
230{
231	fn build(&self) -> T {
232		(self.builder)()
233	}
234}
235
236/// Generate a random test key using UUID
237///
238/// # Examples
239///
240/// ```
241/// use reinhardt_testkit::fixtures::random_test_key;
242///
243/// let key = random_test_key();
244/// assert!(key.starts_with("test_key_"));
245/// ```
246pub fn random_test_key() -> String {
247	use uuid::Uuid;
248	format!("test_key_{}", Uuid::new_v4().simple())
249}
250
251/// Generate test configuration data with timestamp
252///
253/// # Examples
254///
255/// ```
256/// use reinhardt_testkit::fixtures::test_config_value;
257///
258/// let value = test_config_value("my_value");
259/// assert_eq!(value["value"], "my_value");
260/// ```
261pub fn test_config_value(value: &str) -> serde_json::Value {
262	serde_json::json!({
263		"value": value,
264		"timestamp": chrono::Utc::now().to_rfc3339(),
265	})
266}
267
268// ============================================================================
269// rstest integration: Fixtures for common test resources
270// ============================================================================
271
272/// Fixture providing a FixtureLoader instance
273///
274/// Use this fixture in tests that need to load JSON fixture data.
275///
276/// # Examples
277///
278/// ```rust,no_run
279/// use reinhardt_testkit::fixtures::fixture_loader;
280/// use rstest::*;
281///
282/// #[rstest]
283/// #[tokio::test]
284/// async fn test_with_fixtures(fixture_loader: reinhardt_testkit::fixtures::FixtureLoader) {
285///     fixture_loader.load_from_json("test".to_string(), r#"{"id": 1}"#).await.unwrap();
286///     // ...
287/// }
288/// ```
289#[fixture]
290pub fn fixture_loader() -> FixtureLoader {
291	FixtureLoader::new()
292}
293
294/// Fixture providing an APIClient instance
295///
296/// Use this fixture in tests that need to make test HTTP requests.
297///
298/// # Examples
299///
300/// ```rust,no_run
301/// use reinhardt_testkit::fixtures::api_client;
302/// use rstest::*;
303///
304/// #[rstest]
305/// #[tokio::test]
306/// async fn test_api_request(api_client: reinhardt_testkit::client::APIClient) {
307///     // Make requests with client
308/// }
309/// ```
310#[fixture]
311pub fn api_client() -> crate::client::APIClient {
312	crate::client::APIClient::new()
313}
314
315/// Fixture providing a temporary directory that is automatically cleaned up
316///
317/// # Examples
318///
319/// ```rust
320/// use reinhardt_testkit::fixtures::temp_dir;
321/// use rstest::*;
322///
323/// #[rstest]
324/// fn test_with_temp_dir(temp_dir: tempfile::TempDir) {
325///     let path = temp_dir.path();
326///     std::fs::write(path.join("test.txt"), "data").unwrap();
327///     // temp_dir is automatically cleaned up when test ends
328/// }
329/// ```
330#[fixture]
331pub fn temp_dir() -> tempfile::TempDir {
332	tempfile::tempdir().expect("Failed to create temporary directory")
333}
334
335#[cfg(test)]
336mod tests {
337	use super::*;
338	use serde::Serialize;
339
340	#[derive(Debug, Serialize, Deserialize, PartialEq)]
341	struct TestData {
342		id: i32,
343		name: String,
344	}
345
346	#[tokio::test]
347	async fn test_fixture_loader() {
348		let loader = FixtureLoader::new();
349		let json = r#"{"id": 1, "name": "Test"}"#;
350
351		loader
352			.load_from_json("test".to_string(), json)
353			.await
354			.unwrap();
355
356		let data: TestData = loader.load("test").await.unwrap();
357		assert_eq!(data.id, 1);
358		assert_eq!(data.name, "Test");
359	}
360
361	#[tokio::test]
362	async fn test_fixture_not_found() {
363		let loader = FixtureLoader::new();
364		let result: FixtureResult<TestData> = loader.load("missing").await;
365		assert!(result.is_err());
366	}
367
368	#[test]
369	fn test_factory_builder() {
370		let factory = FactoryBuilder::new(|| TestData {
371			id: 1,
372			name: "Test".to_string(),
373		});
374
375		let data = factory.build();
376		assert_eq!(data.id, 1);
377
378		let batch = factory.build_batch(3);
379		assert_eq!(batch.len(), 3);
380	}
381}