sz_configtool_lib/fragments.rs
1//! Fragment (CFG_ERFRAG) operations
2//!
3//! Functions for managing entity resolution fragments in the configuration.
4//! Fragments define matching criteria used by rules.
5
6use crate::error::{Result, SzConfigError};
7use crate::helpers;
8use serde_json::{Value, json};
9
10/// Validates a fragment source XPath expression and computes dependencies
11///
12/// Parses the source string to find all ./FRAGMENT[...] references,
13/// validates that referenced fragments exist, and returns their IDs.
14///
15/// # Arguments
16///
17/// * `config_json` - Configuration JSON string
18/// * `source_string` - Fragment source XPath expression
19///
20/// # Returns
21///
22/// Returns `(dependency_ids, error_message)` tuple
23/// - dependency_ids: Vec of fragment IDs as strings (empty if no dependencies)
24/// - error_message: Empty string on success, error description on failure
25///
26/// # Example
27///
28/// ```
29/// use sz_configtool_lib::fragments;
30/// let source = "./FRAGMENT[./SAME_NAME>0 and ./SAME_STAB>0]";
31/// let config = r#"{"G2_CONFIG": {"CFG_ERFRAG": []}}"#;
32/// // Note: validate_fragment_source is private, used internally by add_fragment
33/// ```
34fn validate_fragment_source(config_json: &str, source_string: &str) -> (Vec<String>, String) {
35 // Validate JSON parses correctly
36 if let Err(e) = serde_json::from_str::<Value>(config_json) {
37 return (vec![], format!("Invalid JSON: {e}"));
38 }
39
40 let mut dependency_list = Vec::new();
41 let mut source = source_string.to_string();
42
43 // Find all FRAGMENT[...] patterns
44 while let Some(start_pos) = source.find("FRAGMENT[") {
45 // Find the matching closing bracket
46 let fragment_start = start_pos;
47 if let Some(bracket_pos) = source[fragment_start..].find(']') {
48 let fragment_string = &source[fragment_start..fragment_start + bracket_pos + 1];
49
50 // Parse fragment references within FRAGMENT[...]
51 let mut current_frag = String::new();
52 let mut in_fragment = false;
53
54 for ch in fragment_string.chars() {
55 if ch == '/' {
56 // Start or continue parsing fragment name
57 if in_fragment && !current_frag.is_empty() {
58 // End of previous fragment, lookup and validate
59 match helpers::find_in_config_array(
60 config_json,
61 "CFG_ERFRAG",
62 "ERFRAG_CODE",
63 ¤t_frag,
64 ) {
65 Ok(Some(frag_record)) => {
66 if let Some(frag_id) =
67 frag_record.get("ERFRAG_ID").and_then(|v| v.as_i64())
68 {
69 dependency_list.push(frag_id.to_string());
70 }
71 }
72 Ok(None) => {
73 return (
74 vec![],
75 format!("Invalid fragment reference: {current_frag}"),
76 );
77 }
78 Err(_) => {
79 return (
80 vec![],
81 format!("Invalid fragment reference: {current_frag}"),
82 );
83 }
84 }
85 }
86 current_frag.clear();
87 in_fragment = true;
88 } else if in_fragment {
89 // Check for delimiters that end fragment name
90 if "|=><)] ".contains(ch) {
91 if !current_frag.is_empty() {
92 // Lookup fragment
93 match helpers::find_in_config_array(
94 config_json,
95 "CFG_ERFRAG",
96 "ERFRAG_CODE",
97 ¤t_frag,
98 ) {
99 Ok(Some(frag_record)) => {
100 if let Some(frag_id) =
101 frag_record.get("ERFRAG_ID").and_then(|v| v.as_i64())
102 {
103 dependency_list.push(frag_id.to_string());
104 }
105 }
106 Ok(None) => {
107 return (
108 vec![],
109 format!("Invalid fragment reference: {current_frag}"),
110 );
111 }
112 Err(_) => {
113 return (
114 vec![],
115 format!("Invalid fragment reference: {current_frag}"),
116 );
117 }
118 }
119 current_frag.clear();
120 }
121 in_fragment = false;
122 } else {
123 current_frag.push(ch);
124 }
125 }
126 }
127
128 // Remove this FRAGMENT[...] from source to find next one
129 source = source.replace(fragment_string, "");
130 } else {
131 break;
132 }
133 }
134
135 // Remove duplicates and return
136 dependency_list.sort();
137 dependency_list.dedup();
138 (dependency_list, String::new())
139}
140
141/// Add a new fragment to the configuration
142///
143/// # Arguments
144///
145/// * `config_json` - Configuration JSON string
146/// * `fragment_config` - JSON configuration for the fragment (must include ERFRAG_CODE)
147///
148/// # Returns
149///
150/// Returns `(modified_config, new_fragment_id)` tuple on success
151///
152/// # Example
153///
154/// ```
155/// use sz_configtool_lib::fragments;
156/// use serde_json::json;
157///
158/// let config = r#"{"G2_CONFIG": {"CFG_ERFRAG": []}}"#;
159/// let frag_config = json!({
160/// "ERFRAG_CODE": "CUSTOM_FRAG",
161/// "ERFRAG_SOURCE": "NAME+ADDRESS"
162/// });
163/// let (modified, frag_id) = fragments::add_fragment(config, &frag_config).unwrap();
164/// assert_eq!(frag_id, 1);
165/// ```
166pub fn add_fragment(config_json: &str, fragment_config: &Value) -> Result<(String, i64)> {
167 let code = fragment_config
168 .get("ERFRAG_CODE")
169 .and_then(|v| v.as_str())
170 .ok_or_else(|| SzConfigError::MissingField("ERFRAG_CODE".to_string()))?;
171
172 let source = fragment_config
173 .get("ERFRAG_SOURCE")
174 .and_then(|v| v.as_str())
175 .ok_or_else(|| SzConfigError::MissingField("ERFRAG_SOURCE".to_string()))?;
176
177 // Check if fragment already exists (Python line 4520-4522)
178 let code_upper = code.to_uppercase();
179 if helpers::find_in_config_array(config_json, "CFG_ERFRAG", "ERFRAG_CODE", &code_upper)?
180 .is_some()
181 {
182 return Err(SzConfigError::AlreadyExists(
183 "Fragment already exists".to_string(),
184 ));
185 }
186
187 // Validate source and compute dependencies
188 let (dependency_list, error_message) = validate_fragment_source(config_json, source);
189 if !error_message.is_empty() {
190 return Err(SzConfigError::InvalidInput(error_message));
191 }
192
193 let config_data: Value = serde_json::from_str(config_json)?;
194
195 // Get next ID
196 let next_id = if let Some(g2_config) = config_data.get("G2_CONFIG") {
197 if let Some(array) = g2_config.get("CFG_ERFRAG").and_then(|v| v.as_array()) {
198 array
199 .iter()
200 .filter_map(|item| item.get("ERFRAG_ID").and_then(|v| v.as_i64()))
201 .max()
202 .unwrap_or(0)
203 + 1
204 } else {
205 1
206 }
207 } else {
208 return Err(SzConfigError::InvalidConfig(
209 "G2_CONFIG not found".to_string(),
210 ));
211 };
212
213 // Create new item with provided config plus ID and computed dependencies
214 let mut new_item = fragment_config.clone();
215 if let Some(obj) = new_item.as_object_mut() {
216 obj.insert("ERFRAG_ID".to_string(), json!(next_id));
217 obj.insert("ERFRAG_CODE".to_string(), json!(code.to_uppercase()));
218 obj.insert("ERFRAG_DESC".to_string(), json!(code.to_uppercase()));
219
220 // Set ERFRAG_DEPENDS to comma-separated list or null
221 if dependency_list.is_empty() {
222 obj.insert("ERFRAG_DEPENDS".to_string(), Value::Null);
223 } else {
224 obj.insert(
225 "ERFRAG_DEPENDS".to_string(),
226 json!(dependency_list.join(",")),
227 );
228 }
229 }
230
231 // Add to config
232 let modified_json = helpers::add_to_config_array(config_json, "CFG_ERFRAG", new_item)?;
233
234 Ok((modified_json, next_id))
235}
236
237/// Delete a fragment from the configuration
238///
239/// # Arguments
240///
241/// * `config_json` - Configuration JSON string
242/// * `fragment_code` - Fragment code to delete
243///
244/// # Returns
245///
246/// Returns modified configuration JSON on success
247///
248/// # Example
249///
250/// ```
251/// use sz_configtool_lib::fragments;
252///
253/// let config = r#"{"G2_CONFIG": {"CFG_ERFRAG": [{"ERFRAG_ID": 1, "ERFRAG_CODE": "TEST"}]}}"#;
254/// let modified = fragments::delete_fragment(config, "TEST").unwrap();
255/// ```
256pub fn delete_fragment(config_json: &str, fragment_code: &str) -> Result<String> {
257 let frag_code = fragment_code.to_uppercase();
258
259 // Verify fragment exists before deletion
260 let _ = helpers::find_in_config_array(config_json, "CFG_ERFRAG", "ERFRAG_CODE", &frag_code)?
261 .ok_or_else(|| SzConfigError::NotFound(format!("Fragment not found: {frag_code}")))?;
262
263 // Remove from config
264 helpers::remove_from_config_array(config_json, "CFG_ERFRAG", "ERFRAG_CODE", &frag_code)
265}
266
267/// Get a fragment by code or ID
268///
269/// # Arguments
270///
271/// * `config_json` - Configuration JSON string
272/// * `code_or_id` - Fragment code or ID to search for
273///
274/// # Returns
275///
276/// Returns the fragment JSON object on success
277///
278/// # Example
279///
280/// ```
281/// use sz_configtool_lib::fragments;
282///
283/// let config = r#"{"G2_CONFIG": {"CFG_ERFRAG": [{"ERFRAG_ID": 1, "ERFRAG_CODE": "TEST"}]}}"#;
284/// let fragment = fragments::get_fragment(config, "TEST").unwrap();
285/// ```
286pub fn get_fragment(config_json: &str, code_or_id: &str) -> Result<Value> {
287 let search_value = code_or_id.to_uppercase();
288
289 // Try to find by CODE first, then by ID
290 let item = if let Some(item) =
291 helpers::find_in_config_array(config_json, "CFG_ERFRAG", "ERFRAG_CODE", &search_value)?
292 {
293 item
294 } else if let Some(item) =
295 helpers::find_in_config_array(config_json, "CFG_ERFRAG", "ERFRAG_ID", &search_value)?
296 {
297 item
298 } else {
299 return Err(SzConfigError::NotFound(format!(
300 "Fragment not found: {search_value}"
301 )));
302 };
303
304 // Transform to lowercase format (matching list_fragments for consistency)
305 Ok(json!({
306 "id": item.get("ERFRAG_ID").and_then(|v| v.as_i64()).unwrap_or(0),
307 "fragment": item.get("ERFRAG_CODE").and_then(|v| v.as_str()).unwrap_or(""),
308 "source": item.get("ERFRAG_SOURCE").and_then(|v| v.as_str()).unwrap_or(""),
309 "depends": item.get("ERFRAG_DEPENDS").and_then(|v| v.as_str()).unwrap_or("")
310 }))
311}
312
313/// List all fragments in the configuration
314///
315/// # Arguments
316///
317/// * `config_json` - Configuration JSON string
318///
319/// # Returns
320///
321/// Returns a vector of fragment objects in Python sz_configtool format
322///
323/// # Example
324///
325/// ```
326/// use sz_configtool_lib::fragments;
327///
328/// let config = r#"{"G2_CONFIG": {"CFG_ERFRAG": [{"ERFRAG_ID": 1, "ERFRAG_CODE": "TEST", "ERFRAG_SOURCE": "NAME", "ERFRAG_DEPENDS": ""}]}}"#;
329/// let fragments = fragments::list_fragments(config).unwrap();
330/// assert_eq!(fragments.len(), 1);
331/// ```
332pub fn list_fragments(config_json: &str) -> Result<Vec<Value>> {
333 let config_data: Value = serde_json::from_str(config_json)?;
334
335 // Extract fragments and transform to Python format
336 let items: Vec<Value> = if let Some(g2_config) = config_data.get("G2_CONFIG") {
337 if let Some(array) = g2_config.get("CFG_ERFRAG").and_then(|v| v.as_array()) {
338 array
339 .iter()
340 .map(|item| {
341 json!({
342 "id": item.get("ERFRAG_ID").and_then(|v| v.as_i64()).unwrap_or(0),
343 "fragment": item.get("ERFRAG_CODE").and_then(|v| v.as_str()).unwrap_or(""),
344 "source": item.get("ERFRAG_SOURCE").and_then(|v| v.as_str()).unwrap_or(""),
345 "depends": item.get("ERFRAG_DEPENDS").and_then(|v| v.as_str()).unwrap_or("")
346 })
347 })
348 .collect()
349 } else {
350 Vec::new()
351 }
352 } else {
353 Vec::new()
354 };
355
356 Ok(items)
357}
358
359/// Update an existing fragment in the configuration
360///
361/// # Arguments
362///
363/// * `config_json` - Configuration JSON string
364/// * `fragment_code` - Fragment code to update
365/// * `fragment_config` - New configuration for the fragment
366///
367/// # Returns
368///
369/// Returns modified configuration JSON on success
370///
371/// # Example
372///
373/// ```
374/// use sz_configtool_lib::fragments;
375/// use serde_json::json;
376///
377/// let config = r#"{"G2_CONFIG": {"CFG_ERFRAG": [{"ERFRAG_ID": 1, "ERFRAG_CODE": "TEST"}]}}"#;
378/// let new_config = json!({"ERFRAG_SOURCE": "NAME+DOB"});
379/// let modified = fragments::set_fragment(config, "TEST", &new_config).unwrap();
380/// ```
381pub fn set_fragment(
382 config_json: &str,
383 fragment_code: &str,
384 fragment_config: &Value,
385) -> Result<String> {
386 let code = fragment_code.to_uppercase();
387
388 // Validate and compute dependencies if SOURCE is being updated
389 let mut updated_item = fragment_config.clone();
390 if let Some(new_source) = fragment_config
391 .get("ERFRAG_SOURCE")
392 .and_then(|v| v.as_str())
393 {
394 let (dependency_list, error_message) = validate_fragment_source(config_json, new_source);
395 if !error_message.is_empty() {
396 return Err(SzConfigError::InvalidInput(error_message));
397 }
398
399 // Update ERFRAG_DEPENDS in the update
400 if let Some(obj) = updated_item.as_object_mut() {
401 if dependency_list.is_empty() {
402 obj.insert("ERFRAG_DEPENDS".to_string(), Value::Null);
403 } else {
404 obj.insert(
405 "ERFRAG_DEPENDS".to_string(),
406 json!(dependency_list.join(",")),
407 );
408 }
409 }
410 }
411
412 // Ensure the code field matches
413 if let Some(obj) = updated_item.as_object_mut() {
414 obj.insert("ERFRAG_CODE".to_string(), json!(code.clone()));
415 }
416
417 // Update the item in the config
418 helpers::update_in_config_array(
419 config_json,
420 "CFG_ERFRAG",
421 "ERFRAG_CODE",
422 &code,
423 updated_item,
424 )
425}