Skip to main content

apif_parser/
ternary.rs

1// Ternary expression parser for EXTRACT section
2// Converts: condition ? true_expr : false_expr
3// To JQ:    if condition then true_expr else false_expr end
4
5/// Convert ternary expression to JQ syntax (recursively handles nested ternaries)
6pub fn ternary_to_jq(expr: &str) -> String {
7    // First, check if the entire expression is a ternary
8    if let Some(pos) = find_top_level_question_mark(expr) {
9        let (condition, rest) = expr.split_at(pos);
10        let rest = &rest[1..]; // Skip '?'
11
12        if let Some(colon_pos) = find_matching_colon(rest) {
13            let true_expr = &rest[..colon_pos].trim();
14            let false_expr = &rest[colon_pos + 1..].trim();
15
16            // Recursively process nested ternaries in all parts
17            return format!(
18                "if {} then {} else {} end",
19                ternary_to_jq(condition.trim()),
20                ternary_to_jq(true_expr),
21                ternary_to_jq(false_expr)
22            );
23        }
24    }
25
26    // Not a top-level ternary, but may contain nested ternaries in parentheses
27    // Process content inside parentheses recursively
28    let mut result = String::new();
29    let mut paren_depth = 0;
30    let mut paren_start = None;
31    let mut in_quotes = false;
32    let mut quote_char = None;
33
34    for (i, c) in expr.char_indices() {
35        match c {
36            '\'' | '"' => {
37                if !in_quotes {
38                    in_quotes = true;
39                    quote_char = Some(c);
40                } else if Some(c) == quote_char {
41                    in_quotes = false;
42                    quote_char = None;
43                }
44            }
45            '(' if !in_quotes => {
46                if paren_depth == 0 {
47                    paren_start = Some(i);
48                }
49                paren_depth += 1;
50            }
51            ')' if !in_quotes => {
52                paren_depth -= 1;
53                if paren_depth == 0
54                    && let Some(start) = paren_start
55                {
56                    // Process content inside parentheses
57                    let content = &expr[start + 1..i];
58                    let processed = ternary_to_jq(content);
59                    result.push('(');
60                    result.push_str(&processed);
61                    result.push(')');
62                    paren_start = None;
63                    continue;
64                }
65            }
66            _ => {}
67        }
68
69        if paren_depth == 0 {
70            result.push(c);
71        }
72    }
73
74    if result.is_empty() {
75        expr.to_string()
76    } else {
77        result
78    }
79}
80
81/// Find '?' that's not inside quotes or parentheses
82fn find_top_level_question_mark(expr: &str) -> Option<usize> {
83    let mut in_quotes = false;
84    let mut quote_char = None;
85    let mut paren_depth = 0;
86    let mut bracket_depth = 0;
87
88    for (i, c) in expr.char_indices() {
89        match c {
90            '\'' | '"' => {
91                if !in_quotes {
92                    in_quotes = true;
93                    quote_char = Some(c);
94                } else if Some(c) == quote_char {
95                    in_quotes = false;
96                    quote_char = None;
97                }
98            }
99            '(' => paren_depth += 1,
100            ')' => paren_depth -= 1,
101            '[' => bracket_depth += 1,
102            ']' => bracket_depth -= 1,
103            '?' if !in_quotes && paren_depth == 0 && bracket_depth == 0 => {
104                return Some(i);
105            }
106            _ => {}
107        }
108    }
109
110    None
111}
112
113/// Find ':' that matches the first '?' (handles right-associative ternaries)
114/// For "a ? b ? c : d : e", finds the second ':' (position after d)
115fn find_matching_colon(expr: &str) -> Option<usize> {
116    let mut in_quotes = false;
117    let mut quote_char = None;
118    let mut paren_depth = 0;
119    let mut bracket_depth = 0;
120    let mut ternary_depth = 0; // Count nested ? without matching :
121
122    for (i, c) in expr.char_indices() {
123        match c {
124            '\'' | '"' => {
125                if !in_quotes {
126                    in_quotes = true;
127                    quote_char = Some(c);
128                } else if Some(c) == quote_char {
129                    in_quotes = false;
130                    quote_char = None;
131                }
132            }
133            '(' => paren_depth += 1,
134            ')' => paren_depth -= 1,
135            '[' => bracket_depth += 1,
136            ']' => bracket_depth -= 1,
137            '?' if !in_quotes && paren_depth == 0 && bracket_depth == 0 => {
138                ternary_depth += 1;
139            }
140            ':' if !in_quotes && paren_depth == 0 && bracket_depth == 0 => {
141                if ternary_depth == 0 {
142                    return Some(i);
143                }
144                ternary_depth -= 1;
145            }
146            _ => {}
147        }
148    }
149
150    None
151}
152
153/// Process EXTRACT value - convert ternary to JQ if needed
154pub fn process_extract_value(value: &str) -> String {
155    // Check if it contains ternary operator
156    if value.contains('?') && value.contains(':') {
157        ternary_to_jq(value)
158    } else {
159        value.to_string()
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_ternary_basic() {
169        let input = ".status == 200 ? \"OK\" : \"Error\"";
170        let expected = "if .status == 200 then \"OK\" else \"Error\" end";
171        assert_eq!(ternary_to_jq(input), expected);
172    }
173
174    #[test]
175    fn test_ternary_with_jq() {
176        let input = ".items | length > 0 ? .items[0] : null";
177        let expected = "if .items | length > 0 then .items[0] else null end";
178        assert_eq!(ternary_to_jq(input), expected);
179    }
180
181    #[test]
182    fn test_ternary_nested() {
183        // Nested ternary - all levels are recursively converted
184        let input = ".a > 0 ? (.a > 10 ? \"big\" : \"medium\") : \"small\"";
185        let expected =
186            "if .a > 0 then (if .a > 10 then \"big\" else \"medium\" end) else \"small\" end";
187        assert_eq!(ternary_to_jq(input), expected);
188    }
189
190    #[test]
191    fn test_not_ternary() {
192        let input = ".data | .value";
193        assert_eq!(ternary_to_jq(input), input);
194    }
195
196    #[test]
197    fn test_ternary_in_quotes() {
198        let input = ".text == \"a ? b : c\" ? \"match\" : \"no match\"";
199        let expected = "if .text == \"a ? b : c\" then \"match\" else \"no match\" end";
200        assert_eq!(ternary_to_jq(input), expected);
201    }
202
203    #[test]
204    fn test_process_extract_value_ternary() {
205        let input = ".status == 200 ? \"OK\" : \"Error\"";
206        let result = process_extract_value(input);
207        assert!(result.starts_with("if"));
208        assert!(result.ends_with("end"));
209    }
210
211    #[test]
212    fn test_process_extract_value_jq() {
213        let input = "if .status == 200 then \"OK\" else \"Error\" end";
214        let result = process_extract_value(input);
215        assert_eq!(result, input);
216    }
217
218    #[test]
219    fn test_process_extract_value_simple() {
220        let input = ".value";
221        let result = process_extract_value(input);
222        assert_eq!(result, input);
223    }
224
225    #[test]
226    fn test_ternary_nested_deep() {
227        // Deep nested: 3 levels
228        let input =
229            ".a > 0 ? (.a > 10 ? (.a > 20 ? \"very big\" : \"big\") : \"medium\") : \"small\"";
230        let result = ternary_to_jq(input);
231        assert!(result.contains("if .a > 0 then"));
232        assert!(result.contains("if .a > 10 then"));
233        assert!(result.contains("if .a > 20 then"));
234        assert_eq!(result.matches(" end").count(), 3);
235    }
236
237    #[test]
238    fn test_ternary_multiple_sequential() {
239        // Multiple ternaries at same level (not nested)
240        let input = ".a > 0 ? .b > 0 ? \"both positive\" : \"a only\" : \"none\"";
241        let result = ternary_to_jq(input);
242        // Should handle right-associative: a>0 ? (b>0 ? ... : ...) : ...
243        assert!(result.starts_with("if .a > 0 then"));
244        assert!(result.ends_with("end"));
245        println!("Sequential: {}", result);
246    }
247
248    #[test]
249    fn test_ternary_jq_validation() {
250        // Verify generated jq is valid by checking structure
251        let input = ".a > 0 ? (.a > 10 ? \"big\" : \"medium\") : \"small\"";
252        let result = ternary_to_jq(input);
253        // Count if/then/else/end balance
254        assert_eq!(result.matches("if ").count(), 2);
255        assert_eq!(result.matches(" then ").count(), 2);
256        assert_eq!(result.matches(" else ").count(), 2);
257        assert_eq!(result.matches(" end").count(), 2);
258        println!("Nested jq: {}", result);
259    }
260
261    #[test]
262    fn test_ternary_with_parentheses_preserved() {
263        // Verify parentheses are preserved correctly
264        let input = ".a > 0 ? (.a > 10 ? \"big\" : \"medium\") : \"small\"";
265        let result = ternary_to_jq(input);
266        assert!(result.contains("(if .a > 10 then"));
267        assert!(result.contains("end)"));
268    }
269}