1use alef_codegen::naming::to_go_name;
8use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
9use std::collections::{HashMap, HashSet};
10
11pub struct FieldResolver {
13 aliases: HashMap<String, String>,
14 optional_fields: HashSet<String>,
15 result_fields: HashSet<String>,
16 array_fields: HashSet<String>,
17 method_calls: HashSet<String>,
18}
19
20#[derive(Debug, Clone)]
22enum PathSegment {
23 Field(String),
25 ArrayField(String),
27 MapAccess { field: String, key: String },
29 Length,
31}
32
33impl FieldResolver {
34 pub fn new(
38 fields: &HashMap<String, String>,
39 optional: &HashSet<String>,
40 result_fields: &HashSet<String>,
41 array_fields: &HashSet<String>,
42 method_calls: &HashSet<String>,
43 ) -> Self {
44 Self {
45 aliases: fields.clone(),
46 optional_fields: optional.clone(),
47 result_fields: result_fields.clone(),
48 array_fields: array_fields.clone(),
49 method_calls: method_calls.clone(),
50 }
51 }
52
53 pub fn resolve<'a>(&'a self, fixture_field: &'a str) -> &'a str {
56 self.aliases
57 .get(fixture_field)
58 .map(String::as_str)
59 .unwrap_or(fixture_field)
60 }
61
62 pub fn is_optional(&self, field: &str) -> bool {
64 if self.optional_fields.contains(field) {
65 return true;
66 }
67 let index_normalized = normalize_numeric_indices(field);
68 if index_normalized != field && self.optional_fields.contains(index_normalized.as_str()) {
69 return true;
70 }
71 let normalized = field.replace("[].", ".");
72 if normalized != field && self.optional_fields.contains(normalized.as_str()) {
73 return true;
74 }
75 for af in &self.array_fields {
76 if let Some(rest) = field.strip_prefix(af.as_str()) {
77 if let Some(rest) = rest.strip_prefix('.') {
78 let with_bracket = format!("{af}[].{rest}");
79 if self.optional_fields.contains(with_bracket.as_str()) {
80 return true;
81 }
82 }
83 }
84 }
85 false
86 }
87
88 pub fn has_alias(&self, fixture_field: &str) -> bool {
90 self.aliases.contains_key(fixture_field)
91 }
92
93 pub fn is_valid_for_result(&self, fixture_field: &str) -> bool {
95 if self.result_fields.is_empty() {
96 return true;
97 }
98 let resolved = self.resolve(fixture_field);
99 let first_segment = resolved.split('.').next().unwrap_or(resolved);
100 let first_segment = first_segment.split('[').next().unwrap_or(first_segment);
101 self.result_fields.contains(first_segment)
102 }
103
104 pub fn is_array(&self, field: &str) -> bool {
106 self.array_fields.contains(field)
107 }
108
109 pub fn tagged_union_split(&self, fixture_field: &str) -> Option<(String, String, String)> {
121 let resolved = self.resolve(fixture_field);
122 let segments: Vec<&str> = resolved.split('.').collect();
123 let mut path_so_far = String::new();
124 for (i, seg) in segments.iter().enumerate() {
125 if !path_so_far.is_empty() {
126 path_so_far.push('.');
127 }
128 path_so_far.push_str(seg);
129 if self.method_calls.contains(&path_so_far) {
130 let prefix = segments[..i].join(".");
132 let variant = (*seg).to_string();
133 let suffix = segments[i + 1..].join(".");
134 return Some((prefix, variant, suffix));
135 }
136 }
137 None
138 }
139
140 pub fn has_map_access(&self, fixture_field: &str) -> bool {
142 let resolved = self.resolve(fixture_field);
143 let segments = parse_path(resolved);
144 segments.iter().any(|s| {
145 if let PathSegment::MapAccess { key, .. } = s {
146 !key.chars().all(|c| c.is_ascii_digit())
147 } else {
148 false
149 }
150 })
151 }
152
153 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
155 let resolved = self.resolve(fixture_field);
156 let segments = parse_path(resolved);
157 let segments = self.inject_array_indexing(segments);
158 match language {
159 "java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
160 "rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
161 "csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
162 "zig" => render_zig_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
163 "swift" => render_swift_with_optionals(&segments, result_var, &self.optional_fields),
164 _ => render_accessor(&segments, language, result_var),
165 }
166 }
167
168 fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
169 if self.array_fields.is_empty() {
170 return segments;
171 }
172 let len = segments.len();
173 let mut result = Vec::with_capacity(len);
174 let mut path_so_far = String::new();
175 for i in 0..len {
176 let seg = &segments[i];
177 match seg {
178 PathSegment::Field(f) => {
179 if !path_so_far.is_empty() {
180 path_so_far.push('.');
181 }
182 path_so_far.push_str(f);
183 let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
184 if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
185 result.push(PathSegment::ArrayField(f.clone()));
186 } else {
187 result.push(seg.clone());
188 }
189 }
190 _ => {
191 result.push(seg.clone());
192 }
193 }
194 }
195 result
196 }
197
198 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
200 let resolved = self.resolve(fixture_field);
201 if !self.is_optional(resolved) {
202 return None;
203 }
204 let segments = parse_path(resolved);
205 let segments = self.inject_array_indexing(segments);
206 let local_var = resolved.replace(['.', '['], "_").replace(']', "");
207 let accessor = render_accessor(&segments, "rust", result_var);
208 let has_map_access = segments.iter().any(|s| {
209 if let PathSegment::MapAccess { key, .. } = s {
210 !key.chars().all(|c| c.is_ascii_digit())
211 } else {
212 false
213 }
214 });
215 let is_array = self.is_array(resolved);
216 let binding = if has_map_access {
217 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
218 } else if is_array {
219 format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
220 } else {
221 format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
222 };
223 Some((binding, local_var))
224 }
225}
226
227fn normalize_numeric_indices(path: &str) -> String {
228 let mut result = String::with_capacity(path.len());
229 let mut chars = path.chars().peekable();
230 while let Some(c) = chars.next() {
231 if c == '[' {
232 let mut key = String::new();
233 let mut closed = false;
234 for inner in chars.by_ref() {
235 if inner == ']' {
236 closed = true;
237 break;
238 }
239 key.push(inner);
240 }
241 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
242 result.push_str("[0]");
243 } else {
244 result.push('[');
245 result.push_str(&key);
246 if closed {
247 result.push(']');
248 }
249 }
250 } else {
251 result.push(c);
252 }
253 }
254 result
255}
256
257fn parse_path(path: &str) -> Vec<PathSegment> {
258 let mut segments = Vec::new();
259 for part in path.split('.') {
260 if part == "length" || part == "count" || part == "size" {
261 segments.push(PathSegment::Length);
262 } else if let Some(bracket_pos) = part.find('[') {
263 let field = part[..bracket_pos].to_string();
264 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
265 if key.is_empty() {
266 segments.push(PathSegment::ArrayField(field));
267 } else {
268 segments.push(PathSegment::MapAccess { field, key });
269 }
270 } else {
271 segments.push(PathSegment::Field(part.to_string()));
272 }
273 }
274 segments
275}
276
277fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
278 match language {
279 "rust" => render_rust(segments, result_var),
280 "python" => render_dot_access(segments, result_var, "python"),
281 "typescript" | "node" => render_typescript(segments, result_var),
282 "wasm" => render_wasm(segments, result_var),
283 "go" => render_go(segments, result_var),
284 "java" => render_java(segments, result_var),
285 "csharp" => render_pascal_dot(segments, result_var),
286 "ruby" => render_dot_access(segments, result_var, "ruby"),
287 "php" => render_php(segments, result_var),
288 "elixir" => render_dot_access(segments, result_var, "elixir"),
289 "r" => render_r(segments, result_var),
290 "c" => render_c(segments, result_var),
291 "swift" => render_swift(segments, result_var),
292 _ => render_dot_access(segments, result_var, language),
293 }
294}
295
296fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
302 let mut out = result_var.to_string();
303 for seg in segments {
304 match seg {
305 PathSegment::Field(f) => {
306 out.push('.');
307 out.push_str(f);
308 out.push_str("()");
309 }
310 PathSegment::ArrayField(f) => {
311 out.push('.');
312 out.push_str(f);
313 out.push_str("()[0]");
314 }
315 PathSegment::MapAccess { field, key } => {
316 out.push('.');
317 out.push_str(field);
318 if key.chars().all(|c| c.is_ascii_digit()) {
319 out.push_str(&format!("()[{key}]"));
320 } else {
321 out.push_str(&format!("()[\"{key}\"]"));
322 }
323 }
324 PathSegment::Length => {
325 out.push_str(".count");
326 }
327 }
328 }
329 out
330}
331
332fn render_swift_with_optionals(
342 segments: &[PathSegment],
343 result_var: &str,
344 optional_fields: &HashSet<String>,
345) -> String {
346 let mut out = result_var.to_string();
347 let mut path_so_far = String::new();
348 let total = segments.len();
349 for (i, seg) in segments.iter().enumerate() {
350 let is_leaf = i == total - 1;
351 match seg {
352 PathSegment::Field(f) => {
353 if !path_so_far.is_empty() {
354 path_so_far.push('.');
355 }
356 path_so_far.push_str(f);
357 out.push('.');
358 out.push_str(f);
359 out.push_str("()");
360 if !is_leaf && optional_fields.contains(&path_so_far) {
363 out.push('?');
364 }
365 }
366 PathSegment::ArrayField(f) => {
367 if !path_so_far.is_empty() {
368 path_so_far.push('.');
369 }
370 path_so_far.push_str(f);
371 out.push('.');
372 out.push_str(f);
373 out.push_str("()[0]");
374 }
375 PathSegment::MapAccess { field, key } => {
376 if !path_so_far.is_empty() {
377 path_so_far.push('.');
378 }
379 path_so_far.push_str(field);
380 out.push('.');
381 out.push_str(field);
382 if key.chars().all(|c| c.is_ascii_digit()) {
383 out.push_str(&format!("()[{key}]"));
384 } else {
385 out.push_str(&format!("()[\"{key}\"]"));
386 }
387 }
388 PathSegment::Length => {
389 out.push_str(".count");
390 }
391 }
392 }
393 out
394}
395
396fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
397 let mut out = result_var.to_string();
398 for seg in segments {
399 match seg {
400 PathSegment::Field(f) => {
401 out.push('.');
402 out.push_str(&f.to_snake_case());
403 }
404 PathSegment::ArrayField(f) => {
405 out.push('.');
406 out.push_str(&f.to_snake_case());
407 out.push_str("[0]");
408 }
409 PathSegment::MapAccess { field, key } => {
410 out.push('.');
411 out.push_str(&field.to_snake_case());
412 if key.chars().all(|c| c.is_ascii_digit()) {
413 out.push_str(&format!("[{key}]"));
414 } else {
415 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
416 }
417 }
418 PathSegment::Length => {
419 out.push_str(".len()");
420 }
421 }
422 }
423 out
424}
425
426fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
427 let mut out = result_var.to_string();
428 for seg in segments {
429 match seg {
430 PathSegment::Field(f) => {
431 out.push('.');
432 out.push_str(f);
433 }
434 PathSegment::ArrayField(f) => {
435 if language == "elixir" {
436 let current = std::mem::take(&mut out);
437 out = format!("Enum.at({current}.{f}, 0)");
438 } else {
439 out.push('.');
440 out.push_str(f);
441 out.push_str("[0]");
442 }
443 }
444 PathSegment::MapAccess { field, key } => {
445 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
446 if is_numeric && language == "elixir" {
447 let current = std::mem::take(&mut out);
448 out = format!("Enum.at({current}.{field}, {key})");
449 } else {
450 out.push('.');
451 out.push_str(field);
452 if is_numeric {
453 let idx: usize = key.parse().unwrap_or(0);
454 out.push_str(&format!("[{idx}]"));
455 } else if language == "elixir" {
456 out.push_str(&format!("[\"{key}\"]"));
457 } else {
458 out.push_str(&format!(".get(\"{key}\")"));
459 }
460 }
461 }
462 PathSegment::Length => match language {
463 "ruby" => out.push_str(".length"),
464 "elixir" => {
465 let current = std::mem::take(&mut out);
466 out = format!("length({current})");
467 }
468 _ => {
469 let current = std::mem::take(&mut out);
470 out = format!("len({current})");
471 }
472 },
473 }
474 }
475 out
476}
477
478fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
479 let mut out = result_var.to_string();
480 for seg in segments {
481 match seg {
482 PathSegment::Field(f) => {
483 out.push('.');
484 out.push_str(&f.to_lower_camel_case());
485 }
486 PathSegment::ArrayField(f) => {
487 out.push('.');
488 out.push_str(&f.to_lower_camel_case());
489 out.push_str("[0]");
490 }
491 PathSegment::MapAccess { field, key } => {
492 out.push('.');
493 out.push_str(&field.to_lower_camel_case());
494 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
497 out.push_str(&format!("[{key}]"));
498 } else {
499 out.push_str(&format!("[\"{key}\"]"));
500 }
501 }
502 PathSegment::Length => {
503 out.push_str(".length");
504 }
505 }
506 }
507 out
508}
509
510fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
511 let mut out = result_var.to_string();
512 for seg in segments {
513 match seg {
514 PathSegment::Field(f) => {
515 out.push('.');
516 out.push_str(&f.to_lower_camel_case());
517 }
518 PathSegment::ArrayField(f) => {
519 out.push('.');
520 out.push_str(&f.to_lower_camel_case());
521 out.push_str("[0]");
522 }
523 PathSegment::MapAccess { field, key } => {
524 out.push('.');
525 out.push_str(&field.to_lower_camel_case());
526 out.push_str(&format!(".get(\"{key}\")"));
527 }
528 PathSegment::Length => {
529 out.push_str(".length");
530 }
531 }
532 }
533 out
534}
535
536fn render_go(segments: &[PathSegment], result_var: &str) -> String {
537 let mut out = result_var.to_string();
538 for seg in segments {
539 match seg {
540 PathSegment::Field(f) => {
541 out.push('.');
542 out.push_str(&to_go_name(f));
543 }
544 PathSegment::ArrayField(f) => {
545 out.push('.');
546 out.push_str(&to_go_name(f));
547 out.push_str("[0]");
548 }
549 PathSegment::MapAccess { field, key } => {
550 out.push('.');
551 out.push_str(&to_go_name(field));
552 if key.chars().all(|c| c.is_ascii_digit()) {
553 out.push_str(&format!("[{key}]"));
554 } else {
555 out.push_str(&format!("[\"{key}\"]"));
556 }
557 }
558 PathSegment::Length => {
559 let current = std::mem::take(&mut out);
560 out = format!("len({current})");
561 }
562 }
563 }
564 out
565}
566
567fn render_java(segments: &[PathSegment], result_var: &str) -> String {
568 let mut out = result_var.to_string();
569 for seg in segments {
570 match seg {
571 PathSegment::Field(f) => {
572 out.push('.');
573 out.push_str(&f.to_lower_camel_case());
574 out.push_str("()");
575 }
576 PathSegment::ArrayField(f) => {
577 out.push('.');
578 out.push_str(&f.to_lower_camel_case());
579 out.push_str("().getFirst()");
580 }
581 PathSegment::MapAccess { field, key } => {
582 out.push('.');
583 out.push_str(&field.to_lower_camel_case());
584 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
586 if is_numeric {
587 out.push_str(&format!("().get({key})"));
588 } else {
589 out.push_str(&format!("().get(\"{key}\")"));
590 }
591 }
592 PathSegment::Length => {
593 out.push_str(".size()");
594 }
595 }
596 }
597 out
598}
599
600fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
601 let mut out = result_var.to_string();
602 let mut path_so_far = String::new();
603 for (i, seg) in segments.iter().enumerate() {
604 let is_leaf = i == segments.len() - 1;
605 match seg {
606 PathSegment::Field(f) => {
607 if !path_so_far.is_empty() {
608 path_so_far.push('.');
609 }
610 path_so_far.push_str(f);
611 out.push('.');
612 out.push_str(&f.to_lower_camel_case());
613 out.push_str("()");
614 let _ = is_leaf;
615 let _ = optional_fields;
616 }
617 PathSegment::ArrayField(f) => {
618 if !path_so_far.is_empty() {
619 path_so_far.push('.');
620 }
621 path_so_far.push_str(f);
622 out.push('.');
623 out.push_str(&f.to_lower_camel_case());
624 out.push_str("().getFirst()");
625 }
626 PathSegment::MapAccess { field, key } => {
627 if !path_so_far.is_empty() {
628 path_so_far.push('.');
629 }
630 path_so_far.push_str(field);
631 out.push('.');
632 out.push_str(&field.to_lower_camel_case());
633 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
635 if is_numeric {
636 out.push_str(&format!("().get({key})"));
637 } else {
638 out.push_str(&format!("().get(\"{key}\")"));
639 }
640 }
641 PathSegment::Length => {
642 out.push_str(".size()");
643 }
644 }
645 }
646 out
647}
648
649fn render_rust_with_optionals(
655 segments: &[PathSegment],
656 result_var: &str,
657 optional_fields: &HashSet<String>,
658 method_calls: &HashSet<String>,
659) -> String {
660 let mut out = result_var.to_string();
661 let mut path_so_far = String::new();
662 for (i, seg) in segments.iter().enumerate() {
663 let is_leaf = i == segments.len() - 1;
664 match seg {
665 PathSegment::Field(f) => {
666 if !path_so_far.is_empty() {
667 path_so_far.push('.');
668 }
669 path_so_far.push_str(f);
670 out.push('.');
671 out.push_str(&f.to_snake_case());
672 let is_method = method_calls.contains(&path_so_far);
673 if is_method {
674 out.push_str("()");
675 if !is_leaf && optional_fields.contains(&path_so_far) {
676 out.push_str(".as_ref().unwrap()");
677 }
678 } else if !is_leaf && optional_fields.contains(&path_so_far) {
679 out.push_str(".as_ref().unwrap()");
680 }
681 }
682 PathSegment::ArrayField(f) => {
683 if !path_so_far.is_empty() {
684 path_so_far.push('.');
685 }
686 path_so_far.push_str(f);
687 out.push('.');
688 out.push_str(&f.to_snake_case());
689 out.push_str("[0]");
690 }
691 PathSegment::MapAccess { field, key } => {
692 if !path_so_far.is_empty() {
693 path_so_far.push('.');
694 }
695 path_so_far.push_str(field);
696 out.push('.');
697 out.push_str(&field.to_snake_case());
698 if key.chars().all(|c| c.is_ascii_digit()) {
699 let is_opt = optional_fields.contains(&path_so_far);
700 if is_opt {
701 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
702 } else {
703 out.push_str(&format!("[{key}]"));
704 }
705 path_so_far.push_str("[0]");
706 } else {
707 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
708 }
709 }
710 PathSegment::Length => {
711 out.push_str(".len()");
712 }
713 }
714 }
715 out
716}
717
718fn render_zig_with_optionals(
731 segments: &[PathSegment],
732 result_var: &str,
733 optional_fields: &HashSet<String>,
734 method_calls: &HashSet<String>,
735) -> String {
736 let mut out = result_var.to_string();
737 let mut path_so_far = String::new();
738 for seg in segments {
739 match seg {
740 PathSegment::Field(f) => {
741 if !path_so_far.is_empty() {
742 path_so_far.push('.');
743 }
744 path_so_far.push_str(f);
745 out.push('.');
746 out.push_str(f);
747 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
748 out.push_str(".?");
749 }
750 }
751 PathSegment::ArrayField(f) => {
752 if !path_so_far.is_empty() {
753 path_so_far.push('.');
754 }
755 path_so_far.push_str(f);
756 out.push('.');
757 out.push_str(f);
758 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
759 out.push_str(".?");
760 }
761 out.push_str("[0]");
762 }
763 PathSegment::MapAccess { field, key } => {
764 if !path_so_far.is_empty() {
765 path_so_far.push('.');
766 }
767 path_so_far.push_str(field);
768 out.push('.');
769 out.push_str(field);
770 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
771 out.push_str(".?");
772 }
773 if key.chars().all(|c| c.is_ascii_digit()) {
774 out.push_str(&format!("[{key}]"));
775 } else {
776 out.push_str(&format!(".get(\"{key}\")"));
777 }
778 }
779 PathSegment::Length => {
780 out.push_str(".len");
781 }
782 }
783 }
784 out
785}
786
787fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
788 let mut out = result_var.to_string();
789 for seg in segments {
790 match seg {
791 PathSegment::Field(f) => {
792 out.push('.');
793 out.push_str(&f.to_pascal_case());
794 }
795 PathSegment::ArrayField(f) => {
796 out.push('.');
797 out.push_str(&f.to_pascal_case());
798 out.push_str("[0]");
799 }
800 PathSegment::MapAccess { field, key } => {
801 out.push('.');
802 out.push_str(&field.to_pascal_case());
803 if key.chars().all(|c| c.is_ascii_digit()) {
804 out.push_str(&format!("[{key}]"));
805 } else {
806 out.push_str(&format!("[\"{key}\"]"));
807 }
808 }
809 PathSegment::Length => {
810 out.push_str(".Count");
811 }
812 }
813 }
814 out
815}
816
817fn render_csharp_with_optionals(
818 segments: &[PathSegment],
819 result_var: &str,
820 optional_fields: &HashSet<String>,
821) -> String {
822 let mut out = result_var.to_string();
823 let mut path_so_far = String::new();
824 for (i, seg) in segments.iter().enumerate() {
825 let is_leaf = i == segments.len() - 1;
826 match seg {
827 PathSegment::Field(f) => {
828 if !path_so_far.is_empty() {
829 path_so_far.push('.');
830 }
831 path_so_far.push_str(f);
832 out.push('.');
833 out.push_str(&f.to_pascal_case());
834 if !is_leaf && optional_fields.contains(&path_so_far) {
835 out.push('!');
836 }
837 }
838 PathSegment::ArrayField(f) => {
839 if !path_so_far.is_empty() {
840 path_so_far.push('.');
841 }
842 path_so_far.push_str(f);
843 out.push('.');
844 out.push_str(&f.to_pascal_case());
845 out.push_str("[0]");
846 }
847 PathSegment::MapAccess { field, key } => {
848 if !path_so_far.is_empty() {
849 path_so_far.push('.');
850 }
851 path_so_far.push_str(field);
852 out.push('.');
853 out.push_str(&field.to_pascal_case());
854 if key.chars().all(|c| c.is_ascii_digit()) {
855 out.push_str(&format!("[{key}]"));
856 } else {
857 out.push_str(&format!("[\"{key}\"]"));
858 }
859 }
860 PathSegment::Length => {
861 out.push_str(".Count");
862 }
863 }
864 }
865 out
866}
867
868fn render_php(segments: &[PathSegment], result_var: &str) -> String {
869 let mut out = result_var.to_string();
870 for seg in segments {
871 match seg {
872 PathSegment::Field(f) => {
873 out.push_str("->");
874 out.push_str(&f.to_lower_camel_case());
877 }
878 PathSegment::ArrayField(f) => {
879 out.push_str("->");
880 out.push_str(&f.to_lower_camel_case());
881 out.push_str("[0]");
882 }
883 PathSegment::MapAccess { field, key } => {
884 out.push_str("->");
885 out.push_str(&field.to_lower_camel_case());
886 out.push_str(&format!("[\"{key}\"]"));
887 }
888 PathSegment::Length => {
889 let current = std::mem::take(&mut out);
890 out = format!("count({current})");
891 }
892 }
893 }
894 out
895}
896
897fn render_r(segments: &[PathSegment], result_var: &str) -> String {
898 let mut out = result_var.to_string();
899 for seg in segments {
900 match seg {
901 PathSegment::Field(f) => {
902 out.push('$');
903 out.push_str(f);
904 }
905 PathSegment::ArrayField(f) => {
906 out.push('$');
907 out.push_str(f);
908 out.push_str("[[1]]");
909 }
910 PathSegment::MapAccess { field, key } => {
911 out.push('$');
912 out.push_str(field);
913 out.push_str(&format!("[[\"{key}\"]]"));
914 }
915 PathSegment::Length => {
916 let current = std::mem::take(&mut out);
917 out = format!("length({current})");
918 }
919 }
920 }
921 out
922}
923
924fn render_c(segments: &[PathSegment], result_var: &str) -> String {
925 let mut parts = Vec::new();
926 let mut trailing_length = false;
927 for seg in segments {
928 match seg {
929 PathSegment::Field(f) | PathSegment::ArrayField(f) => parts.push(f.to_snake_case()),
930 PathSegment::MapAccess { field, key } => {
931 parts.push(field.to_snake_case());
932 parts.push(key.clone());
933 }
934 PathSegment::Length => {
935 trailing_length = true;
936 }
937 }
938 }
939 let suffix = parts.join("_");
940 if trailing_length {
941 format!("result_{suffix}_count({result_var})")
942 } else {
943 format!("result_{suffix}({result_var})")
944 }
945}
946
947#[cfg(test)]
948mod tests {
949 use super::*;
950
951 fn make_resolver() -> FieldResolver {
952 let mut fields = HashMap::new();
953 fields.insert("title".to_string(), "metadata.document.title".to_string());
954 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
955 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
956 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
957 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
958 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
959 let mut optional = HashSet::new();
960 optional.insert("metadata.document.title".to_string());
961 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
962 }
963
964 fn make_resolver_with_doc_optional() -> FieldResolver {
965 let mut fields = HashMap::new();
966 fields.insert("title".to_string(), "metadata.document.title".to_string());
967 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
968 let mut optional = HashSet::new();
969 optional.insert("document".to_string());
970 optional.insert("metadata.document.title".to_string());
971 optional.insert("metadata.document".to_string());
972 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
973 }
974
975 #[test]
976 fn test_resolve_alias() {
977 let r = make_resolver();
978 assert_eq!(r.resolve("title"), "metadata.document.title");
979 }
980
981 #[test]
982 fn test_resolve_passthrough() {
983 let r = make_resolver();
984 assert_eq!(r.resolve("content"), "content");
985 }
986
987 #[test]
988 fn test_is_optional() {
989 let r = make_resolver();
990 assert!(r.is_optional("metadata.document.title"));
991 assert!(!r.is_optional("content"));
992 }
993
994 #[test]
995 fn test_accessor_rust_struct() {
996 let r = make_resolver();
997 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
998 }
999
1000 #[test]
1001 fn test_accessor_rust_map() {
1002 let r = make_resolver();
1003 assert_eq!(
1004 r.accessor("tags", "rust", "result"),
1005 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
1006 );
1007 }
1008
1009 #[test]
1010 fn test_accessor_python() {
1011 let r = make_resolver();
1012 assert_eq!(
1013 r.accessor("title", "python", "result"),
1014 "result.metadata.document.title"
1015 );
1016 }
1017
1018 #[test]
1019 fn test_accessor_go() {
1020 let r = make_resolver();
1021 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
1022 }
1023
1024 #[test]
1025 fn test_accessor_go_initialism_fields() {
1026 let mut fields = std::collections::HashMap::new();
1027 fields.insert("content".to_string(), "html".to_string());
1028 fields.insert("link_url".to_string(), "links.url".to_string());
1029 let r = FieldResolver::new(
1030 &fields,
1031 &HashSet::new(),
1032 &HashSet::new(),
1033 &HashSet::new(),
1034 &HashSet::new(),
1035 );
1036 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
1037 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
1038 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
1039 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
1040 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
1041 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
1042 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
1043 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
1044 }
1045
1046 #[test]
1047 fn test_accessor_typescript() {
1048 let r = make_resolver();
1049 assert_eq!(
1050 r.accessor("title", "typescript", "result"),
1051 "result.metadata.document.title"
1052 );
1053 }
1054
1055 #[test]
1056 fn test_accessor_typescript_snake_to_camel() {
1057 let r = make_resolver();
1058 assert_eq!(
1059 r.accessor("og", "typescript", "result"),
1060 "result.metadata.document.openGraph"
1061 );
1062 assert_eq!(
1063 r.accessor("twitter", "typescript", "result"),
1064 "result.metadata.document.twitterCard"
1065 );
1066 assert_eq!(
1067 r.accessor("canonical", "typescript", "result"),
1068 "result.metadata.document.canonicalUrl"
1069 );
1070 }
1071
1072 #[test]
1073 fn test_accessor_typescript_map_snake_to_camel() {
1074 let r = make_resolver();
1075 assert_eq!(
1076 r.accessor("og_tag", "typescript", "result"),
1077 "result.metadata.openGraphTags[\"og_title\"]"
1078 );
1079 }
1080
1081 #[test]
1082 fn test_accessor_typescript_numeric_index_is_unquoted() {
1083 let mut fields = HashMap::new();
1087 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
1088 let r = FieldResolver::new(
1089 &fields,
1090 &HashSet::new(),
1091 &HashSet::new(),
1092 &HashSet::new(),
1093 &HashSet::new(),
1094 );
1095 assert_eq!(
1096 r.accessor("first_score", "typescript", "result"),
1097 "result.results[0].relevanceScore"
1098 );
1099 }
1100
1101 #[test]
1102 fn test_accessor_node_alias() {
1103 let r = make_resolver();
1104 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
1105 }
1106
1107 #[test]
1108 fn test_accessor_wasm_camel_case() {
1109 let r = make_resolver();
1110 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
1111 assert_eq!(
1112 r.accessor("twitter", "wasm", "result"),
1113 "result.metadata.document.twitterCard"
1114 );
1115 assert_eq!(
1116 r.accessor("canonical", "wasm", "result"),
1117 "result.metadata.document.canonicalUrl"
1118 );
1119 }
1120
1121 #[test]
1122 fn test_accessor_wasm_map_access() {
1123 let r = make_resolver();
1124 assert_eq!(
1125 r.accessor("og_tag", "wasm", "result"),
1126 "result.metadata.openGraphTags.get(\"og_title\")"
1127 );
1128 }
1129
1130 #[test]
1131 fn test_accessor_java() {
1132 let r = make_resolver();
1133 assert_eq!(
1134 r.accessor("title", "java", "result"),
1135 "result.metadata().document().title()"
1136 );
1137 }
1138
1139 #[test]
1140 fn test_accessor_csharp() {
1141 let r = make_resolver();
1142 assert_eq!(
1143 r.accessor("title", "csharp", "result"),
1144 "result.Metadata.Document.Title"
1145 );
1146 }
1147
1148 #[test]
1149 fn test_accessor_php() {
1150 let r = make_resolver();
1151 assert_eq!(
1152 r.accessor("title", "php", "$result"),
1153 "$result->metadata->document->title"
1154 );
1155 }
1156
1157 #[test]
1158 fn test_accessor_r() {
1159 let r = make_resolver();
1160 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
1161 }
1162
1163 #[test]
1164 fn test_accessor_c() {
1165 let r = make_resolver();
1166 assert_eq!(
1167 r.accessor("title", "c", "result"),
1168 "result_metadata_document_title(result)"
1169 );
1170 }
1171
1172 #[test]
1173 fn test_rust_unwrap_binding() {
1174 let r = make_resolver();
1175 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
1176 assert_eq!(var, "metadata_document_title");
1177 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
1178 }
1179
1180 #[test]
1181 fn test_rust_unwrap_binding_non_optional() {
1182 let r = make_resolver();
1183 assert!(r.rust_unwrap_binding("content", "result").is_none());
1184 }
1185
1186 #[test]
1187 fn test_direct_field_no_alias() {
1188 let r = make_resolver();
1189 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1190 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
1191 }
1192
1193 #[test]
1194 fn test_accessor_rust_with_optionals() {
1195 let r = make_resolver_with_doc_optional();
1196 assert_eq!(
1197 r.accessor("title", "rust", "result"),
1198 "result.metadata.document.as_ref().unwrap().title"
1199 );
1200 }
1201
1202 #[test]
1203 fn test_accessor_csharp_with_optionals() {
1204 let r = make_resolver_with_doc_optional();
1205 assert_eq!(
1206 r.accessor("title", "csharp", "result"),
1207 "result.Metadata.Document!.Title"
1208 );
1209 }
1210
1211 #[test]
1212 fn test_accessor_rust_non_optional_field() {
1213 let r = make_resolver();
1214 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1215 }
1216
1217 #[test]
1218 fn test_accessor_csharp_non_optional_field() {
1219 let r = make_resolver();
1220 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
1221 }
1222
1223 #[test]
1224 fn test_accessor_rust_method_call() {
1225 let mut fields = HashMap::new();
1227 fields.insert(
1228 "excel_sheet_count".to_string(),
1229 "metadata.format.excel.sheet_count".to_string(),
1230 );
1231 let mut optional = HashSet::new();
1232 optional.insert("metadata.format".to_string());
1233 optional.insert("metadata.format.excel".to_string());
1234 let mut method_calls = HashSet::new();
1235 method_calls.insert("metadata.format.excel".to_string());
1236 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
1237 assert_eq!(
1238 r.accessor("excel_sheet_count", "rust", "result"),
1239 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
1240 );
1241 }
1242}