Skip to main content

apif_parser/
ternary_ast.rs

1// AST nodes for ternary expressions in EXTRACT section
2// Uses ternary_to_jq from ternary.rs for conversion
3
4use crate::ternary::ternary_to_jq;
5use serde::{Deserialize, Serialize};
6
7/// Extract value type
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub enum ExtractValue {
10    /// Simple JQ path: .user.id
11    Simple(String),
12    /// JQ expression: .items | length
13    JqExpr(String),
14    /// Ternary expression (raw string): .status == 200 ? "OK" : "Error"
15    Ternary(String),
16}
17
18impl ExtractValue {
19    /// Parse extract value string into AST
20    pub fn parse(value: &str) -> Self {
21        if is_ternary(value) {
22            ExtractValue::Ternary(value.to_string())
23        } else if value.contains('|') {
24            ExtractValue::JqExpr(value.to_string())
25        } else {
26            ExtractValue::Simple(value.to_string())
27        }
28    }
29
30    /// Convert to JQ syntax
31    pub fn to_jq(&self) -> String {
32        match self {
33            ExtractValue::Simple(path) => path.clone(),
34            ExtractValue::JqExpr(expr) => expr.clone(),
35            ExtractValue::Ternary(raw) => ternary_to_jq(raw),
36        }
37    }
38}
39
40/// Check if string contains top-level ternary operator
41fn is_ternary(value: &str) -> bool {
42    find_top_level_char(value, '?').is_some() && find_top_level_char(value, ':').is_some()
43}
44
45/// Find character that's not inside quotes, parentheses, or brackets
46fn find_top_level_char(expr: &str, target: char) -> Option<usize> {
47    let mut in_quotes = false;
48    let mut quote_char = None;
49    let mut paren_depth = 0;
50    let mut bracket_depth = 0;
51
52    for (i, c) in expr.char_indices() {
53        match c {
54            '\'' | '"' => {
55                if !in_quotes {
56                    in_quotes = true;
57                    quote_char = Some(c);
58                } else if Some(c) == quote_char {
59                    in_quotes = false;
60                    quote_char = None;
61                }
62            }
63            '(' | '{' => paren_depth += 1,
64            ')' | '}' => paren_depth -= 1,
65            '[' => bracket_depth += 1,
66            ']' => bracket_depth -= 1,
67            _ if c == target && !in_quotes && paren_depth == 0 && bracket_depth == 0 => {
68                return Some(i);
69            }
70            _ => {}
71        }
72    }
73
74    None
75}
76
77/// Extract variable definition
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79pub struct ExtractVar {
80    pub name: String,
81    pub value: ExtractValue,
82}
83
84impl ExtractVar {
85    /// Parse "name = value" into ExtractVar
86    pub fn parse(line: &str) -> Option<Self> {
87        let line = line.trim();
88
89        if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
90            return None;
91        }
92
93        let eq_pos = find_top_level_char(line, '=')?;
94
95        Some(Self {
96            name: line[..eq_pos].trim().to_string(),
97            value: ExtractValue::parse(line[eq_pos + 1..].trim()),
98        })
99    }
100
101    /// Parse pre-split name and value (from tokenizer).
102    pub fn parse_raw(name: &str, value: &str) -> Option<Self> {
103        if name.is_empty() || value.is_empty() {
104            return None;
105        }
106        Some(Self {
107            name: name.to_string(),
108            value: ExtractValue::parse(value),
109        })
110    }
111
112    /// Convert to JQ syntax
113    pub fn to_jq(&self) -> String {
114        format!("{} = {}", self.name, self.value.to_jq())
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_extract_value_simple() {
124        let value = ExtractValue::parse(".user.id");
125        assert!(matches!(value, ExtractValue::Simple(_)));
126        assert_eq!(value.to_jq(), ".user.id");
127    }
128
129    #[test]
130    fn test_extract_value_jq() {
131        let value = ExtractValue::parse(".items | length");
132        assert!(matches!(value, ExtractValue::JqExpr(_)));
133        assert_eq!(value.to_jq(), ".items | length");
134    }
135
136    #[test]
137    fn test_extract_value_ternary() {
138        let value = ExtractValue::parse(".status == 200 ? \"OK\" : \"Error\"");
139        assert!(matches!(value, ExtractValue::Ternary(_)));
140        assert_eq!(
141            value.to_jq(),
142            "if .status == 200 then \"OK\" else \"Error\" end"
143        );
144    }
145
146    #[test]
147    fn test_extract_value_ternary_with_jq() {
148        let value = ExtractValue::parse("(.items | length) > 0 ? \"yes\" : \"no\"");
149        assert!(matches!(value, ExtractValue::Ternary(_)));
150        assert!(value.to_jq().starts_with("if"));
151    }
152
153    #[test]
154    fn test_extract_var_parse() {
155        let var = ExtractVar::parse("status = .status == 200 ? \"OK\" : \"Error\"").unwrap();
156        assert_eq!(var.name, "status");
157        assert!(matches!(var.value, ExtractValue::Ternary(_)));
158        assert_eq!(
159            var.to_jq(),
160            "status = if .status == 200 then \"OK\" else \"Error\" end"
161        );
162    }
163
164    #[test]
165    fn test_extract_var_simple() {
166        let var = ExtractVar::parse("token = .access_token").unwrap();
167        assert_eq!(var.name, "token");
168        assert!(matches!(var.value, ExtractValue::Simple(_)));
169    }
170
171    #[test]
172    fn test_extract_var_jq() {
173        let var = ExtractVar::parse("count = .items | length").unwrap();
174        assert_eq!(var.name, "count");
175        assert!(matches!(var.value, ExtractValue::JqExpr(_)));
176    }
177
178    #[test]
179    fn test_extract_var_skip_comment() {
180        let var = ExtractVar::parse("# this is a comment");
181        assert!(var.is_none());
182    }
183
184    #[test]
185    fn test_extract_var_skip_empty() {
186        let var = ExtractVar::parse("");
187        assert!(var.is_none());
188    }
189
190    #[test]
191    fn test_find_top_level_char() {
192        assert_eq!(find_top_level_char("a ? b : c", '?'), Some(2));
193        assert_eq!(find_top_level_char("a ? b : c", ':'), Some(6));
194    }
195
196    #[test]
197    fn test_find_top_level_in_quotes() {
198        let result = find_top_level_char(".text == \"a ? b\" ? \"yes\" : \"no\"", '?');
199        assert_eq!(result, Some(17));
200    }
201
202    #[test]
203    fn test_find_top_level_in_parens() {
204        assert_eq!(
205            find_top_level_char("(.a > 0 ? \"yes\" : \"no\") : \"other\"", '?'),
206            None
207        );
208    }
209
210    #[test]
211    fn test_extract_var_nested_ternary() {
212        let var = ExtractVar::parse(
213            "size = .count == 0 ? \"empty\" : (.count > 10 ? \"large\" : \"small\")",
214        )
215        .unwrap();
216        assert_eq!(var.name, "size");
217        assert!(matches!(var.value, ExtractValue::Ternary(_)));
218        // Parentheses are preserved in output (valid jq syntax)
219        assert_eq!(
220            var.to_jq(),
221            "size = if .count == 0 then \"empty\" else (if .count > 10 then \"large\" else \"small\" end) end"
222        );
223    }
224
225    #[test]
226    fn test_extract_var_with_header_plugin() {
227        let var = ExtractVar::parse("request_id = @header(\"x-request-id\") != null ? @header(\"x-request-id\") : \"unknown\"").unwrap();
228        assert_eq!(var.name, "request_id");
229        assert!(matches!(var.value, ExtractValue::Ternary(_)));
230    }
231}