1use crate::error::{Result, SzConfigError};
2use crate::helpers;
3use serde_json::{Value, json};
4
5#[derive(Debug, Clone)]
11pub struct AddElementParams<'a> {
12 pub code: &'a str,
13 pub description: Option<&'a str>,
14 pub data_type: Option<&'a str>,
15 pub tokenized: Option<&'a str>,
16}
17
18impl<'a> TryFrom<&'a Value> for AddElementParams<'a> {
19 type Error = SzConfigError;
20
21 fn try_from(json: &'a Value) -> Result<Self> {
22 let code = json
23 .get("code")
24 .and_then(|v| v.as_str())
25 .ok_or_else(|| SzConfigError::MissingField("code".to_string()))?;
26
27 Ok(Self {
28 code,
29 description: json.get("description").and_then(|v| v.as_str()),
30 data_type: json.get("dataType").and_then(|v| v.as_str()),
31 tokenized: json.get("tokenized").and_then(|v| v.as_str()),
32 })
33 }
34}
35
36#[derive(Debug, Clone)]
38pub struct SetElementParams<'a> {
39 pub code: &'a str,
40 pub description: Option<&'a str>,
41 pub data_type: Option<&'a str>,
42 pub tokenized: Option<&'a str>,
43}
44
45impl<'a> TryFrom<&'a Value> for SetElementParams<'a> {
46 type Error = SzConfigError;
47
48 fn try_from(json: &'a Value) -> Result<Self> {
49 let code = json
50 .get("code")
51 .and_then(|v| v.as_str())
52 .ok_or_else(|| SzConfigError::MissingField("code".to_string()))?;
53
54 Ok(Self {
55 code,
56 description: json.get("description").and_then(|v| v.as_str()),
57 data_type: json.get("dataType").and_then(|v| v.as_str()),
58 tokenized: json.get("tokenized").and_then(|v| v.as_str()),
59 })
60 }
61}
62
63#[derive(Debug, Clone, Default)]
65pub struct SetFeatureElementParams<'a> {
66 pub feature_code: Option<&'a str>,
68
69 pub element_code: Option<&'a str>,
71
72 pub exec_order: Option<i64>,
73 pub display_level: Option<i64>,
74 pub display_delim: Option<&'a str>,
75 pub derived: Option<&'a str>,
76}
77
78impl<'a> SetFeatureElementParams<'a> {
79 pub fn new(feature_code: &'a str, element_code: &'a str) -> Self {
89 Self {
90 feature_code: Some(feature_code),
91 element_code: Some(element_code),
92 exec_order: None,
93 display_level: None,
94 display_delim: None,
95 derived: None,
96 }
97 }
98
99 pub fn with_exec_order(mut self, order: i64) -> Self {
101 self.exec_order = Some(order);
102 self
103 }
104
105 pub fn with_display_level(mut self, level: i64) -> Self {
107 self.display_level = Some(level);
108 self
109 }
110
111 pub fn with_display_delim(mut self, delim: &'a str) -> Self {
113 self.display_delim = Some(delim);
114 self
115 }
116
117 pub fn with_derived(mut self, derived: &'a str) -> Self {
119 self.derived = Some(derived);
120 self
121 }
122}
123
124impl<'a> TryFrom<&'a Value> for SetFeatureElementParams<'a> {
125 type Error = SzConfigError;
126
127 fn try_from(json: &'a Value) -> Result<Self> {
128 let feature_code = json
129 .get("featureCode")
130 .and_then(|v| v.as_str())
131 .ok_or_else(|| SzConfigError::MissingField("featureCode".to_string()))?;
132
133 let element_code = json
134 .get("elementCode")
135 .and_then(|v| v.as_str())
136 .ok_or_else(|| SzConfigError::MissingField("elementCode".to_string()))?;
137
138 Ok(Self {
139 feature_code: Some(feature_code),
140 element_code: Some(element_code),
141 exec_order: json.get("execOrder").and_then(|v| v.as_i64()),
142 display_level: json.get("displayLevel").and_then(|v| v.as_i64()),
143 display_delim: json.get("displayDelim").and_then(|v| v.as_str()),
144 derived: json.get("derived").and_then(|v| v.as_str()),
145 })
146 }
147}
148
149pub fn add_element(config_json: &str, params: AddElementParams) -> Result<String> {
158 let config: Value =
159 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
160
161 let code_upper = params.code.to_uppercase();
162
163 let felem_array = config["G2_CONFIG"]["CFG_FELEM"]
165 .as_array()
166 .ok_or_else(|| SzConfigError::MissingSection("CFG_FELEM".to_string()))?;
167
168 if felem_array
169 .iter()
170 .any(|e| e["FELEM_CODE"].as_str() == Some(code_upper.as_str()))
171 {
172 return Err(SzConfigError::AlreadyExists(format!(
173 "Element already exists: {code_upper}"
174 )));
175 }
176
177 let felem_id = helpers::get_next_id_with_min(felem_array, "FELEM_ID", 1000)?;
179
180 let data_type = if let Some(dt) = params.data_type {
182 let dt_lower = dt.to_lowercase();
183 match dt_lower.as_str() {
184 "string" => "string",
185 "number" => "number",
186 "date" => "date",
187 "datetime" => "datetime",
188 "json" => "json",
189 _ => {
190 return Err(SzConfigError::InvalidInput(format!(
191 "Invalid DATATYPE value '{dt}'. Must be one of: string, number, date, datetime, json"
192 )));
193 }
194 }
195 } else {
196 "string" };
198
199 let tokenized = if let Some(tok) = params.tokenized {
201 let tok_upper = tok.to_uppercase();
202 match tok_upper.as_str() {
203 "YES" => "Yes",
204 "NO" => "No",
205 _ => {
206 return Err(SzConfigError::InvalidInput(format!(
207 "Invalid TOKENIZED value '{tok}'. Must be 'Yes' or 'No'"
208 )));
209 }
210 }
211 } else {
212 "No" };
214
215 let mut new_record = json!({
217 "FELEM_ID": felem_id,
218 "FELEM_CODE": code_upper.clone(),
219 "DATA_TYPE": data_type,
220 "TOKENIZE": tokenized,
221 });
222
223 if let Some(obj) = new_record.as_object_mut() {
224 if let Some(desc) = params.description {
225 obj.insert("FELEM_DESC".to_string(), json!(desc));
226 } else {
227 obj.insert("FELEM_DESC".to_string(), json!(code_upper));
228 }
229 }
230
231 helpers::add_to_config_array(config_json, "CFG_FELEM", new_record)
232}
233
234pub fn delete_element(config_json: &str, felem_code: &str) -> Result<String> {
247 let mut config: Value =
248 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
249
250 let code_upper = felem_code.to_uppercase();
251
252 let felem_array = config["G2_CONFIG"]["CFG_FELEM"]
254 .as_array()
255 .ok_or_else(|| SzConfigError::MissingSection("CFG_FELEM".to_string()))?;
256
257 let element_record = felem_array
258 .iter()
259 .find(|e| e["FELEM_CODE"].as_str() == Some(code_upper.as_str()))
260 .ok_or_else(|| SzConfigError::NotFound("Element does not exist".to_string()))?;
261
262 let felem_id = element_record["FELEM_ID"]
263 .as_i64()
264 .ok_or_else(|| SzConfigError::MissingField("FELEM_ID".to_string()))?;
265
266 let fbom_array = config["G2_CONFIG"]["CFG_FBOM"]
268 .as_array()
269 .ok_or_else(|| SzConfigError::MissingSection("CFG_FBOM".to_string()))?;
270
271 let linked_features: Vec<String> = fbom_array
272 .iter()
273 .filter(|fbom| fbom["FELEM_ID"].as_i64() == Some(felem_id))
274 .filter_map(|fbom| {
275 let ftype_id = fbom["FTYPE_ID"].as_i64()?;
276 let ftype_array = config["G2_CONFIG"]["CFG_FTYPE"].as_array()?;
277 let ftype = ftype_array
278 .iter()
279 .find(|f| f["FTYPE_ID"].as_i64() == Some(ftype_id))?;
280 ftype["FTYPE_CODE"].as_str().map(|s| s.to_string())
281 })
282 .collect();
283
284 if !linked_features.is_empty() {
285 return Err(SzConfigError::InvalidInput(format!(
286 "Element linked to the following feature(s): {}",
287 linked_features.join(",")
288 )));
289 }
290
291 let felem_array_mut = config["G2_CONFIG"]["CFG_FELEM"]
293 .as_array_mut()
294 .ok_or_else(|| SzConfigError::MissingSection("CFG_FELEM".to_string()))?;
295
296 if !felem_array_mut
297 .iter()
298 .any(|e| e["FELEM_CODE"].as_str() == Some(code_upper.as_str()))
299 {
300 return Err(SzConfigError::NotFound(format!(
301 "Element not found: {code_upper}"
302 )));
303 }
304
305 if let Some(array) = config["G2_CONFIG"]["CFG_FELEM"].as_array_mut() {
307 array.retain(|e| e["FELEM_CODE"].as_str() != Some(code_upper.as_str()));
308 }
309
310 serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
311}
312
313pub fn get_element(config_json: &str, felem_code: &str) -> Result<Value> {
322 let config: Value =
323 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
324
325 let code_upper = felem_code.to_uppercase();
326
327 let felem_array = config["G2_CONFIG"]["CFG_FELEM"]
328 .as_array()
329 .ok_or_else(|| SzConfigError::MissingSection("CFG_FELEM".to_string()))?;
330
331 let element = felem_array
332 .iter()
333 .find(|e| e["FELEM_CODE"].as_str() == Some(code_upper.as_str()))
334 .ok_or_else(|| SzConfigError::NotFound(format!("Element not found: {code_upper}")))?;
335
336 Ok(json!({
338 "id": element["FELEM_ID"].as_i64().unwrap_or(0),
339 "element": element["FELEM_CODE"].as_str().unwrap_or(""),
340 "datatype": element["DATA_TYPE"].as_str().unwrap_or("")
341 }))
342}
343
344pub fn list_elements(config_json: &str) -> Result<Vec<Value>> {
352 let config: Value =
353 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
354
355 let felem_array = config["G2_CONFIG"]["CFG_FELEM"]
356 .as_array()
357 .ok_or_else(|| SzConfigError::MissingSection("CFG_FELEM".to_string()))?;
358
359 let mut result: Vec<Value> = felem_array
360 .iter()
361 .map(|item| {
362 json!({
363 "id": item["FELEM_ID"].as_i64().unwrap_or(0),
364 "element": item["FELEM_CODE"].as_str().unwrap_or(""),
365 "datatype": item["DATA_TYPE"].as_str().unwrap_or("")
366 })
367 })
368 .collect();
369
370 result.sort_by_key(|e| e["element"].as_str().unwrap_or("").to_string());
372
373 Ok(result)
374}
375
376pub fn set_element(config_json: &str, params: SetElementParams) -> Result<String> {
385 let mut config: Value =
386 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
387
388 let code_upper = params.code.to_uppercase();
389
390 let felem_array = config["G2_CONFIG"]["CFG_FELEM"]
391 .as_array_mut()
392 .ok_or_else(|| SzConfigError::MissingSection("CFG_FELEM".to_string()))?;
393
394 let felem = felem_array
396 .iter_mut()
397 .find(|e| e["FELEM_CODE"].as_str() == Some(code_upper.as_str()))
398 .ok_or_else(|| SzConfigError::NotFound(format!("Element: {}", code_upper.clone())))?;
399
400 if let Some(dest_obj) = felem.as_object_mut() {
402 if let Some(desc) = params.description {
403 dest_obj.insert("FELEM_DESC".to_string(), json!(desc));
404 }
405 if let Some(dt) = params.data_type {
406 dest_obj.insert("DATA_TYPE".to_string(), json!(dt));
407 }
408 if let Some(tok) = params.tokenized {
409 dest_obj.insert("TOKENIZED".to_string(), json!(tok));
410 }
411 }
412
413 serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
414}
415
416pub fn set_feature_element(config_json: &str, params: SetFeatureElementParams) -> Result<String> {
438 let feature_code = params
440 .feature_code
441 .ok_or_else(|| SzConfigError::MissingField("feature_code".to_string()))?;
442 let element_code = params
443 .element_code
444 .ok_or_else(|| SzConfigError::MissingField("element_code".to_string()))?;
445
446 let ftype_id = helpers::lookup_feature_id(config_json, feature_code)?;
447 let felem_id = helpers::lookup_element_id(config_json, element_code)?;
448
449 let mut config: Value =
450 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
451
452 let fbom_array = config["G2_CONFIG"]["CFG_FBOM"]
453 .as_array_mut()
454 .ok_or_else(|| SzConfigError::MissingSection("CFG_FBOM".to_string()))?;
455
456 let fbom = fbom_array
458 .iter_mut()
459 .find(|item| {
460 item["FTYPE_ID"].as_i64() == Some(ftype_id)
461 && item["FELEM_ID"].as_i64() == Some(felem_id)
462 })
463 .ok_or_else(|| {
464 SzConfigError::NotFound(format!(
465 "Feature element mapping not found: FTYPE_ID={ftype_id}, FELEM_ID={felem_id}"
466 ))
467 })?;
468
469 if let Some(order) = params.exec_order {
471 fbom["EXEC_ORDER"] = json!(order);
472 }
473 if let Some(level) = params.display_level {
474 fbom["DISPLAY_LEVEL"] = json!(level);
475 }
476 if let Some(delim) = params.display_delim {
477 fbom["DISPLAY_DELIM"] = json!(delim);
478 }
479 if let Some(der) = params.derived {
480 let der_upper = der.to_uppercase();
482 let validated_derived = match der_upper.as_str() {
483 "YES" => "Yes",
484 "NO" => "No",
485 _ => {
486 return Err(SzConfigError::InvalidInput(format!(
487 "Invalid DERIVED value '{der}'. Must be 'Yes' or 'No'"
488 )));
489 }
490 };
491 fbom["DERIVED"] = json!(validated_derived);
492 }
493
494 serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
495}
496
497pub fn set_feature_element_display_level(
517 config_json: &str,
518 feature_code: &str,
519 element_code: &str,
520 display_level: i64,
521) -> Result<String> {
522 set_feature_element(
523 config_json,
524 SetFeatureElementParams::new(feature_code, element_code).with_display_level(display_level),
525 )
526}
527
528pub fn set_feature_element_derived(
548 config_json: &str,
549 feature_code: &str,
550 element_code: &str,
551 derived: &str,
552) -> Result<String> {
553 set_feature_element(
554 config_json,
555 SetFeatureElementParams::new(feature_code, element_code).with_derived(derived),
556 )
557}
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562
563 const TEST_CONFIG_WITH_FEATURES: &str = r#"{
564 "G2_CONFIG": {
565 "CFG_FTYPE": [
566 {"FTYPE_ID": 1, "FTYPE_CODE": "NAME"},
567 {"FTYPE_ID": 2, "FTYPE_CODE": "ADDRESS"}
568 ],
569 "CFG_FELEM": [
570 {"FELEM_ID": 1, "FELEM_CODE": "FIRST_NAME", "DATA_TYPE": "string"},
571 {"FELEM_ID": 2, "FELEM_CODE": "FULL_NAME", "DATA_TYPE": "string"},
572 {"FELEM_ID": 3, "FELEM_CODE": "ADDR_LINE1", "DATA_TYPE": "string"}
573 ],
574 "CFG_FBOM": [
575 {"FTYPE_ID": 1, "FELEM_ID": 1, "EXEC_ORDER": 1, "DISPLAY_LEVEL": 0},
576 {"FTYPE_ID": 1, "FELEM_ID": 2, "EXEC_ORDER": 2, "DISPLAY_LEVEL": 1},
577 {"FTYPE_ID": 2, "FELEM_ID": 3, "EXEC_ORDER": 1, "DISPLAY_LEVEL": 0}
578 ]
579 }
580 }"#;
581
582 #[test]
583 fn test_set_feature_element_with_codes() {
584 let params = SetFeatureElementParams::new("NAME", "FIRST_NAME").with_display_level(1);
586
587 let result = set_feature_element(TEST_CONFIG_WITH_FEATURES, params);
588 assert!(result.is_ok(), "Should succeed with valid codes");
589
590 let config: Value = serde_json::from_str(&result.unwrap()).unwrap();
591 let fbom = &config["G2_CONFIG"]["CFG_FBOM"][0];
592 assert_eq!(fbom["DISPLAY_LEVEL"], 1);
593 }
594
595 #[test]
596 fn test_set_feature_element_with_codes_all_params() {
597 let params = SetFeatureElementParams::new("NAME", "FIRST_NAME")
599 .with_display_level(2)
600 .with_exec_order(5)
601 .with_display_delim("|")
602 .with_derived("Yes");
603
604 let result = set_feature_element(TEST_CONFIG_WITH_FEATURES, params);
605 assert!(result.is_ok());
606
607 let config: Value = serde_json::from_str(&result.unwrap()).unwrap();
608 let fbom = &config["G2_CONFIG"]["CFG_FBOM"][0];
609 assert_eq!(fbom["DISPLAY_LEVEL"], 2);
610 assert_eq!(fbom["EXEC_ORDER"], 5);
611 assert_eq!(fbom["DISPLAY_DELIM"], "|");
612 assert_eq!(fbom["DERIVED"], "Yes");
613 }
614
615 #[test]
616 fn test_set_feature_element_error_invalid_code() {
617 let params = SetFeatureElementParams::new("INVALID_FEATURE", "FIRST_NAME");
619
620 let result = set_feature_element(TEST_CONFIG_WITH_FEATURES, params);
621 assert!(result.is_err(), "Should error with invalid feature code");
622 }
623
624 #[test]
625 fn test_set_feature_element_error_invalid_element_code() {
626 let params = SetFeatureElementParams::new("NAME", "INVALID_ELEMENT");
628
629 let result = set_feature_element(TEST_CONFIG_WITH_FEATURES, params);
630 assert!(result.is_err(), "Should error with invalid element code");
631 }
632
633 #[test]
634 fn test_set_feature_element_error_mapping_not_found() {
635 let params = SetFeatureElementParams::new("ADDRESS", "FIRST_NAME");
637
638 let result = set_feature_element(TEST_CONFIG_WITH_FEATURES, params);
639 assert!(
640 result.is_err(),
641 "Should error when feature-element mapping doesn't exist"
642 );
643 }
644
645 #[test]
646 fn test_set_feature_element_display_level() {
647 let result =
649 set_feature_element_display_level(TEST_CONFIG_WITH_FEATURES, "NAME", "FIRST_NAME", 5);
650 assert!(result.is_ok());
651
652 let config: Value = serde_json::from_str(&result.unwrap()).unwrap();
653 let fbom = &config["G2_CONFIG"]["CFG_FBOM"][0];
654 assert_eq!(fbom["DISPLAY_LEVEL"], 5);
655 }
656
657 #[test]
658 fn test_set_feature_element_derived() {
659 let result =
661 set_feature_element_derived(TEST_CONFIG_WITH_FEATURES, "NAME", "FIRST_NAME", "Yes");
662 assert!(result.is_ok());
663
664 let config: Value = serde_json::from_str(&result.unwrap()).unwrap();
665 let fbom = &config["G2_CONFIG"]["CFG_FBOM"][0];
666 assert_eq!(fbom["DERIVED"], "Yes");
667 }
668
669 #[test]
670 fn test_set_feature_element_case_insensitive() {
671 let params = SetFeatureElementParams::new("name", "first_name").with_display_level(9);
673
674 let result = set_feature_element(TEST_CONFIG_WITH_FEATURES, params);
675 assert!(
676 result.is_ok(),
677 "Should work with lowercase codes (case-insensitive)"
678 );
679
680 let config: Value = serde_json::from_str(&result.unwrap()).unwrap();
681 let fbom = &config["G2_CONFIG"]["CFG_FBOM"][0];
682 assert_eq!(fbom["DISPLAY_LEVEL"], 9);
683 }
684}