Skip to main content

sz_configtool_lib/
elements.rs

1use crate::error::{Result, SzConfigError};
2use crate::helpers;
3use serde_json::{Value, json};
4
5// ============================================================================
6// Parameter Structs
7// ============================================================================
8
9/// Parameters for adding an element
10#[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/// Parameters for setting (updating) an element
37#[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/// Parameters for setting a feature element
64#[derive(Debug, Clone, Default)]
65pub struct SetFeatureElementParams<'a> {
66    /// Feature code (e.g., "NAME", "ADDRESS")
67    pub feature_code: Option<&'a str>,
68
69    /// Element code (e.g., "FIRST_NAME", "FULL_NAME")
70    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    /// Create new params using feature and element codes
80    ///
81    /// # Example
82    /// ```no_run
83    /// use sz_configtool_lib::elements::SetFeatureElementParams;
84    ///
85    /// let params = SetFeatureElementParams::new("NAME", "FIRST_NAME")
86    ///     .with_display_level(1);
87    /// ```
88    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    /// Set execution order
100    pub fn with_exec_order(mut self, order: i64) -> Self {
101        self.exec_order = Some(order);
102        self
103    }
104
105    /// Set display level
106    pub fn with_display_level(mut self, level: i64) -> Self {
107        self.display_level = Some(level);
108        self
109    }
110
111    /// Set display delimiter
112    pub fn with_display_delim(mut self, delim: &'a str) -> Self {
113        self.display_delim = Some(delim);
114        self
115    }
116
117    /// Set derived flag
118    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
149/// Add a new element (CFG_FELEM record)
150///
151/// # Arguments
152/// * `config_json` - JSON configuration string
153/// * `params` - Element parameters (code required, others optional)
154///
155/// # Returns
156/// Modified configuration JSON string
157pub 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    // Check if already exists
164    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    // Get next ID
178    let felem_id = helpers::get_next_id_with_min(felem_array, "FELEM_ID", 1000)?;
179
180    // Validate and normalize datatype (Python lines 1974-1981)
181    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" // Default
197    };
198
199    // Validate and normalize tokenized (Python lines 1983-1986)
200    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" // Default
213    };
214
215    // Build record from params (Python parity: TOKENIZE not TOKENIZED)
216    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
234/// Delete an element (CFG_FELEM record)
235///
236/// # Arguments
237/// * `config_json` - JSON configuration string
238/// * `felem_code` - Element code
239///
240/// # Returns
241/// Modified configuration JSON string
242///
243/// # Errors
244/// - `NotFound` if element doesn't exist
245/// - `InvalidInput` if element is linked to features (Python parity: linkage check)
246pub 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    // Find element to get its ID for linkage check
253    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    // Check linkage - prevent deletion if element is used in any features (Python line 2068-2074)
267    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    // Safe to delete - get mutable array
292    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    // Remove from array
306    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
313/// Get a specific element by code
314///
315/// # Arguments
316/// * `config_json` - JSON configuration string
317/// * `felem_code` - Element code
318///
319/// # Returns
320/// JSON Value representing the element
321pub 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    // Format to display format with lowercase fields (matching list_elements and Python parity)
337    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
344/// List all elements
345///
346/// # Arguments
347/// * `config_json` - JSON configuration string
348///
349/// # Returns
350/// Vector of JSON Values representing elements with id, element, and datatype fields, sorted by FELEM_ID
351pub 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    // Sort by element code (alphabetic) like Python
371    result.sort_by_key(|e| e["element"].as_str().unwrap_or("").to_string());
372
373    Ok(result)
374}
375
376/// Set (update) an element's properties
377///
378/// # Arguments
379/// * `config_json` - JSON configuration string
380/// * `params` - Element parameters (code required to identify, others optional to update)
381///
382/// # Returns
383/// Modified configuration JSON string
384pub 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    // Find and update the element
395    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    // Update fields from params
401    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
416/// Set feature element (update FBOM record)
417///
418/// This function updates feature-to-element mappings in CFG_FBOM.
419///
420/// # Arguments
421/// * `config_json` - JSON configuration string
422/// * `params` - Feature element parameters (feature_code and element_code required; updates optional)
423///
424/// # Returns
425/// Modified configuration JSON string
426///
427/// # Example
428/// ```no_run
429/// use sz_configtool_lib::elements::{set_feature_element, SetFeatureElementParams};
430///
431/// let config = r#"{ ... }"#;
432/// let params = SetFeatureElementParams::new("NAME", "FIRST_NAME")
433///     .with_display_level(1);
434/// let updated = set_feature_element(&config, params)?;
435/// # Ok::<(), sz_configtool_lib::error::SzConfigError>(())
436/// ```
437pub fn set_feature_element(config_json: &str, params: SetFeatureElementParams) -> Result<String> {
438    // Resolve codes to IDs
439    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    // Find the FBOM record
457    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    // Update fields if provided (with validation for Python parity)
470    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        // Validate derived domain (Python lines 2192-2198)
481        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
497/// Set feature element display level
498///
499/// # Arguments
500/// * `config_json` - JSON configuration string
501/// * `feature_code` - Feature code (e.g., "NAME", "ADDRESS")
502/// * `element_code` - Element code (e.g., "FIRST_NAME", "FULL_NAME")
503/// * `display_level` - Display level value
504///
505/// # Returns
506/// Modified configuration JSON string
507///
508/// # Example
509/// ```no_run
510/// use sz_configtool_lib::elements::set_feature_element_display_level;
511///
512/// let config = r#"{ ... }"#;
513/// let updated = set_feature_element_display_level(&config, "NAME", "FIRST_NAME", 1)?;
514/// # Ok::<(), sz_configtool_lib::error::SzConfigError>(())
515/// ```
516pub 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
528/// Set feature element derived flag
529///
530/// # Arguments
531/// * `config_json` - JSON configuration string
532/// * `feature_code` - Feature code (e.g., "NAME", "ADDRESS")
533/// * `element_code` - Element code (e.g., "FIRST_NAME", "FULL_NAME")
534/// * `derived` - Derived flag value ("Yes" or "No")
535///
536/// # Returns
537/// Modified configuration JSON string
538///
539/// # Example
540/// ```no_run
541/// use sz_configtool_lib::elements::set_feature_element_derived;
542///
543/// let config = r#"{ ... }"#;
544/// let updated = set_feature_element_derived(&config, "NAME", "FIRST_NAME", "Yes")?;
545/// # Ok::<(), sz_configtool_lib::error::SzConfigError>(())
546/// ```
547pub 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        // Test new code-based API
585        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        // Test with all optional parameters
598        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        // Test error with invalid feature code
618        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        // Test error with invalid element code
627        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        // Test error when FBOM mapping doesn't exist
636        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        // Test code-based convenience function
648        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        // Test code-based convenience function
660        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        // Test that codes are case-insensitive (helpers use eq_ignore_ascii_case)
672        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}