Skip to main content

sz_configtool_lib/
attributes.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 attribute
10#[derive(Debug, Clone)]
11pub struct AddAttributeParams<'a> {
12    pub attribute: &'a str,
13    pub feature: &'a str,
14    pub element: &'a str,
15    pub class: &'a str,
16    pub default_value: Option<&'a str>,
17    pub internal: Option<&'a str>,
18    pub required: Option<&'a str>,
19}
20
21impl<'a> TryFrom<&'a Value> for AddAttributeParams<'a> {
22    type Error = SzConfigError;
23
24    fn try_from(json: &'a Value) -> Result<Self> {
25        Ok(Self {
26            attribute: json
27                .get("attribute")
28                .and_then(|v| v.as_str())
29                .ok_or_else(|| SzConfigError::MissingField("attribute".to_string()))?,
30            feature: json
31                .get("feature")
32                .and_then(|v| v.as_str())
33                .ok_or_else(|| SzConfigError::MissingField("feature".to_string()))?,
34            element: json
35                .get("element")
36                .and_then(|v| v.as_str())
37                .ok_or_else(|| SzConfigError::MissingField("element".to_string()))?,
38            class: json
39                .get("class")
40                .and_then(|v| v.as_str())
41                .ok_or_else(|| SzConfigError::MissingField("class".to_string()))?,
42            default_value: json.get("default").and_then(|v| v.as_str()),
43            internal: json.get("internal").and_then(|v| v.as_str()),
44            required: json.get("required").and_then(|v| v.as_str()),
45        })
46    }
47}
48
49/// Parameters for setting an attribute
50#[derive(Debug, Clone, Default)]
51pub struct SetAttributeParams<'a> {
52    pub attribute: &'a str,
53    pub internal: Option<&'a str>,
54    pub required: Option<&'a str>,
55    pub default_value: Option<&'a str>,
56}
57
58impl<'a> TryFrom<&'a Value> for SetAttributeParams<'a> {
59    type Error = SzConfigError;
60
61    fn try_from(json: &'a Value) -> Result<Self> {
62        Ok(Self {
63            attribute: json
64                .get("attribute")
65                .and_then(|v| v.as_str())
66                .ok_or_else(|| SzConfigError::MissingField("attribute".to_string()))?,
67            internal: json.get("internal").and_then(|v| v.as_str()),
68            required: json.get("required").and_then(|v| v.as_str()),
69            default_value: json.get("default").and_then(|v| v.as_str()),
70        })
71    }
72}
73
74/// Add a new attribute to the configuration
75///
76/// # Arguments
77/// * `config_json` - JSON configuration string
78/// * `params` - Attribute parameters (attribute, feature, element, class required; others optional)
79///
80/// # Returns
81/// Tuple of (modified_json, new_attribute_value) - returns both the modified config
82/// and the newly created attribute for display purposes
83///
84/// # Errors
85/// - `AlreadyExists` if attribute code already exists
86/// - `InvalidInput` if attribute class is invalid
87/// - `JsonParse` if config_json is invalid
88/// - `MissingSection` if required sections don't exist
89pub fn add_attribute(config_json: &str, params: AddAttributeParams) -> Result<(String, Value)> {
90    let config: Value =
91        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
92
93    // Validate attribute class (matches Python line 173-181)
94    let valid_classes = [
95        "NAME",
96        "ATTRIBUTE",
97        "IDENTIFIER",
98        "ADDRESS",
99        "PHONE",
100        "RELATIONSHIP",
101        "OTHER",
102    ];
103    if !valid_classes.contains(&params.class) {
104        return Err(SzConfigError::InvalidInput(format!(
105            "Invalid attribute class '{}'. Must be one of: {}",
106            params.class,
107            valid_classes.join(", ")
108        )));
109    }
110
111    let attribute_upper = params.attribute.to_uppercase();
112    let feature_upper = params.feature.to_uppercase();
113    let element_upper = params.element.to_uppercase();
114
115    // Check if attribute already exists
116    let attrs = config
117        .get("G2_CONFIG")
118        .and_then(|g| g.get("CFG_ATTR"))
119        .and_then(|v| v.as_array())
120        .ok_or_else(|| SzConfigError::MissingSection("CFG_ATTR".to_string()))?;
121
122    if attrs
123        .iter()
124        .any(|attr| attr["ATTR_CODE"].as_str() == Some(&attribute_upper))
125    {
126        return Err(SzConfigError::AlreadyExists(format!(
127            "Attribute: {attribute_upper}"
128        )));
129    }
130
131    // Validate feature exists (Python parity)
132    let _ftype_id = helpers::lookup_feature_id(config_json, &feature_upper)?;
133
134    // Validate element exists (Python parity)
135    let _felem_id = helpers::lookup_element_id(config_json, &element_upper)?;
136
137    // Validate REQUIRED domain (Python parity: ["Yes", "No", "Any", "Desired"])
138    let required = if let Some(req) = params.required {
139        let req_upper = req.to_uppercase();
140        match req_upper.as_str() {
141            "YES" => "Yes",
142            "NO" => "No",
143            "ANY" => "Any",
144            "DESIRED" => "Desired",
145            _ => {
146                return Err(SzConfigError::InvalidInput(format!(
147                    "Invalid REQUIRED value '{req}'. Must be one of: Yes, No, Any, Desired"
148                )));
149            }
150        }
151    } else {
152        "No"
153    };
154
155    // Validate INTERNAL domain (Python parity: ["Yes", "No"])
156    let internal = if let Some(int) = params.internal {
157        let int_upper = int.to_uppercase();
158        match int_upper.as_str() {
159            "YES" => "Yes",
160            "NO" => "No",
161            _ => {
162                return Err(SzConfigError::InvalidInput(format!(
163                    "Invalid INTERNAL value '{int}'. Must be 'Yes' or 'No'"
164                )));
165            }
166        }
167    } else {
168        "No"
169    };
170
171    // Get next ATTR_ID
172    let next_attr_id = helpers::get_next_id_from_array(attrs, "ATTR_ID")?;
173
174    // Create CFG_ATTR entry (matching Python lines 2342-2350)
175    let new_attribute = json!({
176        "ATTR_ID": next_attr_id,
177        "ATTR_CODE": attribute_upper.clone(),
178        "ATTR_CLASS": params.class,
179        "FTYPE_CODE": feature_upper,  // Use actual feature code, not Null
180        "FELEM_CODE": element_upper,  // Use actual element code, not Null
181        "FELEM_REQ": required,
182        "DEFAULT_VALUE": params.default_value.map(|v| json!(v)).unwrap_or(Value::Null),
183        "INTERNAL": internal
184    });
185
186    // Add to CFG_ATTR only (Python does not create FBOM in addAttribute)
187    let modified_json =
188        helpers::add_to_config_array(config_json, "CFG_ATTR", new_attribute.clone())?;
189
190    Ok((modified_json, new_attribute))
191}
192
193/// Delete an attribute from the configuration
194///
195/// # Arguments
196/// * `config_json` - JSON configuration string
197/// * `code` - Attribute code to delete
198///
199/// # Returns
200/// Modified configuration JSON string
201///
202/// # Errors
203/// - `NotFound` if attribute doesn't exist
204/// - `JsonParse` if config_json is invalid
205/// - `MissingSection` if CFG_ATTR section doesn't exist
206pub fn delete_attribute(config_json: &str, code: &str) -> Result<String> {
207    helpers::delete_from_config_array(config_json, "CFG_ATTR", "ATTR_CODE", &code.to_uppercase())
208}
209
210/// Get a specific attribute by code
211///
212/// # Arguments
213/// * `config_json` - JSON configuration string
214/// * `code` - Attribute code to retrieve
215///
216/// # Returns
217/// JSON Value representing the attribute
218///
219/// # Errors
220/// - `NotFound` if attribute doesn't exist
221/// - `JsonParse` if config_json is invalid
222/// - `MissingSection` if CFG_ATTR section doesn't exist
223pub fn get_attribute(config_json: &str, code: &str) -> Result<Value> {
224    let config: Value =
225        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
226
227    let code_upper = code.to_uppercase();
228    config
229        .get("G2_CONFIG")
230        .and_then(|g| g.get("CFG_ATTR"))
231        .and_then(|v| v.as_array())
232        .ok_or_else(|| SzConfigError::MissingSection("CFG_ATTR".to_string()))?
233        .iter()
234        .find(|attr| attr["ATTR_CODE"].as_str() == Some(&code_upper))
235        .cloned()
236        .ok_or_else(|| SzConfigError::NotFound(format!("Attribute not found: {code_upper}")))
237}
238
239/// List all attributes
240///
241/// # Arguments
242/// * `config_json` - JSON configuration string
243///
244/// # Returns
245/// Vector of JSON Values representing attributes in Python format
246///
247/// # Errors
248/// - `JsonParse` if config_json is invalid
249/// - `MissingSection` if CFG_ATTR section doesn't exist
250pub fn list_attributes(config_json: &str) -> Result<Vec<Value>> {
251    let config: Value =
252        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
253
254    let attrs = config
255        .get("G2_CONFIG")
256        .and_then(|g| g.get("CFG_ATTR"))
257        .and_then(|v| v.as_array())
258        .ok_or_else(|| SzConfigError::MissingSection("CFG_ATTR".to_string()))?;
259
260    Ok(attrs
261        .iter()
262        .map(|item| {
263            json!({
264                "id": item.get("ATTR_ID").and_then(|v| v.as_i64()).unwrap_or(0),
265                "attribute": item.get("ATTR_CODE").and_then(|v| v.as_str()).unwrap_or(""),
266                "class": item.get("ATTR_CLASS").and_then(|v| v.as_str()).unwrap_or(""),
267                "feature": item.get("FTYPE_CODE").cloned().unwrap_or(Value::Null),
268                "element": item.get("FELEM_CODE").cloned().unwrap_or(Value::Null),
269                "required": item.get("FELEM_REQ").and_then(|v| v.as_str()).unwrap_or(""),
270                "default": item.get("DEFAULT_VALUE").cloned().unwrap_or(Value::Null),
271                "internal": item.get("INTERNAL").and_then(|v| v.as_str()).unwrap_or("")
272            })
273        })
274        .collect())
275}
276
277/// Set (update) an attribute's properties
278///
279/// # Arguments
280/// * `config_json` - JSON configuration string
281/// * `code` - Attribute code to update
282/// * `updates` - JSON Value with fields to update
283///
284/// # Returns
285/// Modified configuration JSON string
286///
287/// # Errors
288/// - `NotFound` if attribute doesn't exist
289/// - `JsonParse` if config_json is invalid
290/// - `MissingSection` if CFG_ATTR section doesn't exist
291pub fn set_attribute(config_json: &str, params: SetAttributeParams) -> Result<String> {
292    let mut config: Value =
293        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
294
295    let code_upper = params.attribute.to_uppercase();
296    let attrs = config
297        .get_mut("G2_CONFIG")
298        .and_then(|g| g.get_mut("CFG_ATTR"))
299        .and_then(|v| v.as_array_mut())
300        .ok_or_else(|| SzConfigError::MissingSection("CFG_ATTR".to_string()))?;
301
302    let attr = attrs
303        .iter_mut()
304        .find(|a| a["ATTR_CODE"].as_str() == Some(&code_upper))
305        .ok_or_else(|| SzConfigError::NotFound(format!("Attribute not found: {code_upper}")))?;
306
307    // Update fields if provided (with domain validation)
308    if let Some(val) = params.internal {
309        // Validate INTERNAL domain (Python parity: ["Yes", "No"])
310        let val_upper = val.to_uppercase();
311        let validated = match val_upper.as_str() {
312            "YES" => "Yes",
313            "NO" => "No",
314            _ => {
315                return Err(SzConfigError::InvalidInput(format!(
316                    "Invalid INTERNAL value '{val}'. Must be 'Yes' or 'No'"
317                )));
318            }
319        };
320        attr["INTERNAL"] = json!(validated);
321    }
322    if let Some(val) = params.required {
323        // Validate REQUIRED domain (Python parity: ["Yes", "No", "Any", "Desired"])
324        let val_upper = val.to_uppercase();
325        let validated = match val_upper.as_str() {
326            "YES" => "Yes",
327            "NO" => "No",
328            "ANY" => "Any",
329            "DESIRED" => "Desired",
330            _ => {
331                return Err(SzConfigError::InvalidInput(format!(
332                    "Invalid REQUIRED value '{val}'. Must be one of: Yes, No, Any, Desired"
333                )));
334            }
335        };
336        attr["FELEM_REQ"] = json!(validated);
337    }
338    if let Some(val) = params.default_value {
339        attr["DEFAULT_VALUE"] = json!(val);
340    }
341
342    serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
343}