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 out.push_str(&format!("[\"{key}\"]"));
495 }
496 PathSegment::Length => {
497 out.push_str(".length");
498 }
499 }
500 }
501 out
502}
503
504fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
505 let mut out = result_var.to_string();
506 for seg in segments {
507 match seg {
508 PathSegment::Field(f) => {
509 out.push('.');
510 out.push_str(&f.to_lower_camel_case());
511 }
512 PathSegment::ArrayField(f) => {
513 out.push('.');
514 out.push_str(&f.to_lower_camel_case());
515 out.push_str("[0]");
516 }
517 PathSegment::MapAccess { field, key } => {
518 out.push('.');
519 out.push_str(&field.to_lower_camel_case());
520 out.push_str(&format!(".get(\"{key}\")"));
521 }
522 PathSegment::Length => {
523 out.push_str(".length");
524 }
525 }
526 }
527 out
528}
529
530fn render_go(segments: &[PathSegment], result_var: &str) -> String {
531 let mut out = result_var.to_string();
532 for seg in segments {
533 match seg {
534 PathSegment::Field(f) => {
535 out.push('.');
536 out.push_str(&to_go_name(f));
537 }
538 PathSegment::ArrayField(f) => {
539 out.push('.');
540 out.push_str(&to_go_name(f));
541 out.push_str("[0]");
542 }
543 PathSegment::MapAccess { field, key } => {
544 out.push('.');
545 out.push_str(&to_go_name(field));
546 if key.chars().all(|c| c.is_ascii_digit()) {
547 out.push_str(&format!("[{key}]"));
548 } else {
549 out.push_str(&format!("[\"{key}\"]"));
550 }
551 }
552 PathSegment::Length => {
553 let current = std::mem::take(&mut out);
554 out = format!("len({current})");
555 }
556 }
557 }
558 out
559}
560
561fn render_java(segments: &[PathSegment], result_var: &str) -> String {
562 let mut out = result_var.to_string();
563 for seg in segments {
564 match seg {
565 PathSegment::Field(f) => {
566 out.push('.');
567 out.push_str(&f.to_lower_camel_case());
568 out.push_str("()");
569 }
570 PathSegment::ArrayField(f) => {
571 out.push('.');
572 out.push_str(&f.to_lower_camel_case());
573 out.push_str("().getFirst()");
574 }
575 PathSegment::MapAccess { field, key } => {
576 out.push('.');
577 out.push_str(&field.to_lower_camel_case());
578 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
580 if is_numeric {
581 out.push_str(&format!("().get({key})"));
582 } else {
583 out.push_str(&format!("().get(\"{key}\")"));
584 }
585 }
586 PathSegment::Length => {
587 out.push_str(".size()");
588 }
589 }
590 }
591 out
592}
593
594fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
595 let mut out = result_var.to_string();
596 let mut path_so_far = String::new();
597 for (i, seg) in segments.iter().enumerate() {
598 let is_leaf = i == segments.len() - 1;
599 match seg {
600 PathSegment::Field(f) => {
601 if !path_so_far.is_empty() {
602 path_so_far.push('.');
603 }
604 path_so_far.push_str(f);
605 out.push('.');
606 out.push_str(&f.to_lower_camel_case());
607 out.push_str("()");
608 let _ = is_leaf;
609 let _ = optional_fields;
610 }
611 PathSegment::ArrayField(f) => {
612 if !path_so_far.is_empty() {
613 path_so_far.push('.');
614 }
615 path_so_far.push_str(f);
616 out.push('.');
617 out.push_str(&f.to_lower_camel_case());
618 out.push_str("().getFirst()");
619 }
620 PathSegment::MapAccess { field, key } => {
621 if !path_so_far.is_empty() {
622 path_so_far.push('.');
623 }
624 path_so_far.push_str(field);
625 out.push('.');
626 out.push_str(&field.to_lower_camel_case());
627 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
629 if is_numeric {
630 out.push_str(&format!("().get({key})"));
631 } else {
632 out.push_str(&format!("().get(\"{key}\")"));
633 }
634 }
635 PathSegment::Length => {
636 out.push_str(".size()");
637 }
638 }
639 }
640 out
641}
642
643fn render_rust_with_optionals(
649 segments: &[PathSegment],
650 result_var: &str,
651 optional_fields: &HashSet<String>,
652 method_calls: &HashSet<String>,
653) -> String {
654 let mut out = result_var.to_string();
655 let mut path_so_far = String::new();
656 for (i, seg) in segments.iter().enumerate() {
657 let is_leaf = i == segments.len() - 1;
658 match seg {
659 PathSegment::Field(f) => {
660 if !path_so_far.is_empty() {
661 path_so_far.push('.');
662 }
663 path_so_far.push_str(f);
664 out.push('.');
665 out.push_str(&f.to_snake_case());
666 let is_method = method_calls.contains(&path_so_far);
667 if is_method {
668 out.push_str("()");
669 if !is_leaf && optional_fields.contains(&path_so_far) {
670 out.push_str(".as_ref().unwrap()");
671 }
672 } else if !is_leaf && optional_fields.contains(&path_so_far) {
673 out.push_str(".as_ref().unwrap()");
674 }
675 }
676 PathSegment::ArrayField(f) => {
677 if !path_so_far.is_empty() {
678 path_so_far.push('.');
679 }
680 path_so_far.push_str(f);
681 out.push('.');
682 out.push_str(&f.to_snake_case());
683 out.push_str("[0]");
684 }
685 PathSegment::MapAccess { field, key } => {
686 if !path_so_far.is_empty() {
687 path_so_far.push('.');
688 }
689 path_so_far.push_str(field);
690 out.push('.');
691 out.push_str(&field.to_snake_case());
692 if key.chars().all(|c| c.is_ascii_digit()) {
693 let is_opt = optional_fields.contains(&path_so_far);
694 if is_opt {
695 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
696 } else {
697 out.push_str(&format!("[{key}]"));
698 }
699 path_so_far.push_str("[0]");
700 } else {
701 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
702 }
703 }
704 PathSegment::Length => {
705 out.push_str(".len()");
706 }
707 }
708 }
709 out
710}
711
712fn render_zig_with_optionals(
725 segments: &[PathSegment],
726 result_var: &str,
727 optional_fields: &HashSet<String>,
728 method_calls: &HashSet<String>,
729) -> String {
730 let mut out = result_var.to_string();
731 let mut path_so_far = String::new();
732 for seg in segments {
733 match seg {
734 PathSegment::Field(f) => {
735 if !path_so_far.is_empty() {
736 path_so_far.push('.');
737 }
738 path_so_far.push_str(f);
739 out.push('.');
740 out.push_str(f);
741 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
742 out.push_str(".?");
743 }
744 }
745 PathSegment::ArrayField(f) => {
746 if !path_so_far.is_empty() {
747 path_so_far.push('.');
748 }
749 path_so_far.push_str(f);
750 out.push('.');
751 out.push_str(f);
752 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
753 out.push_str(".?");
754 }
755 out.push_str("[0]");
756 }
757 PathSegment::MapAccess { field, key } => {
758 if !path_so_far.is_empty() {
759 path_so_far.push('.');
760 }
761 path_so_far.push_str(field);
762 out.push('.');
763 out.push_str(field);
764 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
765 out.push_str(".?");
766 }
767 if key.chars().all(|c| c.is_ascii_digit()) {
768 out.push_str(&format!("[{key}]"));
769 } else {
770 out.push_str(&format!(".get(\"{key}\")"));
771 }
772 }
773 PathSegment::Length => {
774 out.push_str(".len");
775 }
776 }
777 }
778 out
779}
780
781fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
782 let mut out = result_var.to_string();
783 for seg in segments {
784 match seg {
785 PathSegment::Field(f) => {
786 out.push('.');
787 out.push_str(&f.to_pascal_case());
788 }
789 PathSegment::ArrayField(f) => {
790 out.push('.');
791 out.push_str(&f.to_pascal_case());
792 out.push_str("[0]");
793 }
794 PathSegment::MapAccess { field, key } => {
795 out.push('.');
796 out.push_str(&field.to_pascal_case());
797 if key.chars().all(|c| c.is_ascii_digit()) {
798 out.push_str(&format!("[{key}]"));
799 } else {
800 out.push_str(&format!("[\"{key}\"]"));
801 }
802 }
803 PathSegment::Length => {
804 out.push_str(".Count");
805 }
806 }
807 }
808 out
809}
810
811fn render_csharp_with_optionals(
812 segments: &[PathSegment],
813 result_var: &str,
814 optional_fields: &HashSet<String>,
815) -> String {
816 let mut out = result_var.to_string();
817 let mut path_so_far = String::new();
818 for (i, seg) in segments.iter().enumerate() {
819 let is_leaf = i == segments.len() - 1;
820 match seg {
821 PathSegment::Field(f) => {
822 if !path_so_far.is_empty() {
823 path_so_far.push('.');
824 }
825 path_so_far.push_str(f);
826 out.push('.');
827 out.push_str(&f.to_pascal_case());
828 if !is_leaf && optional_fields.contains(&path_so_far) {
829 out.push('!');
830 }
831 }
832 PathSegment::ArrayField(f) => {
833 if !path_so_far.is_empty() {
834 path_so_far.push('.');
835 }
836 path_so_far.push_str(f);
837 out.push('.');
838 out.push_str(&f.to_pascal_case());
839 out.push_str("[0]");
840 }
841 PathSegment::MapAccess { field, key } => {
842 if !path_so_far.is_empty() {
843 path_so_far.push('.');
844 }
845 path_so_far.push_str(field);
846 out.push('.');
847 out.push_str(&field.to_pascal_case());
848 if key.chars().all(|c| c.is_ascii_digit()) {
849 out.push_str(&format!("[{key}]"));
850 } else {
851 out.push_str(&format!("[\"{key}\"]"));
852 }
853 }
854 PathSegment::Length => {
855 out.push_str(".Count");
856 }
857 }
858 }
859 out
860}
861
862fn render_php(segments: &[PathSegment], result_var: &str) -> String {
863 let mut out = result_var.to_string();
864 for seg in segments {
865 match seg {
866 PathSegment::Field(f) => {
867 out.push_str("->");
868 out.push_str(&f.to_lower_camel_case());
871 }
872 PathSegment::ArrayField(f) => {
873 out.push_str("->");
874 out.push_str(&f.to_lower_camel_case());
875 out.push_str("[0]");
876 }
877 PathSegment::MapAccess { field, key } => {
878 out.push_str("->");
879 out.push_str(&field.to_lower_camel_case());
880 out.push_str(&format!("[\"{key}\"]"));
881 }
882 PathSegment::Length => {
883 let current = std::mem::take(&mut out);
884 out = format!("count({current})");
885 }
886 }
887 }
888 out
889}
890
891fn render_r(segments: &[PathSegment], result_var: &str) -> String {
892 let mut out = result_var.to_string();
893 for seg in segments {
894 match seg {
895 PathSegment::Field(f) => {
896 out.push('$');
897 out.push_str(f);
898 }
899 PathSegment::ArrayField(f) => {
900 out.push('$');
901 out.push_str(f);
902 out.push_str("[[1]]");
903 }
904 PathSegment::MapAccess { field, key } => {
905 out.push('$');
906 out.push_str(field);
907 out.push_str(&format!("[[\"{key}\"]]"));
908 }
909 PathSegment::Length => {
910 let current = std::mem::take(&mut out);
911 out = format!("length({current})");
912 }
913 }
914 }
915 out
916}
917
918fn render_c(segments: &[PathSegment], result_var: &str) -> String {
919 let mut parts = Vec::new();
920 let mut trailing_length = false;
921 for seg in segments {
922 match seg {
923 PathSegment::Field(f) | PathSegment::ArrayField(f) => parts.push(f.to_snake_case()),
924 PathSegment::MapAccess { field, key } => {
925 parts.push(field.to_snake_case());
926 parts.push(key.clone());
927 }
928 PathSegment::Length => {
929 trailing_length = true;
930 }
931 }
932 }
933 let suffix = parts.join("_");
934 if trailing_length {
935 format!("result_{suffix}_count({result_var})")
936 } else {
937 format!("result_{suffix}({result_var})")
938 }
939}
940
941#[cfg(test)]
942mod tests {
943 use super::*;
944
945 fn make_resolver() -> FieldResolver {
946 let mut fields = HashMap::new();
947 fields.insert("title".to_string(), "metadata.document.title".to_string());
948 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
949 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
950 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
951 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
952 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
953 let mut optional = HashSet::new();
954 optional.insert("metadata.document.title".to_string());
955 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
956 }
957
958 fn make_resolver_with_doc_optional() -> FieldResolver {
959 let mut fields = HashMap::new();
960 fields.insert("title".to_string(), "metadata.document.title".to_string());
961 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
962 let mut optional = HashSet::new();
963 optional.insert("document".to_string());
964 optional.insert("metadata.document.title".to_string());
965 optional.insert("metadata.document".to_string());
966 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
967 }
968
969 #[test]
970 fn test_resolve_alias() {
971 let r = make_resolver();
972 assert_eq!(r.resolve("title"), "metadata.document.title");
973 }
974
975 #[test]
976 fn test_resolve_passthrough() {
977 let r = make_resolver();
978 assert_eq!(r.resolve("content"), "content");
979 }
980
981 #[test]
982 fn test_is_optional() {
983 let r = make_resolver();
984 assert!(r.is_optional("metadata.document.title"));
985 assert!(!r.is_optional("content"));
986 }
987
988 #[test]
989 fn test_accessor_rust_struct() {
990 let r = make_resolver();
991 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
992 }
993
994 #[test]
995 fn test_accessor_rust_map() {
996 let r = make_resolver();
997 assert_eq!(
998 r.accessor("tags", "rust", "result"),
999 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
1000 );
1001 }
1002
1003 #[test]
1004 fn test_accessor_python() {
1005 let r = make_resolver();
1006 assert_eq!(
1007 r.accessor("title", "python", "result"),
1008 "result.metadata.document.title"
1009 );
1010 }
1011
1012 #[test]
1013 fn test_accessor_go() {
1014 let r = make_resolver();
1015 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
1016 }
1017
1018 #[test]
1019 fn test_accessor_go_initialism_fields() {
1020 let mut fields = std::collections::HashMap::new();
1021 fields.insert("content".to_string(), "html".to_string());
1022 fields.insert("link_url".to_string(), "links.url".to_string());
1023 let r = FieldResolver::new(
1024 &fields,
1025 &HashSet::new(),
1026 &HashSet::new(),
1027 &HashSet::new(),
1028 &HashSet::new(),
1029 );
1030 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
1031 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
1032 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
1033 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
1034 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
1035 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
1036 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
1037 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
1038 }
1039
1040 #[test]
1041 fn test_accessor_typescript() {
1042 let r = make_resolver();
1043 assert_eq!(
1044 r.accessor("title", "typescript", "result"),
1045 "result.metadata.document.title"
1046 );
1047 }
1048
1049 #[test]
1050 fn test_accessor_typescript_snake_to_camel() {
1051 let r = make_resolver();
1052 assert_eq!(
1053 r.accessor("og", "typescript", "result"),
1054 "result.metadata.document.openGraph"
1055 );
1056 assert_eq!(
1057 r.accessor("twitter", "typescript", "result"),
1058 "result.metadata.document.twitterCard"
1059 );
1060 assert_eq!(
1061 r.accessor("canonical", "typescript", "result"),
1062 "result.metadata.document.canonicalUrl"
1063 );
1064 }
1065
1066 #[test]
1067 fn test_accessor_typescript_map_snake_to_camel() {
1068 let r = make_resolver();
1069 assert_eq!(
1070 r.accessor("og_tag", "typescript", "result"),
1071 "result.metadata.openGraphTags[\"og_title\"]"
1072 );
1073 }
1074
1075 #[test]
1076 fn test_accessor_node_alias() {
1077 let r = make_resolver();
1078 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
1079 }
1080
1081 #[test]
1082 fn test_accessor_wasm_camel_case() {
1083 let r = make_resolver();
1084 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
1085 assert_eq!(
1086 r.accessor("twitter", "wasm", "result"),
1087 "result.metadata.document.twitterCard"
1088 );
1089 assert_eq!(
1090 r.accessor("canonical", "wasm", "result"),
1091 "result.metadata.document.canonicalUrl"
1092 );
1093 }
1094
1095 #[test]
1096 fn test_accessor_wasm_map_access() {
1097 let r = make_resolver();
1098 assert_eq!(
1099 r.accessor("og_tag", "wasm", "result"),
1100 "result.metadata.openGraphTags.get(\"og_title\")"
1101 );
1102 }
1103
1104 #[test]
1105 fn test_accessor_java() {
1106 let r = make_resolver();
1107 assert_eq!(
1108 r.accessor("title", "java", "result"),
1109 "result.metadata().document().title()"
1110 );
1111 }
1112
1113 #[test]
1114 fn test_accessor_csharp() {
1115 let r = make_resolver();
1116 assert_eq!(
1117 r.accessor("title", "csharp", "result"),
1118 "result.Metadata.Document.Title"
1119 );
1120 }
1121
1122 #[test]
1123 fn test_accessor_php() {
1124 let r = make_resolver();
1125 assert_eq!(
1126 r.accessor("title", "php", "$result"),
1127 "$result->metadata->document->title"
1128 );
1129 }
1130
1131 #[test]
1132 fn test_accessor_r() {
1133 let r = make_resolver();
1134 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
1135 }
1136
1137 #[test]
1138 fn test_accessor_c() {
1139 let r = make_resolver();
1140 assert_eq!(
1141 r.accessor("title", "c", "result"),
1142 "result_metadata_document_title(result)"
1143 );
1144 }
1145
1146 #[test]
1147 fn test_rust_unwrap_binding() {
1148 let r = make_resolver();
1149 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
1150 assert_eq!(var, "metadata_document_title");
1151 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
1152 }
1153
1154 #[test]
1155 fn test_rust_unwrap_binding_non_optional() {
1156 let r = make_resolver();
1157 assert!(r.rust_unwrap_binding("content", "result").is_none());
1158 }
1159
1160 #[test]
1161 fn test_direct_field_no_alias() {
1162 let r = make_resolver();
1163 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1164 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
1165 }
1166
1167 #[test]
1168 fn test_accessor_rust_with_optionals() {
1169 let r = make_resolver_with_doc_optional();
1170 assert_eq!(
1171 r.accessor("title", "rust", "result"),
1172 "result.metadata.document.as_ref().unwrap().title"
1173 );
1174 }
1175
1176 #[test]
1177 fn test_accessor_csharp_with_optionals() {
1178 let r = make_resolver_with_doc_optional();
1179 assert_eq!(
1180 r.accessor("title", "csharp", "result"),
1181 "result.Metadata.Document!.Title"
1182 );
1183 }
1184
1185 #[test]
1186 fn test_accessor_rust_non_optional_field() {
1187 let r = make_resolver();
1188 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1189 }
1190
1191 #[test]
1192 fn test_accessor_csharp_non_optional_field() {
1193 let r = make_resolver();
1194 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
1195 }
1196
1197 #[test]
1198 fn test_accessor_rust_method_call() {
1199 let mut fields = HashMap::new();
1201 fields.insert(
1202 "excel_sheet_count".to_string(),
1203 "metadata.format.excel.sheet_count".to_string(),
1204 );
1205 let mut optional = HashSet::new();
1206 optional.insert("metadata.format".to_string());
1207 optional.insert("metadata.format.excel".to_string());
1208 let mut method_calls = HashSet::new();
1209 method_calls.insert("metadata.format.excel".to_string());
1210 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
1211 assert_eq!(
1212 r.accessor("excel_sheet_count", "rust", "result"),
1213 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
1214 );
1215 }
1216}