Skip to main content

sz_configtool_lib/calls/
standardize.rs

1//! Standardize call management operations
2//!
3//! Functions for managing CFG_SFCALL (standardize calls) and CFG_SBOM
4//! (standardize bill of materials) configuration sections.
5
6use crate::error::{Result, SzConfigError};
7use crate::helpers::{
8    find_in_config_array, get_next_id, lookup_element_id, lookup_feature_id, lookup_sfunc_id,
9};
10use serde_json::{Value, json};
11
12// ============================================================================
13// Parameter Structs
14// ============================================================================
15
16/// Parameters for adding a standardize call
17#[derive(Debug, Clone)]
18pub struct AddStandardizeCallParams<'a> {
19    pub ftype_code: Option<&'a str>,
20    pub felem_code: Option<&'a str>,
21    pub exec_order: Option<i64>,
22    pub sfunc_code: &'a str,
23}
24
25impl<'a> AddStandardizeCallParams<'a> {
26    pub fn new(sfunc_code: &'a str) -> Self {
27        Self {
28            ftype_code: None,
29            felem_code: None,
30            exec_order: None,
31            sfunc_code,
32        }
33    }
34}
35
36/// Parameters for adding a standardize call element
37#[derive(Debug, Clone)]
38pub struct AddStandardizeCallElementParams {
39    pub ftype_id: i64,
40    pub sfunc_id: i64,
41    pub felem_id: Option<i64>,
42    pub exec_order: Option<i64>,
43}
44
45/// Parameters for setting (updating) a standardize call
46#[derive(Debug, Clone, Default)]
47pub struct SetStandardizeCallParams {
48    pub sfcall_id: i64,
49    pub exec_order: Option<i64>,
50}
51
52impl TryFrom<&Value> for SetStandardizeCallParams {
53    type Error = SzConfigError;
54
55    fn try_from(json: &Value) -> Result<Self> {
56        let sfcall_id = json
57            .get("sfcallId")
58            .and_then(|v| v.as_i64())
59            .ok_or_else(|| SzConfigError::MissingField("sfcallId".to_string()))?;
60
61        Ok(Self {
62            sfcall_id,
63            exec_order: json.get("execOrder").and_then(|v| v.as_i64()),
64        })
65    }
66}
67
68/// Parameters for deleting a standardize call element
69#[derive(Debug, Clone)]
70pub struct DeleteStandardizeCallElementParams {
71    pub ftype_id: i64,
72    pub sfunc_id: i64,
73    pub felem_id: Option<i64>,
74}
75
76/// Parameters for setting a standardize call element
77#[derive(Debug, Clone)]
78pub struct SetStandardizeCallElementParams {
79    pub ftype_id: i64,
80    pub sfunc_id: i64,
81    pub felem_id: Option<i64>,
82    pub updates: Value,
83}
84
85/// Add a new standardize call
86///
87/// Creates a new standardize call linking a function to a feature or element
88/// with an execution order.
89///
90/// # Arguments
91/// * `config` - Configuration JSON string
92/// * `params` - Call parameters (ftype_code, felem_code, exec_order, sfunc_code)
93///
94/// # Returns
95/// Tuple of (modified_config, new_sfcall_record)
96///
97/// # Errors
98/// - `InvalidParameter` if both ftype_code and felem_code are specified or both missing
99/// - `Duplicate` if exec_order is already taken for the feature/element
100/// - `NotFound` if function/feature/element codes don't exist
101pub fn add_standardize_call(
102    config: &str,
103    params: AddStandardizeCallParams,
104) -> Result<(String, Value)> {
105    let mut config_data: Value =
106        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
107
108    // Get next SFCALL_ID (seed at 1000 for user-created calls)
109    let sfcall_id = get_next_id(&config_data, "G2_CONFIG.CFG_SFCALL", "SFCALL_ID", 1000)?;
110
111    // Lookup function ID
112    let sfunc_id = lookup_sfunc_id(config, params.sfunc_code)?;
113
114    // Determine FTYPE_ID and FELEM_ID (-1 means not specified)
115    let mut ftype_id: i64 = -1;
116    let mut felem_id: i64 = -1;
117
118    if let Some(feature) = params.ftype_code.filter(|f| !f.eq_ignore_ascii_case("ALL")) {
119        ftype_id = lookup_feature_id(config, feature)?;
120    }
121
122    if let Some(element) = params.felem_code.filter(|e| !e.eq_ignore_ascii_case("N/A")) {
123        felem_id = lookup_element_id(config, element)?;
124    }
125
126    // Validate: exactly one of (feature, element) must be specified
127    if (ftype_id > 0 && felem_id > 0) || (ftype_id < 0 && felem_id < 0) {
128        return Err(SzConfigError::InvalidInput(
129            "Either a feature or an element must be specified, but not both".to_string(),
130        ));
131    }
132
133    // Determine exec_order: use provided value or get next available for this feature/element
134    let final_exec_order = if let Some(order) = params.exec_order {
135        // Check if this exec_order is already taken for this feature/element
136        let order_taken = config_data["G2_CONFIG"]["CFG_SFCALL"]
137            .as_array()
138            .map(|arr| {
139                arr.iter().any(|call| {
140                    call["FTYPE_ID"].as_i64() == Some(ftype_id)
141                        && call["FELEM_ID"].as_i64() == Some(felem_id)
142                        && call["EXEC_ORDER"].as_i64() == Some(order)
143                })
144            })
145            .unwrap_or(false);
146
147        if order_taken {
148            return Err(SzConfigError::AlreadyExists(format!(
149                "Execution order {order} already taken for this feature/element"
150            )));
151        }
152        order
153    } else {
154        // Get next available exec_order for this feature/element combination
155        config_data["G2_CONFIG"]["CFG_SFCALL"]
156            .as_array()
157            .map(|arr| {
158                arr.iter()
159                    .filter(|call| {
160                        call["FTYPE_ID"].as_i64() == Some(ftype_id)
161                            && call["FELEM_ID"].as_i64() == Some(felem_id)
162                    })
163                    .filter_map(|call| call["EXEC_ORDER"].as_i64())
164                    .max()
165                    .map(|max| max + 1)
166                    .unwrap_or(1)
167            })
168            .unwrap_or(1)
169    };
170
171    // Create new CFG_SFCALL record
172    let new_record = json!({
173        "SFCALL_ID": sfcall_id,
174        "FTYPE_ID": ftype_id,
175        "FELEM_ID": felem_id,
176        "SFUNC_ID": sfunc_id,
177        "EXEC_ORDER": final_exec_order
178    });
179
180    // Add to config
181    if let Some(sfcall_array) = config_data["G2_CONFIG"]["CFG_SFCALL"].as_array_mut() {
182        sfcall_array.push(new_record.clone());
183    } else {
184        return Err(SzConfigError::MissingSection("CFG_SFCALL".to_string()));
185    }
186
187    let modified_config =
188        serde_json::to_string(&config_data).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
189
190    Ok((modified_config, new_record))
191}
192
193/// Delete a standardize call by ID
194///
195/// # Arguments
196/// * `config` - Configuration JSON string
197/// * `sfcall_id` - Standardize call ID to delete
198///
199/// # Returns
200/// Modified configuration JSON string
201///
202/// # Errors
203/// - `NotFound` if call ID doesn't exist
204pub fn delete_standardize_call(config: &str, sfcall_id: i64) -> Result<String> {
205    let mut config_data: Value =
206        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
207
208    // Validate that the call exists
209    let call_exists = config_data["G2_CONFIG"]["CFG_SFCALL"]
210        .as_array()
211        .map(|arr| {
212            arr.iter()
213                .any(|call| call["SFCALL_ID"].as_i64() == Some(sfcall_id))
214        })
215        .unwrap_or(false);
216
217    if !call_exists {
218        return Err(SzConfigError::NotFound(format!(
219            "Standardize call ID {sfcall_id}"
220        )));
221    }
222
223    // Delete the standardize call
224    if let Some(sfcall_array) = config_data["G2_CONFIG"]["CFG_SFCALL"].as_array_mut() {
225        sfcall_array.retain(|record| record["SFCALL_ID"].as_i64() != Some(sfcall_id));
226    }
227
228    serde_json::to_string(&config_data).map_err(|e| SzConfigError::JsonParse(e.to_string()))
229}
230
231/// Get a single standardize call by ID
232///
233/// # Arguments
234/// * `config` - Configuration JSON string
235/// * `sfcall_id` - Standardize call ID
236///
237/// # Returns
238/// JSON Value representing the standardize call record
239///
240/// # Errors
241/// - `NotFound` if call ID doesn't exist
242pub fn get_standardize_call(config: &str, sfcall_id: i64) -> Result<Value> {
243    find_in_config_array(config, "CFG_SFCALL", "SFCALL_ID", &sfcall_id.to_string())?
244        .ok_or_else(|| SzConfigError::NotFound(format!("Standardize call ID {sfcall_id}")))
245}
246
247/// List all standardize calls with resolved names
248///
249/// Returns all standardize calls with feature, element, and function codes resolved.
250///
251/// # Arguments
252/// * `config` - Configuration JSON string
253///
254/// # Returns
255/// Vector of JSON Values with resolved names (id, feature, element, execOrder, function)
256pub fn list_standardize_calls(config: &str) -> Result<Vec<Value>> {
257    let config_data: Value =
258        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
259
260    let empty_array = vec![];
261    let sfcall_array = config_data
262        .get("G2_CONFIG")
263        .and_then(|g| g.get("CFG_SFCALL"))
264        .and_then(|v| v.as_array())
265        .unwrap_or(&empty_array);
266
267    let ftype_array = config_data
268        .get("G2_CONFIG")
269        .and_then(|g| g.get("CFG_FTYPE"))
270        .and_then(|v| v.as_array())
271        .unwrap_or(&empty_array);
272
273    let felem_array = config_data
274        .get("G2_CONFIG")
275        .and_then(|g| g.get("CFG_FELEM"))
276        .and_then(|v| v.as_array())
277        .unwrap_or(&empty_array);
278
279    let sfunc_array = config_data
280        .get("G2_CONFIG")
281        .and_then(|g| g.get("CFG_SFUNC"))
282        .and_then(|v| v.as_array())
283        .unwrap_or(&empty_array);
284
285    // Helper functions for ID resolution
286    let resolve_ftype = |ftype_id: i64| -> String {
287        if ftype_id <= 0 {
288            "all".to_string()
289        } else {
290            ftype_array
291                .iter()
292                .find(|ft| ft.get("FTYPE_ID").and_then(|v| v.as_i64()) == Some(ftype_id))
293                .and_then(|ft| ft.get("FTYPE_CODE"))
294                .and_then(|v| v.as_str())
295                .unwrap_or("all")
296                .to_string()
297        }
298    };
299
300    let resolve_felem = |felem_id: i64| -> String {
301        if felem_id <= 0 {
302            "n/a".to_string()
303        } else {
304            felem_array
305                .iter()
306                .find(|fe| fe.get("FELEM_ID").and_then(|v| v.as_i64()) == Some(felem_id))
307                .and_then(|fe| fe.get("FELEM_CODE"))
308                .and_then(|v| v.as_str())
309                .unwrap_or("n/a")
310                .to_string()
311        }
312    };
313
314    let resolve_sfunc = |sfunc_id: i64| -> String {
315        sfunc_array
316            .iter()
317            .find(|sf| sf.get("SFUNC_ID").and_then(|v| v.as_i64()) == Some(sfunc_id))
318            .and_then(|sf| sf.get("SFUNC_CODE"))
319            .and_then(|v| v.as_str())
320            .unwrap_or("unknown")
321            .to_string()
322    };
323
324    // Transform standardize calls
325    let items: Vec<Value> = sfcall_array
326        .iter()
327        .map(|item| {
328            let ftype_id = item.get("FTYPE_ID").and_then(|v| v.as_i64()).unwrap_or(0);
329            let felem_id = item.get("FELEM_ID").and_then(|v| v.as_i64()).unwrap_or(0);
330            let sfunc_id = item.get("SFUNC_ID").and_then(|v| v.as_i64()).unwrap_or(0);
331
332            json!({
333                "id": item.get("SFCALL_ID").and_then(|v| v.as_i64()).unwrap_or(0),
334                "feature": resolve_ftype(ftype_id),
335                "element": resolve_felem(felem_id),
336                "execOrder": item.get("EXEC_ORDER").and_then(|v| v.as_i64()).unwrap_or(0),
337                "function": resolve_sfunc(sfunc_id)
338            })
339        })
340        .collect();
341
342    Ok(items)
343}
344
345/// Update a standardize call (stub - not implemented in Python)
346///
347/// # Arguments
348/// * `config` - Configuration JSON string
349/// * `params` - Standardize call parameters (sfcall_id required, others optional to update)
350///
351/// # Returns
352/// Modified configuration JSON string
353pub fn set_standardize_call(config: &str, _params: SetStandardizeCallParams) -> Result<String> {
354    // This is a stub - the Python version doesn't implement this
355    Ok(config.to_string())
356}
357
358/// Add a standardize call element (SBOM record)
359///
360/// Creates a new standardize bill of materials entry.
361///
362/// # Arguments
363/// * `config` - Configuration JSON string
364/// * `params` - Element parameters (ftype_id, sfunc_id, felem_id, exec_order)
365///
366/// # Returns
367/// Tuple of (modified_config, new_sbom_record)
368pub fn add_standardize_call_element(
369    config: &str,
370    params: AddStandardizeCallElementParams,
371) -> Result<(String, Value)> {
372    let mut config_data: Value =
373        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
374
375    let final_felem_id = params.felem_id.unwrap_or(-1);
376
377    // Check if call element already exists
378    if let Some(sfcall_array) = config_data
379        .get("G2_CONFIG")
380        .and_then(|g| g.get("CFG_SFCALL"))
381        .and_then(|v| v.as_array())
382    {
383        for item in sfcall_array {
384            if item.get("FTYPE_ID").and_then(|v| v.as_i64()) == Some(params.ftype_id)
385                && item.get("SFUNC_ID").and_then(|v| v.as_i64()) == Some(params.sfunc_id)
386                && item.get("FELEM_ID").and_then(|v| v.as_i64()) == Some(final_felem_id)
387            {
388                return Err(SzConfigError::AlreadyExists(
389                    "Standardize call element already exists".to_string(),
390                ));
391            }
392        }
393    }
394
395    // Get next SFCALL_ID
396    let sfcall_id = get_next_id(&config_data, "G2_CONFIG.CFG_SFCALL", "SFCALL_ID", 1000)?;
397
398    // Create new call element record
399    let mut new_record = json!({
400        "SFCALL_ID": sfcall_id,
401        "FTYPE_ID": params.ftype_id,
402        "FELEM_ID": final_felem_id,
403        "SFUNC_ID": params.sfunc_id,
404    });
405
406    // Add exec_order (always present, null when not specified)
407    new_record["EXEC_ORDER"] = match params.exec_order {
408        Some(order) => json!(order),
409        None => Value::Null,
410    };
411
412    // Add to CFG_SFCALL
413    if let Some(sfcall_array) = config_data["G2_CONFIG"]["CFG_SFCALL"].as_array_mut() {
414        sfcall_array.push(new_record.clone());
415    } else {
416        return Err(SzConfigError::MissingSection("CFG_SFCALL".to_string()));
417    }
418
419    let modified_config =
420        serde_json::to_string(&config_data).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
421
422    Ok((modified_config, new_record))
423}
424
425/// Delete a standardize call element
426///
427/// # Arguments
428/// * `config` - Configuration JSON string
429/// * `params` - Element parameters (ftype_id, sfunc_id, felem_id)
430///
431/// # Returns
432/// Modified configuration JSON string
433pub fn delete_standardize_call_element(
434    config: &str,
435    params: DeleteStandardizeCallElementParams,
436) -> Result<String> {
437    let mut config_data: Value =
438        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
439
440    let final_felem_id = params.felem_id.unwrap_or(-1);
441
442    // Validate that the element exists
443    let element_exists = config_data["G2_CONFIG"]["CFG_SFCALL"]
444        .as_array()
445        .map(|arr| {
446            arr.iter().any(|item| {
447                item.get("FTYPE_ID").and_then(|v| v.as_i64()) == Some(params.ftype_id)
448                    && item.get("SFUNC_ID").and_then(|v| v.as_i64()) == Some(params.sfunc_id)
449                    && item.get("FELEM_ID").and_then(|v| v.as_i64()) == Some(final_felem_id)
450            })
451        })
452        .unwrap_or(false);
453
454    if !element_exists {
455        return Err(SzConfigError::NotFound(
456            "Standardize call element not found".to_string(),
457        ));
458    }
459
460    // Delete the element
461    if let Some(sfcall_array) = config_data["G2_CONFIG"]["CFG_SFCALL"].as_array_mut() {
462        sfcall_array.retain(|item| {
463            !(item.get("FTYPE_ID").and_then(|v| v.as_i64()) == Some(params.ftype_id)
464                && item.get("SFUNC_ID").and_then(|v| v.as_i64()) == Some(params.sfunc_id)
465                && item.get("FELEM_ID").and_then(|v| v.as_i64()) == Some(final_felem_id))
466        });
467    }
468
469    serde_json::to_string(&config_data).map_err(|e| SzConfigError::JsonParse(e.to_string()))
470}
471
472/// Update a standardize call element (stub - not typically used)
473///
474/// # Arguments
475/// * `config` - Configuration JSON string
476/// * `params` - Element parameters including updates
477///
478/// # Returns
479/// Modified configuration JSON string
480pub fn set_standardize_call_element(
481    config: &str,
482    _params: SetStandardizeCallElementParams,
483) -> Result<String> {
484    // This is a stub - not commonly used
485    Ok(config.to_string())
486}