Skip to main content

sz_configtool_lib/
generic_plans.rs

1//! Generic Plan (CFG_GPLAN) operations
2//!
3//! Functions for managing generic threshold plans in the configuration.
4//! Generic plans contain thresholds for entity resolution scoring.
5
6use crate::error::{Result, SzConfigError};
7use crate::helpers;
8use serde_json::{Value, json};
9
10/// Clone a generic plan with all its thresholds
11///
12/// # Arguments
13///
14/// * `config_json` - Configuration JSON string
15/// * `source_gplan_code` - Source plan code to clone from
16/// * `new_gplan_code` - New plan code to create
17/// * `new_gplan_desc` - Optional description for new plan (uses code if None)
18///
19/// # Returns
20///
21/// Returns `(modified_config, new_plan_id)` tuple on success
22///
23/// # Example
24///
25/// ```
26/// use sz_configtool_lib::generic_plans;
27///
28/// let config = r#"{"G2_CONFIG": {"CFG_GPLAN": [{"GPLAN_ID": 1, "GPLAN_CODE": "INGEST"}], "CFG_GENERIC_THRESHOLD": [{"GPLAN_ID": 1, "BEHAVIOR": "NAME"}]}}"#;
29/// let (modified, plan_id) = generic_plans::clone_generic_plan(config, "INGEST", "CUSTOM", None).unwrap();
30/// ```
31pub fn clone_generic_plan(
32    config_json: &str,
33    source_gplan_code: &str,
34    new_gplan_code: &str,
35    new_gplan_desc: Option<&str>,
36) -> Result<(String, i64)> {
37    let source_code = source_gplan_code.to_uppercase();
38    let new_code = new_gplan_code.to_uppercase();
39    let new_desc = new_gplan_desc.unwrap_or(&new_code);
40
41    // Find source plan
42    let source_plan =
43        helpers::find_in_config_array(config_json, "CFG_GPLAN", "GPLAN_CODE", &source_code)?
44            .ok_or_else(|| {
45                SzConfigError::NotFound(format!("Source generic plan not found: {source_code}"))
46            })?;
47
48    let source_gplan_id = source_plan
49        .get("GPLAN_ID")
50        .and_then(|v| v.as_i64())
51        .ok_or_else(|| {
52            SzConfigError::InvalidConfig("Invalid GPLAN_ID in source plan".to_string())
53        })?;
54
55    // Check if new plan already exists
56    if helpers::find_in_config_array(config_json, "CFG_GPLAN", "GPLAN_CODE", &new_code)?.is_some() {
57        return Err(SzConfigError::AlreadyExists(format!(
58            "Generic plan already exists: {new_code}"
59        )));
60    }
61
62    // Get next GPLAN_ID
63    let config_data: Value = serde_json::from_str(config_json)?;
64    let max_gplan_id = config_data
65        .get("G2_CONFIG")
66        .and_then(|g| g.get("CFG_GPLAN"))
67        .and_then(|v| v.as_array())
68        .map(|arr| {
69            arr.iter()
70                .filter_map(|item| item.get("GPLAN_ID").and_then(|v| v.as_i64()))
71                .max()
72                .unwrap_or(0)
73        })
74        .unwrap_or(0);
75
76    let new_gplan_id = max_gplan_id + 1;
77
78    // Create new plan
79    let new_plan = json!({
80        "GPLAN_ID": new_gplan_id,
81        "GPLAN_CODE": new_code,
82        "GPLAN_DESC": new_desc
83    });
84
85    let mut modified_json = helpers::add_to_config_array(config_json, "CFG_GPLAN", new_plan)?;
86
87    // Clone all thresholds from source plan to new plan
88    let config_data: Value = serde_json::from_str(&modified_json)?;
89    if let Some(gthresh_array) = config_data
90        .get("G2_CONFIG")
91        .and_then(|g| g.get("CFG_GENERIC_THRESHOLD"))
92        .and_then(|v| v.as_array())
93    {
94        let mut cloned_thresholds = Vec::new();
95        for item in gthresh_array {
96            if item.get("GPLAN_ID").and_then(|v| v.as_i64()) == Some(source_gplan_id) {
97                let mut cloned = item.clone();
98                if let Some(obj) = cloned.as_object_mut() {
99                    obj.insert("GPLAN_ID".to_string(), json!(new_gplan_id));
100                }
101                cloned_thresholds.push(cloned);
102            }
103        }
104
105        // Add cloned thresholds
106        for threshold in cloned_thresholds {
107            modified_json =
108                helpers::add_to_config_array(&modified_json, "CFG_GENERIC_THRESHOLD", threshold)?;
109        }
110    }
111
112    Ok((modified_json, new_gplan_id))
113}
114
115/// Delete a generic plan and all its thresholds
116///
117/// # Arguments
118///
119/// * `config_json` - Configuration JSON string
120/// * `gplan_code` - Plan code to delete
121///
122/// # Returns
123///
124/// Returns modified configuration JSON on success
125///
126/// # Example
127///
128/// ```
129/// use sz_configtool_lib::generic_plans;
130///
131/// // System plans (ID ≤ 2) cannot be deleted, use ID > 2 for user plans
132/// let config = r#"{"G2_CONFIG": {"CFG_GPLAN": [{"GPLAN_ID": 3, "GPLAN_CODE": "CUSTOM_PLAN"}], "CFG_GENERIC_THRESHOLD": []}}"#;
133/// let modified = generic_plans::delete_generic_plan(config, "CUSTOM_PLAN").unwrap();
134/// ```
135pub fn delete_generic_plan(config_json: &str, gplan_code: &str) -> Result<String> {
136    let gplan_code = gplan_code.to_uppercase();
137
138    // Find the plan
139    let plan = helpers::find_in_config_array(config_json, "CFG_GPLAN", "GPLAN_CODE", &gplan_code)?
140        .ok_or_else(|| SzConfigError::NotFound(format!("Generic plan not found: {gplan_code}")))?;
141
142    let gplan_id = plan
143        .get("GPLAN_ID")
144        .and_then(|v| v.as_i64())
145        .ok_or_else(|| SzConfigError::InvalidConfig("Invalid GPLAN_ID".to_string()))?;
146
147    // System plan protection: Plans with ID <= 2 cannot be deleted (Python line 4206-4208)
148    if gplan_id <= 2 {
149        return Err(SzConfigError::InvalidInput(format!(
150            "The {gplan_code} plan cannot be deleted"
151        )));
152    }
153
154    // Parse and modify config
155    let mut config_data: Value = serde_json::from_str(config_json)?;
156
157    // Delete the plan
158    if let Some(g2_config) = config_data.get_mut("G2_CONFIG") {
159        if let Some(gplan_array) = g2_config
160            .get_mut("CFG_GPLAN")
161            .and_then(|v| v.as_array_mut())
162        {
163            gplan_array
164                .retain(|item| item.get("GPLAN_ID").and_then(|v| v.as_i64()) != Some(gplan_id));
165        }
166
167        // Delete all associated thresholds
168        if let Some(gthresh_array) = g2_config
169            .get_mut("CFG_GENERIC_THRESHOLD")
170            .and_then(|v| v.as_array_mut())
171        {
172            gthresh_array
173                .retain(|item| item.get("GPLAN_ID").and_then(|v| v.as_i64()) != Some(gplan_id));
174        }
175    }
176
177    Ok(serde_json::to_string(&config_data)?)
178}
179
180/// List all generic plans in the configuration
181///
182/// # Arguments
183///
184/// * `config_json` - Configuration JSON string
185/// * `filter` - Optional filter string to search in records
186///
187/// # Returns
188///
189/// Returns a vector of plan objects in Python sz_configtool format
190///
191/// # Example
192///
193/// ```
194/// use sz_configtool_lib::generic_plans;
195///
196/// let config = r#"{"G2_CONFIG": {"CFG_GPLAN": [{"GPLAN_ID": 1, "GPLAN_CODE": "INGEST", "GPLAN_DESC": "Ingest Plan"}]}}"#;
197/// let plans = generic_plans::list_generic_plans(config, None).unwrap();
198/// assert_eq!(plans.len(), 1);
199/// ```
200pub fn list_generic_plans(config_json: &str, filter: Option<&str>) -> Result<Vec<Value>> {
201    // Get all items from CFG_GPLAN
202    let items = helpers::list_from_config_array(config_json, "CFG_GPLAN")?;
203
204    // Transform and filter items
205    let mut result: Vec<Value> = items
206        .into_iter()
207        .filter(|item| {
208            if let Some(f) = filter {
209                // Filter if search term appears anywhere in the record
210                let item_str = item.to_string().to_lowercase();
211                item_str.contains(&f.to_lowercase())
212            } else {
213                true
214            }
215        })
216        .map(|item| {
217            json!({
218                "id": item.get("GPLAN_ID").and_then(|v| v.as_i64()).unwrap_or(0),
219                "plan": item.get("GPLAN_CODE").and_then(|v| v.as_str()).unwrap_or(""),
220                "description": item.get("GPLAN_DESC").and_then(|v| v.as_str()).unwrap_or("")
221            })
222        })
223        .collect();
224
225    // Sort by ID
226    result.sort_by_key(|item| item.get("id").and_then(|v| v.as_i64()).unwrap_or(0));
227
228    Ok(result)
229}
230
231/// Set (create or update) a generic plan
232///
233/// # Arguments
234///
235/// * `config_json` - Configuration JSON string
236/// * `gplan_code` - Plan code
237/// * `gplan_desc` - Plan description
238///
239/// # Returns
240///
241/// Returns `(modified_config, plan_id, was_created)` tuple on success
242///
243/// # Example
244///
245/// ```
246/// use sz_configtool_lib::generic_plans;
247///
248/// let config = r#"{"G2_CONFIG": {"CFG_GPLAN": []}}"#;
249/// let (modified, plan_id, was_created) = generic_plans::set_generic_plan(config, "CUSTOM", "Custom Plan").unwrap();
250/// assert!(was_created);
251/// ```
252pub fn set_generic_plan(
253    config_json: &str,
254    gplan_code: &str,
255    gplan_desc: &str,
256) -> Result<(String, i64, bool)> {
257    let code = gplan_code.to_uppercase();
258
259    // Check if plan already exists
260    if let Some(existing) =
261        helpers::find_in_config_array(config_json, "CFG_GPLAN", "GPLAN_CODE", &code)?
262    {
263        // Update existing plan
264        let plan_id = existing
265            .get("GPLAN_ID")
266            .and_then(|v| v.as_i64())
267            .unwrap_or(0);
268        let mut updated = existing.clone();
269        if let Some(obj) = updated.as_object_mut() {
270            obj.insert("GPLAN_DESC".to_string(), json!(gplan_desc));
271        }
272        let modified = helpers::update_in_config_array(
273            config_json,
274            "CFG_GPLAN",
275            "GPLAN_CODE",
276            &code,
277            updated,
278        )?;
279        Ok((modified, plan_id, false))
280    } else {
281        // Create new plan
282        let config_data: Value = serde_json::from_str(config_json)?;
283        let max_id = config_data
284            .get("G2_CONFIG")
285            .and_then(|g| g.get("CFG_GPLAN"))
286            .and_then(|v| v.as_array())
287            .map(|arr| {
288                arr.iter()
289                    .filter_map(|item| item.get("GPLAN_ID").and_then(|v| v.as_i64()))
290                    .max()
291                    .unwrap_or(0)
292            })
293            .unwrap_or(0);
294
295        let new_id = max_id + 1;
296        let new_plan = json!({
297            "GPLAN_ID": new_id,
298            "GPLAN_CODE": code,
299            "GPLAN_DESC": gplan_desc
300        });
301
302        let modified = helpers::add_to_config_array(config_json, "CFG_GPLAN", new_plan)?;
303        Ok((modified, new_id, true))
304    }
305}