Skip to main content

sz_configtool_lib/
helpers.rs

1use crate::error::{Result, SzConfigError};
2use serde_json::Value;
3
4/// Get the next available ID for a config array
5///
6/// Finds the maximum value of the specified ID field and returns max + 1
7///
8/// # Arguments
9/// * `array` - Array of configuration items
10/// * `id_field` - Name of the ID field (e.g., "DSRC_ID", "ATTR_ID")
11///
12/// # Returns
13/// Next available ID value
14pub fn get_next_id_from_array(array: &[Value], id_field: &str) -> Result<i64> {
15    let max_id = array
16        .iter()
17        .filter_map(|item| item.get(id_field))
18        .filter_map(|v| v.as_i64())
19        .max()
20        .unwrap_or(0);
21
22    Ok(max_id + 1)
23}
24
25/// Get the next available ID for a config section with optional seed value
26///
27/// Navigates to a config section using a path and finds the next available ID.
28/// Useful for user-created items that should start at a specific ID (e.g., 1000).
29///
30/// # Arguments
31/// * `config_data` - Parsed configuration JSON Value
32/// * `section_path` - Dot-separated path (e.g., "G2_CONFIG.CFG_SFCALL")
33/// * `id_field` - Name of the ID field (e.g., "SFCALL_ID")
34/// * `seed_value` - Minimum value to return (e.g., 1000 for user items)
35///
36/// # Returns
37/// Next available ID value, at least seed_value
38///
39/// # Errors
40/// Returns error if section path not found
41pub fn get_next_id(
42    config_data: &Value,
43    section_path: &str,
44    id_field: &str,
45    seed_value: i64,
46) -> Result<i64> {
47    // Parse section path (e.g., "G2_CONFIG.CFG_SFCALL")
48    let parts: Vec<&str> = section_path.split('.').collect();
49
50    let mut current = config_data;
51    for part in &parts {
52        current = current.get(part).ok_or_else(|| {
53            SzConfigError::MissingSection(format!("Section path '{section_path}' not found"))
54        })?;
55    }
56
57    // Get max ID from array
58    let max_id = if let Some(items) = current.as_array() {
59        items
60            .iter()
61            .filter_map(|item| item.get(id_field).and_then(|v| v.as_i64()))
62            .max()
63            .unwrap_or(seed_value - 1)
64    } else {
65        seed_value - 1
66    };
67
68    Ok(std::cmp::max(max_id + 1, seed_value))
69}
70
71/// Get the next available ID for a config array with minimum value
72///
73/// Finds the maximum value of the specified ID field and returns max(max_id + 1, min_value)
74/// This is useful for user-created items that should start at a high ID (e.g., 1000)
75///
76/// # Arguments
77/// * `array` - Array of configuration items
78/// * `id_field` - Name of the ID field (e.g., "FTYPE_ID", "FELEM_ID")
79/// * `min_value` - Minimum value to return (e.g., 1000 for user items)
80///
81/// # Returns
82/// Next available ID value, at least min_value
83pub fn get_next_id_with_min(array: &[Value], id_field: &str, min_value: i64) -> Result<i64> {
84    let max_id = array
85        .iter()
86        .filter_map(|item| item.get(id_field))
87        .filter_map(|v| v.as_i64())
88        .max()
89        .unwrap_or(min_value - 1);
90
91    Ok(std::cmp::max(max_id + 1, min_value))
92}
93
94/// Check if an ID is already taken in a config array
95///
96/// # Arguments
97/// * `array` - Array of configuration items
98/// * `id_field` - Name of the ID field (e.g., "DSRC_ID", "ATTR_ID")
99/// * `id_value` - ID value to check
100///
101/// # Returns
102/// true if ID is taken, false otherwise
103pub fn is_id_taken(array: &[Value], id_field: &str, id_value: i64) -> bool {
104    array
105        .iter()
106        .any(|item| item.get(id_field).and_then(|v| v.as_i64()) == Some(id_value))
107}
108
109/// Get the next available ID or use desired ID if specified and available
110///
111/// Matches Python's getDesiredValueOrNext behavior:
112/// - If desired_id is Some and available, returns it
113/// - If desired_id is Some but taken, returns error
114/// - If desired_id is None, returns next available ID
115///
116/// # Arguments
117/// * `array` - Array of configuration items
118/// * `id_field` - Name of the ID field (e.g., "DSRC_ID", "ATTR_ID")
119/// * `desired_id` - Optional user-specified ID
120/// * `min_value` - Minimum value to return (e.g., 1000 for user items)
121///
122/// # Returns
123/// ID to use (either desired_id or next available)
124///
125/// # Errors
126/// Returns error if desired_id is already taken
127pub fn get_desired_or_next_id(
128    array: &[Value],
129    id_field: &str,
130    desired_id: Option<i64>,
131    min_value: i64,
132) -> Result<i64> {
133    if let Some(id) = desired_id {
134        if id > 0 {
135            if is_id_taken(array, id_field, id) {
136                return Err(SzConfigError::AlreadyExists(format!(
137                    "The specified ID {id} is already taken"
138                )));
139            }
140            return Ok(id);
141        }
142    }
143
144    // No desired ID or invalid, get next available
145    get_next_id_with_min(array, id_field, min_value)
146}
147
148/// Get the next available ID or use desired ID (for config sections)
149///
150/// Same as get_desired_or_next_id but works with section paths
151///
152/// # Arguments
153/// * `config_data` - Parsed configuration JSON Value
154/// * `section_path` - Dot-separated path (e.g., "G2_CONFIG.CFG_SFCALL")
155/// * `id_field` - Name of the ID field (e.g., "SFCALL_ID")
156/// * `desired_id` - Optional user-specified ID
157/// * `seed_value` - Minimum value to return (e.g., 1000 for user items)
158///
159/// # Returns
160/// ID to use (either desired_id or next available)
161///
162/// # Errors
163/// Returns error if section not found or desired_id is already taken
164pub fn get_desired_or_next_id_from_section(
165    config_data: &Value,
166    section_path: &str,
167    id_field: &str,
168    desired_id: Option<i64>,
169    seed_value: i64,
170) -> Result<i64> {
171    // Parse section path
172    let parts: Vec<&str> = section_path.split('.').collect();
173
174    let mut current = config_data;
175    for part in &parts {
176        current = current.get(part).ok_or_else(|| {
177            SzConfigError::MissingSection(format!("Section path '{section_path}' not found"))
178        })?;
179    }
180
181    let array = current.as_array().ok_or_else(|| {
182        SzConfigError::MissingSection(format!("Section '{section_path}' is not an array"))
183    })?;
184
185    get_desired_or_next_id(array, id_field, desired_id, seed_value)
186}
187
188/// Find item in config array by field value
189///
190/// # Arguments
191/// * `array` - Array of configuration items
192/// * `field` - Field name to search
193/// * `value` - Value to match
194///
195/// # Returns
196/// Reference to matching item, or None if not found
197pub fn find_in_array<'a>(array: &'a [Value], field: &str, value: &str) -> Option<&'a Value> {
198    array.iter().find(|item| {
199        item.get(field)
200            .and_then(|v| v.as_str())
201            .map(|s| s == value)
202            .unwrap_or(false)
203    })
204}
205
206/// Get mutable reference to item in config array
207///
208/// # Arguments
209/// * `array` - Mutable array of configuration items
210/// * `field` - Field name to search
211/// * `value` - Value to match
212///
213/// # Returns
214/// Mutable reference to matching item, or None if not found
215pub fn find_in_array_mut<'a>(
216    array: &'a mut [Value],
217    field: &str,
218    value: &str,
219) -> Option<&'a mut Value> {
220    array.iter_mut().find(|item| {
221        item.get(field)
222            .and_then(|v| v.as_str())
223            .map(|s| s == value)
224            .unwrap_or(false)
225    })
226}
227
228/// Add item to config array (generic)
229///
230/// # Arguments
231/// * `config_json` - JSON configuration string
232/// * `section` - Section name (e.g., "CFG_DSRC", "CFG_ATTR")
233/// * `item` - JSON Value to add
234///
235/// # Returns
236/// Modified configuration JSON string
237///
238/// # Errors
239/// - `JsonParse` if config_json is invalid
240/// - `MissingSection` if section doesn't exist
241pub fn add_to_config_array(config_json: &str, section: &str, item: Value) -> Result<String> {
242    let mut config: Value =
243        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
244
245    let array = config
246        .get_mut("G2_CONFIG")
247        .and_then(|g| g.get_mut(section))
248        .and_then(|v| v.as_array_mut())
249        .ok_or_else(|| SzConfigError::MissingSection(section.to_string()))?;
250
251    array.push(item);
252
253    serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
254}
255
256/// Delete item from config array by field value
257///
258/// # Arguments
259/// * `config_json` - JSON configuration string
260/// * `section` - Section name (e.g., "CFG_DSRC", "CFG_ATTR")
261/// * `field` - Field name to match (e.g., "DSRC_CODE", "ATTR_CODE")
262/// * `value` - Value to match for deletion
263///
264/// # Returns
265/// Modified configuration JSON string
266///
267/// # Errors
268/// - `JsonParse` if config_json is invalid
269/// - `MissingSection` if section doesn't exist
270/// - `NotFound` if no item matches the criteria
271pub fn delete_from_config_array(
272    config_json: &str,
273    section: &str,
274    field: &str,
275    value: &str,
276) -> Result<String> {
277    let mut config: Value =
278        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
279
280    let array = config
281        .get_mut("G2_CONFIG")
282        .and_then(|g| g.get_mut(section))
283        .and_then(|v| v.as_array_mut())
284        .ok_or_else(|| SzConfigError::MissingSection(section.to_string()))?;
285
286    let original_len = array.len();
287    array.retain(|item| {
288        item.get(field)
289            .and_then(|v| v.as_str())
290            .map(|s| s != value)
291            .unwrap_or(true)
292    });
293
294    if array.len() == original_len {
295        return Err(SzConfigError::NotFound(format!(
296            "{section} '{value}' not found"
297        )));
298    }
299
300    serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
301}
302
303/// Find item in config array by field value (returns owned value)
304///
305/// # Arguments
306/// * `config_json` - JSON configuration string
307/// * `section` - Section name (e.g., "CFG_DSRC", "CFG_ATTR")
308/// * `field` - Field name to match (e.g., "DSRC_CODE", "ATTR_CODE")
309/// * `value` - Value to match
310///
311/// # Returns
312/// Cloned item if found, None otherwise
313///
314/// # Errors
315/// - `JsonParse` if config_json is invalid
316pub fn find_in_config_array(
317    config_json: &str,
318    section: &str,
319    field: &str,
320    value: &str,
321) -> Result<Option<Value>> {
322    let config: Value =
323        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
324
325    let array = config
326        .get("G2_CONFIG")
327        .and_then(|g| g.get(section))
328        .and_then(|v| v.as_array());
329
330    if let Some(arr) = array {
331        let item = arr.iter().find(|item| {
332            item.get(field)
333                .and_then(|v| v.as_str())
334                .map(|s| s == value)
335                .or_else(|| {
336                    // Also try numeric comparison
337                    item.get(field)
338                        .and_then(|v| v.as_i64())
339                        .and_then(|id| value.parse::<i64>().ok().map(|val| id == val))
340                })
341                .unwrap_or(false)
342        });
343        Ok(item.cloned())
344    } else {
345        Ok(None)
346    }
347}
348
349/// Alias for delete_from_config_array for compatibility
350pub fn remove_from_config_array(
351    config_json: &str,
352    section: &str,
353    field: &str,
354    value: &str,
355) -> Result<String> {
356    delete_from_config_array(config_json, section, field, value)
357}
358
359/// Update item in config array (complete replacement)
360///
361/// # Arguments
362/// * `config_json` - JSON configuration string
363/// * `section` - Section name (e.g., "CFG_DSRC", "CFG_ATTR")
364/// * `field` - Field name to match (e.g., "DSRC_CODE", "ATTR_CODE")
365/// * `value` - Value to match
366/// * `new_item` - Complete new item value (replaces old item entirely)
367///
368/// # Returns
369/// Modified configuration JSON string
370pub fn update_in_config_array(
371    config_json: &str,
372    section: &str,
373    field: &str,
374    value: &str,
375    new_item: Value,
376) -> Result<String> {
377    let mut config: Value =
378        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
379
380    let array = config
381        .get_mut("G2_CONFIG")
382        .and_then(|g| g.get_mut(section))
383        .and_then(|v| v.as_array_mut())
384        .ok_or_else(|| SzConfigError::MissingSection(section.to_string()))?;
385
386    let item = find_in_array_mut(array, field, value)
387        .ok_or_else(|| SzConfigError::NotFound(format!("{section} '{value}' not found")))?;
388
389    // Replace the entire item
390    *item = new_item;
391
392    serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
393}
394
395/// List all items from a config array
396///
397/// # Arguments
398/// * `config_json` - JSON configuration string
399/// * `section` - Section name (e.g., "CFG_DSRC", "CFG_ATTR")
400///
401/// # Returns
402/// Vector of all items in the section
403///
404/// # Errors
405/// - `JsonParse` if config_json is invalid
406pub fn list_from_config_array(config_json: &str, section: &str) -> Result<Vec<Value>> {
407    let config: Value =
408        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
409
410    let items = if let Some(g2_config) = config.get("G2_CONFIG") {
411        if let Some(array) = g2_config.get(section).and_then(|v| v.as_array()) {
412            array.clone()
413        } else {
414            Vec::new()
415        }
416    } else {
417        Vec::new()
418    };
419
420    Ok(items)
421}
422
423/// Lookup feature ID by feature code
424///
425/// # Arguments
426/// * `config_json` - JSON configuration string
427/// * `feature_code` - Feature code to look up (case-insensitive)
428///
429/// # Returns
430/// Feature ID (FTYPE_ID)
431///
432/// # Errors
433/// Returns error if feature not found or JSON is invalid
434pub fn lookup_feature_id(config_json: &str, feature_code: &str) -> Result<i64> {
435    let config: Value =
436        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
437
438    config
439        .get("G2_CONFIG")
440        .and_then(|g| g.get("CFG_FTYPE"))
441        .and_then(|v| v.as_array())
442        .and_then(|arr| {
443            arr.iter()
444                .find(|f| {
445                    f.get("FTYPE_CODE")
446                        .and_then(|v| v.as_str())
447                        .map(|s| s.eq_ignore_ascii_case(feature_code))
448                        .unwrap_or(false)
449                })
450                .and_then(|f| f.get("FTYPE_ID"))
451                .and_then(|v| v.as_i64())
452        })
453        .ok_or_else(|| SzConfigError::NotFound(format!("Feature '{feature_code}' not found")))
454}
455
456/// Lookup element ID by element code
457///
458/// # Arguments
459/// * `config_json` - JSON configuration string
460/// * `element_code` - Element code to look up (case-insensitive)
461///
462/// # Returns
463/// Element ID (FELEM_ID)
464///
465/// # Errors
466/// Returns error if element not found or JSON is invalid
467pub fn lookup_element_id(config_json: &str, element_code: &str) -> Result<i64> {
468    let config: Value =
469        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
470
471    config
472        .get("G2_CONFIG")
473        .and_then(|g| g.get("CFG_FELEM"))
474        .and_then(|v| v.as_array())
475        .and_then(|arr| {
476            arr.iter()
477                .find(|e| {
478                    e.get("FELEM_CODE")
479                        .and_then(|v| v.as_str())
480                        .map(|s| s.eq_ignore_ascii_case(element_code))
481                        .unwrap_or(false)
482                })
483                .and_then(|e| e.get("FELEM_ID"))
484                .and_then(|v| v.as_i64())
485        })
486        .ok_or_else(|| SzConfigError::NotFound(format!("Element '{element_code}' not found")))
487}
488
489/// Lookup standardize function ID by function code
490///
491/// # Arguments
492/// * `config_json` - JSON configuration string
493/// * `func_code` - Function code to look up (case-insensitive)
494///
495/// # Returns
496/// Function ID (SFUNC_ID)
497///
498/// # Errors
499/// Returns error if function not found or JSON is invalid
500pub fn lookup_sfunc_id(config_json: &str, func_code: &str) -> Result<i64> {
501    let config: Value =
502        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
503
504    config
505        .get("G2_CONFIG")
506        .and_then(|g| g.get("CFG_SFUNC"))
507        .and_then(|v| v.as_array())
508        .and_then(|arr| {
509            arr.iter()
510                .find(|f| {
511                    f.get("SFUNC_CODE")
512                        .and_then(|v| v.as_str())
513                        .map(|s| s.eq_ignore_ascii_case(func_code))
514                        .unwrap_or(false)
515                })
516                .and_then(|f| f.get("SFUNC_ID"))
517                .and_then(|v| v.as_i64())
518        })
519        .ok_or_else(|| {
520            SzConfigError::NotFound(format!("Standardize function '{func_code}' not found"))
521        })
522}
523
524/// Lookup expression function ID by function code
525///
526/// # Arguments
527/// * `config_json` - JSON configuration string
528/// * `func_code` - Function code to look up (case-insensitive)
529///
530/// # Returns
531/// Function ID (EFUNC_ID)
532///
533/// # Errors
534/// Returns error if function not found or JSON is invalid
535pub fn lookup_efunc_id(config_json: &str, func_code: &str) -> Result<i64> {
536    let config: Value =
537        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
538
539    config
540        .get("G2_CONFIG")
541        .and_then(|g| g.get("CFG_EFUNC"))
542        .and_then(|v| v.as_array())
543        .and_then(|arr| {
544            arr.iter()
545                .find(|f| {
546                    f.get("EFUNC_CODE")
547                        .and_then(|v| v.as_str())
548                        .map(|s| s.eq_ignore_ascii_case(func_code))
549                        .unwrap_or(false)
550                })
551                .and_then(|f| f.get("EFUNC_ID"))
552                .and_then(|v| v.as_i64())
553        })
554        .ok_or_else(|| {
555            SzConfigError::NotFound(format!("Expression function '{func_code}' not found"))
556        })
557}
558
559/// Lookup comparison function ID by function code
560///
561/// # Arguments
562/// * `config_json` - JSON configuration string
563/// * `func_code` - Function code to look up (case-insensitive)
564///
565/// # Returns
566/// Function ID (CFUNC_ID)
567///
568/// # Errors
569/// Returns error if function not found or JSON is invalid
570pub fn lookup_cfunc_id(config_json: &str, func_code: &str) -> Result<i64> {
571    let config: Value =
572        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
573
574    config
575        .get("G2_CONFIG")
576        .and_then(|g| g.get("CFG_CFUNC"))
577        .and_then(|v| v.as_array())
578        .and_then(|arr| {
579            arr.iter()
580                .find(|f| {
581                    f.get("CFUNC_CODE")
582                        .and_then(|v| v.as_str())
583                        .map(|s| s.eq_ignore_ascii_case(func_code))
584                        .unwrap_or(false)
585                })
586                .and_then(|f| f.get("CFUNC_ID"))
587                .and_then(|v| v.as_i64())
588        })
589        .ok_or_else(|| {
590            SzConfigError::NotFound(format!("Comparison function '{func_code}' not found"))
591        })
592}
593
594/// Lookup distinct function ID by function code
595///
596/// # Arguments
597/// * `config_json` - JSON configuration string
598/// * `func_code` - Function code to look up (case-insensitive)
599///
600/// # Returns
601/// Function ID (DFUNC_ID)
602///
603/// # Errors
604/// Returns error if function not found or JSON is invalid
605pub fn lookup_dfunc_id(config_json: &str, func_code: &str) -> Result<i64> {
606    let config: Value =
607        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
608
609    config
610        .get("G2_CONFIG")
611        .and_then(|g| g.get("CFG_DFUNC"))
612        .and_then(|v| v.as_array())
613        .and_then(|arr| {
614            arr.iter()
615                .find(|f| {
616                    f.get("DFUNC_CODE")
617                        .and_then(|v| v.as_str())
618                        .map(|s| s.eq_ignore_ascii_case(func_code))
619                        .unwrap_or(false)
620                })
621                .and_then(|f| f.get("DFUNC_ID"))
622                .and_then(|v| v.as_i64())
623        })
624        .ok_or_else(|| {
625            SzConfigError::NotFound(format!("Distinct function '{func_code}' not found"))
626        })
627}
628
629/// Lookup generic plan ID by plan code
630///
631/// # Arguments
632/// * `config_json` - JSON configuration string
633/// * `plan_code` - Plan code to look up (case-insensitive, e.g., "INGEST", "SEARCH")
634///
635/// # Returns
636/// Plan ID (GPLAN_ID)
637///
638/// # Errors
639/// Returns error if plan not found or JSON is invalid
640pub fn lookup_gplan_id(config_json: &str, plan_code: &str) -> Result<i64> {
641    let config: Value =
642        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
643
644    config
645        .get("G2_CONFIG")
646        .and_then(|g| g.get("CFG_GPLAN"))
647        .and_then(|v| v.as_array())
648        .and_then(|arr| {
649            arr.iter()
650                .find(|p| {
651                    p.get("GPLAN_CODE")
652                        .and_then(|v| v.as_str())
653                        .map(|s| s.eq_ignore_ascii_case(plan_code))
654                        .unwrap_or(false)
655                })
656                .and_then(|p| p.get("GPLAN_ID"))
657                .and_then(|v| v.as_i64())
658        })
659        .ok_or_else(|| SzConfigError::NotFound(format!("Generic plan '{plan_code}' not found")))
660}
661
662/// Internal: Lookup generic plan code by plan ID (for FFI use)
663///
664/// # Arguments
665/// * `config_json` - JSON configuration string
666/// * `gplan_id` - Plan ID to look up
667///
668/// # Returns
669/// Plan code (GPLAN_CODE)
670///
671/// # Errors
672/// Returns error if plan not found or JSON is invalid
673pub(crate) fn lookup_gplan_code(config_json: &str, gplan_id: i64) -> Result<String> {
674    let config: Value =
675        serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
676
677    config
678        .get("G2_CONFIG")
679        .and_then(|g| g.get("CFG_GPLAN"))
680        .and_then(|v| v.as_array())
681        .and_then(|arr| {
682            arr.iter()
683                .find(|p| p.get("GPLAN_ID").and_then(|v| v.as_i64()) == Some(gplan_id))
684                .and_then(|p| p.get("GPLAN_CODE"))
685                .and_then(|v| v.as_str())
686                .map(|s| s.to_string())
687        })
688        .ok_or_else(|| SzConfigError::NotFound(format!("Generic plan ID: {gplan_id}")))
689}