Skip to main content

reinhardt_db/orm/
composite_synonym.rs

1//! Composite synonym functionality
2//!
3//! Allows creating aliases (synonyms) from multiple field combinations.
4//! Similar to SQLAlchemy's synonym but for composite fields.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fmt;
9
10/// Error type for composite synonym operations
11#[non_exhaustive]
12#[derive(Debug)]
13pub enum SynonymError {
14	/// FieldNotFound variant.
15	FieldNotFound(String),
16	/// InvalidCombination variant.
17	InvalidCombination(String),
18	/// ComputationError variant.
19	ComputationError(String),
20}
21
22impl fmt::Display for SynonymError {
23	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24		match self {
25			SynonymError::FieldNotFound(msg) => write!(f, "Field not found: {}", msg),
26			SynonymError::InvalidCombination(msg) => {
27				write!(f, "Invalid field combination: {}", msg)
28			}
29			SynonymError::ComputationError(msg) => write!(f, "Computation error: {}", msg),
30		}
31	}
32}
33
34impl std::error::Error for SynonymError {}
35
36/// Value type for field values
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(untagged)]
39pub enum FieldValue {
40	/// Integer variant.
41	Integer(i64),
42	/// Float variant.
43	Float(f64),
44	/// String variant.
45	String(String),
46	/// Boolean variant.
47	Boolean(bool),
48	/// Null variant.
49	Null,
50}
51
52impl std::fmt::Display for FieldValue {
53	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54		match self {
55			FieldValue::Integer(i) => write!(f, "{}", i),
56			FieldValue::Float(fl) => write!(f, "{}", fl),
57			FieldValue::String(s) => write!(f, "{}", s),
58			FieldValue::Boolean(b) => write!(f, "{}", b),
59			FieldValue::Null => Ok(()),
60		}
61	}
62}
63
64/// Represents a composite synonym (alias) for multiple fields
65///
66/// # Examples
67///
68/// ```
69/// use reinhardt_db::orm::composite_synonym::CompositeSynonym;
70///
71/// let full_name = CompositeSynonym::new(
72///     "full_name".to_string(),
73///     vec!["first_name".to_string(), "last_name".to_string()],
74/// );
75///
76/// assert_eq!(full_name.name(), "full_name");
77/// assert_eq!(full_name.fields().len(), 2);
78/// ```
79pub struct CompositeSynonym {
80	/// Name of the synonym
81	name: String,
82	/// Fields that compose this synonym
83	fields: Vec<String>,
84	/// Separator for joining field values
85	separator: String,
86}
87
88impl CompositeSynonym {
89	/// Creates a new CompositeSynonym with default separator (space)
90	///
91	/// # Examples
92	///
93	/// ```
94	/// use reinhardt_db::orm::composite_synonym::CompositeSynonym;
95	///
96	/// let address = CompositeSynonym::new(
97	///     "address".to_string(),
98	///     vec!["street".to_string(), "city".to_string(), "zip".to_string()],
99	/// );
100	///
101	/// assert_eq!(address.separator(), " ");
102	/// ```
103	pub fn new(name: String, fields: Vec<String>) -> Self {
104		Self {
105			name,
106			fields,
107			separator: " ".to_string(),
108		}
109	}
110
111	/// Sets a custom separator for the synonym
112	///
113	/// # Examples
114	///
115	/// ```
116	/// use reinhardt_db::orm::composite_synonym::CompositeSynonym;
117	///
118	/// let csv_fields = CompositeSynonym::new(
119	///     "csv_data".to_string(),
120	///     vec!["field1".to_string(), "field2".to_string()],
121	/// )
122	/// .with_separator(",");
123	///
124	/// assert_eq!(csv_fields.separator(), ",");
125	/// ```
126	pub fn with_separator(mut self, separator: impl Into<String>) -> Self {
127		self.separator = separator.into();
128		self
129	}
130
131	/// Gets the name of the synonym
132	pub fn name(&self) -> &str {
133		&self.name
134	}
135
136	/// Gets the fields that compose this synonym
137	pub fn fields(&self) -> &[String] {
138		&self.fields
139	}
140
141	/// Gets the separator
142	pub fn separator(&self) -> &str {
143		&self.separator
144	}
145
146	/// Generates SQL expression for this synonym
147	///
148	/// # Examples
149	///
150	/// ```
151	/// use reinhardt_db::orm::composite_synonym::CompositeSynonym;
152	///
153	/// let full_name = CompositeSynonym::new(
154	///     "full_name".to_string(),
155	///     vec!["first_name".to_string(), "last_name".to_string()],
156	/// );
157	///
158	/// let sql = full_name.to_sql();
159	/// assert!(sql.contains("CONCAT"));
160	/// assert!(sql.contains("first_name"));
161	/// assert!(sql.contains("last_name"));
162	/// ```
163	pub fn to_sql(&self) -> String {
164		if self.fields.is_empty() {
165			return String::from("NULL");
166		}
167
168		if self.fields.len() == 1 {
169			return self.fields[0].clone();
170		}
171
172		let field_refs: Vec<String> = self.fields.iter().map(|f| format!("'{}'", f)).collect();
173
174		format!("CONCAT_WS('{}', {})", self.separator, field_refs.join(", "))
175	}
176
177	/// Computes the synonym value from an object's field values
178	///
179	/// # Examples
180	///
181	/// ```
182	/// use reinhardt_db::orm::composite_synonym::{CompositeSynonym, FieldValue};
183	/// use std::collections::HashMap;
184	///
185	/// let full_name = CompositeSynonym::new(
186	///     "full_name".to_string(),
187	///     vec!["first_name".to_string(), "last_name".to_string()],
188	/// );
189	///
190	/// let mut object = HashMap::new();
191	/// object.insert("first_name".to_string(), FieldValue::String("John".to_string()));
192	/// object.insert("last_name".to_string(), FieldValue::String("Doe".to_string()));
193	///
194	/// let result = full_name.compute_value(&object);
195	/// assert_eq!(result, "John Doe");
196	/// ```
197	pub fn compute_value(&self, object: &HashMap<String, FieldValue>) -> String {
198		self.fields
199			.iter()
200			.filter_map(|field| object.get(field))
201			.map(|value| value.to_string())
202			.filter(|s| !s.is_empty())
203			.collect::<Vec<_>>()
204			.join(&self.separator)
205	}
206
207	/// Validates that all required fields exist in the object
208	pub fn validate_fields(
209		&self,
210		object: &HashMap<String, FieldValue>,
211	) -> Result<(), SynonymError> {
212		for field in &self.fields {
213			if !object.contains_key(field) {
214				return Err(SynonymError::FieldNotFound(field.clone()));
215			}
216		}
217		Ok(())
218	}
219
220	/// Computes the synonym value with strict validation
221	pub fn compute_value_strict(
222		&self,
223		object: &HashMap<String, FieldValue>,
224	) -> Result<String, SynonymError> {
225		self.validate_fields(object)?;
226		Ok(self.compute_value(object))
227	}
228
229	/// Creates a synonym for concatenating fields without separator
230	pub fn concat(name: String, fields: Vec<String>) -> Self {
231		Self {
232			name,
233			fields,
234			separator: String::new(),
235		}
236	}
237
238	/// Creates a synonym for comma-separated values
239	pub fn csv(name: String, fields: Vec<String>) -> Self {
240		Self {
241			name,
242			fields,
243			separator: ",".to_string(),
244		}
245	}
246}
247
248#[cfg(test)]
249mod tests {
250	use super::*;
251
252	#[test]
253	fn test_composite_synonym_creation() {
254		let synonym = CompositeSynonym::new(
255			"full_name".to_string(),
256			vec!["first_name".to_string(), "last_name".to_string()],
257		);
258
259		assert_eq!(synonym.name(), "full_name");
260		assert_eq!(synonym.fields().len(), 2);
261		assert_eq!(synonym.separator(), " ");
262	}
263
264	#[test]
265	fn test_composite_synonym_with_custom_separator() {
266		let synonym = CompositeSynonym::new(
267			"csv_data".to_string(),
268			vec!["field1".to_string(), "field2".to_string()],
269		)
270		.with_separator(", ");
271
272		assert_eq!(synonym.separator(), ", ");
273	}
274
275	#[test]
276	fn test_to_sql() {
277		let synonym = CompositeSynonym::new(
278			"full_name".to_string(),
279			vec!["first_name".to_string(), "last_name".to_string()],
280		);
281
282		let sql = synonym.to_sql();
283		assert!(sql.contains("CONCAT_WS"));
284		assert!(sql.contains("' '"));
285	}
286
287	#[test]
288	fn test_to_sql_single_field() {
289		let synonym = CompositeSynonym::new("alias".to_string(), vec!["field".to_string()]);
290
291		let sql = synonym.to_sql();
292		assert_eq!(sql, "field");
293	}
294
295	#[test]
296	fn test_to_sql_empty_fields() {
297		let synonym = CompositeSynonym::new("empty".to_string(), vec![]);
298
299		let sql = synonym.to_sql();
300		assert_eq!(sql, "NULL");
301	}
302
303	#[test]
304	fn test_compute_value() {
305		let synonym = CompositeSynonym::new(
306			"full_name".to_string(),
307			vec!["first_name".to_string(), "last_name".to_string()],
308		);
309
310		let mut object = HashMap::new();
311		object.insert(
312			"first_name".to_string(),
313			FieldValue::String("Alice".to_string()),
314		);
315		object.insert(
316			"last_name".to_string(),
317			FieldValue::String("Smith".to_string()),
318		);
319
320		let result = synonym.compute_value(&object);
321		assert_eq!(result, "Alice Smith");
322	}
323
324	#[test]
325	fn test_compute_value_with_custom_separator() {
326		let synonym = CompositeSynonym::new(
327			"address".to_string(),
328			vec!["street".to_string(), "city".to_string()],
329		)
330		.with_separator(", ");
331
332		let mut object = HashMap::new();
333		object.insert(
334			"street".to_string(),
335			FieldValue::String("123 Main St".to_string()),
336		);
337		object.insert(
338			"city".to_string(),
339			FieldValue::String("Springfield".to_string()),
340		);
341
342		let result = synonym.compute_value(&object);
343		assert_eq!(result, "123 Main St, Springfield");
344	}
345
346	#[test]
347	fn test_compute_value_with_missing_fields() {
348		let synonym = CompositeSynonym::new(
349			"full_name".to_string(),
350			vec!["first_name".to_string(), "last_name".to_string()],
351		);
352
353		let mut object = HashMap::new();
354		object.insert(
355			"first_name".to_string(),
356			FieldValue::String("Alice".to_string()),
357		);
358
359		let result = synonym.compute_value(&object);
360		assert_eq!(result, "Alice");
361	}
362
363	#[test]
364	fn test_validate_fields() {
365		let synonym = CompositeSynonym::new(
366			"full_name".to_string(),
367			vec!["first_name".to_string(), "last_name".to_string()],
368		);
369
370		let mut object = HashMap::new();
371		object.insert(
372			"first_name".to_string(),
373			FieldValue::String("Alice".to_string()),
374		);
375		object.insert(
376			"last_name".to_string(),
377			FieldValue::String("Smith".to_string()),
378		);
379
380		assert!(synonym.validate_fields(&object).is_ok());
381
382		let mut incomplete_object = HashMap::new();
383		incomplete_object.insert(
384			"first_name".to_string(),
385			FieldValue::String("Alice".to_string()),
386		);
387
388		assert!(synonym.validate_fields(&incomplete_object).is_err());
389	}
390
391	#[test]
392	fn test_compute_value_strict() {
393		let synonym = CompositeSynonym::new(
394			"full_name".to_string(),
395			vec!["first_name".to_string(), "last_name".to_string()],
396		);
397
398		let mut object = HashMap::new();
399		object.insert(
400			"first_name".to_string(),
401			FieldValue::String("Alice".to_string()),
402		);
403		object.insert(
404			"last_name".to_string(),
405			FieldValue::String("Smith".to_string()),
406		);
407
408		let result = synonym.compute_value_strict(&object);
409		assert!(result.is_ok());
410		assert_eq!(result.unwrap(), "Alice Smith");
411
412		let mut incomplete_object = HashMap::new();
413		incomplete_object.insert(
414			"first_name".to_string(),
415			FieldValue::String("Alice".to_string()),
416		);
417
418		let result = synonym.compute_value_strict(&incomplete_object);
419		assert!(result.is_err());
420	}
421
422	#[test]
423	fn test_concat_synonym() {
424		let synonym = CompositeSynonym::concat(
425			"code".to_string(),
426			vec!["prefix".to_string(), "number".to_string()],
427		);
428
429		let mut object = HashMap::new();
430		object.insert("prefix".to_string(), FieldValue::String("ABC".to_string()));
431		object.insert("number".to_string(), FieldValue::String("123".to_string()));
432
433		let result = synonym.compute_value(&object);
434		assert_eq!(result, "ABC123");
435		assert_eq!(synonym.separator(), "");
436	}
437
438	#[test]
439	fn test_csv_synonym() {
440		let synonym = CompositeSynonym::csv(
441			"tags".to_string(),
442			vec!["tag1".to_string(), "tag2".to_string()],
443		);
444
445		let mut object = HashMap::new();
446		object.insert("tag1".to_string(), FieldValue::String("rust".to_string()));
447		object.insert("tag2".to_string(), FieldValue::String("orm".to_string()));
448
449		let result = synonym.compute_value(&object);
450		assert_eq!(result, "rust,orm");
451		assert_eq!(synonym.separator(), ",");
452	}
453
454	#[test]
455	fn test_field_value_to_string() {
456		assert_eq!(FieldValue::Integer(42).to_string(), "42");
457		assert_eq!(FieldValue::Float(3.15).to_string(), "3.15");
458		assert_eq!(FieldValue::String("test".to_string()).to_string(), "test");
459		assert_eq!(FieldValue::Boolean(true).to_string(), "true");
460		assert_eq!(FieldValue::Null.to_string(), "");
461	}
462}