1use heck::{ToPascalCase, ToSnakeCase};
8use std::collections::{HashMap, HashSet};
9
10pub struct FieldResolver {
12 aliases: HashMap<String, String>,
13 optional_fields: HashSet<String>,
14}
15
16#[derive(Debug, Clone)]
18enum PathSegment {
19 Field(String),
21 MapAccess { field: String, key: String },
23}
24
25impl FieldResolver {
26 pub fn new(fields: &HashMap<String, String>, optional: &HashSet<String>) -> Self {
29 Self {
30 aliases: fields.clone(),
31 optional_fields: optional.clone(),
32 }
33 }
34
35 pub fn resolve<'a>(&'a self, fixture_field: &'a str) -> &'a str {
38 self.aliases
39 .get(fixture_field)
40 .map(String::as_str)
41 .unwrap_or(fixture_field)
42 }
43
44 pub fn is_optional(&self, field: &str) -> bool {
46 self.optional_fields.contains(field)
47 }
48
49 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
52 let resolved = self.resolve(fixture_field);
53 let segments = parse_path(resolved);
54 render_accessor(&segments, language, result_var)
55 }
56
57 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
60 let resolved = self.resolve(fixture_field);
61 if !self.is_optional(resolved) {
62 return None;
63 }
64 let segments = parse_path(resolved);
65 let local_var = resolved.replace(['.', '['], "_").replace(']', "");
66 let accessor = render_accessor(&segments, "rust", result_var);
67 let binding = format!("let {local_var} = {accessor}.as_deref().unwrap_or(\"\");");
68 Some((binding, local_var))
69 }
70}
71
72fn parse_path(path: &str) -> Vec<PathSegment> {
74 let mut segments = Vec::new();
75 for part in path.split('.') {
76 if let Some(bracket_pos) = part.find('[') {
77 let field = part[..bracket_pos].to_string();
78 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
79 segments.push(PathSegment::MapAccess { field, key });
80 } else {
81 segments.push(PathSegment::Field(part.to_string()));
82 }
83 }
84 segments
85}
86
87fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
89 match language {
90 "rust" => render_rust(segments, result_var),
91 "python" => render_dot_access(segments, result_var, false),
92 "typescript" | "node" => render_typescript(segments, result_var),
93 "go" => render_go(segments, result_var),
94 "java" => render_java(segments, result_var),
95 "csharp" => render_pascal_dot(segments, result_var),
96 "ruby" => render_dot_access(segments, result_var, false),
97 "php" => render_php(segments, result_var),
98 "elixir" => render_dot_access(segments, result_var, false),
99 "r" => render_r(segments, result_var),
100 "c" => render_c(segments, result_var),
101 _ => render_dot_access(segments, result_var, false),
102 }
103}
104
105fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
111 let mut out = result_var.to_string();
112 for seg in segments {
113 match seg {
114 PathSegment::Field(f) => {
115 out.push('.');
116 out.push_str(&f.to_snake_case());
117 }
118 PathSegment::MapAccess { field, key } => {
119 out.push('.');
120 out.push_str(&field.to_snake_case());
121 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
122 }
123 }
124 }
125 out
126}
127
128fn render_dot_access(segments: &[PathSegment], result_var: &str, _pascal: bool) -> String {
130 let mut out = result_var.to_string();
131 for seg in segments {
132 match seg {
133 PathSegment::Field(f) => {
134 out.push('.');
135 out.push_str(f);
136 }
137 PathSegment::MapAccess { field, key } => {
138 out.push('.');
139 out.push_str(field);
140 out.push_str(&format!(".get(\"{key}\")"));
141 }
142 }
143 }
144 out
145}
146
147fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
149 let mut out = result_var.to_string();
150 for seg in segments {
151 match seg {
152 PathSegment::Field(f) => {
153 out.push('.');
154 out.push_str(f);
155 }
156 PathSegment::MapAccess { field, key } => {
157 out.push('.');
158 out.push_str(field);
159 out.push_str(&format!("[\"{key}\"]"));
160 }
161 }
162 }
163 out
164}
165
166fn render_go(segments: &[PathSegment], result_var: &str) -> String {
168 let mut out = result_var.to_string();
169 for seg in segments {
170 match seg {
171 PathSegment::Field(f) => {
172 out.push('.');
173 out.push_str(&f.to_pascal_case());
174 }
175 PathSegment::MapAccess { field, key } => {
176 out.push('.');
177 out.push_str(&field.to_pascal_case());
178 out.push_str(&format!("[\"{key}\"]"));
179 }
180 }
181 }
182 out
183}
184
185fn render_java(segments: &[PathSegment], result_var: &str) -> String {
187 let mut out = result_var.to_string();
188 for seg in segments {
189 match seg {
190 PathSegment::Field(f) => {
191 out.push('.');
192 out.push_str(f);
193 out.push_str("()");
194 }
195 PathSegment::MapAccess { field, key } => {
196 out.push('.');
197 out.push_str(field);
198 out.push_str(&format!("().get(\"{key}\")"));
199 }
200 }
201 }
202 out
203}
204
205fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
207 let mut out = result_var.to_string();
208 for seg in segments {
209 match seg {
210 PathSegment::Field(f) => {
211 out.push('.');
212 out.push_str(&f.to_pascal_case());
213 }
214 PathSegment::MapAccess { field, key } => {
215 out.push('.');
216 out.push_str(&field.to_pascal_case());
217 out.push_str(&format!("[\"{key}\"]"));
218 }
219 }
220 }
221 out
222}
223
224fn render_php(segments: &[PathSegment], result_var: &str) -> String {
226 let mut out = result_var.to_string();
227 for seg in segments {
228 match seg {
229 PathSegment::Field(f) => {
230 out.push_str("->");
231 out.push_str(f);
232 }
233 PathSegment::MapAccess { field, key } => {
234 out.push_str("->");
235 out.push_str(field);
236 out.push_str(&format!("[\"{key}\"]"));
237 }
238 }
239 }
240 out
241}
242
243fn render_r(segments: &[PathSegment], result_var: &str) -> String {
245 let mut out = result_var.to_string();
246 for seg in segments {
247 match seg {
248 PathSegment::Field(f) => {
249 out.push('$');
250 out.push_str(f);
251 }
252 PathSegment::MapAccess { field, key } => {
253 out.push('$');
254 out.push_str(field);
255 out.push_str(&format!("[[\"{key}\"]]"));
256 }
257 }
258 }
259 out
260}
261
262fn render_c(segments: &[PathSegment], result_var: &str) -> String {
264 let mut parts = Vec::new();
265 for seg in segments {
266 match seg {
267 PathSegment::Field(f) => parts.push(f.to_snake_case()),
268 PathSegment::MapAccess { field, key } => {
269 parts.push(field.to_snake_case());
270 parts.push(key.clone());
271 }
272 }
273 }
274 let suffix = parts.join("_");
275 format!("result_{suffix}({result_var})")
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 fn make_resolver() -> FieldResolver {
283 let mut fields = HashMap::new();
284 fields.insert("title".to_string(), "metadata.document.title".to_string());
285 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
286
287 let mut optional = HashSet::new();
288 optional.insert("metadata.document.title".to_string());
289
290 FieldResolver::new(&fields, &optional)
291 }
292
293 #[test]
294 fn test_resolve_alias() {
295 let r = make_resolver();
296 assert_eq!(r.resolve("title"), "metadata.document.title");
297 }
298
299 #[test]
300 fn test_resolve_passthrough() {
301 let r = make_resolver();
302 assert_eq!(r.resolve("content"), "content");
303 }
304
305 #[test]
306 fn test_is_optional() {
307 let r = make_resolver();
308 assert!(r.is_optional("metadata.document.title"));
309 assert!(!r.is_optional("content"));
310 }
311
312 #[test]
313 fn test_accessor_rust_struct() {
314 let r = make_resolver();
315 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
316 }
317
318 #[test]
319 fn test_accessor_rust_map() {
320 let r = make_resolver();
321 assert_eq!(
322 r.accessor("tags", "rust", "result"),
323 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
324 );
325 }
326
327 #[test]
328 fn test_accessor_python() {
329 let r = make_resolver();
330 assert_eq!(
331 r.accessor("title", "python", "result"),
332 "result.metadata.document.title"
333 );
334 }
335
336 #[test]
337 fn test_accessor_go() {
338 let r = make_resolver();
339 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
340 }
341
342 #[test]
343 fn test_accessor_typescript() {
344 let r = make_resolver();
345 assert_eq!(
346 r.accessor("title", "typescript", "result"),
347 "result.metadata.document.title"
348 );
349 }
350
351 #[test]
352 fn test_accessor_java() {
353 let r = make_resolver();
354 assert_eq!(
355 r.accessor("title", "java", "result"),
356 "result.metadata().document().title()"
357 );
358 }
359
360 #[test]
361 fn test_accessor_csharp() {
362 let r = make_resolver();
363 assert_eq!(
364 r.accessor("title", "csharp", "result"),
365 "result.Metadata.Document.Title"
366 );
367 }
368
369 #[test]
370 fn test_accessor_php() {
371 let r = make_resolver();
372 assert_eq!(
373 r.accessor("title", "php", "$result"),
374 "$result->metadata->document->title"
375 );
376 }
377
378 #[test]
379 fn test_accessor_r() {
380 let r = make_resolver();
381 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
382 }
383
384 #[test]
385 fn test_accessor_c() {
386 let r = make_resolver();
387 assert_eq!(
388 r.accessor("title", "c", "result"),
389 "result_metadata_document_title(result)"
390 );
391 }
392
393 #[test]
394 fn test_rust_unwrap_binding() {
395 let r = make_resolver();
396 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
397 assert_eq!(var, "metadata_document_title");
398 assert!(binding.contains("as_deref().unwrap_or(\"\")"));
399 }
400
401 #[test]
402 fn test_rust_unwrap_binding_non_optional() {
403 let r = make_resolver();
404 assert!(r.rust_unwrap_binding("content", "result").is_none());
405 }
406
407 #[test]
408 fn test_direct_field_no_alias() {
409 let r = make_resolver();
410 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
411 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
412 }
413}