Skip to main content

sz_configtool_lib/
datasources.rs

1use crate::error::{Result, SzConfigError};
2use crate::helpers;
3use serde_json::{Value, json};
4
5// ============================================================================
6// Parameter Structs
7// ============================================================================
8
9/// Parameters for adding a data source
10#[derive(Debug, Clone, Default)]
11pub struct AddDataSourceParams<'a> {
12    pub code: &'a str,
13    pub retention_level: Option<&'a str>,
14    pub conversational: Option<&'a str>,
15    pub reliability: Option<i64>,
16}
17
18/// Parameters for setting (updating) a data source
19#[derive(Debug, Clone, Default)]
20pub struct SetDataSourceParams<'a> {
21    pub code: &'a str,
22    pub retention_level: Option<&'a str>,
23    pub conversational: Option<&'a str>,
24    pub reliability: Option<i64>,
25}
26
27impl<'a> TryFrom<&'a Value> for AddDataSourceParams<'a> {
28    type Error = SzConfigError;
29
30    fn try_from(json: &'a Value) -> Result<Self> {
31        let code = json
32            .get("code")
33            .and_then(|v| v.as_str())
34            .ok_or_else(|| SzConfigError::MissingField("code".to_string()))?;
35
36        Ok(Self {
37            code,
38            retention_level: json.get("retentionLevel").and_then(|v| v.as_str()),
39            conversational: json.get("conversational").and_then(|v| v.as_str()),
40            reliability: json.get("reliability").and_then(|v| v.as_i64()),
41        })
42    }
43}
44
45impl<'a> TryFrom<&'a Value> for SetDataSourceParams<'a> {
46    type Error = SzConfigError;
47
48    fn try_from(json: &'a Value) -> Result<Self> {
49        let code = json
50            .get("code")
51            .and_then(|v| v.as_str())
52            .ok_or_else(|| SzConfigError::MissingField("code".to_string()))?;
53
54        Ok(Self {
55            code,
56            retention_level: json.get("retentionLevel").and_then(|v| v.as_str()),
57            conversational: json.get("conversational").and_then(|v| v.as_str()),
58            reliability: json.get("reliability").and_then(|v| v.as_i64()),
59        })
60    }
61}
62
63/// Add a new data source to the configuration
64///
65/// # Arguments
66/// * `config_json` - JSON configuration string
67/// * `params` - Data source parameters (code required, others optional)
68///
69/// # Returns
70/// Modified configuration JSON string
71///
72/// # Errors
73/// - `AlreadyExists` if data source code already exists
74/// - `JsonParse` if config_json is invalid
75/// - `MissingSection` if CFG_DSRC section doesn't exist
76pub fn add_data_source(config_json: &str, params: AddDataSourceParams) -> Result<String> {
77    let mut config: Value =
78        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
79
80    let dsrcs = config
81        .get_mut("G2_CONFIG")
82        .and_then(|g| g.get_mut("CFG_DSRC"))
83        .and_then(|v| v.as_array_mut())
84        .ok_or_else(|| SzConfigError::MissingSection("CFG_DSRC".to_string()))?;
85
86    // Check for duplicates
87    let code_upper = params.code.to_uppercase();
88    if dsrcs
89        .iter()
90        .any(|d| d["DSRC_CODE"].as_str() == Some(&code_upper))
91    {
92        return Err(SzConfigError::AlreadyExists(format!(
93            "Data source already exists: {code_upper}"
94        )));
95    }
96
97    let next_id = helpers::get_next_id_from_array(dsrcs, "DSRC_ID")?;
98
99    // Validate and use parameters or defaults (case-insensitive with normalization)
100    let retention = if let Some(level) = params.retention_level {
101        // Validate retentionLevel domain (case-insensitive)
102        let level_upper = level.to_uppercase();
103        match level_upper.as_str() {
104            "REMEMBER" => "Remember",
105            "FORGET" => "Forget",
106            _ => {
107                return Err(SzConfigError::InvalidInput(format!(
108                    "Invalid RETENTIONLEVEL value '{level}'. Must be 'Remember' or 'Forget'"
109                )));
110            }
111        }
112    } else {
113        "Remember"
114    };
115
116    let conversational_flag = if let Some(conv) = params.conversational {
117        // Validate conversational domain (case-insensitive)
118        let conv_upper = conv.to_uppercase();
119        match conv_upper.as_str() {
120            "YES" => "Yes",
121            "NO" => "No",
122            _ => {
123                return Err(SzConfigError::InvalidInput(format!(
124                    "Invalid CONVERSATIONAL value '{conv}'. Must be 'Yes' or 'No'"
125                )));
126            }
127        }
128    } else {
129        "No"
130    };
131
132    let reliability_score = params.reliability.unwrap_or(1);
133
134    dsrcs.push(json!({
135        "DSRC_ID": next_id,
136        "DSRC_CODE": code_upper.clone(),
137        "DSRC_DESC": code_upper,  // Python uses code as description, not formatted string
138        "DSRC_RELY": reliability_score,
139        "RETENTION_LEVEL": retention,
140        "CONVERSATIONAL": conversational_flag,
141    }));
142
143    serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
144}
145
146/// Delete a data source from the configuration
147///
148/// # Arguments
149/// * `config_json` - JSON configuration string
150/// * `code` - Data source code to delete
151///
152/// # Returns
153/// Modified configuration JSON string
154///
155/// # Errors
156/// - `NotFound` if data source doesn't exist
157/// - `InvalidInput` if attempting to delete system datasource (ID <= 2)
158/// - `JsonParse` if config_json is invalid
159/// - `MissingSection` if CFG_DSRC section doesn't exist
160pub fn delete_data_source(config_json: &str, code: &str) -> Result<String> {
161    let mut config: Value =
162        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
163
164    let dsrcs = config
165        .get_mut("G2_CONFIG")
166        .and_then(|g| g.get_mut("CFG_DSRC"))
167        .and_then(|v| v.as_array_mut())
168        .ok_or_else(|| SzConfigError::MissingSection("CFG_DSRC".to_string()))?;
169
170    let code_upper = code.to_uppercase();
171
172    // Check if datasource exists and get its ID for protection check
173    let dsrc_to_delete = dsrcs
174        .iter()
175        .find(|d| d["DSRC_CODE"].as_str() == Some(&code_upper))
176        .ok_or_else(|| SzConfigError::NotFound(format!("Data source not found: {code_upper}")))?;
177
178    // Protect system datasources (Python parity: if dsrc_record["DSRC_ID"] <= 2)
179    if let Some(dsrc_id) = dsrc_to_delete.get("DSRC_ID").and_then(|v| v.as_i64()) {
180        if dsrc_id <= 2 {
181            return Err(SzConfigError::InvalidInput(format!(
182                "The {code_upper} data source cannot be deleted"
183            )));
184        }
185    }
186
187    // Safe to delete
188    let original_len = dsrcs.len();
189    dsrcs.retain(|d| d["DSRC_CODE"].as_str() != Some(&code_upper));
190
191    if dsrcs.len() == original_len {
192        return Err(SzConfigError::NotFound(format!(
193            "Data source not found: {code_upper}"
194        )));
195    }
196
197    serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
198}
199
200/// Get a specific data source by code
201///
202/// # Arguments
203/// * `config_json` - JSON configuration string
204/// * `code` - Data source code to retrieve
205///
206/// # Returns
207/// JSON Value representing the data source
208///
209/// # Errors
210/// - `NotFound` if data source doesn't exist
211/// - `JsonParse` if config_json is invalid
212/// - `MissingSection` if CFG_DSRC section doesn't exist
213pub fn get_data_source(config_json: &str, code: &str) -> Result<Value> {
214    let config: Value =
215        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
216
217    let code_upper = code.to_uppercase();
218    config
219        .get("G2_CONFIG")
220        .and_then(|g| g.get("CFG_DSRC"))
221        .and_then(|v| v.as_array())
222        .ok_or_else(|| SzConfigError::MissingSection("CFG_DSRC".to_string()))?
223        .iter()
224        .find(|d| d["DSRC_CODE"].as_str() == Some(&code_upper))
225        .cloned()
226        .ok_or_else(|| SzConfigError::NotFound(format!("Data source not found: {code_upper}")))
227}
228
229/// List all data sources
230///
231/// # Arguments
232/// * `config_json` - JSON configuration string
233///
234/// # Returns
235/// Vector of JSON Values representing data sources in Python format
236/// (with "id" and "dataSource" fields)
237///
238/// # Errors
239/// - `JsonParse` if config_json is invalid
240/// - `MissingSection` if CFG_DSRC section doesn't exist
241pub fn list_data_sources(config_json: &str) -> Result<Vec<Value>> {
242    let config: Value =
243        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
244
245    let dsrcs = config
246        .get("G2_CONFIG")
247        .and_then(|g| g.get("CFG_DSRC"))
248        .and_then(|v| v.as_array())
249        .ok_or_else(|| SzConfigError::MissingSection("CFG_DSRC".to_string()))?;
250
251    Ok(dsrcs
252        .iter()
253        .map(|item| {
254            json!({
255                "id": item.get("DSRC_ID").and_then(|v| v.as_i64()).unwrap_or(0),
256                "dataSource": item.get("DSRC_CODE").and_then(|v| v.as_str()).unwrap_or("")
257            })
258        })
259        .collect())
260}
261
262/// Set (update) a data source's properties
263///
264/// # Arguments
265/// * `config_json` - JSON configuration string
266/// * `params` - Data source parameters (code required, others optional to update)
267///
268/// # Returns
269/// Modified configuration JSON string
270///
271/// # Errors
272/// - `NotFound` if data source doesn't exist
273/// - `JsonParse` if config_json is invalid
274/// - `MissingSection` if CFG_DSRC section doesn't exist
275pub fn set_data_source(config_json: &str, params: SetDataSourceParams) -> Result<String> {
276    let mut config: Value =
277        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
278
279    let code_upper = params.code.to_uppercase();
280    let dsrcs = config
281        .get_mut("G2_CONFIG")
282        .and_then(|g| g.get_mut("CFG_DSRC"))
283        .and_then(|v| v.as_array_mut())
284        .ok_or_else(|| SzConfigError::MissingSection("CFG_DSRC".to_string()))?;
285
286    let dsrc = dsrcs
287        .iter_mut()
288        .find(|d| d["DSRC_CODE"].as_str() == Some(&code_upper))
289        .ok_or_else(|| SzConfigError::NotFound(format!("Data source not found: {code_upper}")))?;
290
291    // Update fields if provided
292    if let Some(dsrc_obj) = dsrc.as_object_mut() {
293        if let Some(retention) = params.retention_level {
294            dsrc_obj.insert("RETENTION_LEVEL".to_string(), json!(retention));
295        }
296        if let Some(conversational) = params.conversational {
297            dsrc_obj.insert("CONVERSATIONAL".to_string(), json!(conversational));
298        }
299        if let Some(reliability) = params.reliability {
300            dsrc_obj.insert("DSRC_RELY".to_string(), json!(reliability));
301        }
302    }
303
304    serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
305}