apif_parser/
ternary_ast.rs1use crate::ternary::ternary_to_jq;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub enum ExtractValue {
10 Simple(String),
12 JqExpr(String),
14 Ternary(String),
16}
17
18impl ExtractValue {
19 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 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
40fn is_ternary(value: &str) -> bool {
42 find_top_level_char(value, '?').is_some() && find_top_level_char(value, ':').is_some()
43}
44
45fn 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79pub struct ExtractVar {
80 pub name: String,
81 pub value: ExtractValue,
82}
83
84impl ExtractVar {
85 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 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 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 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}