Skip to main content

sz_configtool_lib/functions/
comparison.rs

1//! Comparison function operations for Senzing configuration
2//!
3//! This module provides functions for managing comparison functions (CFG_CFUNC)
4//! and comparison function return codes (CFG_CFRTN) in the Senzing configuration JSON.
5
6use crate::error::SzConfigError;
7use crate::helpers::{
8    add_to_config_array, delete_from_config_array, find_in_config_array, get_next_id,
9};
10use serde_json::{Value, json};
11
12// ============================================================================
13// Parameter Structs
14// ============================================================================
15
16/// Parameters for adding a comparison function
17#[derive(Debug, Clone, Default)]
18pub struct AddComparisonFunctionParams<'a> {
19    pub connect_str: &'a str,
20    pub description: Option<&'a str>,
21    pub language: Option<&'a str>,
22    pub anon_support: Option<&'a str>,
23}
24
25impl<'a> TryFrom<&'a Value> for AddComparisonFunctionParams<'a> {
26    type Error = SzConfigError;
27
28    fn try_from(json: &'a Value) -> Result<Self, SzConfigError> {
29        Ok(Self {
30            connect_str: json
31                .get("connectStr")
32                .and_then(|v| v.as_str())
33                .ok_or_else(|| SzConfigError::MissingField("connectStr".to_string()))?,
34            description: json.get("description").and_then(|v| v.as_str()),
35            language: json.get("language").and_then(|v| v.as_str()),
36            anon_support: json.get("anonSupport").and_then(|v| v.as_str()),
37        })
38    }
39}
40
41/// Parameters for setting a comparison function
42#[derive(Debug, Clone, Default)]
43pub struct SetComparisonFunctionParams<'a> {
44    pub connect_str: Option<&'a str>,
45    pub description: Option<&'a str>,
46    pub language: Option<&'a str>,
47    pub anon_support: Option<&'a str>,
48}
49
50/// Add a new comparison function
51///
52/// # Arguments
53/// * `config_json` - The configuration JSON string
54/// * `cfunc_code` - Function code (will be uppercased)
55/// * `params` - Function parameters (connect_str required, others optional)
56///
57/// # Returns
58/// Result with modified JSON string and the new function record
59///
60/// # Errors
61/// Returns error if function already exists or JSON is invalid
62pub fn add_comparison_function(
63    config_json: &str,
64    cfunc_code: &str,
65    params: AddComparisonFunctionParams,
66) -> Result<(String, Value), SzConfigError> {
67    let cfunc_code = cfunc_code.to_uppercase();
68
69    // Check if function already exists
70    if find_in_config_array(config_json, "CFG_CFUNC", "CFUNC_CODE", &cfunc_code)?.is_some() {
71        return Err(SzConfigError::validation(format!(
72            "Comparison function already exists: {cfunc_code}"
73        )));
74    }
75
76    // Validate ANON_SUPPORT domain (Python parity: ["Yes", "No"], default "No")
77    let anon_support = if let Some(val) = params.anon_support {
78        let val_upper = val.to_uppercase();
79        match val_upper.as_str() {
80            "YES" => "Yes",
81            "NO" => "No",
82            _ => {
83                return Err(SzConfigError::validation(format!(
84                    "Invalid ANON_SUPPORT value '{val}'. Must be 'Yes' or 'No'"
85                )));
86            }
87        }
88    } else {
89        "No"
90    };
91
92    // Get next CFUNC_ID
93    let config_data: Value =
94        serde_json::from_str(config_json).map_err(|e| SzConfigError::json_parse(e.to_string()))?;
95    let cfunc_id = get_next_id(&config_data, "G2_CONFIG.CFG_CFUNC", "CFUNC_ID", 1)?;
96
97    // Create new function record
98    let mut new_record = json!({
99        "CFUNC_ID": cfunc_id,
100        "CFUNC_CODE": cfunc_code,
101        "CONNECT_STR": params.connect_str,
102        "ANON_SUPPORT": anon_support,
103    });
104
105    // Add optional fields (always present, null when not specified)
106    new_record["CFUNC_DESC"] = match params.description {
107        Some(desc) => json!(desc),
108        None => Value::Null,
109    };
110    new_record["LANGUAGE"] = match params.language {
111        Some(lang) => json!(lang),
112        None => Value::Null,
113    };
114
115    // Add to CFG_CFUNC
116    let modified_json = add_to_config_array(config_json, "CFG_CFUNC", new_record.clone())?;
117
118    Ok((modified_json, new_record))
119}
120
121/// Delete a comparison function by code
122///
123/// # Arguments
124/// * `config_json` - The configuration JSON string
125/// * `cfunc_code` - Function code to delete
126///
127/// # Returns
128/// Result with modified JSON string and the deleted function record
129///
130/// # Errors
131/// Returns error if function not found or JSON is invalid
132pub fn delete_comparison_function(
133    config_json: &str,
134    cfunc_code: &str,
135) -> Result<(String, Value), SzConfigError> {
136    let cfunc_code = cfunc_code.to_uppercase();
137
138    // Find the function
139    let function = find_in_config_array(config_json, "CFG_CFUNC", "CFUNC_CODE", &cfunc_code)?
140        .ok_or_else(|| {
141            SzConfigError::not_found(format!("Comparison function not found: {cfunc_code}"))
142        })?;
143
144    // Delete from CFG_CFUNC
145    let modified_json =
146        delete_from_config_array(config_json, "CFG_CFUNC", "CFUNC_CODE", &cfunc_code)?;
147
148    Ok((modified_json, function))
149}
150
151/// Get a comparison function by code
152///
153/// # Arguments
154/// * `config_json` - The configuration JSON string
155/// * `cfunc_code` - Function code to retrieve
156///
157/// # Returns
158/// Result with the function record
159///
160/// # Errors
161/// Returns error if function not found or JSON is invalid
162pub fn get_comparison_function(
163    config_json: &str,
164    cfunc_code: &str,
165) -> Result<Value, SzConfigError> {
166    let cfunc_code = cfunc_code.to_uppercase();
167
168    find_in_config_array(config_json, "CFG_CFUNC", "CFUNC_CODE", &cfunc_code)?.ok_or_else(|| {
169        SzConfigError::not_found(format!("Comparison function not found: {cfunc_code}"))
170    })
171}
172
173/// List all comparison functions
174///
175/// # Arguments
176/// * `config_json` - The configuration JSON string
177///
178/// # Returns
179/// Result with vector of function records in display format
180///
181/// # Errors
182/// Returns error if JSON is invalid
183pub fn list_comparison_functions(config_json: &str) -> Result<Vec<Value>, SzConfigError> {
184    let config_data: Value =
185        serde_json::from_str(config_json).map_err(|e| SzConfigError::json_parse(e.to_string()))?;
186
187    let items: Vec<Value> = if let Some(g2_config) = config_data.get("G2_CONFIG")
188        && let Some(array) = g2_config.get("CFG_CFUNC")
189        && let Some(items) = array.as_array()
190    {
191        items
192            .iter()
193            .map(|item| {
194                json!({
195                    "id": item.get("CFUNC_ID").and_then(|v| v.as_i64()).unwrap_or(0),
196                    "function": item.get("CFUNC_CODE").and_then(|v| v.as_str()).unwrap_or(""),
197                    "connectStr": item.get("CONNECT_STR").and_then(|v| v.as_str()).unwrap_or(""),
198                    "language": item.get("LANGUAGE").and_then(|v| v.as_str()).unwrap_or(""),
199                    "anonSupport": item.get("ANON_SUPPORT").and_then(|v| v.as_str()).unwrap_or("")
200                })
201            })
202            .collect()
203    } else {
204        Vec::new()
205    };
206
207    Ok(items)
208}
209
210/// Set (update) a comparison function
211///
212/// # Arguments
213/// * `config_json` - The configuration JSON string
214/// * `cfunc_code` - Function code to update
215/// * `params` - Function parameters to update (all optional)
216///
217/// # Returns
218/// Result with modified JSON string and the updated function record
219///
220/// # Errors
221/// Returns error if function not found or JSON is invalid
222pub fn set_comparison_function(
223    config_json: &str,
224    cfunc_code: &str,
225    params: SetComparisonFunctionParams,
226) -> Result<(String, Value), SzConfigError> {
227    let cfunc_code = cfunc_code.to_uppercase();
228
229    // Find existing function
230    let mut function = find_in_config_array(config_json, "CFG_CFUNC", "CFUNC_CODE", &cfunc_code)?
231        .ok_or_else(|| {
232        SzConfigError::not_found(format!("Comparison function not found: {cfunc_code}"))
233    })?;
234
235    // Update fields if provided
236    if let Some(obj) = function.as_object_mut() {
237        if let Some(conn) = params.connect_str {
238            obj.insert("CONNECT_STR".to_string(), json!(conn));
239        }
240        if let Some(desc) = params.description {
241            obj.insert("CFUNC_DESC".to_string(), json!(desc));
242        }
243        if let Some(lang) = params.language {
244            obj.insert("LANGUAGE".to_string(), json!(lang));
245        }
246        if let Some(anon) = params.anon_support {
247            obj.insert("ANON_SUPPORT".to_string(), json!(anon));
248        }
249    }
250
251    // Delete old and add updated
252    let temp_json = delete_from_config_array(config_json, "CFG_CFUNC", "CFUNC_CODE", &cfunc_code)?;
253    let modified_json = add_to_config_array(&temp_json, "CFG_CFUNC", function.clone())?;
254
255    Ok((modified_json, function))
256}
257
258/// Add a comparison function return code
259///
260/// # Arguments
261/// * `config_json` - The configuration JSON string
262/// * `cfunc_code` - Function code (will be uppercased)
263/// * `cfrtn_code` - Return code (will be uppercased)
264/// * `cfrtn_desc` - Optional description
265///
266/// # Returns
267/// Result with modified JSON string and the new return code record
268///
269/// # Errors
270/// Returns error if function not found, return code exists, or JSON is invalid
271pub fn add_comparison_func_return_code(
272    config_json: &str,
273    cfunc_code: &str,
274    cfrtn_code: &str,
275    cfrtn_desc: Option<&str>,
276) -> Result<(String, Value), SzConfigError> {
277    let cfunc_code = cfunc_code.to_uppercase();
278    let cfrtn_code = cfrtn_code.to_uppercase();
279
280    // Find the function
281    let config_data: Value =
282        serde_json::from_str(config_json).map_err(|e| SzConfigError::json_parse(e.to_string()))?;
283
284    let cfunc = find_in_config_array(config_json, "CFG_CFUNC", "CFUNC_CODE", &cfunc_code)?
285        .ok_or_else(|| {
286            SzConfigError::not_found(format!("Comparison function not found: {cfunc_code}"))
287        })?;
288
289    let cfunc_id = cfunc
290        .get("CFUNC_ID")
291        .and_then(|v| v.as_i64())
292        .ok_or_else(|| SzConfigError::validation("CFUNC_ID not found"))?;
293
294    // Check if return code already exists for this function
295    if let Some(g2_config) = config_data.get("G2_CONFIG")
296        && let Some(array) = g2_config.get("CFG_CFRTN")
297        && let Some(items) = array.as_array()
298        && items.iter().any(|item| {
299            item.get("CFUNC_ID").and_then(|v| v.as_i64()) == Some(cfunc_id)
300                && item.get("CFRTN_CODE").and_then(|v| v.as_str()) == Some(&cfrtn_code)
301        })
302    {
303        return Err(SzConfigError::validation(format!(
304            "Return code {cfrtn_code} already exists for function {cfunc_code}"
305        )));
306    }
307
308    // Get next CFRTN_ID
309    let cfrtn_id = get_next_id(&config_data, "G2_CONFIG.CFG_CFRTN", "CFRTN_ID", 1)?;
310
311    // Create new return code record
312    let mut new_record = json!({
313        "CFRTN_ID": cfrtn_id,
314        "CFUNC_ID": cfunc_id,
315        "CFRTN_CODE": cfrtn_code,
316    });
317
318    // Add optional description (always present, null when not specified)
319    new_record["CFRTN_DESC"] = match cfrtn_desc {
320        Some(desc) => json!(desc),
321        None => Value::Null,
322    };
323
324    // Add to CFG_CFRTN
325    let modified_json = add_to_config_array(config_json, "CFG_CFRTN", new_record.clone())?;
326
327    Ok((modified_json, new_record))
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    fn get_test_config() -> String {
335        json!({
336            "G2_CONFIG": {
337                "CFG_CFUNC": [
338                    {
339                        "CFUNC_ID": 1,
340                        "CFUNC_CODE": "CMP_NAME",
341                        "CONNECT_STR": "g2CmpName",
342                        "LANGUAGE": "en"
343                    }
344                ],
345                "CFG_CFRTN": []
346            }
347        })
348        .to_string()
349    }
350
351    #[test]
352    fn test_add_comparison_function() {
353        let config = get_test_config();
354        let result = add_comparison_function(
355            &config,
356            "custom_cmp",
357            AddComparisonFunctionParams {
358                connect_str: "g2CustomCmp",
359                description: Some("Custom compare"),
360                language: Some("en"),
361                anon_support: Some("Yes"),
362            },
363        );
364        assert!(result.is_ok());
365        let (modified, record) = result.unwrap();
366        assert!(modified.contains("CUSTOM_CMP"));
367        assert_eq!(record["CFUNC_CODE"], "CUSTOM_CMP");
368    }
369
370    #[test]
371    fn test_list_comparison_functions() {
372        let config = get_test_config();
373        let result = list_comparison_functions(&config);
374        assert!(result.is_ok());
375        let items = result.unwrap();
376        assert_eq!(items.len(), 1);
377        assert_eq!(items[0]["function"], "CMP_NAME");
378    }
379}