1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fmt;
9
10#[non_exhaustive]
12#[derive(Debug)]
13pub enum SynonymError {
14 FieldNotFound(String),
16 InvalidCombination(String),
18 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#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(untagged)]
39pub enum FieldValue {
40 Integer(i64),
42 Float(f64),
44 String(String),
46 Boolean(bool),
48 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
64pub struct CompositeSynonym {
80 name: String,
82 fields: Vec<String>,
84 separator: String,
86}
87
88impl CompositeSynonym {
89 pub fn new(name: String, fields: Vec<String>) -> Self {
104 Self {
105 name,
106 fields,
107 separator: " ".to_string(),
108 }
109 }
110
111 pub fn with_separator(mut self, separator: impl Into<String>) -> Self {
127 self.separator = separator.into();
128 self
129 }
130
131 pub fn name(&self) -> &str {
133 &self.name
134 }
135
136 pub fn fields(&self) -> &[String] {
138 &self.fields
139 }
140
141 pub fn separator(&self) -> &str {
143 &self.separator
144 }
145
146 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 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 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 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 pub fn concat(name: String, fields: Vec<String>) -> Self {
231 Self {
232 name,
233 fields,
234 separator: String::new(),
235 }
236 }
237
238 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}