Skip to main content

sz_configtool_lib/calls/
distinct.rs

1//! Distinct call management operations
2//!
3//! Functions for managing CFG_DFCALL (distinct calls) and CFG_DFBOM
4//! (distinct bill of materials) configuration sections.
5
6use crate::error::{Result, SzConfigError};
7use crate::helpers::{
8    find_in_config_array, get_next_id, lookup_dfunc_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 distinct call
17#[derive(Debug, Clone)]
18pub struct AddDistinctCallParams {
19    pub ftype_code: String,
20    pub dfunc_code: String,
21    pub element_list: Vec<String>,
22}
23
24impl TryFrom<&Value> for AddDistinctCallParams {
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            dfunc_code: json
35                .get("dfuncCode")
36                .and_then(|v| v.as_str())
37                .ok_or_else(|| SzConfigError::MissingField("dfuncCode".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 distinct call element
53#[derive(Debug, Clone)]
54pub struct AddDistinctCallElementParams {
55    pub dfcall_id: i64,
56    pub ftype_id: i64,
57    pub felem_id: i64,
58    pub exec_order: i64,
59}
60
61/// Parameters for deleting a distinct call element
62#[derive(Debug, Clone)]
63pub struct DeleteDistinctCallElementParams {
64    pub dfcall_id: i64,
65    pub ftype_id: i64,
66    pub felem_id: i64,
67    pub exec_order: i64,
68}
69
70/// Parameters for setting (updating) a distinct call
71#[derive(Debug, Clone, Default)]
72pub struct SetDistinctCallParams {
73    pub dfcall_id: i64,
74    pub exec_order: Option<i64>,
75}
76
77impl TryFrom<&Value> for SetDistinctCallParams {
78    type Error = SzConfigError;
79
80    fn try_from(json: &Value) -> Result<Self> {
81        let dfcall_id = json
82            .get("dfcallId")
83            .and_then(|v| v.as_i64())
84            .ok_or_else(|| SzConfigError::MissingField("dfcallId".to_string()))?;
85
86        Ok(Self {
87            dfcall_id,
88            exec_order: json.get("execOrder").and_then(|v| v.as_i64()),
89        })
90    }
91}
92
93/// Parameters for setting a distinct call element
94#[derive(Debug, Clone)]
95pub struct SetDistinctCallElementParams {
96    pub dfcall_id: i64,
97    pub ftype_id: i64,
98    pub felem_id: i64,
99    pub exec_order: i64,
100    pub updates: Value,
101}
102
103/// Add a new distinct call with element list
104///
105/// Creates a new distinct call linking a function to a feature
106/// with associated elements (DBOM records).
107/// Note: Only one distinct call is allowed per feature.
108///
109/// # Arguments
110/// * `config` - Configuration JSON string
111/// * `params` - Distinct call parameters (ftype_code, dfunc_code, element_list required)
112///
113/// # Returns
114/// Tuple of (modified_config, new_dfcall_record)
115///
116/// # Errors
117/// - `Duplicate` if a distinct call already exists for this feature
118/// - `NotFound` if function/feature/element codes don't exist
119pub fn add_distinct_call(config: &str, params: AddDistinctCallParams) -> Result<(String, Value)> {
120    let mut config_data: Value =
121        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
122
123    // Validate element list is not empty
124    if params.element_list.is_empty() {
125        return Err(SzConfigError::InvalidInput(
126            "No elements were found in the elementList".to_string(),
127        ));
128    }
129
130    // Validate each element is not blank
131    for (idx, element_code) in params.element_list.iter().enumerate() {
132        if element_code.trim().is_empty() {
133            return Err(SzConfigError::InvalidInput(format!(
134                "Element cannot be blank in item {} on the element list",
135                idx + 1
136            )));
137        }
138    }
139
140    // Get next DFCALL_ID (seed at 1000 for user-created calls)
141    let dfcall_id = get_next_id(&config_data, "G2_CONFIG.CFG_DFCALL", "DFCALL_ID", 1000)?;
142
143    // Lookup feature ID
144    let ftype_id = lookup_feature_id(config, &params.ftype_code)?;
145
146    // Check if distinct call already exists for this feature (only one allowed per feature)
147    let call_exists = config_data["G2_CONFIG"]["CFG_DFCALL"]
148        .as_array()
149        .map(|arr| {
150            arr.iter()
151                .any(|call| call["FTYPE_ID"].as_i64() == Some(ftype_id))
152        })
153        .unwrap_or(false);
154
155    if call_exists {
156        return Err(SzConfigError::AlreadyExists(format!(
157            "Distinct call for feature {} already set",
158            params.ftype_code
159        )));
160    }
161
162    // Lookup function ID
163    let dfunc_id = lookup_dfunc_id(config, &params.dfunc_code)?;
164
165    // Process element list and create DFBOM records
166    let mut dfbom_records = Vec::new();
167    let mut exec_order = 0;
168
169    for (idx, element_code) in params.element_list.iter().enumerate() {
170        exec_order += 1;
171
172        // Validate element is not blank (already checked in add_distinct_call, defensive)
173        if element_code.trim().is_empty() {
174            return Err(SzConfigError::InvalidInput(format!(
175                "Element cannot be blank in item {} on the element list",
176                idx + 1
177            )));
178        }
179
180        // Lookup element ID (global lookup - Python allows any element in call)
181        let bom_felem_id = lookup_element_id(config, element_code)?;
182
183        // Create DFBOM record
184        dfbom_records.push(json!({
185            "DFCALL_ID": dfcall_id,
186            "FTYPE_ID": ftype_id,
187            "FELEM_ID": bom_felem_id,
188            "EXEC_ORDER": exec_order
189        }));
190    }
191
192    // Create new CFG_DFCALL record (EXEC_ORDER is always 1 for distinct calls)
193    let new_record = json!({
194        "DFCALL_ID": dfcall_id,
195        "FTYPE_ID": ftype_id,
196        "DFUNC_ID": dfunc_id,
197        "EXEC_ORDER": 1
198    });
199
200    // Add to config
201    if let Some(dfcall_array) = config_data["G2_CONFIG"]["CFG_DFCALL"].as_array_mut() {
202        dfcall_array.push(new_record.clone());
203    } else {
204        return Err(SzConfigError::MissingSection("CFG_DFCALL".to_string()));
205    }
206
207    if let Some(dfbom_array) = config_data["G2_CONFIG"]["CFG_DFBOM"].as_array_mut() {
208        dfbom_array.extend(dfbom_records);
209    } else {
210        return Err(SzConfigError::MissingSection("CFG_DFBOM".to_string()));
211    }
212
213    let modified_config =
214        serde_json::to_string(&config_data).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
215
216    Ok((modified_config, new_record))
217}
218
219/// Delete a distinct call by ID
220///
221/// Also deletes associated DFBOM records.
222///
223/// # Arguments
224/// * `config` - Configuration JSON string
225/// * `dfcall_id` - Distinct call ID to delete
226///
227/// # Returns
228/// Modified configuration JSON string
229///
230/// # Errors
231/// - `NotFound` if call ID doesn't exist
232pub fn delete_distinct_call(config: &str, dfcall_id: i64) -> Result<String> {
233    let mut config_data: Value =
234        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
235
236    // Validate that the call exists
237    let call_exists = config_data["G2_CONFIG"]["CFG_DFCALL"]
238        .as_array()
239        .map(|arr| {
240            arr.iter()
241                .any(|call| call["DFCALL_ID"].as_i64() == Some(dfcall_id))
242        })
243        .unwrap_or(false);
244
245    if !call_exists {
246        return Err(SzConfigError::NotFound(format!(
247            "Distinct call ID {dfcall_id} does not exist"
248        )));
249    }
250
251    // Delete the distinct call
252    if let Some(dfcall_array) = config_data["G2_CONFIG"]["CFG_DFCALL"].as_array_mut() {
253        dfcall_array.retain(|record| record["DFCALL_ID"].as_i64() != Some(dfcall_id));
254    }
255
256    // Delete associated DFBOM records
257    if let Some(dfbom_array) = config_data["G2_CONFIG"]["CFG_DFBOM"].as_array_mut() {
258        dfbom_array.retain(|record| record["DFCALL_ID"].as_i64() != Some(dfcall_id));
259    }
260
261    serde_json::to_string(&config_data).map_err(|e| SzConfigError::JsonParse(e.to_string()))
262}
263
264/// Get a single distinct call by ID
265///
266/// # Arguments
267/// * `config` - Configuration JSON string
268/// * `dfcall_id` - Distinct call ID
269///
270/// # Returns
271/// JSON Value representing the distinct call record
272///
273/// # Errors
274/// - `NotFound` if call ID doesn't exist
275pub fn get_distinct_call(config: &str, dfcall_id: i64) -> Result<Value> {
276    find_in_config_array(config, "CFG_DFCALL", "DFCALL_ID", &dfcall_id.to_string())?.ok_or_else(
277        || SzConfigError::NotFound(format!("Distinct call ID {dfcall_id} does not exist")),
278    )
279}
280
281/// List all distinct calls with resolved names
282///
283/// Returns all distinct calls with feature and function codes resolved.
284///
285/// # Arguments
286/// * `config` - Configuration JSON string
287///
288/// # Returns
289/// Vector of JSON Values with resolved names
290pub fn list_distinct_calls(config: &str) -> Result<Vec<Value>> {
291    let config_data: Value =
292        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
293
294    let empty_array = vec![];
295    let dfcall_array = config_data
296        .get("G2_CONFIG")
297        .and_then(|g| g.get("CFG_DFCALL"))
298        .and_then(|v| v.as_array())
299        .unwrap_or(&empty_array);
300
301    let ftype_array = config_data
302        .get("G2_CONFIG")
303        .and_then(|g| g.get("CFG_FTYPE"))
304        .and_then(|v| v.as_array())
305        .unwrap_or(&empty_array);
306
307    let dfunc_array = config_data
308        .get("G2_CONFIG")
309        .and_then(|g| g.get("CFG_DFUNC"))
310        .and_then(|v| v.as_array())
311        .unwrap_or(&empty_array);
312
313    // Helper functions for ID resolution
314    let resolve_ftype = |ftype_id: i64| -> String {
315        ftype_array
316            .iter()
317            .find(|ft| ft.get("FTYPE_ID").and_then(|v| v.as_i64()) == Some(ftype_id))
318            .and_then(|ft| ft.get("FTYPE_CODE"))
319            .and_then(|v| v.as_str())
320            .unwrap_or("unknown")
321            .to_string()
322    };
323
324    let resolve_dfunc = |dfunc_id: i64| -> String {
325        dfunc_array
326            .iter()
327            .find(|df| df.get("DFUNC_ID").and_then(|v| v.as_i64()) == Some(dfunc_id))
328            .and_then(|df| df.get("DFUNC_CODE"))
329            .and_then(|v| v.as_str())
330            .unwrap_or("unknown")
331            .to_string()
332    };
333
334    // Transform distinct calls
335    let items: Vec<Value> = dfcall_array
336        .iter()
337        .map(|item| {
338            let ftype_id = item.get("FTYPE_ID").and_then(|v| v.as_i64()).unwrap_or(0);
339            let dfunc_id = item.get("DFUNC_ID").and_then(|v| v.as_i64()).unwrap_or(0);
340
341            json!({
342                "id": item.get("DFCALL_ID").and_then(|v| v.as_i64()).unwrap_or(0),
343                "feature": resolve_ftype(ftype_id),
344                "function": resolve_dfunc(dfunc_id),
345                "execOrder": item.get("EXEC_ORDER").and_then(|v| v.as_i64()).unwrap_or(1)
346            })
347        })
348        .collect();
349
350    Ok(items)
351}
352
353/// Update a distinct call (stub - not implemented in Python)
354///
355/// # Arguments
356/// * `config` - Configuration JSON string
357/// * `params` - Distinct call parameters (dfcall_id required, others optional to update)
358///
359/// # Returns
360/// Modified configuration JSON string
361pub fn set_distinct_call(config: &str, _params: SetDistinctCallParams) -> Result<String> {
362    // This is a stub - the Python version doesn't implement this
363    Ok(config.to_string())
364}
365
366/// Add a distinct call element (DBOM record)
367///
368/// Creates a new distinct bill of materials entry.
369///
370/// # Arguments
371/// * `config` - Configuration JSON string
372/// * `params` - Element parameters (dfcall_id, ftype_id, felem_id, exec_order)
373///
374/// # Returns
375/// Tuple of (modified_config, new_dbom_record)
376pub fn add_distinct_call_element(
377    config: &str,
378    params: AddDistinctCallElementParams,
379) -> Result<(String, Value)> {
380    let mut config_data: Value =
381        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
382
383    // Check if element already exists
384    if let Some(dbom_array) = config_data
385        .get("G2_CONFIG")
386        .and_then(|g| g.get("CFG_DFBOM"))
387        .and_then(|v| v.as_array())
388    {
389        for item in dbom_array {
390            if item.get("DFCALL_ID").and_then(|v| v.as_i64()) == Some(params.dfcall_id)
391                && item.get("FTYPE_ID").and_then(|v| v.as_i64()) == Some(params.ftype_id)
392                && item.get("FELEM_ID").and_then(|v| v.as_i64()) == Some(params.felem_id)
393                && item.get("EXEC_ORDER").and_then(|v| v.as_i64()) == Some(params.exec_order)
394            {
395                return Err(SzConfigError::AlreadyExists(
396                    "Distinct call element already exists".to_string(),
397                ));
398            }
399        }
400    }
401
402    // Create new DBOM record
403    let new_record = json!({
404        "DFCALL_ID": params.dfcall_id,
405        "FTYPE_ID": params.ftype_id,
406        "FELEM_ID": params.felem_id,
407        "EXEC_ORDER": params.exec_order
408    });
409
410    // Add to CFG_DFBOM
411    if let Some(dbom_array) = config_data["G2_CONFIG"]["CFG_DFBOM"].as_array_mut() {
412        dbom_array.push(new_record.clone());
413    } else {
414        return Err(SzConfigError::MissingSection("CFG_DFBOM".to_string()));
415    }
416
417    let modified_config =
418        serde_json::to_string(&config_data).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
419
420    Ok((modified_config, new_record))
421}
422
423/// Delete a distinct call element
424///
425/// # Arguments
426/// * `config` - Configuration JSON string
427/// * `params` - Element parameters (dfcall_id, ftype_id, felem_id, exec_order)
428///
429/// # Returns
430/// Modified configuration JSON string
431pub fn delete_distinct_call_element(
432    config: &str,
433    params: DeleteDistinctCallElementParams,
434) -> Result<String> {
435    let mut config_data: Value =
436        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
437
438    // Validate that the element exists
439    let element_exists = config_data["G2_CONFIG"]["CFG_DFBOM"]
440        .as_array()
441        .map(|arr| {
442            arr.iter().any(|item| {
443                item.get("DFCALL_ID").and_then(|v| v.as_i64()) == Some(params.dfcall_id)
444                    && item.get("FTYPE_ID").and_then(|v| v.as_i64()) == Some(params.ftype_id)
445                    && item.get("FELEM_ID").and_then(|v| v.as_i64()) == Some(params.felem_id)
446                    && item.get("EXEC_ORDER").and_then(|v| v.as_i64()) == Some(params.exec_order)
447            })
448        })
449        .unwrap_or(false);
450
451    if !element_exists {
452        return Err(SzConfigError::NotFound(
453            "Distinct call element not found".to_string(),
454        ));
455    }
456
457    // Delete the element
458    if let Some(dbom_array) = config_data["G2_CONFIG"]["CFG_DFBOM"].as_array_mut() {
459        dbom_array.retain(|item| {
460            !(item.get("DFCALL_ID").and_then(|v| v.as_i64()) == Some(params.dfcall_id)
461                && item.get("FTYPE_ID").and_then(|v| v.as_i64()) == Some(params.ftype_id)
462                && item.get("FELEM_ID").and_then(|v| v.as_i64()) == Some(params.felem_id)
463                && item.get("EXEC_ORDER").and_then(|v| v.as_i64()) == Some(params.exec_order))
464        });
465    }
466
467    serde_json::to_string(&config_data).map_err(|e| SzConfigError::JsonParse(e.to_string()))
468}
469
470/// Update a distinct call element (stub - not typically used)
471///
472/// # Arguments
473/// * `config` - Configuration JSON string
474/// * `params` - Element parameters including updates
475///
476/// # Returns
477/// Modified configuration JSON string
478pub fn set_distinct_call_element(
479    config: &str,
480    _params: SetDistinctCallElementParams,
481) -> Result<String> {
482    // This is a stub - not commonly used
483    Ok(config.to_string())
484}