Skip to main content

sz_configtool_lib/calls/
expression.rs

1//! Expression call management operations
2//!
3//! Functions for managing CFG_EFCALL (expression calls) and CFG_EFBOM
4//! (expression bill of materials) configuration sections.
5
6use crate::error::{Result, SzConfigError};
7use crate::helpers::{
8    find_in_config_array, get_next_id, lookup_efunc_id, lookup_element_id, lookup_feature_id,
9};
10use serde_json::{Value, json};
11
12// ============================================================================
13// Parameter Structs
14// ============================================================================
15
16/// Parameters for adding an expression call
17#[derive(Debug, Clone)]
18pub struct AddExpressionCallParams<'a> {
19    pub efunc_code: &'a str,
20    pub element_list: Vec<(String, String, Option<String>)>, // (element, required, feature)
21    pub ftype_code: Option<&'a str>,
22    pub felem_code: Option<&'a str>,
23    pub exec_order: Option<i64>,
24    pub expression_feature: Option<&'a str>,
25    pub is_virtual: &'a str,
26}
27
28impl<'a> AddExpressionCallParams<'a> {
29    pub fn new(efunc_code: &'a str, element_list: Vec<(String, String, Option<String>)>) -> Self {
30        Self {
31            efunc_code,
32            element_list,
33            ftype_code: None,
34            felem_code: None,
35            exec_order: None,
36            expression_feature: None,
37            is_virtual: "No",
38        }
39    }
40}
41
42/// Parameters for expression call element operations
43#[derive(Debug, Clone)]
44pub struct ExpressionCallElementParams {
45    pub ftype_id: i64,
46    pub felem_id: i64,
47    pub exec_order: i64,
48    pub felem_req: String,
49}
50
51impl ExpressionCallElementParams {
52    pub fn new(ftype_id: i64, felem_id: i64, exec_order: i64, felem_req: String) -> Self {
53        Self {
54            ftype_id,
55            felem_id,
56            exec_order,
57            felem_req,
58        }
59    }
60}
61
62/// Parameters for identifying expression call element (for delete operations)
63#[derive(Debug, Clone)]
64pub struct ExpressionCallElementKey {
65    pub ftype_id: i64,
66    pub felem_id: i64,
67    pub exec_order: i64,
68}
69
70impl ExpressionCallElementKey {
71    pub fn new(ftype_id: i64, felem_id: i64, exec_order: i64) -> Self {
72        Self {
73            ftype_id,
74            felem_id,
75            exec_order,
76        }
77    }
78}
79
80/// Parameters for setting (updating) an expression call
81#[derive(Debug, Clone, Default)]
82pub struct SetExpressionCallParams {
83    pub efcall_id: i64,
84    pub exec_order: Option<i64>,
85}
86
87impl TryFrom<&Value> for SetExpressionCallParams {
88    type Error = SzConfigError;
89
90    fn try_from(json: &Value) -> Result<Self> {
91        let efcall_id = json
92            .get("efcallId")
93            .and_then(|v| v.as_i64())
94            .ok_or_else(|| SzConfigError::MissingField("efcallId".to_string()))?;
95
96        Ok(Self {
97            efcall_id,
98            exec_order: json.get("execOrder").and_then(|v| v.as_i64()),
99        })
100    }
101}
102
103/// Add a new expression call with element list
104///
105/// Creates a new expression call linking a function to a feature or element
106/// with an execution order and associated elements (EBOM records).
107///
108/// # Arguments
109/// * `config` - Configuration JSON string
110/// * `params` - Expression call parameters
111///
112/// # Returns
113/// Tuple of (modified_config, new_efcall_record)
114///
115/// # Errors
116/// - `InvalidParameter` if both ftype_code and felem_code are specified or both missing
117/// - `Duplicate` if exec_order is already taken for the feature/element
118/// - `NotFound` if function/feature/element codes don't exist
119pub fn add_expression_call(
120    config: &str,
121    params: AddExpressionCallParams,
122) -> Result<(String, Value)> {
123    let mut config_data: Value =
124        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
125
126    // Get next EFCALL_ID (seed at 1000 for user-created calls)
127    let efcall_id = get_next_id(&config_data, "G2_CONFIG.CFG_EFCALL", "EFCALL_ID", 1000)?;
128
129    // Lookup function ID
130    let efunc_id = lookup_efunc_id(config, params.efunc_code)?;
131
132    // Determine FTYPE_ID and FELEM_ID (-1 means not specified)
133    let mut ftype_id: i64 = -1;
134    let mut felem_id: i64 = -1;
135
136    if let Some(feature) = params.ftype_code.filter(|f| !f.eq_ignore_ascii_case("ALL")) {
137        ftype_id = lookup_feature_id(config, feature)?;
138    }
139
140    if let Some(element) = params.felem_code.filter(|e| !e.eq_ignore_ascii_case("N/A")) {
141        felem_id = lookup_element_id(config, element)?;
142    }
143
144    // Validate: exactly one of (feature, element) must be specified
145    if (ftype_id > 0 && felem_id > 0) || (ftype_id < 0 && felem_id < 0) {
146        return Err(SzConfigError::InvalidInput(
147            "Either a feature or an element must be specified, but not both".to_string(),
148        ));
149    }
150
151    // Determine exec_order
152    let final_exec_order = if let Some(order) = params.exec_order {
153        // Check if this exec_order is already taken for this feature/element
154        let order_taken = config_data["G2_CONFIG"]["CFG_EFCALL"]
155            .as_array()
156            .map(|arr| {
157                arr.iter().any(|call| {
158                    call["FTYPE_ID"].as_i64() == Some(ftype_id)
159                        && call["FELEM_ID"].as_i64() == Some(felem_id)
160                        && call["EXEC_ORDER"].as_i64() == Some(order)
161                })
162            })
163            .unwrap_or(false);
164
165        if order_taken {
166            return Err(SzConfigError::AlreadyExists(format!(
167                "Execution order {order} already taken for this feature/element"
168            )));
169        }
170        order
171    } else {
172        // Get next available exec_order for this feature/element combination
173        config_data["G2_CONFIG"]["CFG_EFCALL"]
174            .as_array()
175            .map(|arr| {
176                arr.iter()
177                    .filter(|call| {
178                        call["FTYPE_ID"].as_i64() == Some(ftype_id)
179                            && call["FELEM_ID"].as_i64() == Some(felem_id)
180                    })
181                    .filter_map(|call| call["EXEC_ORDER"].as_i64())
182                    .max()
183                    .map(|max| max + 1)
184                    .unwrap_or(1)
185            })
186            .unwrap_or(1)
187    };
188
189    // Lookup expression feature ID if specified
190    let efeat_ftype_id = if let Some(expr_feat) = params
191        .expression_feature
192        .filter(|f| !f.eq_ignore_ascii_case("N/A"))
193    {
194        lookup_feature_id(config, expr_feat)?
195    } else {
196        -1
197    };
198
199    // Process element list and create EFBOM records
200    let mut efbom_records = Vec::new();
201    let mut bom_exec_order = 0;
202
203    for (element_code, required, feature_opt) in params.element_list {
204        bom_exec_order += 1;
205
206        // Keep feature name for error messages (clone before consuming)
207        let _bom_feature_name_for_errors = feature_opt.clone();
208
209        // Determine BOM FTYPE_ID
210        let bom_ftype_id =
211            if let Some(bom_feature) = feature_opt.filter(|f| !f.eq_ignore_ascii_case("PARENT")) {
212                if bom_feature.eq_ignore_ascii_case("parent") {
213                    0 // Special value for parent feature link
214                } else {
215                    lookup_feature_id(config, &bom_feature)?
216                }
217            } else {
218                -1
219            };
220
221        // Lookup element ID (always global lookup - feature field is just metadata for EFBOM)
222        let bom_felem_id = lookup_element_id(config, &element_code)?;
223
224        // Create EFBOM record
225        efbom_records.push(json!({
226            "EFCALL_ID": efcall_id,
227            "FTYPE_ID": bom_ftype_id,
228            "FELEM_ID": bom_felem_id,
229            "EXEC_ORDER": bom_exec_order,
230            "FELEM_REQ": required
231        }));
232    }
233
234    // Create new CFG_EFCALL record
235    let new_record = json!({
236        "EFCALL_ID": efcall_id,
237        "FTYPE_ID": ftype_id,
238        "FELEM_ID": felem_id,
239        "EFUNC_ID": efunc_id,
240        "EXEC_ORDER": final_exec_order,
241        "EFEAT_FTYPE_ID": efeat_ftype_id,
242        "IS_VIRTUAL": params.is_virtual
243    });
244
245    // Add to config
246    if let Some(efcall_array) = config_data["G2_CONFIG"]["CFG_EFCALL"].as_array_mut() {
247        efcall_array.push(new_record.clone());
248    } else {
249        return Err(SzConfigError::MissingSection("CFG_EFCALL".to_string()));
250    }
251
252    if let Some(efbom_array) = config_data["G2_CONFIG"]["CFG_EFBOM"].as_array_mut() {
253        efbom_array.extend(efbom_records);
254    } else {
255        return Err(SzConfigError::MissingSection("CFG_EFBOM".to_string()));
256    }
257
258    let modified_config =
259        serde_json::to_string(&config_data).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
260
261    Ok((modified_config, new_record))
262}
263
264/// Delete an expression call by ID
265///
266/// Also deletes associated EFBOM records.
267///
268/// # Arguments
269/// * `config` - Configuration JSON string
270/// * `efcall_id` - Expression call ID to delete
271///
272/// # Returns
273/// Modified configuration JSON string
274///
275/// # Errors
276/// - `NotFound` if call ID doesn't exist
277pub fn delete_expression_call(config: &str, efcall_id: i64) -> Result<String> {
278    let mut config_data: Value =
279        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
280
281    // Validate that the call exists
282    let call_exists = config_data["G2_CONFIG"]["CFG_EFCALL"]
283        .as_array()
284        .map(|arr| {
285            arr.iter()
286                .any(|call| call["EFCALL_ID"].as_i64() == Some(efcall_id))
287        })
288        .unwrap_or(false);
289
290    if !call_exists {
291        return Err(SzConfigError::NotFound(format!(
292            "Expression call ID {efcall_id} does not exist"
293        )));
294    }
295
296    // Delete the expression call
297    if let Some(efcall_array) = config_data["G2_CONFIG"]["CFG_EFCALL"].as_array_mut() {
298        efcall_array.retain(|record| record["EFCALL_ID"].as_i64() != Some(efcall_id));
299    }
300
301    // Delete associated EFBOM records
302    if let Some(efbom_array) = config_data["G2_CONFIG"]["CFG_EFBOM"].as_array_mut() {
303        efbom_array.retain(|record| record["EFCALL_ID"].as_i64() != Some(efcall_id));
304    }
305
306    serde_json::to_string(&config_data).map_err(|e| SzConfigError::JsonParse(e.to_string()))
307}
308
309/// Get a single expression call by ID
310///
311/// # Arguments
312/// * `config` - Configuration JSON string
313/// * `efcall_id` - Expression call ID
314///
315/// # Returns
316/// JSON Value representing the expression call record
317///
318/// # Errors
319/// - `NotFound` if call ID doesn't exist
320pub fn get_expression_call(config: &str, efcall_id: i64) -> Result<Value> {
321    find_in_config_array(config, "CFG_EFCALL", "EFCALL_ID", &efcall_id.to_string())?.ok_or_else(
322        || SzConfigError::NotFound(format!("Expression call ID {efcall_id} does not exist")),
323    )
324}
325
326/// List all expression calls with resolved names
327///
328/// Returns all expression calls with feature, element, and function codes resolved.
329///
330/// # Arguments
331/// * `config` - Configuration JSON string
332///
333/// # Returns
334/// Vector of JSON Values with resolved names
335pub fn list_expression_calls(config: &str) -> Result<Vec<Value>> {
336    let config_data: Value =
337        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
338
339    let empty_array = vec![];
340    let efcall_array = config_data
341        .get("G2_CONFIG")
342        .and_then(|g| g.get("CFG_EFCALL"))
343        .and_then(|v| v.as_array())
344        .unwrap_or(&empty_array);
345
346    let ftype_array = config_data
347        .get("G2_CONFIG")
348        .and_then(|g| g.get("CFG_FTYPE"))
349        .and_then(|v| v.as_array())
350        .unwrap_or(&empty_array);
351
352    let felem_array = config_data
353        .get("G2_CONFIG")
354        .and_then(|g| g.get("CFG_FELEM"))
355        .and_then(|v| v.as_array())
356        .unwrap_or(&empty_array);
357
358    let efunc_array = config_data
359        .get("G2_CONFIG")
360        .and_then(|g| g.get("CFG_EFUNC"))
361        .and_then(|v| v.as_array())
362        .unwrap_or(&empty_array);
363
364    // Helper functions for ID resolution
365    let resolve_ftype = |ftype_id: i64| -> String {
366        if ftype_id <= 0 {
367            "all".to_string()
368        } else {
369            ftype_array
370                .iter()
371                .find(|ft| ft.get("FTYPE_ID").and_then(|v| v.as_i64()) == Some(ftype_id))
372                .and_then(|ft| ft.get("FTYPE_CODE"))
373                .and_then(|v| v.as_str())
374                .unwrap_or("all")
375                .to_string()
376        }
377    };
378
379    let resolve_felem = |felem_id: i64| -> String {
380        if felem_id <= 0 {
381            "n/a".to_string()
382        } else {
383            felem_array
384                .iter()
385                .find(|fe| fe.get("FELEM_ID").and_then(|v| v.as_i64()) == Some(felem_id))
386                .and_then(|fe| fe.get("FELEM_CODE"))
387                .and_then(|v| v.as_str())
388                .unwrap_or("n/a")
389                .to_string()
390        }
391    };
392
393    let resolve_efunc = |efunc_id: i64| -> String {
394        efunc_array
395            .iter()
396            .find(|ef| ef.get("EFUNC_ID").and_then(|v| v.as_i64()) == Some(efunc_id))
397            .and_then(|ef| ef.get("EFUNC_CODE"))
398            .and_then(|v| v.as_str())
399            .unwrap_or("unknown")
400            .to_string()
401    };
402
403    // Transform expression calls
404    let items: Vec<Value> = efcall_array
405        .iter()
406        .map(|item| {
407            let ftype_id = item.get("FTYPE_ID").and_then(|v| v.as_i64()).unwrap_or(0);
408            let felem_id = item.get("FELEM_ID").and_then(|v| v.as_i64()).unwrap_or(0);
409            let efunc_id = item.get("EFUNC_ID").and_then(|v| v.as_i64()).unwrap_or(0);
410
411            let efeat_ftype_id = item.get("EFEAT_FTYPE_ID").and_then(|v| v.as_i64()).unwrap_or(-1);
412
413            json!({
414                "id": item.get("EFCALL_ID").and_then(|v| v.as_i64()).unwrap_or(0),
415                "feature": resolve_ftype(ftype_id),
416                "element": resolve_felem(felem_id),
417                "execOrder": item.get("EXEC_ORDER").and_then(|v| v.as_i64()).unwrap_or(0),
418                "function": resolve_efunc(efunc_id),
419                "isVirtual": item.get("IS_VIRTUAL").and_then(|v| v.as_str()).unwrap_or("No"),
420                "expressionFeature": if efeat_ftype_id <= 0 { "n/a".to_string() } else { resolve_ftype(efeat_ftype_id) }
421            })
422        })
423        .collect();
424
425    Ok(items)
426}
427
428/// Update an expression call (stub - not implemented in Python)
429///
430/// # Arguments
431/// * `config` - Configuration JSON string
432/// * `params` - Expression call parameters (efcall_id required, others optional to update)
433///
434/// # Returns
435/// Modified configuration JSON string
436pub fn set_expression_call(config: &str, _params: SetExpressionCallParams) -> Result<String> {
437    // This is a stub - the Python version doesn't implement this
438    Ok(config.to_string())
439}
440
441/// Add an expression call element (EBOM record)
442///
443/// Creates a new expression bill of materials entry.
444///
445/// # Arguments
446/// * `config` - Configuration JSON string
447/// * `efcall_id` - Expression call ID
448/// * `params` - Expression call element parameters
449///
450/// # Returns
451/// Tuple of (modified_config, new_ebom_record)
452pub fn add_expression_call_element(
453    config: &str,
454    efcall_id: i64,
455    params: ExpressionCallElementParams,
456) -> Result<(String, Value)> {
457    let mut config_data: Value =
458        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
459
460    // Validate ftype_id is a valid feature ID (not -1 sentinel value)
461    if params.ftype_id < 0 {
462        return Err(SzConfigError::InvalidInput(format!(
463            "{} is not a valid feature ID",
464            params.ftype_id
465        )));
466    }
467
468    // Check if element already exists
469    // Python duplicate check (line 2941): checks [call_id_field, "FTYPE_ID", "FELEM_ID"] - 3 fields only
470    // EXEC_ORDER excluded from check because same element at different positions is still a duplicate
471    if let Some(ebom_array) = config_data
472        .get("G2_CONFIG")
473        .and_then(|g| g.get("CFG_EFBOM"))
474        .and_then(|v| v.as_array())
475    {
476        for item in ebom_array {
477            if item.get("EFCALL_ID").and_then(|v| v.as_i64()) == Some(efcall_id)
478                && item.get("FTYPE_ID").and_then(|v| v.as_i64()) == Some(params.ftype_id)
479                && item.get("FELEM_ID").and_then(|v| v.as_i64()) == Some(params.felem_id)
480            // && item.get("EXEC_ORDER").and_then(|v| v.as_i64()) == Some(params.exec_order)
481            // ↑ Commented out: Python excludes EXEC_ORDER from duplicate check (sz_configtool line 2941)
482            // Reason: Same element in same call is duplicate regardless of position/order
483            {
484                return Err(SzConfigError::AlreadyExists(
485                    "Feature/element already exists for call".to_string(),
486                ));
487            }
488        }
489    }
490
491    // Create new EBOM record (use params.exec_order directly - no auto-assignment)
492    let new_record = json!({
493        "EFCALL_ID": efcall_id,
494        "FTYPE_ID": params.ftype_id,
495        "FELEM_ID": params.felem_id,
496        "EXEC_ORDER": params.exec_order,
497        "FELEM_REQ": params.felem_req
498    });
499
500    // Add to CFG_EFBOM
501    if let Some(ebom_array) = config_data["G2_CONFIG"]["CFG_EFBOM"].as_array_mut() {
502        ebom_array.push(new_record.clone());
503    } else {
504        return Err(SzConfigError::MissingSection("CFG_EFBOM".to_string()));
505    }
506
507    let modified_config =
508        serde_json::to_string(&config_data).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
509
510    Ok((modified_config, new_record))
511}
512
513/// Delete an expression call element
514///
515/// # Arguments
516/// * `config` - Configuration JSON string
517/// * `efcall_id` - Expression call ID
518/// * `key` - Expression call element key (identifying the element to delete)
519///
520/// # Returns
521/// Modified configuration JSON string
522pub fn delete_expression_call_element(
523    config: &str,
524    efcall_id: i64,
525    key: ExpressionCallElementKey,
526) -> Result<String> {
527    let mut config_data: Value =
528        serde_json::from_str(config).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
529
530    // Validate that the element exists
531    let element_exists = config_data["G2_CONFIG"]["CFG_EFBOM"]
532        .as_array()
533        .map(|arr| {
534            arr.iter().any(|item| {
535                item.get("EFCALL_ID").and_then(|v| v.as_i64()) == Some(efcall_id)
536                    && item.get("FTYPE_ID").and_then(|v| v.as_i64()) == Some(key.ftype_id)
537                    && item.get("FELEM_ID").and_then(|v| v.as_i64()) == Some(key.felem_id)
538                    && item.get("EXEC_ORDER").and_then(|v| v.as_i64()) == Some(key.exec_order)
539            })
540        })
541        .unwrap_or(false);
542
543    if !element_exists {
544        return Err(SzConfigError::NotFound(
545            "Expression call element not found".to_string(),
546        ));
547    }
548
549    // Delete the element
550    if let Some(ebom_array) = config_data["G2_CONFIG"]["CFG_EFBOM"].as_array_mut() {
551        ebom_array.retain(|item| {
552            !(item.get("EFCALL_ID").and_then(|v| v.as_i64()) == Some(efcall_id)
553                && item.get("FTYPE_ID").and_then(|v| v.as_i64()) == Some(key.ftype_id)
554                && item.get("FELEM_ID").and_then(|v| v.as_i64()) == Some(key.felem_id)
555                && item.get("EXEC_ORDER").and_then(|v| v.as_i64()) == Some(key.exec_order))
556        });
557    }
558
559    serde_json::to_string(&config_data).map_err(|e| SzConfigError::JsonParse(e.to_string()))
560}
561
562/// Update an expression call element (stub - not typically used)
563///
564/// # Arguments
565/// * `config` - Configuration JSON string
566/// * `params` - Expression call element parameters (efcall_id, ftype_id, felem_id, exec_order, updates)
567///
568/// # Returns
569/// Modified configuration JSON string
570pub fn set_expression_call_element(
571    config: &str,
572    _params: ExpressionCallElementParams,
573) -> Result<String> {
574    // This is a stub - not commonly used
575    Ok(config.to_string())
576}