1use crate::error::{Result, SzConfigError};
2use crate::helpers;
3use serde_json::{Value, json};
4
5#[derive(Debug, Clone, Default)]
11pub struct AddFeatureParams<'a> {
12 pub feature: &'a str,
13 pub element_list: &'a Value,
14 pub class: Option<&'a str>,
15 pub behavior: Option<&'a str>,
16 pub candidates: Option<&'a str>,
17 pub anonymize: Option<&'a str>,
18 pub derived: Option<&'a str>,
19 pub history: Option<&'a str>,
20 pub matchkey: Option<&'a str>,
21 pub standardize: Option<&'a str>,
22 pub expression: Option<&'a str>,
23 pub comparison: Option<&'a str>,
24 pub version: Option<i64>,
25 pub rtype_id: Option<i64>,
26}
27
28impl<'a> AddFeatureParams<'a> {
29 pub fn new(feature: &'a str, element_list: &'a Value) -> Self {
30 Self {
31 feature,
32 element_list,
33 ..Default::default()
34 }
35 }
36}
37
38impl<'a> TryFrom<&'a Value> for AddFeatureParams<'a> {
39 type Error = SzConfigError;
40
41 fn try_from(json: &'a Value) -> Result<Self> {
42 let feature = json
43 .get("feature")
44 .and_then(|v| v.as_str())
45 .ok_or_else(|| SzConfigError::MissingField("feature".to_string()))?;
46
47 let element_list = json
48 .get("elementList")
49 .ok_or_else(|| SzConfigError::MissingField("elementList".to_string()))?;
50
51 Ok(Self {
52 feature,
53 element_list,
54 class: json.get("class").and_then(|v| v.as_str()),
55 behavior: json.get("behavior").and_then(|v| v.as_str()),
56 candidates: json.get("candidates").and_then(|v| v.as_str()),
57 anonymize: json.get("anonymize").and_then(|v| v.as_str()),
58 derived: json.get("derived").and_then(|v| v.as_str()),
59 history: json.get("history").and_then(|v| v.as_str()),
60 matchkey: json.get("matchKey").and_then(|v| v.as_str()),
61 standardize: json
62 .get("standardize")
63 .and_then(|v| v.as_str())
64 .filter(|s| !s.is_empty()),
65 expression: json
66 .get("expression")
67 .and_then(|v| v.as_str())
68 .filter(|s| !s.is_empty()),
69 comparison: json
70 .get("comparison")
71 .and_then(|v| v.as_str())
72 .filter(|s| !s.is_empty()),
73 version: json.get("version").and_then(|v| v.as_i64()),
74 rtype_id: json.get("rtypeId").and_then(|v| v.as_i64()),
75 })
76 }
77}
78
79#[derive(Debug, Clone, Default)]
81pub struct SetFeatureParams<'a> {
82 pub feature: &'a str,
83 pub candidates: Option<&'a str>,
84 pub anonymize: Option<&'a str>,
85 pub derived: Option<&'a str>,
86 pub history: Option<&'a str>,
87 pub matchkey: Option<&'a str>,
88 pub behavior: Option<&'a str>,
89 pub class: Option<&'a str>,
90 pub version: Option<i64>,
91 pub rtype_id: Option<i64>,
92}
93
94impl<'a> SetFeatureParams<'a> {
95 pub fn new(feature: &'a str) -> Self {
96 Self {
97 feature,
98 ..Default::default()
99 }
100 }
101}
102
103impl<'a> TryFrom<&'a Value> for SetFeatureParams<'a> {
104 type Error = SzConfigError;
105
106 fn try_from(json: &'a Value) -> Result<Self> {
107 let feature = json
108 .get("feature")
109 .and_then(|v| v.as_str())
110 .ok_or_else(|| SzConfigError::MissingField("feature".to_string()))?;
111
112 Ok(Self {
113 feature,
114 candidates: json.get("candidates").and_then(|v| v.as_str()),
115 anonymize: json.get("anonymize").and_then(|v| v.as_str()),
116 derived: json.get("derived").and_then(|v| v.as_str()),
117 history: json.get("history").and_then(|v| v.as_str()),
118 matchkey: json.get("matchKey").and_then(|v| v.as_str()),
119 behavior: json.get("behavior").and_then(|v| v.as_str()),
120 class: json.get("class").and_then(|v| v.as_str()),
121 version: json.get("version").and_then(|v| v.as_i64()),
122 rtype_id: json.get("rtypeId").and_then(|v| v.as_i64()),
123 })
124 }
125}
126
127#[derive(Debug, Clone, Default)]
129pub struct AddFeatureComparisonParams<'a> {
130 pub feature_code: Option<&'a str>,
131 pub element_code: Option<&'a str>,
132 pub exec_order: Option<i64>,
133 pub display_level: Option<i64>,
134 pub display_delim: Option<&'a str>,
135 pub derived: Option<&'a str>,
136}
137
138impl<'a> AddFeatureComparisonParams<'a> {
139 pub fn new(feature_code: &'a str, element_code: &'a str) -> Self {
140 Self {
141 feature_code: Some(feature_code),
142 element_code: Some(element_code),
143 exec_order: None,
144 display_level: None,
145 display_delim: None,
146 derived: None,
147 }
148 }
149
150 pub fn with_exec_order(mut self, order: i64) -> Self {
151 self.exec_order = Some(order);
152 self
153 }
154
155 pub fn with_display_level(mut self, level: i64) -> Self {
156 self.display_level = Some(level);
157 self
158 }
159
160 pub fn with_display_delim(mut self, delim: &'a str) -> Self {
161 self.display_delim = Some(delim);
162 self
163 }
164
165 pub fn with_derived(mut self, derived: &'a str) -> Self {
166 self.derived = Some(derived);
167 self
168 }
169}
170
171impl<'a> TryFrom<&'a Value> for AddFeatureComparisonParams<'a> {
172 type Error = SzConfigError;
173
174 fn try_from(json: &'a Value) -> Result<Self> {
175 let feature_code = json
176 .get("featureCode")
177 .and_then(|v| v.as_str())
178 .ok_or_else(|| SzConfigError::MissingField("featureCode".to_string()))?;
179
180 let element_code = json
181 .get("elementCode")
182 .and_then(|v| v.as_str())
183 .ok_or_else(|| SzConfigError::MissingField("elementCode".to_string()))?;
184
185 Ok(Self {
186 feature_code: Some(feature_code),
187 element_code: Some(element_code),
188 exec_order: json.get("execOrder").and_then(|v| v.as_i64()),
189 display_level: json.get("displayLevel").and_then(|v| v.as_i64()),
190 display_delim: json.get("displayDelim").and_then(|v| v.as_str()),
191 derived: json.get("derived").and_then(|v| v.as_str()),
192 })
193 }
194}
195
196#[derive(Debug, Clone, Default)]
198pub struct GetFeatureComparisonParams<'a> {
199 pub feature_code: Option<&'a str>,
200 pub element_code: Option<&'a str>,
201}
202
203impl<'a> GetFeatureComparisonParams<'a> {
204 pub fn new(feature_code: &'a str, element_code: &'a str) -> Self {
205 Self {
206 feature_code: Some(feature_code),
207 element_code: Some(element_code),
208 }
209 }
210}
211
212impl<'a> TryFrom<&'a Value> for GetFeatureComparisonParams<'a> {
213 type Error = SzConfigError;
214
215 fn try_from(json: &'a Value) -> Result<Self> {
216 let feature_code = json
217 .get("featureCode")
218 .and_then(|v| v.as_str())
219 .ok_or_else(|| SzConfigError::MissingField("featureCode".to_string()))?;
220
221 let element_code = json
222 .get("elementCode")
223 .and_then(|v| v.as_str())
224 .ok_or_else(|| SzConfigError::MissingField("elementCode".to_string()))?;
225
226 Ok(Self {
227 feature_code: Some(feature_code),
228 element_code: Some(element_code),
229 })
230 }
231}
232
233#[derive(Debug, Clone, Default)]
235pub struct AddFeatureDistinctCallElementParams<'a> {
236 pub feature_code: Option<&'a str>,
237 pub distinct_func_code: Option<&'a str>,
238 pub element_code: Option<&'a str>,
239 pub exec_order: Option<i64>,
240}
241
242impl<'a> AddFeatureDistinctCallElementParams<'a> {
243 pub fn new(feature_code: &'a str, distinct_func_code: &'a str) -> Self {
244 Self {
245 feature_code: Some(feature_code),
246 distinct_func_code: Some(distinct_func_code),
247 element_code: None,
248 exec_order: None,
249 }
250 }
251
252 pub fn with_element_code(mut self, element_code: &'a str) -> Self {
253 self.element_code = Some(element_code);
254 self
255 }
256
257 pub fn with_exec_order(mut self, order: i64) -> Self {
258 self.exec_order = Some(order);
259 self
260 }
261}
262
263const LOCKED_FEATURES: &[&str] = &[
265 "NAME",
266 "ADDRESS",
267 "PHONE",
268 "EMAIL",
269 "RECORD_TYPE",
270 "DATE_OF_BIRTH",
271 "NATIONAL_ID",
272 "TAX_ID",
273 "ACCT_NUM",
274 "SSN_NUM",
275 "PASSPORT_NUM",
276 "DRIVERS_LICENSE_NUM",
277];
278
279pub fn add_feature(config_json: &str, params: AddFeatureParams) -> Result<String> {
305 let mut config: Value =
306 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
307
308 let feature_upper = params.feature.to_uppercase();
309
310 let ftypes = config
312 .get("G2_CONFIG")
313 .and_then(|g| g.get("CFG_FTYPE"))
314 .and_then(|v| v.as_array())
315 .ok_or_else(|| SzConfigError::MissingSection("CFG_FTYPE".to_string()))?;
316
317 if ftypes
318 .iter()
319 .any(|f| f["FTYPE_CODE"].as_str() == Some(&feature_upper))
320 {
321 return Err(SzConfigError::AlreadyExists(format!(
322 "Feature already exists: {feature_upper}"
323 )));
324 }
325
326 let elements = params
328 .element_list
329 .as_array()
330 .ok_or_else(|| SzConfigError::InvalidInput("elementList must be an array".to_string()))?;
331
332 if elements.is_empty() {
333 return Err(SzConfigError::InvalidInput(
334 "elementList must contain at least one element".to_string(),
335 ));
336 }
337
338 let class = params.class.unwrap_or("OTHER");
340 let behavior = params.behavior.unwrap_or("FM");
341
342 let candidates_val = if let Some(val) = params.candidates {
344 let val_upper = val.to_uppercase();
345 match val_upper.as_str() {
346 "YES" => "Yes",
347 "NO" => "No",
348 _ => {
349 return Err(SzConfigError::InvalidInput(format!(
350 "Invalid CANDIDATES value '{val}'. Must be 'Yes' or 'No'"
351 )));
352 }
353 }
354 } else {
355 "No"
356 };
357
358 let anonymize_val = if let Some(val) = params.anonymize {
360 let val_upper = val.to_uppercase();
361 match val_upper.as_str() {
362 "YES" => "Yes",
363 "NO" => "No",
364 _ => {
365 return Err(SzConfigError::InvalidInput(format!(
366 "Invalid ANONYMIZE value '{val}'. Must be 'Yes' or 'No'"
367 )));
368 }
369 }
370 } else {
371 "No"
372 };
373
374 let derived_val = if let Some(val) = params.derived {
376 let val_upper = val.to_uppercase();
377 match val_upper.as_str() {
378 "YES" => "Yes",
379 "NO" => "No",
380 _ => {
381 return Err(SzConfigError::InvalidInput(format!(
382 "Invalid DERIVED value '{val}'. Must be 'Yes' or 'No'"
383 )));
384 }
385 }
386 } else {
387 "No"
388 };
389
390 let history_val = if let Some(val) = params.history {
392 let val_upper = val.to_uppercase();
393 match val_upper.as_str() {
394 "YES" => "Yes",
395 "NO" => "No",
396 _ => {
397 return Err(SzConfigError::InvalidInput(format!(
398 "Invalid HISTORY value '{val}'. Must be 'Yes' or 'No'"
399 )));
400 }
401 }
402 } else {
403 "Yes"
404 };
405
406 let matchkey_default = if params.comparison.is_some() {
408 "Yes"
409 } else {
410 "No"
411 };
412 let matchkey_val = if let Some(val) = params.matchkey {
413 let val_upper = val.to_uppercase();
414 match val_upper.as_str() {
415 "YES" => "Yes",
416 "NO" => "No",
417 "CONFIRM" => "Confirm",
418 "DENIAL" => "Denial",
419 _ => {
420 return Err(SzConfigError::InvalidInput(format!(
421 "Invalid MATCHKEY value '{val}'. Must be one of: Yes, No, Confirm, Denial"
422 )));
423 }
424 }
425 } else {
426 matchkey_default
427 };
428
429 let ftype_id = helpers::get_next_id_with_min(ftypes, "FTYPE_ID", 1000)?;
431
432 let behavior_upper = behavior.to_uppercase();
437 let (frequency, exclusivity, stability) = parse_behavior_code(&behavior_upper)?;
438
439 let fclass_array = config["G2_CONFIG"]["CFG_FCLASS"]
441 .as_array()
442 .ok_or_else(|| SzConfigError::MissingSection("CFG_FCLASS".to_string()))?;
443
444 let fclass_id = fclass_array
445 .iter()
446 .find(|c| {
447 c["FCLASS_CODE"]
448 .as_str()
449 .map(|s| s.eq_ignore_ascii_case(class))
450 .unwrap_or(false)
451 })
452 .and_then(|c| c["FCLASS_ID"].as_i64())
453 .ok_or_else(|| SzConfigError::NotFound(format!("Feature class: {class}")))?;
454
455 let sfunc_id = if let Some(func_code) = params.standardize {
457 helpers::lookup_sfunc_id(config_json, func_code)?
458 } else {
459 0
460 };
461
462 let efunc_id = if let Some(func_code) = params.expression {
463 helpers::lookup_efunc_id(config_json, func_code)?
464 } else {
465 0
466 };
467
468 let cfunc_id = if let Some(func_code) = params.comparison {
469 helpers::lookup_cfunc_id(config_json, func_code)?
470 } else {
471 0
472 };
473
474 if efunc_id > 0 || cfunc_id > 0 {
476 let mut expressed_cnt = 0;
477 let mut compared_cnt = 0;
478
479 for element_item in elements {
480 if let Some(obj) = element_item.as_object() {
481 if obj
482 .get("expressed")
483 .or_else(|| obj.get("EXPRESSED"))
484 .and_then(|v| v.as_str())
485 .map(|s| s.eq_ignore_ascii_case("yes"))
486 .unwrap_or(false)
487 {
488 expressed_cnt += 1;
489 }
490 if obj
491 .get("compared")
492 .or_else(|| obj.get("COMPARED"))
493 .and_then(|v| v.as_str())
494 .map(|s| s.eq_ignore_ascii_case("yes"))
495 .unwrap_or(false)
496 {
497 compared_cnt += 1;
498 }
499 }
500 }
501
502 if efunc_id > 0 && expressed_cnt == 0 {
503 return Err(SzConfigError::InvalidInput(
504 "No elements marked \"expressed\" for expression routine".to_string(),
505 ));
506 }
507 if cfunc_id > 0 && compared_cnt == 0 {
508 return Err(SzConfigError::InvalidInput(
509 "No elements marked \"compared\" for comparison routine".to_string(),
510 ));
511 }
512 }
513
514 let ftype_record = json!({
516 "FTYPE_ID": ftype_id,
517 "FTYPE_CODE": feature_upper.clone(),
518 "FTYPE_DESC": feature_upper.clone(),
519 "FCLASS_ID": fclass_id,
520 "FTYPE_FREQ": frequency,
521 "FTYPE_EXCL": exclusivity,
522 "FTYPE_STAB": stability,
523 "ANONYMIZE": anonymize_val,
524 "DERIVED": derived_val,
525 "USED_FOR_CAND": candidates_val,
526 "SHOW_IN_MATCH_KEY": matchkey_val,
527 "PERSIST_HISTORY": history_val,
528 "VERSION": params.version.unwrap_or(1),
529 "RTYPE_ID": params.rtype_id.unwrap_or(0)
530 });
531
532 if let Some(ftype_array) = config["G2_CONFIG"]["CFG_FTYPE"].as_array_mut() {
534 ftype_array.push(ftype_record);
535 }
536
537 if sfunc_id > 0 {
539 let sfcall_array = config["G2_CONFIG"]["CFG_SFCALL"]
540 .as_array()
541 .ok_or_else(|| SzConfigError::MissingSection("CFG_SFCALL".to_string()))?;
542 let id = helpers::get_next_id_with_min(sfcall_array, "SFCALL_ID", 1000)?;
543 let record = json!({
544 "SFCALL_ID": id,
545 "SFUNC_ID": sfunc_id,
546 "EXEC_ORDER": 1,
547 "FTYPE_ID": ftype_id,
548 "FELEM_ID": -1
549 });
550 if let Some(array) = config["G2_CONFIG"]["CFG_SFCALL"].as_array_mut() {
551 array.push(record);
552 }
553 }
554
555 let efcall_id = if efunc_id > 0 {
557 let efcall_array = config["G2_CONFIG"]["CFG_EFCALL"]
558 .as_array()
559 .ok_or_else(|| SzConfigError::MissingSection("CFG_EFCALL".to_string()))?;
560 let id = helpers::get_next_id_with_min(efcall_array, "EFCALL_ID", 1000)?;
561 let record = json!({
562 "EFCALL_ID": id,
563 "EFUNC_ID": efunc_id,
564 "EXEC_ORDER": 1,
565 "FTYPE_ID": ftype_id,
566 "FELEM_ID": -1,
567 "EFEAT_FTYPE_ID": -1,
568 "IS_VIRTUAL": "No"
569 });
570 if let Some(array) = config["G2_CONFIG"]["CFG_EFCALL"].as_array_mut() {
571 array.push(record);
572 }
573 id
574 } else {
575 0
576 };
577
578 let cfcall_id = if cfunc_id > 0 {
580 let cfcall_array = config["G2_CONFIG"]["CFG_CFCALL"]
581 .as_array()
582 .ok_or_else(|| SzConfigError::MissingSection("CFG_CFCALL".to_string()))?;
583 let id = helpers::get_next_id_with_min(cfcall_array, "CFCALL_ID", 1000)?;
584 let record = json!({
585 "CFCALL_ID": id,
586 "CFUNC_ID": cfunc_id,
587 "FTYPE_ID": ftype_id
588 });
589 if let Some(array) = config["G2_CONFIG"]["CFG_CFCALL"].as_array_mut() {
590 array.push(record);
591 }
592 id
593 } else {
594 0
595 };
596
597 let mut fbom_order = 0;
599 for element_item in elements {
600 fbom_order += 1;
601
602 let (element_code, expressed, compared, display_level, display_delim, elem_derived) =
604 if let Some(elem_str) = element_item.as_str() {
605 (
606 elem_str.to_uppercase(),
607 "No".to_string(),
608 "No".to_string(),
609 1,
610 None,
611 "No".to_string(),
612 )
613 } else if let Some(elem_obj) = element_item.as_object() {
614 let code = elem_obj
615 .get("element")
616 .or_else(|| elem_obj.get("ELEMENT"))
617 .and_then(|v| v.as_str())
618 .ok_or_else(|| {
619 SzConfigError::InvalidInput(format!(
620 "Missing element code in elementList item {fbom_order}"
621 ))
622 })?
623 .to_uppercase();
624
625 let expr = elem_obj
626 .get("expressed")
627 .or_else(|| elem_obj.get("EXPRESSED"))
628 .and_then(|v| v.as_str())
629 .unwrap_or("No")
630 .to_uppercase();
631
632 let comp = elem_obj
633 .get("compared")
634 .or_else(|| elem_obj.get("COMPARED"))
635 .and_then(|v| v.as_str())
636 .unwrap_or("No")
637 .to_uppercase();
638
639 let disp_level = if let Some(display) = elem_obj
641 .get("display")
642 .or_else(|| elem_obj.get("DISPLAY"))
643 .and_then(|v| v.as_str())
644 {
645 if display.eq_ignore_ascii_case("yes") {
646 1
647 } else {
648 0
649 }
650 } else {
651 elem_obj
652 .get("displaylevel")
653 .or_else(|| elem_obj.get("DISPLAYLEVEL"))
654 .or_else(|| elem_obj.get("display_level"))
655 .and_then(|v| v.as_i64())
656 .unwrap_or(1)
657 };
658
659 let disp_delim = elem_obj
660 .get("displaydelim")
661 .or_else(|| elem_obj.get("DISPLAYDELIM"))
662 .or_else(|| elem_obj.get("display_delim"))
663 .and_then(|v| v.as_str())
664 .map(|s| s.to_string());
665
666 let elem_deriv = elem_obj
667 .get("derived")
668 .or_else(|| elem_obj.get("DERIVED"))
669 .and_then(|v| v.as_str())
670 .map(|s| {
671 if s.eq_ignore_ascii_case("yes") {
672 "Yes"
673 } else {
674 "No"
675 }
676 .to_string()
677 })
678 .unwrap_or_else(|| "No".to_string());
679
680 (code, expr, comp, disp_level, disp_delim, elem_deriv)
681 } else {
682 return Err(SzConfigError::InvalidInput(format!(
683 "Invalid element in elementList item {fbom_order}"
684 )));
685 };
686
687 let felem_array = config["G2_CONFIG"]["CFG_FELEM"]
689 .as_array()
690 .ok_or_else(|| SzConfigError::MissingSection("CFG_FELEM".to_string()))?;
691
692 let felem_id = if let Some(felem) = felem_array
693 .iter()
694 .find(|e| e["FELEM_CODE"].as_str() == Some(element_code.as_str()))
695 {
696 felem["FELEM_ID"]
697 .as_i64()
698 .ok_or_else(|| SzConfigError::InvalidStructure("Invalid FELEM_ID".to_string()))?
699 } else {
700 let new_id = helpers::get_next_id_with_min(felem_array, "FELEM_ID", 1000)?;
702 let new_element = json!({
703 "FELEM_ID": new_id,
704 "FELEM_CODE": element_code.clone(),
705 "FELEM_DESC": element_code.clone(),
706 "DATA_TYPE": "string",
707 "TOKENIZE": "No"
708 });
709 if let Some(array) = config["G2_CONFIG"]["CFG_FELEM"].as_array_mut() {
710 array.push(new_element);
711 }
712 new_id
713 };
714
715 if efcall_id > 0 && expressed.eq_ignore_ascii_case("yes") {
717 let record = json!({
718 "EFCALL_ID": efcall_id,
719 "EXEC_ORDER": fbom_order,
720 "FTYPE_ID": ftype_id,
721 "FELEM_ID": felem_id,
722 "FELEM_REQ": "Yes"
723 });
724 if let Some(array) = config["G2_CONFIG"]["CFG_EFBOM"].as_array_mut() {
725 array.push(record);
726 }
727 }
728
729 if cfcall_id > 0 && compared.eq_ignore_ascii_case("yes") {
731 let record = json!({
732 "CFCALL_ID": cfcall_id,
733 "EXEC_ORDER": fbom_order,
734 "FTYPE_ID": ftype_id,
735 "FELEM_ID": felem_id
736 });
737 if let Some(array) = config["G2_CONFIG"]["CFG_CFBOM"].as_array_mut() {
738 array.push(record);
739 }
740 }
741
742 let mut fbom_record = json!({
744 "FTYPE_ID": ftype_id,
745 "FELEM_ID": felem_id,
746 "EXEC_ORDER": fbom_order,
747 "DISPLAY_LEVEL": display_level,
748 "DERIVED": elem_derived
749 });
750
751 fbom_record["DISPLAY_DELIM"] = match display_delim {
752 Some(delim) => json!(delim),
753 None => Value::Null,
754 };
755
756 if let Some(array) = config["G2_CONFIG"]["CFG_FBOM"].as_array_mut() {
757 array.push(fbom_record);
758 }
759 }
760
761 serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
762}
763
764pub fn delete_feature(config_json: &str, feature_code_or_id: &str) -> Result<String> {
777 let mut config: Value =
778 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
779
780 let ftype_id = if let Ok(id) = feature_code_or_id.trim().parse::<i64>() {
782 let ftypes = config["G2_CONFIG"]["CFG_FTYPE"]
784 .as_array()
785 .ok_or_else(|| SzConfigError::MissingSection("CFG_FTYPE".to_string()))?;
786
787 if !ftypes.iter().any(|f| f["FTYPE_ID"].as_i64() == Some(id)) {
788 return Err(SzConfigError::NotFound(format!("Feature: {id}")));
789 }
790 id
791 } else {
792 lookup_feature_id(&config, feature_code_or_id)?
793 };
794
795 let feature_code = config["G2_CONFIG"]["CFG_FTYPE"]
797 .as_array()
798 .and_then(|arr| {
799 arr.iter()
800 .find(|f| f["FTYPE_ID"].as_i64() == Some(ftype_id))
801 .and_then(|f| f["FTYPE_CODE"].as_str())
802 })
803 .ok_or_else(|| SzConfigError::NotFound(format!("Feature: {ftype_id}")))?
804 .to_string();
805
806 if LOCKED_FEATURES
808 .iter()
809 .any(|&locked| locked.eq_ignore_ascii_case(&feature_code))
810 {
811 return Err(SzConfigError::InvalidInput(format!(
812 "The feature {feature_code} cannot be deleted (it is a protected system feature)"
813 )));
814 }
815
816 if let Some(fbom_array) = config["G2_CONFIG"]["CFG_FBOM"].as_array_mut() {
818 fbom_array.retain(|record| record["FTYPE_ID"].as_i64() != Some(ftype_id));
819 }
820
821 if let Some(attr_array) = config["G2_CONFIG"]["CFG_ATTR"].as_array_mut() {
823 attr_array.retain(|record| {
824 record["FTYPE_CODE"]
825 .as_str()
826 .map(|s| !s.eq_ignore_ascii_case(&feature_code))
827 .unwrap_or(true)
828 });
829 }
830
831 if let Some(sfcall_array) = config["G2_CONFIG"]["CFG_SFCALL"].as_array_mut() {
833 sfcall_array.retain(|record| record["FTYPE_ID"].as_i64() != Some(ftype_id));
834 }
835
836 let efcall_ids: Vec<i64> = config["G2_CONFIG"]["CFG_EFCALL"]
838 .as_array()
839 .map(|arr| {
840 arr.iter()
841 .filter(|call| call["FTYPE_ID"].as_i64() == Some(ftype_id))
842 .filter_map(|call| call["EFCALL_ID"].as_i64())
843 .collect()
844 })
845 .unwrap_or_default();
846
847 if let Some(efbom_array) = config["G2_CONFIG"]["CFG_EFBOM"].as_array_mut() {
848 efbom_array.retain(|record| {
849 record["EFCALL_ID"]
850 .as_i64()
851 .map(|id| !efcall_ids.contains(&id))
852 .unwrap_or(true)
853 });
854 }
855
856 if let Some(efcall_array) = config["G2_CONFIG"]["CFG_EFCALL"].as_array_mut() {
857 efcall_array.retain(|record| record["FTYPE_ID"].as_i64() != Some(ftype_id));
858 }
859
860 let cfcall_ids: Vec<i64> = config["G2_CONFIG"]["CFG_CFCALL"]
862 .as_array()
863 .map(|arr| {
864 arr.iter()
865 .filter(|call| call["FTYPE_ID"].as_i64() == Some(ftype_id))
866 .filter_map(|call| call["CFCALL_ID"].as_i64())
867 .collect()
868 })
869 .unwrap_or_default();
870
871 if let Some(cfbom_array) = config["G2_CONFIG"]["CFG_CFBOM"].as_array_mut() {
872 cfbom_array.retain(|record| {
873 record["CFCALL_ID"]
874 .as_i64()
875 .map(|id| !cfcall_ids.contains(&id))
876 .unwrap_or(true)
877 });
878 }
879
880 if let Some(cfcall_array) = config["G2_CONFIG"]["CFG_CFCALL"].as_array_mut() {
881 cfcall_array.retain(|record| record["FTYPE_ID"].as_i64() != Some(ftype_id));
882 }
883
884 let dfcall_ids: Vec<i64> = config["G2_CONFIG"]["CFG_DFCALL"]
886 .as_array()
887 .map(|arr| {
888 arr.iter()
889 .filter(|call| call["FTYPE_ID"].as_i64() == Some(ftype_id))
890 .filter_map(|call| call["DFCALL_ID"].as_i64())
891 .collect()
892 })
893 .unwrap_or_default();
894
895 if let Some(dfbom_array) = config["G2_CONFIG"]["CFG_DFBOM"].as_array_mut() {
896 dfbom_array.retain(|record| {
897 record["DFCALL_ID"]
898 .as_i64()
899 .map(|id| !dfcall_ids.contains(&id))
900 .unwrap_or(true)
901 });
902 }
903
904 if let Some(dfcall_array) = config["G2_CONFIG"]["CFG_DFCALL"].as_array_mut() {
905 dfcall_array.retain(|record| record["FTYPE_ID"].as_i64() != Some(ftype_id));
906 }
907
908 if let Some(ftype_array) = config["G2_CONFIG"]["CFG_FTYPE"].as_array_mut() {
910 ftype_array.retain(|record| record["FTYPE_ID"].as_i64() != Some(ftype_id));
911 }
912
913 serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
914}
915
916pub fn get_feature(config_json: &str, feature_code_or_id: &str) -> Result<Value> {
925 let config: Value =
926 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
927
928 let ftype = if let Ok(id) = feature_code_or_id.trim().parse::<i64>() {
930 config["G2_CONFIG"]["CFG_FTYPE"]
931 .as_array()
932 .and_then(|arr| arr.iter().find(|f| f["FTYPE_ID"].as_i64() == Some(id)))
933 .ok_or_else(|| SzConfigError::NotFound(format!("Feature: {id}")))?
934 } else {
935 let code_upper = feature_code_or_id.to_uppercase();
936 config["G2_CONFIG"]["CFG_FTYPE"]
937 .as_array()
938 .and_then(|arr| {
939 arr.iter()
940 .find(|f| f["FTYPE_CODE"].as_str() == Some(code_upper.as_str()))
941 })
942 .ok_or_else(|| SzConfigError::NotFound(format!("Feature: {code_upper}")))?
943 };
944
945 build_feature_json(&config, ftype)
946}
947
948pub fn list_features(config_json: &str) -> Result<Vec<Value>> {
956 let config: Value =
957 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
958
959 let ftype_array = config["G2_CONFIG"]["CFG_FTYPE"]
960 .as_array()
961 .ok_or_else(|| SzConfigError::MissingSection("CFG_FTYPE".to_string()))?;
962
963 let mut result: Vec<Value> = ftype_array
964 .iter()
965 .map(|ftype| build_feature_json(&config, ftype))
966 .collect::<Result<Vec<_>>>()?;
967
968 result.sort_by_key(|item| item["id"].as_i64().unwrap_or(0));
970
971 Ok(result)
972}
973
974pub fn set_feature(config_json: &str, params: SetFeatureParams) -> Result<String> {
998 let mut config: Value =
999 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
1000
1001 let ftype_id = if let Ok(id) = params.feature.trim().parse::<i64>() {
1003 id
1004 } else {
1005 lookup_feature_id(&config, params.feature)?
1006 };
1007
1008 let ftypes = config["G2_CONFIG"]["CFG_FTYPE"]
1009 .as_array_mut()
1010 .ok_or_else(|| SzConfigError::MissingSection("CFG_FTYPE".to_string()))?;
1011
1012 let ftype = ftypes
1013 .iter_mut()
1014 .find(|f| f["FTYPE_ID"].as_i64() == Some(ftype_id))
1015 .ok_or_else(|| SzConfigError::NotFound("Feature not found".to_string()))?;
1016
1017 let mut changes_made = false;
1019
1020 if let Some(val) = params.candidates {
1022 let normalized = validate_and_normalize_domain(val, &["Yes", "No"], "CANDIDATES")?;
1024 if ftype["USED_FOR_CAND"].as_str() != Some(normalized.as_str()) {
1025 ftype["USED_FOR_CAND"] = json!(normalized);
1026 changes_made = true;
1027 }
1028 }
1029 if let Some(val) = params.anonymize {
1030 if ftype["ANONYMIZE"].as_str() != Some(val) {
1031 ftype["ANONYMIZE"] = json!(val);
1032 changes_made = true;
1033 }
1034 }
1035 if let Some(val) = params.derived {
1036 if ftype["DERIVED"].as_str() != Some(val) {
1037 ftype["DERIVED"] = json!(val);
1038 changes_made = true;
1039 }
1040 }
1041 if let Some(val) = params.history {
1042 if ftype["PERSIST_HISTORY"].as_str() != Some(val) {
1043 ftype["PERSIST_HISTORY"] = json!(val);
1044 changes_made = true;
1045 }
1046 }
1047 if let Some(val) = params.matchkey {
1048 let normalized =
1050 validate_and_normalize_domain(val, &["Yes", "No", "Confirm", "Denial"], "MATCHKEY")?;
1051 if ftype["SHOW_IN_MATCH_KEY"].as_str() != Some(normalized.as_str()) {
1052 ftype["SHOW_IN_MATCH_KEY"] = json!(normalized);
1053 changes_made = true;
1054 }
1055 }
1056 if let Some(val) = params.version {
1057 if ftype["VERSION"].as_i64() != Some(val) {
1058 ftype["VERSION"] = json!(val);
1059 changes_made = true;
1060 }
1061 }
1062 if let Some(val) = params.rtype_id {
1063 if ftype["RTYPE_ID"].as_i64() != Some(val) {
1064 ftype["RTYPE_ID"] = json!(val);
1065 changes_made = true;
1066 }
1067 }
1068
1069 if let Some(behavior_code) = params.behavior {
1071 let (frequency, exclusivity, stability) = parse_behavior_code(behavior_code)?;
1072 let freq_changed = ftype["FTYPE_FREQ"].as_str() != Some(frequency);
1073 let excl_changed = ftype["FTYPE_EXCL"].as_str() != Some(exclusivity);
1074 let stab_changed = ftype["FTYPE_STAB"].as_str() != Some(stability);
1075 if freq_changed || excl_changed || stab_changed {
1076 ftype["FTYPE_FREQ"] = json!(frequency);
1077 ftype["FTYPE_EXCL"] = json!(exclusivity);
1078 ftype["FTYPE_STAB"] = json!(stability);
1079 changes_made = true;
1080 }
1081 }
1082
1083 if let Some(class_name) = params.class {
1085 let config_for_lookup: Value = serde_json::from_str(config_json)
1087 .map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
1088
1089 let fclass_array = config_for_lookup["G2_CONFIG"]["CFG_FCLASS"]
1090 .as_array()
1091 .ok_or_else(|| SzConfigError::MissingSection("CFG_FCLASS".to_string()))?;
1092
1093 let fclass_id = fclass_array
1094 .iter()
1095 .find(|c| {
1096 c["FCLASS_CODE"]
1097 .as_str()
1098 .map(|s| s.eq_ignore_ascii_case(class_name))
1099 .unwrap_or(false)
1100 })
1101 .and_then(|c| c["FCLASS_ID"].as_i64())
1102 .ok_or_else(|| SzConfigError::NotFound(format!("Feature class: {class_name}")))?;
1103
1104 if ftype["FCLASS_ID"].as_i64() != Some(fclass_id) {
1105 ftype["FCLASS_ID"] = json!(fclass_id);
1106 changes_made = true;
1107 }
1108 }
1109
1110 if !changes_made {
1112 return Err(SzConfigError::InvalidInput(
1113 "No changes detected".to_string(),
1114 ));
1115 }
1116
1117 serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
1118}
1119
1120fn validate_and_normalize_domain(value: &str, domain: &[&str], field_name: &str) -> Result<String> {
1122 let value_upper = value.to_uppercase();
1123 for valid_value in domain {
1124 if valid_value.to_uppercase() == value_upper {
1125 return Ok(valid_value.to_string());
1126 }
1127 }
1128 Err(SzConfigError::InvalidInput(format!(
1129 "{field_name} value must be in {domain:?}"
1130 )))
1131}
1132
1133pub fn build_feature_json(config: &Value, ftype: &Value) -> Result<Value> {
1137 let empty_array = vec![];
1138
1139 let fclass_array = config["G2_CONFIG"]["CFG_FCLASS"]
1140 .as_array()
1141 .unwrap_or(&empty_array);
1142 let sfcall_array = config["G2_CONFIG"]["CFG_SFCALL"]
1143 .as_array()
1144 .unwrap_or(&empty_array);
1145 let efcall_array = config["G2_CONFIG"]["CFG_EFCALL"]
1146 .as_array()
1147 .unwrap_or(&empty_array);
1148 let cfcall_array = config["G2_CONFIG"]["CFG_CFCALL"]
1149 .as_array()
1150 .unwrap_or(&empty_array);
1151 let sfunc_array = config["G2_CONFIG"]["CFG_SFUNC"]
1152 .as_array()
1153 .unwrap_or(&empty_array);
1154 let efunc_array = config["G2_CONFIG"]["CFG_EFUNC"]
1155 .as_array()
1156 .unwrap_or(&empty_array);
1157 let cfunc_array = config["G2_CONFIG"]["CFG_CFUNC"]
1158 .as_array()
1159 .unwrap_or(&empty_array);
1160 let felem_array = config["G2_CONFIG"]["CFG_FELEM"]
1161 .as_array()
1162 .unwrap_or(&empty_array);
1163 let fbom_array = config["G2_CONFIG"]["CFG_FBOM"]
1164 .as_array()
1165 .unwrap_or(&empty_array);
1166 let efbom_array = config["G2_CONFIG"]["CFG_EFBOM"]
1167 .as_array()
1168 .unwrap_or(&empty_array);
1169 let cfbom_array = config["G2_CONFIG"]["CFG_CFBOM"]
1170 .as_array()
1171 .unwrap_or(&empty_array);
1172
1173 let ftype_id = ftype["FTYPE_ID"].as_i64().unwrap_or(0);
1174 let fclass_id = ftype["FCLASS_ID"].as_i64().unwrap_or(0);
1175
1176 let class_name = fclass_array
1178 .iter()
1179 .find(|fc| fc["FCLASS_ID"].as_i64() == Some(fclass_id))
1180 .and_then(|fc| fc["FCLASS_CODE"].as_str())
1181 .unwrap_or("OTHER")
1182 .to_string();
1183
1184 let behavior = compute_behavior(ftype);
1186
1187 let standardize = sfcall_array
1189 .iter()
1190 .filter(|sc| sc["FTYPE_ID"].as_i64() == Some(ftype_id))
1191 .min_by_key(|sc| sc["EXEC_ORDER"].as_i64().unwrap_or(0))
1192 .and_then(|sc| sc["SFUNC_ID"].as_i64())
1193 .and_then(|sfunc_id| {
1194 sfunc_array
1195 .iter()
1196 .find(|sf| sf["SFUNC_ID"].as_i64() == Some(sfunc_id))
1197 })
1198 .and_then(|sf| sf["SFUNC_CODE"].as_str())
1199 .unwrap_or("")
1200 .to_string();
1201
1202 let efcall = efcall_array
1204 .iter()
1205 .filter(|ec| ec["FTYPE_ID"].as_i64() == Some(ftype_id))
1206 .min_by_key(|ec| ec["EXEC_ORDER"].as_i64().unwrap_or(0));
1207
1208 let expression = efcall
1209 .and_then(|ec| ec["EFUNC_ID"].as_i64())
1210 .and_then(|efunc_id| {
1211 efunc_array
1212 .iter()
1213 .find(|ef| ef["EFUNC_ID"].as_i64() == Some(efunc_id))
1214 })
1215 .and_then(|ef| ef["EFUNC_CODE"].as_str())
1216 .unwrap_or("")
1217 .to_string();
1218
1219 let cfcall = cfcall_array
1221 .iter()
1222 .filter(|cc| cc["FTYPE_ID"].as_i64() == Some(ftype_id))
1223 .min_by_key(|cc| cc["CFCALL_ID"].as_i64().unwrap_or(0));
1224
1225 let comparison = cfcall
1226 .and_then(|cc| cc["CFUNC_ID"].as_i64())
1227 .and_then(|cfunc_id| {
1228 cfunc_array
1229 .iter()
1230 .find(|cf| cf["CFUNC_ID"].as_i64() == Some(cfunc_id))
1231 })
1232 .and_then(|cf| cf["CFUNC_CODE"].as_str())
1233 .unwrap_or("")
1234 .to_string();
1235
1236 let mut element_list: Vec<(i64, Value)> = fbom_array
1238 .iter()
1239 .filter(|fbom| fbom["FTYPE_ID"].as_i64() == Some(ftype_id))
1240 .map(|fbom| {
1241 let felem_id = fbom["FELEM_ID"].as_i64().unwrap_or(0);
1242 let exec_order = fbom["EXEC_ORDER"].as_i64().unwrap_or(0);
1243
1244 let element_code = felem_array
1245 .iter()
1246 .find(|fe| fe["FELEM_ID"].as_i64() == Some(felem_id))
1247 .and_then(|fe| fe["FELEM_CODE"].as_str())
1248 .unwrap_or("")
1249 .to_string();
1250
1251 let expressed = efcall
1252 .and_then(|ec| ec["EFCALL_ID"].as_i64())
1253 .map(|efcall_id| {
1254 efbom_array.iter().any(|efbom| {
1255 efbom["EFCALL_ID"].as_i64() == Some(efcall_id)
1256 && efbom["FTYPE_ID"].as_i64() == Some(ftype_id)
1257 && efbom["FELEM_ID"].as_i64() == Some(felem_id)
1258 })
1259 })
1260 .unwrap_or(false);
1261
1262 let compared = cfcall
1263 .and_then(|cc| cc["CFCALL_ID"].as_i64())
1264 .map(|cfcall_id| {
1265 cfbom_array.iter().any(|cfbom| {
1266 cfbom["CFCALL_ID"].as_i64() == Some(cfcall_id)
1267 && cfbom["FTYPE_ID"].as_i64() == Some(ftype_id)
1268 && cfbom["FELEM_ID"].as_i64() == Some(felem_id)
1269 })
1270 })
1271 .unwrap_or(false);
1272
1273 let derived = fbom["DERIVED"].as_str().unwrap_or("No");
1274 let display_level = fbom["DISPLAY_LEVEL"].as_i64().unwrap_or(1);
1275 let display = if display_level == 0 { "No" } else { "Yes" };
1276
1277 (
1278 exec_order,
1279 json!({
1280 "element": element_code,
1281 "expressed": if expressed { "Yes" } else { "No" },
1282 "compared": if compared { "Yes" } else { "No" },
1283 "derived": derived,
1284 "display": display
1285 }),
1286 )
1287 })
1288 .collect();
1289
1290 element_list.sort_by_key(|(order, _)| *order);
1291 let element_list: Vec<Value> = element_list.into_iter().map(|(_, v)| v).collect();
1292
1293 Ok(json!({
1294 "id": ftype_id,
1295 "feature": ftype["FTYPE_CODE"].as_str().unwrap_or(""),
1296 "class": class_name,
1297 "behavior": behavior,
1298 "anonymize": ftype["ANONYMIZE"].as_str().unwrap_or(""),
1299 "candidates": ftype["USED_FOR_CAND"].as_str().unwrap_or(""),
1300 "standardize": standardize,
1301 "expression": expression,
1302 "comparison": comparison,
1303 "matchKey": ftype["SHOW_IN_MATCH_KEY"].as_str().unwrap_or(""),
1304 "version": ftype["VERSION"].as_i64().unwrap_or(0),
1305 "elementList": element_list
1306 }))
1307}
1308
1309fn parse_behavior_code(behavior: &str) -> Result<(&'static str, &'static str, &'static str)> {
1314 let mut code = behavior.to_uppercase();
1315 let mut exclusivity = "No";
1316 let mut stability = "No";
1317
1318 if code != "NAME" && code != "NONE" {
1320 if code.contains('E') {
1321 exclusivity = "Yes";
1322 code = code.replace('E', "");
1323 }
1324 if code.contains('S') {
1325 stability = "Yes";
1326 code = code.replace('S', "");
1327 }
1328 }
1329
1330 let frequency: &'static str = match code.as_str() {
1332 "A1" => "A1",
1333 "F1" => "F1",
1334 "FF" => "FF",
1335 "FM" => "FM",
1336 "FVM" => "FVM",
1337 "NONE" => "NONE",
1338 "NAME" => "NAME",
1339 _ => {
1340 return Err(SzConfigError::InvalidInput(format!(
1341 "Invalid behavior code '{behavior}'. Valid codes: A1, F1, FF, FM, FVM, NONE, NAME (with optional E/S suffixes)"
1342 )));
1343 }
1344 };
1345
1346 Ok((frequency, exclusivity, stability))
1347}
1348
1349fn compute_behavior(ftype: &Value) -> String {
1350 let freq = ftype["FTYPE_FREQ"].as_str().unwrap_or("");
1351 let excl = ftype["FTYPE_EXCL"].as_str().unwrap_or("");
1352 let stab = ftype["FTYPE_STAB"].as_str().unwrap_or("");
1353
1354 let mut behavior = freq.to_string();
1355 if excl.to_uppercase() == "Y" || excl == "1" || excl.to_uppercase() == "YES" {
1356 behavior.push('E');
1357 }
1358 if stab.to_uppercase() == "Y" || stab == "1" || stab.to_uppercase() == "YES" {
1359 behavior.push('S');
1360 }
1361 behavior
1362}
1363
1364fn lookup_feature_id(config: &Value, feature_code: &str) -> Result<i64> {
1365 let code_upper = feature_code.to_uppercase();
1366 config["G2_CONFIG"]["CFG_FTYPE"]
1367 .as_array()
1368 .and_then(|arr| {
1369 arr.iter()
1370 .find(|f| f["FTYPE_CODE"].as_str() == Some(code_upper.as_str()))
1371 })
1372 .and_then(|f| f["FTYPE_ID"].as_i64())
1373 .ok_or_else(|| SzConfigError::NotFound("Feature not found".to_string()))
1374}
1375
1376#[allow(dead_code)]
1377fn lookup_sfunc_id(config: &Value, func_code: &str) -> Result<i64> {
1378 let code_upper = func_code.to_uppercase();
1379 config["G2_CONFIG"]["CFG_SFUNC"]
1380 .as_array()
1381 .and_then(|arr| {
1382 arr.iter()
1383 .find(|f| f["SFUNC_CODE"].as_str() == Some(code_upper.as_str()))
1384 })
1385 .and_then(|f| f["SFUNC_ID"].as_i64())
1386 .ok_or_else(|| SzConfigError::NotFound(format!("Standardize function: {code_upper}")))
1387}
1388
1389#[allow(dead_code)]
1390fn lookup_efunc_id(config: &Value, func_code: &str) -> Result<i64> {
1391 let code_upper = func_code.to_uppercase();
1392 config["G2_CONFIG"]["CFG_EFUNC"]
1393 .as_array()
1394 .and_then(|arr| {
1395 arr.iter()
1396 .find(|f| f["EFUNC_CODE"].as_str() == Some(code_upper.as_str()))
1397 })
1398 .and_then(|f| f["EFUNC_ID"].as_i64())
1399 .ok_or_else(|| SzConfigError::NotFound(format!("Expression function: {code_upper}")))
1400}
1401
1402#[allow(dead_code)]
1403fn lookup_cfunc_id(config: &Value, func_code: &str) -> Result<i64> {
1404 let code_upper = func_code.to_uppercase();
1405 config["G2_CONFIG"]["CFG_CFUNC"]
1406 .as_array()
1407 .and_then(|arr| {
1408 arr.iter()
1409 .find(|f| f["CFUNC_CODE"].as_str() == Some(code_upper.as_str()))
1410 })
1411 .and_then(|f| f["CFUNC_ID"].as_i64())
1412 .ok_or_else(|| SzConfigError::NotFound(format!("Comparison function: {code_upper}")))
1413}
1414
1415pub fn add_feature_comparison(
1424 config_json: &str,
1425 params: AddFeatureComparisonParams,
1426) -> Result<String> {
1427 let feature_code = params
1428 .feature_code
1429 .ok_or_else(|| SzConfigError::MissingField("feature_code".to_string()))?;
1430 let element_code = params
1431 .element_code
1432 .ok_or_else(|| SzConfigError::MissingField("element_code".to_string()))?;
1433
1434 let ftype_id = helpers::lookup_feature_id(config_json, feature_code)?;
1435 let felem_id = helpers::lookup_element_id(config_json, element_code)?;
1436
1437 let config: Value =
1438 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
1439
1440 let fbom_array = config["G2_CONFIG"]["CFG_FBOM"]
1442 .as_array()
1443 .ok_or_else(|| SzConfigError::MissingSection("CFG_FBOM".to_string()))?;
1444
1445 if fbom_array.iter().any(|item| {
1446 item["FTYPE_ID"].as_i64() == Some(ftype_id) && item["FELEM_ID"].as_i64() == Some(felem_id)
1447 }) {
1448 return Err(SzConfigError::AlreadyExists(format!(
1449 "Feature comparison: {:?}+{:?}",
1450 params.feature_code, params.element_code
1451 )));
1452 }
1453
1454 let mut record = json!({
1456 "FTYPE_ID": ftype_id,
1457 "FELEM_ID": felem_id,
1458 });
1459
1460 record["EXEC_ORDER"] = match params.exec_order {
1461 Some(order) => json!(order),
1462 None => Value::Null,
1463 };
1464 record["DISPLAY_LEVEL"] = match params.display_level {
1465 Some(level) => json!(level),
1466 None => Value::Null,
1467 };
1468 record["DISPLAY_DELIM"] = match params.display_delim {
1469 Some(delim) => json!(delim),
1470 None => Value::Null,
1471 };
1472 record["DERIVED"] = match params.derived {
1473 Some(der) => json!(der),
1474 None => Value::Null,
1475 };
1476
1477 helpers::add_to_config_array(config_json, "CFG_FBOM", record)
1478}
1479
1480pub fn delete_feature_comparison(
1490 config_json: &str,
1491 feature_code: &str,
1492 element_code: &str,
1493) -> Result<String> {
1494 let ftype_id = helpers::lookup_feature_id(config_json, feature_code)?;
1495 let felem_id = helpers::lookup_element_id(config_json, element_code)?;
1496
1497 let mut config: Value =
1498 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
1499
1500 let mut found = false;
1501
1502 if let Some(fbom_array) = config["G2_CONFIG"]["CFG_FBOM"].as_array_mut() {
1503 fbom_array.retain(|item| {
1504 let matches = item["FTYPE_ID"].as_i64() == Some(ftype_id)
1505 && item["FELEM_ID"].as_i64() == Some(felem_id);
1506 if matches {
1507 found = true;
1508 }
1509 !matches
1510 });
1511 }
1512
1513 if !found {
1514 return Err(SzConfigError::NotFound(format!(
1515 "Feature comparison: FTYPE_ID={ftype_id}, FELEM_ID={felem_id}"
1516 )));
1517 }
1518
1519 serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
1520}
1521
1522pub fn get_feature_comparison(
1531 config_json: &str,
1532 params: GetFeatureComparisonParams,
1533) -> Result<Value> {
1534 let feature_code = params
1535 .feature_code
1536 .ok_or_else(|| SzConfigError::MissingField("feature_code".to_string()))?;
1537 let element_code = params
1538 .element_code
1539 .ok_or_else(|| SzConfigError::MissingField("element_code".to_string()))?;
1540
1541 let ftype_id = helpers::lookup_feature_id(config_json, feature_code)?;
1542 let felem_id = helpers::lookup_element_id(config_json, element_code)?;
1543
1544 let config: Value =
1545 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
1546
1547 let fbom_array = config["G2_CONFIG"]["CFG_FBOM"]
1548 .as_array()
1549 .ok_or_else(|| SzConfigError::MissingSection("CFG_FBOM".to_string()))?;
1550
1551 fbom_array
1552 .iter()
1553 .find(|item| {
1554 item["FTYPE_ID"].as_i64() == Some(ftype_id)
1555 && item["FELEM_ID"].as_i64() == Some(felem_id)
1556 })
1557 .cloned()
1558 .ok_or_else(|| {
1559 SzConfigError::NotFound(format!(
1560 "Feature comparison: {:?}+{:?}",
1561 params.feature_code, params.element_code
1562 ))
1563 })
1564}
1565
1566pub fn list_feature_comparisons(config_json: &str) -> Result<Vec<Value>> {
1574 let config: Value =
1575 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
1576
1577 let fbom_array = config["G2_CONFIG"]["CFG_FBOM"]
1578 .as_array()
1579 .ok_or_else(|| SzConfigError::MissingSection("CFG_FBOM".to_string()))?;
1580
1581 let mut result: Vec<Value> = fbom_array.to_vec();
1582
1583 result.sort_by(|a, b| {
1585 let a_ftype = a["FTYPE_ID"].as_i64().unwrap_or(0);
1586 let b_ftype = b["FTYPE_ID"].as_i64().unwrap_or(0);
1587 let a_exec = a["EXEC_ORDER"].as_i64().unwrap_or(0);
1588 let b_exec = b["EXEC_ORDER"].as_i64().unwrap_or(0);
1589 (a_ftype, a_exec).cmp(&(b_ftype, b_exec))
1590 });
1591
1592 Ok(result)
1593}
1594
1595pub fn add_feature_comparison_element(
1604 config_json: &str,
1605 params: AddFeatureComparisonParams,
1606) -> Result<String> {
1607 add_feature_comparison(config_json, params)
1608}
1609
1610pub fn delete_feature_comparison_element(
1620 config_json: &str,
1621 feature_code: &str,
1622 element_code: &str,
1623) -> Result<String> {
1624 delete_feature_comparison(config_json, feature_code, element_code)
1625}
1626
1627pub fn add_feature_distinct_call_element(
1639 config_json: &str,
1640 params: AddFeatureDistinctCallElementParams,
1641) -> Result<String> {
1642 let feature_code = params
1643 .feature_code
1644 .ok_or_else(|| SzConfigError::MissingField("feature_code".to_string()))?;
1645 let distinct_func_code = params
1646 .distinct_func_code
1647 .ok_or_else(|| SzConfigError::MissingField("distinct_func_code".to_string()))?;
1648
1649 let ftype_id = helpers::lookup_feature_id(config_json, feature_code)?;
1650 let dfunc_id = helpers::lookup_dfunc_id(config_json, distinct_func_code)?;
1651 let felem_id = if let Some(code) = params.element_code {
1652 helpers::lookup_element_id(config_json, code)?
1653 } else {
1654 -1
1655 };
1656
1657 let config: Value =
1658 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
1659
1660 let dfcall_array = config["G2_CONFIG"]["CFG_DFCALL"]
1662 .as_array()
1663 .ok_or_else(|| SzConfigError::MissingSection("CFG_DFCALL".to_string()))?;
1664
1665 if dfcall_array.iter().any(|item| {
1666 item["FTYPE_ID"].as_i64() == Some(ftype_id)
1667 && item["DFUNC_ID"].as_i64() == Some(dfunc_id)
1668 && item["FELEM_ID"].as_i64() == Some(felem_id)
1669 }) {
1670 return Err(SzConfigError::AlreadyExists(format!(
1671 "Feature distinct call element: {:?}+{:?}",
1672 params.feature_code, params.distinct_func_code
1673 )));
1674 }
1675
1676 let dfcall_id = helpers::get_next_id_with_min(dfcall_array, "DFCALL_ID", 1000)?;
1678
1679 let mut record = json!({
1681 "DFCALL_ID": dfcall_id,
1682 "FTYPE_ID": ftype_id,
1683 "DFUNC_ID": dfunc_id,
1684 "FELEM_ID": felem_id,
1685 });
1686
1687 record["EXEC_ORDER"] = match params.exec_order {
1688 Some(order) => json!(order),
1689 None => Value::Null,
1690 };
1691
1692 helpers::add_to_config_array(config_json, "CFG_DFCALL", record)
1693}
1694
1695pub fn list_feature_classes(config_json: &str) -> Result<Vec<Value>> {
1703 let config: Value =
1704 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
1705
1706 let fclass_array = config["G2_CONFIG"]["CFG_FCLASS"]
1707 .as_array()
1708 .ok_or_else(|| SzConfigError::MissingSection("CFG_FCLASS".to_string()))?;
1709
1710 let mut result: Vec<Value> = fclass_array.to_vec();
1711
1712 result.sort_by_key(|item| item["FCLASS_ID"].as_i64().unwrap_or(0));
1714
1715 Ok(result)
1716}
1717
1718pub fn get_feature_class(config_json: &str, fclass_id_or_code: &str) -> Result<Value> {
1727 let config: Value =
1728 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
1729
1730 let fclass_array = config["G2_CONFIG"]["CFG_FCLASS"]
1731 .as_array()
1732 .ok_or_else(|| SzConfigError::MissingSection("CFG_FCLASS".to_string()))?;
1733
1734 if let Ok(id) = fclass_id_or_code.trim().parse::<i64>() {
1736 fclass_array
1737 .iter()
1738 .find(|item| item["FCLASS_ID"].as_i64() == Some(id))
1739 .cloned()
1740 .ok_or_else(|| SzConfigError::NotFound(format!("Feature class: {id}")))
1741 } else {
1742 let code_upper = fclass_id_or_code.to_uppercase();
1744 fclass_array
1745 .iter()
1746 .find(|item| item["FCLASS_CODE"].as_str() == Some(code_upper.as_str()))
1747 .cloned()
1748 .ok_or_else(|| SzConfigError::NotFound(format!("Feature class: {code_upper}")))
1749 }
1750}
1751
1752pub fn update_feature_version(config_json: &str, version: &str) -> Result<String> {
1761 let mut config: Value =
1762 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
1763
1764 let compat_version = config["G2_CONFIG"]["CONFIG_BASE_VERSION"]["COMPATIBILITY_VERSION"]
1766 .as_object_mut()
1767 .ok_or_else(|| SzConfigError::MissingSection("COMPATIBILITY_VERSION".to_string()))?;
1768
1769 compat_version.insert("FEATURE_VERSION".to_string(), json!(version));
1770
1771 serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
1772}