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 error_field_aliases: HashMap<String, String>,
22}
23
24#[derive(Debug, Clone)]
26enum PathSegment {
27 Field(String),
29 ArrayField { name: String, index: usize },
34 MapAccess { field: String, key: String },
36 Length,
38}
39
40impl FieldResolver {
41 pub fn new(
45 fields: &HashMap<String, String>,
46 optional: &HashSet<String>,
47 result_fields: &HashSet<String>,
48 array_fields: &HashSet<String>,
49 method_calls: &HashSet<String>,
50 ) -> Self {
51 Self {
52 aliases: fields.clone(),
53 optional_fields: optional.clone(),
54 result_fields: result_fields.clone(),
55 array_fields: array_fields.clone(),
56 method_calls: method_calls.clone(),
57 error_field_aliases: HashMap::new(),
58 }
59 }
60
61 pub fn new_with_error_aliases(
67 fields: &HashMap<String, String>,
68 optional: &HashSet<String>,
69 result_fields: &HashSet<String>,
70 array_fields: &HashSet<String>,
71 method_calls: &HashSet<String>,
72 error_field_aliases: &HashMap<String, String>,
73 ) -> Self {
74 Self {
75 aliases: fields.clone(),
76 optional_fields: optional.clone(),
77 result_fields: result_fields.clone(),
78 array_fields: array_fields.clone(),
79 method_calls: method_calls.clone(),
80 error_field_aliases: error_field_aliases.clone(),
81 }
82 }
83
84 pub fn resolve<'a>(&'a self, fixture_field: &'a str) -> &'a str {
87 self.aliases
88 .get(fixture_field)
89 .map(String::as_str)
90 .unwrap_or(fixture_field)
91 }
92
93 pub fn is_optional(&self, field: &str) -> bool {
95 if self.optional_fields.contains(field) {
96 return true;
97 }
98 let index_normalized = normalize_numeric_indices(field);
99 if index_normalized != field && self.optional_fields.contains(index_normalized.as_str()) {
100 return true;
101 }
102 let de_indexed = strip_numeric_indices(field);
105 if de_indexed != field && self.optional_fields.contains(de_indexed.as_str()) {
106 return true;
107 }
108 let normalized = field.replace("[].", ".");
109 if normalized != field && self.optional_fields.contains(normalized.as_str()) {
110 return true;
111 }
112 for af in &self.array_fields {
113 if let Some(rest) = field.strip_prefix(af.as_str()) {
114 if let Some(rest) = rest.strip_prefix('.') {
115 let with_bracket = format!("{af}[].{rest}");
116 if self.optional_fields.contains(with_bracket.as_str()) {
117 return true;
118 }
119 }
120 }
121 }
122 false
123 }
124
125 pub fn has_alias(&self, fixture_field: &str) -> bool {
127 self.aliases.contains_key(fixture_field)
128 }
129
130 pub fn has_explicit_field(&self, field_name: &str) -> bool {
136 if self.result_fields.is_empty() {
137 return false;
138 }
139 self.result_fields.contains(field_name)
140 }
141
142 pub fn is_valid_for_result(&self, fixture_field: &str) -> bool {
144 if self.result_fields.is_empty() {
145 return true;
146 }
147 let resolved = self.resolve(fixture_field);
148 let first_segment = resolved.split('.').next().unwrap_or(resolved);
149 let first_segment = first_segment.split('[').next().unwrap_or(first_segment);
150 self.result_fields.contains(first_segment)
151 }
152
153 pub fn is_array(&self, field: &str) -> bool {
155 self.array_fields.contains(field)
156 }
157
158 pub fn is_collection_root(&self, field: &str) -> bool {
171 let prefix = format!("{field}[");
172 self.array_fields.iter().any(|af| af.starts_with(&prefix))
173 || self.optional_fields.iter().any(|of| of.starts_with(&prefix))
174 }
175
176 pub fn tagged_union_split(&self, fixture_field: &str) -> Option<(String, String, String)> {
188 let resolved = self.resolve(fixture_field);
189 let segments: Vec<&str> = resolved.split('.').collect();
190 let mut path_so_far = String::new();
191 for (i, seg) in segments.iter().enumerate() {
192 if !path_so_far.is_empty() {
193 path_so_far.push('.');
194 }
195 path_so_far.push_str(seg);
196 if self.method_calls.contains(&path_so_far) {
197 let prefix = segments[..i].join(".");
199 let variant = (*seg).to_string();
200 let suffix = segments[i + 1..].join(".");
201 return Some((prefix, variant, suffix));
202 }
203 }
204 None
205 }
206
207 pub fn has_map_access(&self, fixture_field: &str) -> bool {
209 let resolved = self.resolve(fixture_field);
210 let segments = parse_path(resolved);
211 segments.iter().any(|s| {
212 if let PathSegment::MapAccess { key, .. } = s {
213 !key.chars().all(|c| c.is_ascii_digit())
214 } else {
215 false
216 }
217 })
218 }
219
220 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
222 let resolved = self.resolve(fixture_field);
223 let segments = parse_path(resolved);
224 let segments = self.inject_array_indexing(segments);
225 match language {
226 "java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
227 "kotlin" => render_kotlin_with_optionals(&segments, result_var, &self.optional_fields),
228 "kotlin_android" => render_kotlin_android_with_optionals(&segments, result_var, &self.optional_fields),
231 "rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
232 "csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
233 "zig" => render_zig_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
234 "swift" => render_swift_with_optionals(&segments, result_var, &self.optional_fields),
235 "dart" => render_dart_with_optionals(&segments, result_var, &self.optional_fields),
236 _ => render_accessor(&segments, language, result_var),
237 }
238 }
239
240 pub fn accessor_for_error(&self, sub_field: &str, language: &str, err_var: &str) -> String {
254 let resolved = self
255 .error_field_aliases
256 .get(sub_field)
257 .map(String::as_str)
258 .unwrap_or(sub_field);
259 let segments = parse_path(resolved);
260 match language {
263 "rust" => render_rust_with_optionals(&segments, err_var, &self.optional_fields, &self.method_calls),
264 _ => render_accessor(&segments, language, err_var),
265 }
266 }
267
268 pub fn has_error_aliases(&self) -> bool {
275 !self.error_field_aliases.is_empty()
276 }
277
278 fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
279 if self.array_fields.is_empty() {
280 return segments;
281 }
282 let len = segments.len();
283 let mut result = Vec::with_capacity(len);
284 let mut path_so_far = String::new();
285 for i in 0..len {
286 let seg = &segments[i];
287 match seg {
288 PathSegment::Field(f) => {
289 if !path_so_far.is_empty() {
290 path_so_far.push('.');
291 }
292 path_so_far.push_str(f);
293 let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
294 if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
295 result.push(PathSegment::ArrayField {
297 name: f.clone(),
298 index: 0,
299 });
300 } else {
301 result.push(seg.clone());
302 }
303 }
304 PathSegment::ArrayField { .. } => {
307 result.push(seg.clone());
308 }
309 PathSegment::MapAccess { field, key } => {
310 if !path_so_far.is_empty() {
311 path_so_far.push('.');
312 }
313 path_so_far.push_str(field);
314 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
315 if is_numeric && self.array_fields.contains(&path_so_far) {
316 let index: usize = key.parse().unwrap_or(0);
318 result.push(PathSegment::ArrayField {
319 name: field.clone(),
320 index,
321 });
322 } else {
323 result.push(seg.clone());
324 }
325 }
326 _ => {
327 result.push(seg.clone());
328 }
329 }
330 }
331 result
332 }
333
334 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
336 let resolved = self.resolve(fixture_field);
337 if !self.is_optional(resolved) {
338 return None;
339 }
340 let segments = parse_path(resolved);
341 let segments = self.inject_array_indexing(segments);
342 let local_var = {
347 let raw = resolved.replace(['.', '['], "_").replace(']', "");
348 let mut collapsed = String::with_capacity(raw.len());
349 let mut prev_underscore = false;
350 for ch in raw.chars() {
351 if ch == '_' {
352 if !prev_underscore {
353 collapsed.push('_');
354 }
355 prev_underscore = true;
356 } else {
357 collapsed.push(ch);
358 prev_underscore = false;
359 }
360 }
361 collapsed.trim_matches('_').to_string()
362 };
363 let accessor = render_accessor(&segments, "rust", result_var);
364 let has_map_access = segments.iter().any(|s| {
365 if let PathSegment::MapAccess { key, .. } = s {
366 !key.chars().all(|c| c.is_ascii_digit())
367 } else {
368 false
369 }
370 });
371 let is_array = self.is_array(resolved);
372 let binding = if has_map_access {
373 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
374 } else if is_array {
375 format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
376 } else {
377 format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
378 };
379 Some((binding, local_var))
380 }
381}
382
383fn strip_numeric_indices(path: &str) -> String {
388 let mut result = String::with_capacity(path.len());
389 let mut chars = path.chars().peekable();
390 while let Some(c) = chars.next() {
391 if c == '[' {
392 let mut key = String::new();
393 let mut closed = false;
394 for inner in chars.by_ref() {
395 if inner == ']' {
396 closed = true;
397 break;
398 }
399 key.push(inner);
400 }
401 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
402 } else {
404 result.push('[');
405 result.push_str(&key);
406 if closed {
407 result.push(']');
408 }
409 }
410 } else {
411 result.push(c);
412 }
413 }
414 while result.contains("..") {
416 result = result.replace("..", ".");
417 }
418 if result.starts_with('.') {
419 result.remove(0);
420 }
421 result
422}
423
424fn normalize_numeric_indices(path: &str) -> String {
425 let mut result = String::with_capacity(path.len());
426 let mut chars = path.chars().peekable();
427 while let Some(c) = chars.next() {
428 if c == '[' {
429 let mut key = String::new();
430 let mut closed = false;
431 for inner in chars.by_ref() {
432 if inner == ']' {
433 closed = true;
434 break;
435 }
436 key.push(inner);
437 }
438 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
439 result.push_str("[0]");
440 } else {
441 result.push('[');
442 result.push_str(&key);
443 if closed {
444 result.push(']');
445 }
446 }
447 } else {
448 result.push(c);
449 }
450 }
451 result
452}
453
454fn parse_path(path: &str) -> Vec<PathSegment> {
455 let mut segments = Vec::new();
456 for part in path.split('.') {
457 if part == "length" || part == "count" || part == "size" {
458 segments.push(PathSegment::Length);
459 } else if let Some(bracket_pos) = part.find('[') {
460 let name = part[..bracket_pos].to_string();
461 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
462 if key.is_empty() {
463 segments.push(PathSegment::ArrayField { name, index: 0 });
465 } else if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
466 let index: usize = key.parse().unwrap_or(0);
468 segments.push(PathSegment::ArrayField { name, index });
469 } else {
470 segments.push(PathSegment::MapAccess { field: name, key });
472 }
473 } else {
474 segments.push(PathSegment::Field(part.to_string()));
475 }
476 }
477 segments
478}
479
480fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
481 match language {
482 "rust" => render_rust(segments, result_var),
483 "python" => render_dot_access(segments, result_var, "python"),
484 "typescript" | "node" => render_typescript(segments, result_var),
485 "wasm" => render_wasm(segments, result_var),
486 "go" => render_go(segments, result_var),
487 "java" => render_java(segments, result_var),
488 "kotlin" => render_kotlin(segments, result_var),
489 "kotlin_android" => render_kotlin_android(segments, result_var),
490 "csharp" => render_pascal_dot(segments, result_var),
491 "ruby" => render_dot_access(segments, result_var, "ruby"),
492 "php" => render_php(segments, result_var),
493 "elixir" => render_dot_access(segments, result_var, "elixir"),
494 "r" => render_r(segments, result_var),
495 "c" => render_c(segments, result_var),
496 "swift" => render_swift(segments, result_var),
497 "dart" => render_dart(segments, result_var),
498 _ => render_dot_access(segments, result_var, language),
499 }
500}
501
502fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
508 let mut out = result_var.to_string();
509 for seg in segments {
510 match seg {
511 PathSegment::Field(f) => {
512 out.push('.');
513 out.push_str(f);
514 out.push_str("()");
515 }
516 PathSegment::ArrayField { name, index } => {
517 out.push('.');
518 out.push_str(name);
519 out.push_str(&format!("()[{index}]"));
520 }
521 PathSegment::MapAccess { field, key } => {
522 out.push('.');
523 out.push_str(field);
524 if key.chars().all(|c| c.is_ascii_digit()) {
525 out.push_str(&format!("()[{key}]"));
526 } else {
527 out.push_str(&format!("()[\"{key}\"]"));
528 }
529 }
530 PathSegment::Length => {
531 out.push_str(".count");
532 }
533 }
534 }
535 out
536}
537
538fn render_swift_with_optionals(
548 segments: &[PathSegment],
549 result_var: &str,
550 optional_fields: &HashSet<String>,
551) -> String {
552 let mut out = result_var.to_string();
553 let mut path_so_far = String::new();
554 let total = segments.len();
555 for (i, seg) in segments.iter().enumerate() {
556 let is_leaf = i == total - 1;
557 match seg {
558 PathSegment::Field(f) => {
559 if !path_so_far.is_empty() {
560 path_so_far.push('.');
561 }
562 path_so_far.push_str(f);
563 out.push('.');
564 out.push_str(f);
565 out.push_str("()");
566 if !is_leaf && optional_fields.contains(&path_so_far) {
569 out.push('?');
570 }
571 }
572 PathSegment::ArrayField { name, index } => {
573 if !path_so_far.is_empty() {
574 path_so_far.push('.');
575 }
576 path_so_far.push_str(name);
577 let is_optional = optional_fields.contains(&path_so_far);
581 out.push('.');
582 out.push_str(name);
583 if is_optional {
584 out.push_str(&format!("()?[{index}]"));
587 } else {
588 out.push_str(&format!("()[{index}]"));
589 }
590 path_so_far.push_str("[0]");
594 let _ = is_leaf;
601 }
602 PathSegment::MapAccess { field, key } => {
603 if !path_so_far.is_empty() {
604 path_so_far.push('.');
605 }
606 path_so_far.push_str(field);
607 out.push('.');
608 out.push_str(field);
609 if key.chars().all(|c| c.is_ascii_digit()) {
610 out.push_str(&format!("()[{key}]"));
611 } else {
612 out.push_str(&format!("()[\"{key}\"]"));
613 }
614 }
615 PathSegment::Length => {
616 out.push_str(".count");
617 }
618 }
619 }
620 out
621}
622
623fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
624 let mut out = result_var.to_string();
625 for seg in segments {
626 match seg {
627 PathSegment::Field(f) => {
628 out.push('.');
629 out.push_str(&f.to_snake_case());
630 }
631 PathSegment::ArrayField { name, index } => {
632 out.push('.');
633 out.push_str(&name.to_snake_case());
634 out.push_str(&format!("[{index}]"));
635 }
636 PathSegment::MapAccess { field, key } => {
637 out.push('.');
638 out.push_str(&field.to_snake_case());
639 if key.chars().all(|c| c.is_ascii_digit()) {
640 out.push_str(&format!("[{key}]"));
641 } else {
642 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
643 }
644 }
645 PathSegment::Length => {
646 out.push_str(".len()");
647 }
648 }
649 }
650 out
651}
652
653fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
654 let mut out = result_var.to_string();
655 for seg in segments {
656 match seg {
657 PathSegment::Field(f) => {
658 out.push('.');
659 out.push_str(f);
660 }
661 PathSegment::ArrayField { name, index } => {
662 if language == "elixir" {
663 let current = std::mem::take(&mut out);
664 out = format!("Enum.at({current}.{name}, {index})");
665 } else {
666 out.push('.');
667 out.push_str(name);
668 out.push_str(&format!("[{index}]"));
669 }
670 }
671 PathSegment::MapAccess { field, key } => {
672 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
673 if is_numeric && language == "elixir" {
674 let current = std::mem::take(&mut out);
675 out = format!("Enum.at({current}.{field}, {key})");
676 } else {
677 out.push('.');
678 out.push_str(field);
679 if is_numeric {
680 let idx: usize = key.parse().unwrap_or(0);
681 out.push_str(&format!("[{idx}]"));
682 } else if language == "elixir" || language == "ruby" {
683 out.push_str(&format!("[\"{key}\"]"));
686 } else {
687 out.push_str(&format!(".get(\"{key}\")"));
688 }
689 }
690 }
691 PathSegment::Length => match language {
692 "ruby" => out.push_str(".length"),
693 "elixir" => {
694 let current = std::mem::take(&mut out);
695 out = format!("length({current})");
696 }
697 "gleam" => {
698 let current = std::mem::take(&mut out);
699 out = format!("list.length({current})");
700 }
701 _ => {
702 let current = std::mem::take(&mut out);
703 out = format!("len({current})");
704 }
705 },
706 }
707 }
708 out
709}
710
711fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
712 let mut out = result_var.to_string();
713 for seg in segments {
714 match seg {
715 PathSegment::Field(f) => {
716 out.push('.');
717 out.push_str(&f.to_lower_camel_case());
718 }
719 PathSegment::ArrayField { name, index } => {
720 out.push('.');
721 out.push_str(&name.to_lower_camel_case());
722 out.push_str(&format!("[{index}]"));
723 }
724 PathSegment::MapAccess { field, key } => {
725 out.push('.');
726 out.push_str(&field.to_lower_camel_case());
727 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
730 out.push_str(&format!("[{key}]"));
731 } else {
732 out.push_str(&format!("[\"{key}\"]"));
733 }
734 }
735 PathSegment::Length => {
736 out.push_str(".length");
737 }
738 }
739 }
740 out
741}
742
743fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
744 let mut out = result_var.to_string();
745 for seg in segments {
746 match seg {
747 PathSegment::Field(f) => {
748 out.push('.');
749 out.push_str(&f.to_lower_camel_case());
750 }
751 PathSegment::ArrayField { name, index } => {
752 out.push('.');
753 out.push_str(&name.to_lower_camel_case());
754 out.push_str(&format!("[{index}]"));
755 }
756 PathSegment::MapAccess { field, key } => {
757 out.push('.');
758 out.push_str(&field.to_lower_camel_case());
759 out.push_str(&format!(".get(\"{key}\")"));
760 }
761 PathSegment::Length => {
762 out.push_str(".length");
763 }
764 }
765 }
766 out
767}
768
769fn render_go(segments: &[PathSegment], result_var: &str) -> String {
770 let mut out = result_var.to_string();
771 for seg in segments {
772 match seg {
773 PathSegment::Field(f) => {
774 out.push('.');
775 out.push_str(&to_go_name(f));
776 }
777 PathSegment::ArrayField { name, index } => {
778 out.push('.');
779 out.push_str(&to_go_name(name));
780 out.push_str(&format!("[{index}]"));
781 }
782 PathSegment::MapAccess { field, key } => {
783 out.push('.');
784 out.push_str(&to_go_name(field));
785 if key.chars().all(|c| c.is_ascii_digit()) {
786 out.push_str(&format!("[{key}]"));
787 } else {
788 out.push_str(&format!("[\"{key}\"]"));
789 }
790 }
791 PathSegment::Length => {
792 let current = std::mem::take(&mut out);
793 out = format!("len({current})");
794 }
795 }
796 }
797 out
798}
799
800fn render_java(segments: &[PathSegment], result_var: &str) -> String {
801 let mut out = result_var.to_string();
802 for seg in segments {
803 match seg {
804 PathSegment::Field(f) => {
805 out.push('.');
806 out.push_str(&f.to_lower_camel_case());
807 out.push_str("()");
808 }
809 PathSegment::ArrayField { name, index } => {
810 out.push('.');
811 out.push_str(&name.to_lower_camel_case());
812 out.push_str(&format!("().get({index})"));
813 }
814 PathSegment::MapAccess { field, key } => {
815 out.push('.');
816 out.push_str(&field.to_lower_camel_case());
817 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
819 if is_numeric {
820 out.push_str(&format!("().get({key})"));
821 } else {
822 out.push_str(&format!("().get(\"{key}\")"));
823 }
824 }
825 PathSegment::Length => {
826 out.push_str(".size()");
827 }
828 }
829 }
830 out
831}
832
833fn kotlin_getter(name: &str) -> String {
838 let camel = name.to_lower_camel_case();
839 match camel.as_str() {
840 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
841 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
842 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
843 _ => camel,
844 }
845}
846
847fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
848 let mut out = result_var.to_string();
849 for seg in segments {
850 match seg {
851 PathSegment::Field(f) => {
852 out.push('.');
853 out.push_str(&kotlin_getter(f));
854 out.push_str("()");
855 }
856 PathSegment::ArrayField { name, index } => {
857 out.push('.');
858 out.push_str(&kotlin_getter(name));
859 if *index == 0 {
860 out.push_str("().first()");
861 } else {
862 out.push_str(&format!("().get({index})"));
863 }
864 }
865 PathSegment::MapAccess { field, key } => {
866 out.push('.');
867 out.push_str(&kotlin_getter(field));
868 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
869 if is_numeric {
870 out.push_str(&format!("().get({key})"));
871 } else {
872 out.push_str(&format!("().get(\"{key}\")"));
873 }
874 }
875 PathSegment::Length => {
876 out.push_str(".size");
877 }
878 }
879 }
880 out
881}
882
883fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
884 let mut out = result_var.to_string();
885 let mut path_so_far = String::new();
886 for (i, seg) in segments.iter().enumerate() {
887 let is_leaf = i == segments.len() - 1;
888 match seg {
889 PathSegment::Field(f) => {
890 if !path_so_far.is_empty() {
891 path_so_far.push('.');
892 }
893 path_so_far.push_str(f);
894 out.push('.');
895 out.push_str(&f.to_lower_camel_case());
896 out.push_str("()");
897 let _ = is_leaf;
898 let _ = optional_fields;
899 }
900 PathSegment::ArrayField { name, index } => {
901 if !path_so_far.is_empty() {
902 path_so_far.push('.');
903 }
904 path_so_far.push_str(name);
905 out.push('.');
906 out.push_str(&name.to_lower_camel_case());
907 out.push_str(&format!("().get({index})"));
908 }
909 PathSegment::MapAccess { field, key } => {
910 if !path_so_far.is_empty() {
911 path_so_far.push('.');
912 }
913 path_so_far.push_str(field);
914 out.push('.');
915 out.push_str(&field.to_lower_camel_case());
916 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
918 if is_numeric {
919 out.push_str(&format!("().get({key})"));
920 } else {
921 out.push_str(&format!("().get(\"{key}\")"));
922 }
923 }
924 PathSegment::Length => {
925 out.push_str(".size()");
926 }
927 }
928 }
929 out
930}
931
932fn render_kotlin_with_optionals(
947 segments: &[PathSegment],
948 result_var: &str,
949 optional_fields: &HashSet<String>,
950) -> String {
951 let mut out = result_var.to_string();
952 let mut path_so_far = String::new();
953 let mut prev_was_nullable = false;
961 for seg in segments {
962 let nav = if prev_was_nullable { "?." } else { "." };
963 match seg {
964 PathSegment::Field(f) => {
965 if !path_so_far.is_empty() {
966 path_so_far.push('.');
967 }
968 path_so_far.push_str(f);
969 let is_optional = optional_fields.contains(&path_so_far);
974 out.push_str(nav);
975 out.push_str(&kotlin_getter(f));
976 out.push_str("()");
977 prev_was_nullable = prev_was_nullable || is_optional;
978 }
979 PathSegment::ArrayField { name, index } => {
980 if !path_so_far.is_empty() {
981 path_so_far.push('.');
982 }
983 path_so_far.push_str(name);
984 let is_optional = optional_fields.contains(&path_so_far);
985 out.push_str(nav);
986 out.push_str(&kotlin_getter(name));
987 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
988 if *index == 0 {
989 out.push_str(&format!("(){safe}.first()"));
990 } else {
991 out.push_str(&format!("(){safe}.get({index})"));
992 }
993 path_so_far.push_str("[0]");
997 prev_was_nullable = prev_was_nullable || is_optional;
998 }
999 PathSegment::MapAccess { field, key } => {
1000 if !path_so_far.is_empty() {
1001 path_so_far.push('.');
1002 }
1003 path_so_far.push_str(field);
1004 let is_optional = optional_fields.contains(&path_so_far);
1005 out.push_str(nav);
1006 out.push_str(&kotlin_getter(field));
1007 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1008 if is_numeric {
1009 if prev_was_nullable || is_optional {
1010 out.push_str(&format!("()?.get({key})"));
1011 } else {
1012 out.push_str(&format!("().get({key})"));
1013 }
1014 } else if prev_was_nullable || is_optional {
1015 out.push_str(&format!("()?.get(\"{key}\")"));
1016 } else {
1017 out.push_str(&format!("().get(\"{key}\")"));
1018 }
1019 prev_was_nullable = prev_was_nullable || is_optional;
1020 }
1021 PathSegment::Length => {
1022 let size_nav = if prev_was_nullable { "?" } else { "" };
1025 out.push_str(&format!("{size_nav}.size"));
1026 prev_was_nullable = false;
1027 }
1028 }
1029 }
1030 out
1031}
1032
1033fn render_kotlin_android_with_optionals(
1044 segments: &[PathSegment],
1045 result_var: &str,
1046 optional_fields: &HashSet<String>,
1047) -> String {
1048 let mut out = result_var.to_string();
1049 let mut path_so_far = String::new();
1050 let mut prev_was_nullable = false;
1051 for seg in segments {
1052 let nav = if prev_was_nullable { "?." } else { "." };
1053 match seg {
1054 PathSegment::Field(f) => {
1055 if !path_so_far.is_empty() {
1056 path_so_far.push('.');
1057 }
1058 path_so_far.push_str(f);
1059 let is_optional = optional_fields.contains(&path_so_far);
1060 out.push_str(nav);
1061 out.push_str(&kotlin_getter(f));
1063 prev_was_nullable = prev_was_nullable || is_optional;
1064 }
1065 PathSegment::ArrayField { name, index } => {
1066 if !path_so_far.is_empty() {
1067 path_so_far.push('.');
1068 }
1069 path_so_far.push_str(name);
1070 let is_optional = optional_fields.contains(&path_so_far);
1071 out.push_str(nav);
1072 out.push_str(&kotlin_getter(name));
1074 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1075 if *index == 0 {
1076 out.push_str(&format!("{safe}.first()"));
1077 } else {
1078 out.push_str(&format!("{safe}.get({index})"));
1079 }
1080 path_so_far.push_str("[0]");
1081 prev_was_nullable = prev_was_nullable || is_optional;
1082 }
1083 PathSegment::MapAccess { field, key } => {
1084 if !path_so_far.is_empty() {
1085 path_so_far.push('.');
1086 }
1087 path_so_far.push_str(field);
1088 let is_optional = optional_fields.contains(&path_so_far);
1089 out.push_str(nav);
1090 out.push_str(&kotlin_getter(field));
1092 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1093 if is_numeric {
1094 if prev_was_nullable || is_optional {
1095 out.push_str(&format!("?.get({key})"));
1096 } else {
1097 out.push_str(&format!(".get({key})"));
1098 }
1099 } else if prev_was_nullable || is_optional {
1100 out.push_str(&format!("?.get(\"{key}\")"));
1101 } else {
1102 out.push_str(&format!(".get(\"{key}\")"));
1103 }
1104 prev_was_nullable = prev_was_nullable || is_optional;
1105 }
1106 PathSegment::Length => {
1107 let size_nav = if prev_was_nullable { "?" } else { "" };
1108 out.push_str(&format!("{size_nav}.size"));
1109 prev_was_nullable = false;
1110 }
1111 }
1112 }
1113 out
1114}
1115
1116fn render_kotlin_android(segments: &[PathSegment], result_var: &str) -> String {
1120 let mut out = result_var.to_string();
1121 for seg in segments {
1122 match seg {
1123 PathSegment::Field(f) => {
1124 out.push('.');
1125 out.push_str(&kotlin_getter(f));
1126 }
1128 PathSegment::ArrayField { name, index } => {
1129 out.push('.');
1130 out.push_str(&kotlin_getter(name));
1131 if *index == 0 {
1132 out.push_str(".first()");
1133 } else {
1134 out.push_str(&format!(".get({index})"));
1135 }
1136 }
1137 PathSegment::MapAccess { field, key } => {
1138 out.push('.');
1139 out.push_str(&kotlin_getter(field));
1140 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1141 if is_numeric {
1142 out.push_str(&format!(".get({key})"));
1143 } else {
1144 out.push_str(&format!(".get(\"{key}\")"));
1145 }
1146 }
1147 PathSegment::Length => {
1148 out.push_str(".size");
1149 }
1150 }
1151 }
1152 out
1153}
1154
1155fn render_rust_with_optionals(
1161 segments: &[PathSegment],
1162 result_var: &str,
1163 optional_fields: &HashSet<String>,
1164 method_calls: &HashSet<String>,
1165) -> String {
1166 let mut out = result_var.to_string();
1167 let mut path_so_far = String::new();
1168 for (i, seg) in segments.iter().enumerate() {
1169 let is_leaf = i == segments.len() - 1;
1170 match seg {
1171 PathSegment::Field(f) => {
1172 if !path_so_far.is_empty() {
1173 path_so_far.push('.');
1174 }
1175 path_so_far.push_str(f);
1176 out.push('.');
1177 out.push_str(&f.to_snake_case());
1178 let is_method = method_calls.contains(&path_so_far);
1179 if is_method {
1180 out.push_str("()");
1181 if !is_leaf && optional_fields.contains(&path_so_far) {
1182 out.push_str(".as_ref().unwrap()");
1183 }
1184 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1185 out.push_str(".as_ref().unwrap()");
1186 }
1187 }
1188 PathSegment::ArrayField { name, index } => {
1189 if !path_so_far.is_empty() {
1190 path_so_far.push('.');
1191 }
1192 path_so_far.push_str(name);
1193 out.push('.');
1194 out.push_str(&name.to_snake_case());
1195 let path_with_idx = format!("{path_so_far}[0]");
1199 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1200 if is_opt {
1201 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1202 } else {
1203 out.push_str(&format!("[{index}]"));
1204 }
1205 path_so_far.push_str("[0]");
1210 }
1211 PathSegment::MapAccess { field, key } => {
1212 if !path_so_far.is_empty() {
1213 path_so_far.push('.');
1214 }
1215 path_so_far.push_str(field);
1216 out.push('.');
1217 out.push_str(&field.to_snake_case());
1218 if key.chars().all(|c| c.is_ascii_digit()) {
1219 let path_with_idx = format!("{path_so_far}[0]");
1221 let is_opt =
1222 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1223 if is_opt {
1224 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1225 } else {
1226 out.push_str(&format!("[{key}]"));
1227 }
1228 path_so_far.push_str("[0]");
1229 } else {
1230 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1231 }
1232 }
1233 PathSegment::Length => {
1234 out.push_str(".len()");
1235 }
1236 }
1237 }
1238 out
1239}
1240
1241fn render_zig_with_optionals(
1254 segments: &[PathSegment],
1255 result_var: &str,
1256 optional_fields: &HashSet<String>,
1257 method_calls: &HashSet<String>,
1258) -> String {
1259 let mut out = result_var.to_string();
1260 let mut path_so_far = String::new();
1261 for seg in segments {
1262 match seg {
1263 PathSegment::Field(f) => {
1264 if !path_so_far.is_empty() {
1265 path_so_far.push('.');
1266 }
1267 path_so_far.push_str(f);
1268 out.push('.');
1269 out.push_str(f);
1270 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1271 out.push_str(".?");
1272 }
1273 }
1274 PathSegment::ArrayField { name, index } => {
1275 if !path_so_far.is_empty() {
1276 path_so_far.push('.');
1277 }
1278 path_so_far.push_str(name);
1279 out.push('.');
1280 out.push_str(name);
1281 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1282 out.push_str(".?");
1283 }
1284 out.push_str(&format!("[{index}]"));
1285 }
1286 PathSegment::MapAccess { field, key } => {
1287 if !path_so_far.is_empty() {
1288 path_so_far.push('.');
1289 }
1290 path_so_far.push_str(field);
1291 out.push('.');
1292 out.push_str(field);
1293 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1294 out.push_str(".?");
1295 }
1296 if key.chars().all(|c| c.is_ascii_digit()) {
1297 out.push_str(&format!("[{key}]"));
1298 } else {
1299 out.push_str(&format!(".get(\"{key}\")"));
1300 }
1301 }
1302 PathSegment::Length => {
1303 out.push_str(".len");
1304 }
1305 }
1306 }
1307 out
1308}
1309
1310fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1311 let mut out = result_var.to_string();
1312 for seg in segments {
1313 match seg {
1314 PathSegment::Field(f) => {
1315 out.push('.');
1316 out.push_str(&f.to_pascal_case());
1317 }
1318 PathSegment::ArrayField { name, index } => {
1319 out.push('.');
1320 out.push_str(&name.to_pascal_case());
1321 out.push_str(&format!("[{index}]"));
1322 }
1323 PathSegment::MapAccess { field, key } => {
1324 out.push('.');
1325 out.push_str(&field.to_pascal_case());
1326 if key.chars().all(|c| c.is_ascii_digit()) {
1327 out.push_str(&format!("[{key}]"));
1328 } else {
1329 out.push_str(&format!("[\"{key}\"]"));
1330 }
1331 }
1332 PathSegment::Length => {
1333 out.push_str(".Count");
1334 }
1335 }
1336 }
1337 out
1338}
1339
1340fn render_csharp_with_optionals(
1341 segments: &[PathSegment],
1342 result_var: &str,
1343 optional_fields: &HashSet<String>,
1344) -> String {
1345 let mut out = result_var.to_string();
1346 let mut path_so_far = String::new();
1347 for (i, seg) in segments.iter().enumerate() {
1348 let is_leaf = i == segments.len() - 1;
1349 match seg {
1350 PathSegment::Field(f) => {
1351 if !path_so_far.is_empty() {
1352 path_so_far.push('.');
1353 }
1354 path_so_far.push_str(f);
1355 out.push('.');
1356 out.push_str(&f.to_pascal_case());
1357 if !is_leaf && optional_fields.contains(&path_so_far) {
1358 out.push('!');
1359 }
1360 }
1361 PathSegment::ArrayField { name, index } => {
1362 if !path_so_far.is_empty() {
1363 path_so_far.push('.');
1364 }
1365 path_so_far.push_str(name);
1366 out.push('.');
1367 out.push_str(&name.to_pascal_case());
1368 out.push_str(&format!("[{index}]"));
1369 }
1370 PathSegment::MapAccess { field, key } => {
1371 if !path_so_far.is_empty() {
1372 path_so_far.push('.');
1373 }
1374 path_so_far.push_str(field);
1375 out.push('.');
1376 out.push_str(&field.to_pascal_case());
1377 if key.chars().all(|c| c.is_ascii_digit()) {
1378 out.push_str(&format!("[{key}]"));
1379 } else {
1380 out.push_str(&format!("[\"{key}\"]"));
1381 }
1382 }
1383 PathSegment::Length => {
1384 out.push_str(".Count");
1385 }
1386 }
1387 }
1388 out
1389}
1390
1391fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1392 let mut out = result_var.to_string();
1393 for seg in segments {
1394 match seg {
1395 PathSegment::Field(f) => {
1396 out.push_str("->");
1397 out.push_str(&f.to_lower_camel_case());
1400 }
1401 PathSegment::ArrayField { name, index } => {
1402 out.push_str("->");
1403 out.push_str(&name.to_lower_camel_case());
1404 out.push_str(&format!("[{index}]"));
1405 }
1406 PathSegment::MapAccess { field, key } => {
1407 out.push_str("->");
1408 out.push_str(&field.to_lower_camel_case());
1409 out.push_str(&format!("[\"{key}\"]"));
1410 }
1411 PathSegment::Length => {
1412 let current = std::mem::take(&mut out);
1413 out = format!("count({current})");
1414 }
1415 }
1416 }
1417 out
1418}
1419
1420fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1421 let mut out = result_var.to_string();
1422 for seg in segments {
1423 match seg {
1424 PathSegment::Field(f) => {
1425 out.push('$');
1426 out.push_str(f);
1427 }
1428 PathSegment::ArrayField { name, index } => {
1429 out.push('$');
1430 out.push_str(name);
1431 out.push_str(&format!("[[{}]]", index + 1));
1433 }
1434 PathSegment::MapAccess { field, key } => {
1435 out.push('$');
1436 out.push_str(field);
1437 out.push_str(&format!("[[\"{key}\"]]"));
1438 }
1439 PathSegment::Length => {
1440 let current = std::mem::take(&mut out);
1441 out = format!("length({current})");
1442 }
1443 }
1444 }
1445 out
1446}
1447
1448fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1449 let mut parts = Vec::new();
1450 let mut trailing_length = false;
1451 for seg in segments {
1452 match seg {
1453 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1454 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1455 PathSegment::MapAccess { field, key } => {
1456 parts.push(field.to_snake_case());
1457 parts.push(key.clone());
1458 }
1459 PathSegment::Length => {
1460 trailing_length = true;
1461 }
1462 }
1463 }
1464 let suffix = parts.join("_");
1465 if trailing_length {
1466 format!("result_{suffix}_count({result_var})")
1467 } else {
1468 format!("result_{suffix}({result_var})")
1469 }
1470}
1471
1472fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
1479 let mut out = result_var.to_string();
1480 for seg in segments {
1481 match seg {
1482 PathSegment::Field(f) => {
1483 out.push('.');
1484 out.push_str(&f.to_lower_camel_case());
1485 }
1486 PathSegment::ArrayField { name, index } => {
1487 out.push('.');
1488 out.push_str(&name.to_lower_camel_case());
1489 out.push_str(&format!("[{index}]"));
1490 }
1491 PathSegment::MapAccess { field, key } => {
1492 out.push('.');
1493 out.push_str(&field.to_lower_camel_case());
1494 if key.chars().all(|c| c.is_ascii_digit()) {
1495 out.push_str(&format!("[{key}]"));
1496 } else {
1497 out.push_str(&format!("[\"{key}\"]"));
1498 }
1499 }
1500 PathSegment::Length => {
1501 out.push_str(".length");
1502 }
1503 }
1504 }
1505 out
1506}
1507
1508fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1514 let mut out = result_var.to_string();
1515 let mut path_so_far = String::new();
1516 let mut prev_was_nullable = false;
1517 for seg in segments {
1518 let nav = if prev_was_nullable { "?." } else { "." };
1519 match seg {
1520 PathSegment::Field(f) => {
1521 if !path_so_far.is_empty() {
1522 path_so_far.push('.');
1523 }
1524 path_so_far.push_str(f);
1525 let is_optional = optional_fields.contains(&path_so_far);
1526 out.push_str(nav);
1527 out.push_str(&f.to_lower_camel_case());
1528 prev_was_nullable = is_optional;
1529 }
1530 PathSegment::ArrayField { name, index } => {
1531 if !path_so_far.is_empty() {
1532 path_so_far.push('.');
1533 }
1534 path_so_far.push_str(name);
1535 let is_optional = optional_fields.contains(&path_so_far);
1536 out.push_str(nav);
1537 out.push_str(&name.to_lower_camel_case());
1538 if is_optional {
1542 out.push('!');
1543 }
1544 out.push_str(&format!("[{index}]"));
1545 prev_was_nullable = false;
1546 }
1547 PathSegment::MapAccess { field, key } => {
1548 if !path_so_far.is_empty() {
1549 path_so_far.push('.');
1550 }
1551 path_so_far.push_str(field);
1552 let is_optional = optional_fields.contains(&path_so_far);
1553 out.push_str(nav);
1554 out.push_str(&field.to_lower_camel_case());
1555 if key.chars().all(|c| c.is_ascii_digit()) {
1556 out.push_str(&format!("[{key}]"));
1557 } else {
1558 out.push_str(&format!("[\"{key}\"]"));
1559 }
1560 prev_was_nullable = is_optional;
1561 }
1562 PathSegment::Length => {
1563 out.push_str(nav);
1566 out.push_str("length");
1567 prev_was_nullable = false;
1568 }
1569 }
1570 }
1571 out
1572}
1573
1574#[cfg(test)]
1575mod tests {
1576 use super::*;
1577
1578 fn make_resolver() -> FieldResolver {
1579 let mut fields = HashMap::new();
1580 fields.insert("title".to_string(), "metadata.document.title".to_string());
1581 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1582 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
1583 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
1584 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
1585 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
1586 let mut optional = HashSet::new();
1587 optional.insert("metadata.document.title".to_string());
1588 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1589 }
1590
1591 fn make_resolver_with_doc_optional() -> FieldResolver {
1592 let mut fields = HashMap::new();
1593 fields.insert("title".to_string(), "metadata.document.title".to_string());
1594 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1595 let mut optional = HashSet::new();
1596 optional.insert("document".to_string());
1597 optional.insert("metadata.document.title".to_string());
1598 optional.insert("metadata.document".to_string());
1599 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1600 }
1601
1602 #[test]
1603 fn test_resolve_alias() {
1604 let r = make_resolver();
1605 assert_eq!(r.resolve("title"), "metadata.document.title");
1606 }
1607
1608 #[test]
1609 fn test_resolve_passthrough() {
1610 let r = make_resolver();
1611 assert_eq!(r.resolve("content"), "content");
1612 }
1613
1614 #[test]
1615 fn test_is_optional() {
1616 let r = make_resolver();
1617 assert!(r.is_optional("metadata.document.title"));
1618 assert!(!r.is_optional("content"));
1619 }
1620
1621 #[test]
1622 fn test_accessor_rust_struct() {
1623 let r = make_resolver();
1624 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
1625 }
1626
1627 #[test]
1628 fn test_accessor_rust_map() {
1629 let r = make_resolver();
1630 assert_eq!(
1631 r.accessor("tags", "rust", "result"),
1632 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
1633 );
1634 }
1635
1636 #[test]
1637 fn test_accessor_python() {
1638 let r = make_resolver();
1639 assert_eq!(
1640 r.accessor("title", "python", "result"),
1641 "result.metadata.document.title"
1642 );
1643 }
1644
1645 #[test]
1646 fn test_accessor_go() {
1647 let r = make_resolver();
1648 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
1649 }
1650
1651 #[test]
1652 fn test_accessor_go_initialism_fields() {
1653 let mut fields = std::collections::HashMap::new();
1654 fields.insert("content".to_string(), "html".to_string());
1655 fields.insert("link_url".to_string(), "links.url".to_string());
1656 let r = FieldResolver::new(
1657 &fields,
1658 &HashSet::new(),
1659 &HashSet::new(),
1660 &HashSet::new(),
1661 &HashSet::new(),
1662 );
1663 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
1664 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
1665 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
1666 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
1667 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
1668 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
1669 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
1670 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
1671 }
1672
1673 #[test]
1674 fn test_accessor_typescript() {
1675 let r = make_resolver();
1676 assert_eq!(
1677 r.accessor("title", "typescript", "result"),
1678 "result.metadata.document.title"
1679 );
1680 }
1681
1682 #[test]
1683 fn test_accessor_typescript_snake_to_camel() {
1684 let r = make_resolver();
1685 assert_eq!(
1686 r.accessor("og", "typescript", "result"),
1687 "result.metadata.document.openGraph"
1688 );
1689 assert_eq!(
1690 r.accessor("twitter", "typescript", "result"),
1691 "result.metadata.document.twitterCard"
1692 );
1693 assert_eq!(
1694 r.accessor("canonical", "typescript", "result"),
1695 "result.metadata.document.canonicalUrl"
1696 );
1697 }
1698
1699 #[test]
1700 fn test_accessor_typescript_map_snake_to_camel() {
1701 let r = make_resolver();
1702 assert_eq!(
1703 r.accessor("og_tag", "typescript", "result"),
1704 "result.metadata.openGraphTags[\"og_title\"]"
1705 );
1706 }
1707
1708 #[test]
1709 fn test_accessor_typescript_numeric_index_is_unquoted() {
1710 let mut fields = HashMap::new();
1714 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
1715 let r = FieldResolver::new(
1716 &fields,
1717 &HashSet::new(),
1718 &HashSet::new(),
1719 &HashSet::new(),
1720 &HashSet::new(),
1721 );
1722 assert_eq!(
1723 r.accessor("first_score", "typescript", "result"),
1724 "result.results[0].relevanceScore"
1725 );
1726 }
1727
1728 #[test]
1729 fn test_accessor_node_alias() {
1730 let r = make_resolver();
1731 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
1732 }
1733
1734 #[test]
1735 fn test_accessor_wasm_camel_case() {
1736 let r = make_resolver();
1737 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
1738 assert_eq!(
1739 r.accessor("twitter", "wasm", "result"),
1740 "result.metadata.document.twitterCard"
1741 );
1742 assert_eq!(
1743 r.accessor("canonical", "wasm", "result"),
1744 "result.metadata.document.canonicalUrl"
1745 );
1746 }
1747
1748 #[test]
1749 fn test_accessor_wasm_map_access() {
1750 let r = make_resolver();
1751 assert_eq!(
1752 r.accessor("og_tag", "wasm", "result"),
1753 "result.metadata.openGraphTags.get(\"og_title\")"
1754 );
1755 }
1756
1757 #[test]
1758 fn test_accessor_java() {
1759 let r = make_resolver();
1760 assert_eq!(
1761 r.accessor("title", "java", "result"),
1762 "result.metadata().document().title()"
1763 );
1764 }
1765
1766 #[test]
1767 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
1768 let mut fields = HashMap::new();
1769 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1770 fields.insert("node_count".to_string(), "nodes.length".to_string());
1771 let mut arrays = HashSet::new();
1772 arrays.insert("nodes".to_string());
1773 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
1774 assert_eq!(
1775 r.accessor("first_node_name", "kotlin", "result"),
1776 "result.nodes().first().name()"
1777 );
1778 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
1779 }
1780
1781 #[test]
1782 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
1783 let r = make_resolver_with_doc_optional();
1784 assert_eq!(
1785 r.accessor("title", "kotlin", "result"),
1786 "result.metadata().document()?.title()"
1787 );
1788 }
1789
1790 #[test]
1791 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
1792 let mut fields = HashMap::new();
1793 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1794 fields.insert("tag".to_string(), "tags[name]".to_string());
1795 let mut optional = HashSet::new();
1796 optional.insert("nodes".to_string());
1797 optional.insert("tags".to_string());
1798 let mut arrays = HashSet::new();
1799 arrays.insert("nodes".to_string());
1800 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
1801 assert_eq!(
1802 r.accessor("first_node_name", "kotlin", "result"),
1803 "result.nodes()?.first()?.name()"
1804 );
1805 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
1806 }
1807
1808 #[test]
1814 fn test_accessor_kotlin_optional_field_after_indexed_array() {
1815 let mut fields = HashMap::new();
1818 fields.insert(
1819 "tool_call_name".to_string(),
1820 "choices[0].message.tool_calls[0].function.name".to_string(),
1821 );
1822 let mut optional = HashSet::new();
1823 optional.insert("choices[0].message.tool_calls".to_string());
1824 let mut arrays = HashSet::new();
1825 arrays.insert("choices".to_string());
1826 arrays.insert("choices[0].message.tool_calls".to_string());
1827 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
1828 let expr = r.accessor("tool_call_name", "kotlin", "result");
1829 assert!(
1831 expr.contains("toolCalls()?.first()"),
1832 "expected toolCalls()?.first() for optional list, got: {expr}"
1833 );
1834 }
1835
1836 #[test]
1837 fn test_accessor_csharp() {
1838 let r = make_resolver();
1839 assert_eq!(
1840 r.accessor("title", "csharp", "result"),
1841 "result.Metadata.Document.Title"
1842 );
1843 }
1844
1845 #[test]
1846 fn test_accessor_php() {
1847 let r = make_resolver();
1848 assert_eq!(
1849 r.accessor("title", "php", "$result"),
1850 "$result->metadata->document->title"
1851 );
1852 }
1853
1854 #[test]
1855 fn test_accessor_r() {
1856 let r = make_resolver();
1857 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
1858 }
1859
1860 #[test]
1861 fn test_accessor_c() {
1862 let r = make_resolver();
1863 assert_eq!(
1864 r.accessor("title", "c", "result"),
1865 "result_metadata_document_title(result)"
1866 );
1867 }
1868
1869 #[test]
1870 fn test_rust_unwrap_binding() {
1871 let r = make_resolver();
1872 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
1873 assert_eq!(var, "metadata_document_title");
1874 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
1875 }
1876
1877 #[test]
1878 fn test_rust_unwrap_binding_non_optional() {
1879 let r = make_resolver();
1880 assert!(r.rust_unwrap_binding("content", "result").is_none());
1881 }
1882
1883 #[test]
1884 fn test_rust_unwrap_binding_collapses_double_underscore() {
1885 let mut aliases = HashMap::new();
1890 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
1891 let mut optional = HashSet::new();
1892 optional.insert("json_ld[].name".to_string());
1893 let mut array = HashSet::new();
1894 array.insert("json_ld".to_string());
1895 let result_fields = HashSet::new();
1896 let method_calls = HashSet::new();
1897 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
1898 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
1899 assert_eq!(var, "json_ld_name");
1900 }
1901
1902 #[test]
1903 fn test_direct_field_no_alias() {
1904 let r = make_resolver();
1905 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1906 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
1907 }
1908
1909 #[test]
1910 fn test_accessor_rust_with_optionals() {
1911 let r = make_resolver_with_doc_optional();
1912 assert_eq!(
1913 r.accessor("title", "rust", "result"),
1914 "result.metadata.document.as_ref().unwrap().title"
1915 );
1916 }
1917
1918 #[test]
1919 fn test_accessor_csharp_with_optionals() {
1920 let r = make_resolver_with_doc_optional();
1921 assert_eq!(
1922 r.accessor("title", "csharp", "result"),
1923 "result.Metadata.Document!.Title"
1924 );
1925 }
1926
1927 #[test]
1928 fn test_accessor_rust_non_optional_field() {
1929 let r = make_resolver();
1930 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1931 }
1932
1933 #[test]
1934 fn test_accessor_csharp_non_optional_field() {
1935 let r = make_resolver();
1936 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
1937 }
1938
1939 #[test]
1940 fn test_accessor_rust_method_call() {
1941 let mut fields = HashMap::new();
1943 fields.insert(
1944 "excel_sheet_count".to_string(),
1945 "metadata.format.excel.sheet_count".to_string(),
1946 );
1947 let mut optional = HashSet::new();
1948 optional.insert("metadata.format".to_string());
1949 optional.insert("metadata.format.excel".to_string());
1950 let mut method_calls = HashSet::new();
1951 method_calls.insert("metadata.format.excel".to_string());
1952 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
1953 assert_eq!(
1954 r.accessor("excel_sheet_count", "rust", "result"),
1955 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
1956 );
1957 }
1958}