Skip to main content

sz_configtool_lib/calls/
comparison.rs

1//! Comparison call management operations
2//!
3//! Functions for managing CFG_CFCALL (comparison calls) and CFG_CFBOM
4//! (comparison bill of materials) configuration sections.
5
6use crate::error::{Result, SzConfigError};
7use crate::helpers::{
8    find_in_config_array, get_next_id, lookup_cfunc_id, lookup_element_id, lookup_feature_id,
9};
10use serde_json::{Value, json};
11
12// ============================================================================
13// Parameter Structs
14// ============================================================================
15
16/// Parameters for adding a comparison call
17#[derive(Debug, Clone)]
18pub struct AddComparisonCallParams {
19    pub ftype_code: String,
20    pub cfunc_code: String,
21    pub element_list: Vec<String>,
22}
23
24impl TryFrom<&Value> for AddComparisonCallParams {
25    type Error = SzConfigError;
26
27    fn try_from(json: &Value) -> Result<Self> {
28        Ok(Self {
29            ftype_code: json
30                .get("ftypeCode")
31                .and_then(|v| v.as_str())
32                .ok_or_else(|| SzConfigError::MissingField("ftypeCode".to_string()))?
33                .to_string(),
34            cfunc_code: json
35                .get("cfuncCode")
36                .and_then(|v| v.as_str())
37                .ok_or_else(|| SzConfigError::MissingField("cfuncCode".to_string()))?
38                .to_string(),
39            element_list: json
40                .get("elementList")
41                .and_then(|v| v.as_array())
42                .map(|arr| {
43                    arr.iter()
44                        .filter_map(|v| v.as_str().map(|s| s.to_string()))
45                        .collect()
46                })
47                .unwrap_or_default(),
48        })
49    }
50}
51
52/// Parameters for adding a comparison call element (CBOM record)
53#[derive(Debug, Clone)]
54pub struct AddComparisonCallElementParams {
55    pub cfcall_id: i64,
56    pub ftype_id: i64,
57    pub felem_id: i64,
58    pub exec_order: i64,
59}
60
61impl TryFrom<&Value> for AddComparisonCallElementParams {
62    type Error = SzConfigError;
63
64    fn try_from(json: &Value) -> Result<Self> {
65        Ok(Self {
66            cfcall_id: json
67                .get("cfcallId")
68                .and_then(|v| v.as_i64())
69                .ok_or_else(|| SzConfigError::MissingField("cfcallId".to_string()))?,
70            ftype_id: json
71                .get("ftypeId")
72                .and_then(|v| v.as_i64())
73                .ok_or_else(|| SzConfigError::MissingField("ftypeId".to_string()))?,
74            felem_id: json
75                .get("felemId")
76                .and_then(|v| v.as_i64())
77                .ok_or_else(|| SzConfigError::MissingField("felemId".to_string()))?,
78            exec_order: json
79                .get("execOrder")
80                .and_then(|v| v.as_i64())
81                .ok_or_else(|| SzConfigError::MissingField("execOrder".to_string()))?,
82        })
83    }
84}
85
86/// Parameters for deleting a comparison call element
87#[derive(Debug, Clone)]
88pub struct DeleteComparisonCallElementParams {
89    pub ftype_id: i64,
90    pub felem_id: i64,
91    pub exec_order: i64,
92}
93
94/// Parameters for setting (updating) a comparison call
95#[derive(Debug, Clone, Default)]
96pub struct SetComparisonCallParams {
97    pub cfcall_id: i64,
98    pub exec_order: Option<i64>,
99}
100
101impl TryFrom<&Value> for SetComparisonCallParams {
102    type Error = SzConfigError;
103
104    fn try_from(json: &Value) -> Result<Self> {
105        let cfcall_id = json
106            .get("cfcallId")
107            .and_then(|v| v.as_i64())
108            .ok_or_else(|| SzConfigError::MissingField("cfcallId".to_string()))?;
109
110        Ok(Self {
111            cfcall_id,
112            exec_order: json.get("execOrder").and_then(|v| v.as_i64()),
113        })
114    }
115}
116
117/// Parameters for setting a comparison call element
118#[derive(Debug, Clone)]
119pub struct SetComparisonCallElementParams {
120    pub ftype_id: i64,
121    pub felem_id: i64,
122    pub exec_order: i64,
123    pub updates: Value,
124}
125
126/// Add a new comparison call with element list
127///
128/// Creates a new comparison call linking a function to a feature
129/// with associated elements (CBOM records).
130/// Note: Only one comparison call is allowed per feature.
131///
132/// # Arguments
133/// * `config` - Configuration JSON string
134/// * `params` - Comparison call parameters (ftype_code, cfunc_code, element_list required)
135///
136/// # Returns
137/// Tuple of (modified_config, new_cfcall_record)
138///
139/// # Errors
140/// - `Duplicate` if a comparison call already exists for this feature
141/// - `NotFound` if function/feature/element codes don't exist
142pub fn add_comparison_call(
143    config: &str,
144    params: AddComparisonCallParams,
145) -> Result<(String, Value)> {
146    let mut config_data: Value =
147        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
148
149    // Get next CFCALL_ID (seed at 1000 for user-created calls)
150    let cfcall_id = get_next_id(&config_data, "G2_CONFIG.CFG_CFCALL", "CFCALL_ID", 1000)?;
151
152    // Lookup feature ID
153    let ftype_id = lookup_feature_id(config, &params.ftype_code)?;
154
155    // Check if comparison call already exists for this feature (only one allowed per feature)
156    let call_exists = config_data["G2_CONFIG"]["CFG_CFCALL"]
157        .as_array()
158        .map(|arr| {
159            arr.iter()
160                .any(|call| call["FTYPE_ID"].as_i64() == Some(ftype_id))
161        })
162        .unwrap_or(false);
163
164    if call_exists {
165        return Err(SzConfigError::AlreadyExists(format!(
166            "Comparison call for feature {} already set",
167            params.ftype_code
168        )));
169    }
170
171    // Lookup function ID
172    let cfunc_id = lookup_cfunc_id(config, &params.cfunc_code)?;
173
174    // Validate element list is not empty
175    if params.element_list.is_empty() {
176        return Err(SzConfigError::InvalidInput(
177            "No elements were found in the elementList".to_string(),
178        ));
179    }
180
181    // Process element list and create CFBOM records
182    let mut cfbom_records = Vec::new();
183    let mut exec_order = 0;
184
185    for (idx, element_code) in params.element_list.iter().enumerate() {
186        exec_order += 1;
187
188        // Validate element is not blank
189        if element_code.trim().is_empty() {
190            return Err(SzConfigError::InvalidInput(format!(
191                "Element cannot be blank in item {} on the element list",
192                idx + 1
193            )));
194        }
195
196        // Lookup element ID (global lookup - Python allows any element in call)
197        let bom_felem_id = lookup_element_id(config, element_code)?;
198
199        // Create CFBOM record
200        cfbom_records.push(json!({
201            "CFCALL_ID": cfcall_id,
202            "FTYPE_ID": ftype_id,
203            "FELEM_ID": bom_felem_id,
204            "EXEC_ORDER": exec_order
205        }));
206    }
207
208    // Create new CFG_CFCALL record
209    let new_record = json!({
210        "CFCALL_ID": cfcall_id,
211        "FTYPE_ID": ftype_id,
212        "CFUNC_ID": cfunc_id
213    });
214
215    // Add to config
216    if let Some(cfcall_array) = config_data["G2_CONFIG"]["CFG_CFCALL"].as_array_mut() {
217        cfcall_array.push(new_record.clone());
218    } else {
219        return Err(SzConfigError::MissingSection("CFG_CFCALL".to_string()));
220    }
221
222    if let Some(cfbom_array) = config_data["G2_CONFIG"]["CFG_CFBOM"].as_array_mut() {
223        cfbom_array.extend(cfbom_records);
224    } else {
225        return Err(SzConfigError::MissingSection("CFG_CFBOM".to_string()));
226    }
227
228    let modified_config =
229        serde_json::to_string(&config_data).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
230
231    Ok((modified_config, new_record))
232}
233
234/// Delete a comparison call by ID
235///
236/// Also deletes associated CFBOM records.
237///
238/// # Arguments
239/// * `config` - Configuration JSON string
240/// * `cfcall_id` - Comparison call ID to delete
241///
242/// # Returns
243/// Modified configuration JSON string
244///
245/// # Errors
246/// - `NotFound` if call ID doesn't exist
247pub fn delete_comparison_call(config: &str, cfcall_id: i64) -> Result<String> {
248    let mut config_data: Value =
249        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
250
251    // Validate that the call exists
252    let call_exists = config_data["G2_CONFIG"]["CFG_CFCALL"]
253        .as_array()
254        .map(|arr| {
255            arr.iter()
256                .any(|call| call["CFCALL_ID"].as_i64() == Some(cfcall_id))
257        })
258        .unwrap_or(false);
259
260    if !call_exists {
261        return Err(SzConfigError::NotFound(format!(
262            "Comparison call ID {cfcall_id} does not exist"
263        )));
264    }
265
266    // Delete the comparison call
267    if let Some(cfcall_array) = config_data["G2_CONFIG"]["CFG_CFCALL"].as_array_mut() {
268        cfcall_array.retain(|record| record["CFCALL_ID"].as_i64() != Some(cfcall_id));
269    }
270
271    // Delete associated CFBOM records
272    if let Some(cfbom_array) = config_data["G2_CONFIG"]["CFG_CFBOM"].as_array_mut() {
273        cfbom_array.retain(|record| record["CFCALL_ID"].as_i64() != Some(cfcall_id));
274    }
275
276    serde_json::to_string(&config_data).map_err(|e| SzConfigError::JsonParse(e.to_string()))
277}
278
279/// Get a single comparison call by ID
280///
281/// # Arguments
282/// * `config` - Configuration JSON string
283/// * `cfcall_id` - Comparison call ID
284///
285/// # Returns
286/// JSON Value representing the comparison call record
287///
288/// # Errors
289/// - `NotFound` if call ID doesn't exist
290pub fn get_comparison_call(config: &str, cfcall_id: i64) -> Result<Value> {
291    find_in_config_array(config, "CFG_CFCALL", "CFCALL_ID", &cfcall_id.to_string())?
292        .ok_or_else(|| SzConfigError::NotFound(format!("Comparison call ID {cfcall_id}")))
293}
294
295/// List all comparison calls with resolved names
296///
297/// Returns all comparison calls with feature and function codes resolved.
298///
299/// # Arguments
300/// * `config` - Configuration JSON string
301///
302/// # Returns
303/// Vector of JSON Values with resolved names
304pub fn list_comparison_calls(config: &str) -> Result<Vec<Value>> {
305    let config_data: Value =
306        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
307
308    let empty_array = vec![];
309    let cfcall_array = config_data
310        .get("G2_CONFIG")
311        .and_then(|g| g.get("CFG_CFCALL"))
312        .and_then(|v| v.as_array())
313        .unwrap_or(&empty_array);
314
315    let ftype_array = config_data
316        .get("G2_CONFIG")
317        .and_then(|g| g.get("CFG_FTYPE"))
318        .and_then(|v| v.as_array())
319        .unwrap_or(&empty_array);
320
321    let cfunc_array = config_data
322        .get("G2_CONFIG")
323        .and_then(|g| g.get("CFG_CFUNC"))
324        .and_then(|v| v.as_array())
325        .unwrap_or(&empty_array);
326
327    // Helper functions for ID resolution
328    let resolve_ftype = |ftype_id: i64| -> String {
329        ftype_array
330            .iter()
331            .find(|ft| ft.get("FTYPE_ID").and_then(|v| v.as_i64()) == Some(ftype_id))
332            .and_then(|ft| ft.get("FTYPE_CODE"))
333            .and_then(|v| v.as_str())
334            .unwrap_or("unknown")
335            .to_string()
336    };
337
338    let resolve_cfunc = |cfunc_id: i64| -> String {
339        cfunc_array
340            .iter()
341            .find(|cf| cf.get("CFUNC_ID").and_then(|v| v.as_i64()) == Some(cfunc_id))
342            .and_then(|cf| cf.get("CFUNC_CODE"))
343            .and_then(|v| v.as_str())
344            .unwrap_or("unknown")
345            .to_string()
346    };
347
348    // Transform comparison calls
349    let items: Vec<Value> = cfcall_array
350        .iter()
351        .map(|item| {
352            let ftype_id = item.get("FTYPE_ID").and_then(|v| v.as_i64()).unwrap_or(0);
353            let cfunc_id = item.get("CFUNC_ID").and_then(|v| v.as_i64()).unwrap_or(0);
354
355            json!({
356                "id": item.get("CFCALL_ID").and_then(|v| v.as_i64()).unwrap_or(0),
357                "feature": resolve_ftype(ftype_id),
358                "function": resolve_cfunc(cfunc_id)
359            })
360        })
361        .collect();
362
363    Ok(items)
364}
365
366/// Update a comparison call (stub - not implemented in Python)
367///
368/// # Arguments
369/// * `config` - Configuration JSON string
370/// * `params` - Comparison call parameters (cfcall_id required, others optional to update)
371///
372/// # Returns
373/// Modified configuration JSON string
374pub fn set_comparison_call(config: &str, _params: SetComparisonCallParams) -> Result<String> {
375    // This is a stub - the Python version doesn't implement this
376    Ok(config.to_string())
377}
378
379/// Add a comparison call element (CBOM record)
380///
381/// Creates a new comparison bill of materials entry.
382///
383/// # Arguments
384/// * `config` - Configuration JSON string
385/// * `params` - Element parameters (cfcall_id, ftype_id, felem_id, exec_order required)
386///
387/// # Returns
388/// Tuple of (modified_config, new_cbom_record)
389pub fn add_comparison_call_element(
390    config: &str,
391    params: AddComparisonCallElementParams,
392) -> Result<(String, Value)> {
393    let mut config_data: Value =
394        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
395
396    // Validate ftype_id is a valid feature ID (not -1 sentinel value)
397    if params.ftype_id < 0 {
398        return Err(SzConfigError::InvalidInput(format!(
399            "{} is not a valid feature ID",
400            params.ftype_id
401        )));
402    }
403
404    // Check if element already exists
405    // Python duplicate check (line 2941): checks [call_id_field, "FTYPE_ID", "FELEM_ID"] - 3 fields only
406    // EXEC_ORDER excluded from check because same element at different positions is still a duplicate
407    if let Some(cbom_array) = config_data
408        .get("G2_CONFIG")
409        .and_then(|g| g.get("CFG_CFBOM"))
410        .and_then(|v| v.as_array())
411    {
412        for item in cbom_array {
413            if item.get("CFCALL_ID").and_then(|v| v.as_i64()) == Some(params.cfcall_id)
414                && item.get("FTYPE_ID").and_then(|v| v.as_i64()) == Some(params.ftype_id)
415                && item.get("FELEM_ID").and_then(|v| v.as_i64()) == Some(params.felem_id)
416            // && item.get("EXEC_ORDER").and_then(|v| v.as_i64()) == Some(params.exec_order)
417            // ↑ Commented out: Python excludes EXEC_ORDER from duplicate check (sz_configtool line 2941)
418            // Reason: Same element in same call is duplicate regardless of position/order
419            {
420                return Err(SzConfigError::AlreadyExists(
421                    "Feature/element already exists for call".to_string(),
422                ));
423            }
424        }
425    }
426
427    // Create new CBOM record (use params.exec_order directly - no auto-assignment)
428    let new_record = json!({
429        "CFCALL_ID": params.cfcall_id,
430        "FTYPE_ID": params.ftype_id,
431        "FELEM_ID": params.felem_id,
432        "EXEC_ORDER": params.exec_order
433    });
434
435    // Add to CFG_CFBOM
436    if let Some(cbom_array) = config_data["G2_CONFIG"]["CFG_CFBOM"].as_array_mut() {
437        cbom_array.push(new_record.clone());
438    } else {
439        return Err(SzConfigError::MissingSection("CFG_CFBOM".to_string()));
440    }
441
442    let modified_config =
443        serde_json::to_string(&config_data).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
444
445    Ok((modified_config, new_record))
446}
447
448/// Delete a comparison call element
449///
450/// # Arguments
451/// * `config` - Configuration JSON string
452/// * `cfcall_id` - Comparison call ID
453/// * `params` - Element parameters (ftype_id, felem_id, exec_order)
454///
455/// # Returns
456/// Modified configuration JSON string
457pub fn delete_comparison_call_element(
458    config: &str,
459    cfcall_id: i64,
460    params: DeleteComparisonCallElementParams,
461) -> Result<String> {
462    let mut config_data: Value =
463        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
464
465    // Validate that the element exists
466    let element_exists = config_data["G2_CONFIG"]["CFG_CFBOM"]
467        .as_array()
468        .map(|arr| {
469            arr.iter().any(|item| {
470                item.get("CFCALL_ID").and_then(|v| v.as_i64()) == Some(cfcall_id)
471                    && item.get("FTYPE_ID").and_then(|v| v.as_i64()) == Some(params.ftype_id)
472                    && item.get("FELEM_ID").and_then(|v| v.as_i64()) == Some(params.felem_id)
473                    && item.get("EXEC_ORDER").and_then(|v| v.as_i64()) == Some(params.exec_order)
474            })
475        })
476        .unwrap_or(false);
477
478    if !element_exists {
479        return Err(SzConfigError::NotFound(
480            "Comparison call element not found".to_string(),
481        ));
482    }
483
484    // Delete the element
485    if let Some(cbom_array) = config_data["G2_CONFIG"]["CFG_CFBOM"].as_array_mut() {
486        cbom_array.retain(|item| {
487            !(item.get("CFCALL_ID").and_then(|v| v.as_i64()) == Some(cfcall_id)
488                && item.get("FTYPE_ID").and_then(|v| v.as_i64()) == Some(params.ftype_id)
489                && item.get("FELEM_ID").and_then(|v| v.as_i64()) == Some(params.felem_id)
490                && item.get("EXEC_ORDER").and_then(|v| v.as_i64()) == Some(params.exec_order))
491        });
492    }
493
494    serde_json::to_string(&config_data).map_err(|e| SzConfigError::JsonParse(e.to_string()))
495}
496
497/// Update a comparison call element (stub - not typically used)
498///
499/// # Arguments
500/// * `config` - Configuration JSON string
501/// * `cfcall_id` - Comparison call ID
502/// * `params` - Element parameters including updates
503///
504/// # Returns
505/// Modified configuration JSON string
506pub fn set_comparison_call_element(
507    config: &str,
508    _cfcall_id: i64,
509    _params: SetComparisonCallElementParams,
510) -> Result<String> {
511    // This is a stub - not commonly used
512    Ok(config.to_string())
513}