Skip to main content

sz_configtool_lib/functions/
distinct.rs

1//! Distinct function operations for Senzing configuration
2//!
3//! This module provides functions for managing distinct functions (CFG_DFUNC)
4//! 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 distinct function
17#[derive(Debug, Clone, Default)]
18pub struct AddDistinctFunctionParams<'a> {
19    pub connect_str: &'a str,
20    pub description: Option<&'a str>,
21    pub language: Option<&'a str>,
22}
23
24impl<'a> TryFrom<&'a Value> for AddDistinctFunctionParams<'a> {
25    type Error = SzConfigError;
26
27    fn try_from(json: &'a Value) -> Result<Self, SzConfigError> {
28        Ok(Self {
29            connect_str: json
30                .get("connectStr")
31                .and_then(|v| v.as_str())
32                .ok_or_else(|| SzConfigError::MissingField("connectStr".to_string()))?,
33            description: json.get("description").and_then(|v| v.as_str()),
34            language: json.get("language").and_then(|v| v.as_str()),
35        })
36    }
37}
38
39/// Parameters for setting a distinct function
40#[derive(Debug, Clone, Default)]
41pub struct SetDistinctFunctionParams<'a> {
42    pub connect_str: Option<&'a str>,
43    pub description: Option<&'a str>,
44    pub language: Option<&'a str>,
45}
46
47/// Add a new distinct function
48///
49/// # Arguments
50/// * `config_json` - The configuration JSON string
51/// * `dfunc_code` - Function code (will be uppercased)
52/// * `params` - Function parameters (connect_str required, others optional)
53///
54/// # Returns
55/// Result with modified JSON string and the new function record
56///
57/// # Errors
58/// Returns error if function already exists or JSON is invalid
59pub fn add_distinct_function(
60    config_json: &str,
61    dfunc_code: &str,
62    params: AddDistinctFunctionParams,
63) -> Result<(String, Value), SzConfigError> {
64    let dfunc_code = dfunc_code.to_uppercase();
65
66    // Check if function already exists
67    if find_in_config_array(config_json, "CFG_DFUNC", "DFUNC_CODE", &dfunc_code)?.is_some() {
68        return Err(SzConfigError::validation(format!(
69            "Distinct function already exists: {dfunc_code}"
70        )));
71    }
72
73    // Validate blank CONNECTSTR (Python parity)
74    if params.connect_str.trim().is_empty() {
75        return Err(SzConfigError::validation(
76            "CONNECTSTR cannot be blank".to_string(),
77        ));
78    }
79
80    // Get next DFUNC_ID
81    let config_data: Value =
82        serde_json::from_str(config_json).map_err(|e| SzConfigError::json_parse(e.to_string()))?;
83    let dfunc_id = get_next_id(&config_data, "G2_CONFIG.CFG_DFUNC", "DFUNC_ID", 1)?;
84
85    // Create new function record
86    let mut new_record = json!({
87        "DFUNC_ID": dfunc_id,
88        "DFUNC_CODE": dfunc_code,
89        "CONNECT_STR": params.connect_str,
90    });
91
92    // Add optional fields (always present, null when not specified)
93    new_record["DFUNC_DESC"] = match params.description {
94        Some(desc) => json!(desc),
95        None => Value::Null,
96    };
97    new_record["LANGUAGE"] = match params.language {
98        Some(lang) => json!(lang),
99        None => Value::Null,
100    };
101
102    // Add to CFG_DFUNC
103    let modified_json = add_to_config_array(config_json, "CFG_DFUNC", new_record.clone())?;
104
105    Ok((modified_json, new_record))
106}
107
108/// Delete a distinct function by code
109///
110/// # Arguments
111/// * `config_json` - The configuration JSON string
112/// * `dfunc_code` - Function code to delete
113///
114/// # Returns
115/// Result with modified JSON string and the deleted function record
116///
117/// # Errors
118/// Returns error if function not found or JSON is invalid
119pub fn delete_distinct_function(
120    config_json: &str,
121    dfunc_code: &str,
122) -> Result<(String, Value), SzConfigError> {
123    let dfunc_code = dfunc_code.to_uppercase();
124
125    // Find the function
126    let function = find_in_config_array(config_json, "CFG_DFUNC", "DFUNC_CODE", &dfunc_code)?
127        .ok_or_else(|| {
128            SzConfigError::not_found(format!("Distinct function not found: {dfunc_code}"))
129        })?;
130
131    // Delete from CFG_DFUNC
132    let modified_json =
133        delete_from_config_array(config_json, "CFG_DFUNC", "DFUNC_CODE", &dfunc_code)?;
134
135    Ok((modified_json, function))
136}
137
138/// Get a distinct function by code
139///
140/// # Arguments
141/// * `config_json` - The configuration JSON string
142/// * `dfunc_code` - Function code to retrieve
143///
144/// # Returns
145/// Result with the function record
146///
147/// # Errors
148/// Returns error if function not found or JSON is invalid
149pub fn get_distinct_function(config_json: &str, dfunc_code: &str) -> Result<Value, SzConfigError> {
150    let dfunc_code = dfunc_code.to_uppercase();
151
152    find_in_config_array(config_json, "CFG_DFUNC", "DFUNC_CODE", &dfunc_code)?.ok_or_else(|| {
153        SzConfigError::not_found(format!("Distinct function not found: {dfunc_code}"))
154    })
155}
156
157/// List all distinct functions
158///
159/// # Arguments
160/// * `config_json` - The configuration JSON string
161///
162/// # Returns
163/// Result with vector of function records in display format
164///
165/// # Errors
166/// Returns error if JSON is invalid
167pub fn list_distinct_functions(config_json: &str) -> Result<Vec<Value>, SzConfigError> {
168    let config_data: Value =
169        serde_json::from_str(config_json).map_err(|e| SzConfigError::json_parse(e.to_string()))?;
170
171    let items: Vec<Value> = if let Some(g2_config) = config_data.get("G2_CONFIG")
172        && let Some(array) = g2_config.get("CFG_DFUNC")
173        && let Some(items) = array.as_array()
174    {
175        items
176            .iter()
177            .map(|item| {
178                json!({
179                    "id": item.get("DFUNC_ID").and_then(|v| v.as_i64()).unwrap_or(0),
180                    "function": item.get("DFUNC_CODE").and_then(|v| v.as_str()).unwrap_or(""),
181                    "connectStr": item.get("CONNECT_STR").and_then(|v| v.as_str()).unwrap_or(""),
182                    "anonSupport": item.get("ANON_SUPPORT").and_then(|v| v.as_str()).unwrap_or(""),
183                    "language": item.get("LANGUAGE").and_then(|v| v.as_str()).unwrap_or("")
184                })
185            })
186            .collect()
187    } else {
188        Vec::new()
189    };
190
191    Ok(items)
192}
193
194/// Set (update) a distinct function
195///
196/// # Arguments
197/// * `config_json` - The configuration JSON string
198/// * `dfunc_code` - Function code to update
199/// * `params` - Function parameters to update (all optional)
200///
201/// # Returns
202/// Result with modified JSON string and the updated function record
203///
204/// # Errors
205/// Returns error if function not found or JSON is invalid
206pub fn set_distinct_function(
207    config_json: &str,
208    dfunc_code: &str,
209    params: SetDistinctFunctionParams,
210) -> Result<(String, Value), SzConfigError> {
211    let dfunc_code = dfunc_code.to_uppercase();
212
213    // Find existing function
214    let mut function = find_in_config_array(config_json, "CFG_DFUNC", "DFUNC_CODE", &dfunc_code)?
215        .ok_or_else(|| {
216        SzConfigError::not_found(format!("Distinct function not found: {dfunc_code}"))
217    })?;
218
219    // Update fields if provided
220    if let Some(obj) = function.as_object_mut() {
221        if let Some(conn) = params.connect_str {
222            obj.insert("CONNECT_STR".to_string(), json!(conn));
223        }
224        if let Some(desc) = params.description {
225            obj.insert("DFUNC_DESC".to_string(), json!(desc));
226        }
227        if let Some(lang) = params.language {
228            obj.insert("LANGUAGE".to_string(), json!(lang));
229        }
230    }
231
232    // Delete old and add updated
233    let temp_json = delete_from_config_array(config_json, "CFG_DFUNC", "DFUNC_CODE", &dfunc_code)?;
234    let modified_json = add_to_config_array(&temp_json, "CFG_DFUNC", function.clone())?;
235
236    Ok((modified_json, function))
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    fn get_test_config() -> String {
244        json!({
245            "G2_CONFIG": {
246                "CFG_DFUNC": [
247                    {
248                        "DFUNC_ID": 1,
249                        "DFUNC_CODE": "DIST_NAME",
250                        "CONNECT_STR": "g2DistName",
251                        "LANGUAGE": "en"
252                    }
253                ]
254            }
255        })
256        .to_string()
257    }
258
259    #[test]
260    fn test_add_distinct_function() {
261        let config = get_test_config();
262        let result = add_distinct_function(
263            &config,
264            "custom_dist",
265            AddDistinctFunctionParams {
266                connect_str: "g2CustomDist",
267                description: Some("Custom distinct"),
268                language: Some("en"),
269            },
270        );
271        assert!(result.is_ok());
272        let (modified, record) = result.unwrap();
273        assert!(modified.contains("CUSTOM_DIST"));
274        assert_eq!(record["DFUNC_CODE"], "CUSTOM_DIST");
275    }
276
277    #[test]
278    fn test_list_distinct_functions() {
279        let config = get_test_config();
280        let result = list_distinct_functions(&config);
281        assert!(result.is_ok());
282        let items = result.unwrap();
283        assert_eq!(items.len(), 1);
284        assert_eq!(items[0]["function"], "DIST_NAME");
285    }
286}