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 tagged_union_split(&self, fixture_field: &str) -> Option<(String, String, String)> {
170 let resolved = self.resolve(fixture_field);
171 let segments: Vec<&str> = resolved.split('.').collect();
172 let mut path_so_far = String::new();
173 for (i, seg) in segments.iter().enumerate() {
174 if !path_so_far.is_empty() {
175 path_so_far.push('.');
176 }
177 path_so_far.push_str(seg);
178 if self.method_calls.contains(&path_so_far) {
179 let prefix = segments[..i].join(".");
181 let variant = (*seg).to_string();
182 let suffix = segments[i + 1..].join(".");
183 return Some((prefix, variant, suffix));
184 }
185 }
186 None
187 }
188
189 pub fn has_map_access(&self, fixture_field: &str) -> bool {
191 let resolved = self.resolve(fixture_field);
192 let segments = parse_path(resolved);
193 segments.iter().any(|s| {
194 if let PathSegment::MapAccess { key, .. } = s {
195 !key.chars().all(|c| c.is_ascii_digit())
196 } else {
197 false
198 }
199 })
200 }
201
202 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
204 let resolved = self.resolve(fixture_field);
205 let segments = parse_path(resolved);
206 let segments = self.inject_array_indexing(segments);
207 match language {
208 "java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
209 "kotlin" => render_kotlin_with_optionals(&segments, result_var, &self.optional_fields),
210 "rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
211 "csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
212 "zig" => render_zig_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
213 "swift" => render_swift_with_optionals(&segments, result_var, &self.optional_fields),
214 "dart" => render_dart_with_optionals(&segments, result_var, &self.optional_fields),
215 _ => render_accessor(&segments, language, result_var),
216 }
217 }
218
219 pub fn accessor_for_error(&self, sub_field: &str, language: &str, err_var: &str) -> String {
233 let resolved = self
234 .error_field_aliases
235 .get(sub_field)
236 .map(String::as_str)
237 .unwrap_or(sub_field);
238 let segments = parse_path(resolved);
239 match language {
242 "rust" => render_rust_with_optionals(&segments, err_var, &self.optional_fields, &self.method_calls),
243 _ => render_accessor(&segments, language, err_var),
244 }
245 }
246
247 pub fn has_error_aliases(&self) -> bool {
254 !self.error_field_aliases.is_empty()
255 }
256
257 fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
258 if self.array_fields.is_empty() {
259 return segments;
260 }
261 let len = segments.len();
262 let mut result = Vec::with_capacity(len);
263 let mut path_so_far = String::new();
264 for i in 0..len {
265 let seg = &segments[i];
266 match seg {
267 PathSegment::Field(f) => {
268 if !path_so_far.is_empty() {
269 path_so_far.push('.');
270 }
271 path_so_far.push_str(f);
272 let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
273 if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
274 result.push(PathSegment::ArrayField {
276 name: f.clone(),
277 index: 0,
278 });
279 } else {
280 result.push(seg.clone());
281 }
282 }
283 PathSegment::ArrayField { .. } => {
286 result.push(seg.clone());
287 }
288 PathSegment::MapAccess { field, key } => {
289 if !path_so_far.is_empty() {
290 path_so_far.push('.');
291 }
292 path_so_far.push_str(field);
293 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
294 if is_numeric && self.array_fields.contains(&path_so_far) {
295 let index: usize = key.parse().unwrap_or(0);
297 result.push(PathSegment::ArrayField {
298 name: field.clone(),
299 index,
300 });
301 } else {
302 result.push(seg.clone());
303 }
304 }
305 _ => {
306 result.push(seg.clone());
307 }
308 }
309 }
310 result
311 }
312
313 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
315 let resolved = self.resolve(fixture_field);
316 if !self.is_optional(resolved) {
317 return None;
318 }
319 let segments = parse_path(resolved);
320 let segments = self.inject_array_indexing(segments);
321 let local_var = resolved.replace(['.', '['], "_").replace(']', "");
322 let accessor = render_accessor(&segments, "rust", result_var);
323 let has_map_access = segments.iter().any(|s| {
324 if let PathSegment::MapAccess { key, .. } = s {
325 !key.chars().all(|c| c.is_ascii_digit())
326 } else {
327 false
328 }
329 });
330 let is_array = self.is_array(resolved);
331 let binding = if has_map_access {
332 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
333 } else if is_array {
334 format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
335 } else {
336 format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
337 };
338 Some((binding, local_var))
339 }
340}
341
342fn strip_numeric_indices(path: &str) -> String {
347 let mut result = String::with_capacity(path.len());
348 let mut chars = path.chars().peekable();
349 while let Some(c) = chars.next() {
350 if c == '[' {
351 let mut key = String::new();
352 let mut closed = false;
353 for inner in chars.by_ref() {
354 if inner == ']' {
355 closed = true;
356 break;
357 }
358 key.push(inner);
359 }
360 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
361 } else {
363 result.push('[');
364 result.push_str(&key);
365 if closed {
366 result.push(']');
367 }
368 }
369 } else {
370 result.push(c);
371 }
372 }
373 while result.contains("..") {
375 result = result.replace("..", ".");
376 }
377 if result.starts_with('.') {
378 result.remove(0);
379 }
380 result
381}
382
383fn normalize_numeric_indices(path: &str) -> String {
384 let mut result = String::with_capacity(path.len());
385 let mut chars = path.chars().peekable();
386 while let Some(c) = chars.next() {
387 if c == '[' {
388 let mut key = String::new();
389 let mut closed = false;
390 for inner in chars.by_ref() {
391 if inner == ']' {
392 closed = true;
393 break;
394 }
395 key.push(inner);
396 }
397 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
398 result.push_str("[0]");
399 } else {
400 result.push('[');
401 result.push_str(&key);
402 if closed {
403 result.push(']');
404 }
405 }
406 } else {
407 result.push(c);
408 }
409 }
410 result
411}
412
413fn parse_path(path: &str) -> Vec<PathSegment> {
414 let mut segments = Vec::new();
415 for part in path.split('.') {
416 if part == "length" || part == "count" || part == "size" {
417 segments.push(PathSegment::Length);
418 } else if let Some(bracket_pos) = part.find('[') {
419 let name = part[..bracket_pos].to_string();
420 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
421 if key.is_empty() {
422 segments.push(PathSegment::ArrayField { name, index: 0 });
424 } else if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
425 let index: usize = key.parse().unwrap_or(0);
427 segments.push(PathSegment::ArrayField { name, index });
428 } else {
429 segments.push(PathSegment::MapAccess { field: name, key });
431 }
432 } else {
433 segments.push(PathSegment::Field(part.to_string()));
434 }
435 }
436 segments
437}
438
439fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
440 match language {
441 "rust" => render_rust(segments, result_var),
442 "python" => render_dot_access(segments, result_var, "python"),
443 "typescript" | "node" => render_typescript(segments, result_var),
444 "wasm" => render_wasm(segments, result_var),
445 "go" => render_go(segments, result_var),
446 "java" => render_java(segments, result_var),
447 "kotlin" => render_kotlin(segments, result_var),
448 "csharp" => render_pascal_dot(segments, result_var),
449 "ruby" => render_dot_access(segments, result_var, "ruby"),
450 "php" => render_php(segments, result_var),
451 "elixir" => render_dot_access(segments, result_var, "elixir"),
452 "r" => render_r(segments, result_var),
453 "c" => render_c(segments, result_var),
454 "swift" => render_swift(segments, result_var),
455 "dart" => render_dart(segments, result_var),
456 _ => render_dot_access(segments, result_var, language),
457 }
458}
459
460fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
466 let mut out = result_var.to_string();
467 for seg in segments {
468 match seg {
469 PathSegment::Field(f) => {
470 out.push('.');
471 out.push_str(f);
472 out.push_str("()");
473 }
474 PathSegment::ArrayField { name, index } => {
475 out.push('.');
476 out.push_str(name);
477 out.push_str(&format!("()[{index}]"));
478 }
479 PathSegment::MapAccess { field, key } => {
480 out.push('.');
481 out.push_str(field);
482 if key.chars().all(|c| c.is_ascii_digit()) {
483 out.push_str(&format!("()[{key}]"));
484 } else {
485 out.push_str(&format!("()[\"{key}\"]"));
486 }
487 }
488 PathSegment::Length => {
489 out.push_str(".count");
490 }
491 }
492 }
493 out
494}
495
496fn render_swift_with_optionals(
506 segments: &[PathSegment],
507 result_var: &str,
508 optional_fields: &HashSet<String>,
509) -> String {
510 let mut out = result_var.to_string();
511 let mut path_so_far = String::new();
512 let total = segments.len();
513 for (i, seg) in segments.iter().enumerate() {
514 let is_leaf = i == total - 1;
515 match seg {
516 PathSegment::Field(f) => {
517 if !path_so_far.is_empty() {
518 path_so_far.push('.');
519 }
520 path_so_far.push_str(f);
521 out.push('.');
522 out.push_str(f);
523 out.push_str("()");
524 if !is_leaf && optional_fields.contains(&path_so_far) {
527 out.push('?');
528 }
529 }
530 PathSegment::ArrayField { name, index } => {
531 if !path_so_far.is_empty() {
532 path_so_far.push('.');
533 }
534 path_so_far.push_str(name);
535 let is_optional = optional_fields.contains(&path_so_far);
539 out.push('.');
540 out.push_str(name);
541 if is_optional {
542 out.push_str(&format!("()?[{index}]"));
545 } else {
546 out.push_str(&format!("()[{index}]"));
547 }
548 path_so_far.push_str("[0]");
552 let _ = is_leaf;
559 }
560 PathSegment::MapAccess { field, key } => {
561 if !path_so_far.is_empty() {
562 path_so_far.push('.');
563 }
564 path_so_far.push_str(field);
565 out.push('.');
566 out.push_str(field);
567 if key.chars().all(|c| c.is_ascii_digit()) {
568 out.push_str(&format!("()[{key}]"));
569 } else {
570 out.push_str(&format!("()[\"{key}\"]"));
571 }
572 }
573 PathSegment::Length => {
574 out.push_str(".count");
575 }
576 }
577 }
578 out
579}
580
581fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
582 let mut out = result_var.to_string();
583 for seg in segments {
584 match seg {
585 PathSegment::Field(f) => {
586 out.push('.');
587 out.push_str(&f.to_snake_case());
588 }
589 PathSegment::ArrayField { name, index } => {
590 out.push('.');
591 out.push_str(&name.to_snake_case());
592 out.push_str(&format!("[{index}]"));
593 }
594 PathSegment::MapAccess { field, key } => {
595 out.push('.');
596 out.push_str(&field.to_snake_case());
597 if key.chars().all(|c| c.is_ascii_digit()) {
598 out.push_str(&format!("[{key}]"));
599 } else {
600 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
601 }
602 }
603 PathSegment::Length => {
604 out.push_str(".len()");
605 }
606 }
607 }
608 out
609}
610
611fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
612 let mut out = result_var.to_string();
613 for seg in segments {
614 match seg {
615 PathSegment::Field(f) => {
616 out.push('.');
617 out.push_str(f);
618 }
619 PathSegment::ArrayField { name, index } => {
620 if language == "elixir" {
621 let current = std::mem::take(&mut out);
622 out = format!("Enum.at({current}.{name}, {index})");
623 } else {
624 out.push('.');
625 out.push_str(name);
626 out.push_str(&format!("[{index}]"));
627 }
628 }
629 PathSegment::MapAccess { field, key } => {
630 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
631 if is_numeric && language == "elixir" {
632 let current = std::mem::take(&mut out);
633 out = format!("Enum.at({current}.{field}, {key})");
634 } else {
635 out.push('.');
636 out.push_str(field);
637 if is_numeric {
638 let idx: usize = key.parse().unwrap_or(0);
639 out.push_str(&format!("[{idx}]"));
640 } else if language == "elixir" || language == "ruby" {
641 out.push_str(&format!("[\"{key}\"]"));
644 } else {
645 out.push_str(&format!(".get(\"{key}\")"));
646 }
647 }
648 }
649 PathSegment::Length => match language {
650 "ruby" => out.push_str(".length"),
651 "elixir" => {
652 let current = std::mem::take(&mut out);
653 out = format!("length({current})");
654 }
655 "gleam" => {
656 let current = std::mem::take(&mut out);
657 out = format!("list.length({current})");
658 }
659 _ => {
660 let current = std::mem::take(&mut out);
661 out = format!("len({current})");
662 }
663 },
664 }
665 }
666 out
667}
668
669fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
670 let mut out = result_var.to_string();
671 for seg in segments {
672 match seg {
673 PathSegment::Field(f) => {
674 out.push('.');
675 out.push_str(&f.to_lower_camel_case());
676 }
677 PathSegment::ArrayField { name, index } => {
678 out.push('.');
679 out.push_str(&name.to_lower_camel_case());
680 out.push_str(&format!("[{index}]"));
681 }
682 PathSegment::MapAccess { field, key } => {
683 out.push('.');
684 out.push_str(&field.to_lower_camel_case());
685 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
688 out.push_str(&format!("[{key}]"));
689 } else {
690 out.push_str(&format!("[\"{key}\"]"));
691 }
692 }
693 PathSegment::Length => {
694 out.push_str(".length");
695 }
696 }
697 }
698 out
699}
700
701fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
702 let mut out = result_var.to_string();
703 for seg in segments {
704 match seg {
705 PathSegment::Field(f) => {
706 out.push('.');
707 out.push_str(&f.to_lower_camel_case());
708 }
709 PathSegment::ArrayField { name, index } => {
710 out.push('.');
711 out.push_str(&name.to_lower_camel_case());
712 out.push_str(&format!("[{index}]"));
713 }
714 PathSegment::MapAccess { field, key } => {
715 out.push('.');
716 out.push_str(&field.to_lower_camel_case());
717 out.push_str(&format!(".get(\"{key}\")"));
718 }
719 PathSegment::Length => {
720 out.push_str(".length");
721 }
722 }
723 }
724 out
725}
726
727fn render_go(segments: &[PathSegment], result_var: &str) -> String {
728 let mut out = result_var.to_string();
729 for seg in segments {
730 match seg {
731 PathSegment::Field(f) => {
732 out.push('.');
733 out.push_str(&to_go_name(f));
734 }
735 PathSegment::ArrayField { name, index } => {
736 out.push('.');
737 out.push_str(&to_go_name(name));
738 out.push_str(&format!("[{index}]"));
739 }
740 PathSegment::MapAccess { field, key } => {
741 out.push('.');
742 out.push_str(&to_go_name(field));
743 if key.chars().all(|c| c.is_ascii_digit()) {
744 out.push_str(&format!("[{key}]"));
745 } else {
746 out.push_str(&format!("[\"{key}\"]"));
747 }
748 }
749 PathSegment::Length => {
750 let current = std::mem::take(&mut out);
751 out = format!("len({current})");
752 }
753 }
754 }
755 out
756}
757
758fn render_java(segments: &[PathSegment], result_var: &str) -> String {
759 let mut out = result_var.to_string();
760 for seg in segments {
761 match seg {
762 PathSegment::Field(f) => {
763 out.push('.');
764 out.push_str(&f.to_lower_camel_case());
765 out.push_str("()");
766 }
767 PathSegment::ArrayField { name, index } => {
768 out.push('.');
769 out.push_str(&name.to_lower_camel_case());
770 out.push_str(&format!("().get({index})"));
771 }
772 PathSegment::MapAccess { field, key } => {
773 out.push('.');
774 out.push_str(&field.to_lower_camel_case());
775 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
777 if is_numeric {
778 out.push_str(&format!("().get({key})"));
779 } else {
780 out.push_str(&format!("().get(\"{key}\")"));
781 }
782 }
783 PathSegment::Length => {
784 out.push_str(".size()");
785 }
786 }
787 }
788 out
789}
790
791fn kotlin_getter(name: &str) -> String {
801 let camel = name.to_lower_camel_case();
802 match camel.as_str() {
803 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
804 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
805 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
806 _ => camel,
807 }
808}
809
810fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
811 let mut out = result_var.to_string();
812 for seg in segments {
813 match seg {
814 PathSegment::Field(f) => {
815 out.push('.');
816 out.push_str(&kotlin_getter(f));
817 out.push_str("()");
818 }
819 PathSegment::ArrayField { name, index } => {
820 out.push('.');
821 out.push_str(&kotlin_getter(name));
822 if *index == 0 {
823 out.push_str("().first()");
824 } else {
825 out.push_str(&format!("().get({index})"));
826 }
827 }
828 PathSegment::MapAccess { field, key } => {
829 out.push('.');
830 out.push_str(&kotlin_getter(field));
831 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
832 if is_numeric {
833 out.push_str(&format!("().get({key})"));
834 } else {
835 out.push_str(&format!("().get(\"{key}\")"));
836 }
837 }
838 PathSegment::Length => {
839 out.push_str(".size");
840 }
841 }
842 }
843 out
844}
845
846fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
847 let mut out = result_var.to_string();
848 let mut path_so_far = String::new();
849 for (i, seg) in segments.iter().enumerate() {
850 let is_leaf = i == segments.len() - 1;
851 match seg {
852 PathSegment::Field(f) => {
853 if !path_so_far.is_empty() {
854 path_so_far.push('.');
855 }
856 path_so_far.push_str(f);
857 out.push('.');
858 out.push_str(&f.to_lower_camel_case());
859 out.push_str("()");
860 let _ = is_leaf;
861 let _ = optional_fields;
862 }
863 PathSegment::ArrayField { name, index } => {
864 if !path_so_far.is_empty() {
865 path_so_far.push('.');
866 }
867 path_so_far.push_str(name);
868 out.push('.');
869 out.push_str(&name.to_lower_camel_case());
870 out.push_str(&format!("().get({index})"));
871 }
872 PathSegment::MapAccess { field, key } => {
873 if !path_so_far.is_empty() {
874 path_so_far.push('.');
875 }
876 path_so_far.push_str(field);
877 out.push('.');
878 out.push_str(&field.to_lower_camel_case());
879 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
881 if is_numeric {
882 out.push_str(&format!("().get({key})"));
883 } else {
884 out.push_str(&format!("().get(\"{key}\")"));
885 }
886 }
887 PathSegment::Length => {
888 out.push_str(".size()");
889 }
890 }
891 }
892 out
893}
894
895fn render_kotlin_with_optionals(
910 segments: &[PathSegment],
911 result_var: &str,
912 optional_fields: &HashSet<String>,
913) -> String {
914 let mut out = result_var.to_string();
915 let mut path_so_far = String::new();
916 let mut prev_was_nullable = false;
924 for seg in segments {
925 let nav = if prev_was_nullable { "?." } else { "." };
926 match seg {
927 PathSegment::Field(f) => {
928 if !path_so_far.is_empty() {
929 path_so_far.push('.');
930 }
931 path_so_far.push_str(f);
932 let is_optional = optional_fields.contains(&path_so_far);
937 out.push_str(nav);
938 out.push_str(&kotlin_getter(f));
939 out.push_str("()");
940 prev_was_nullable = prev_was_nullable || is_optional;
941 }
942 PathSegment::ArrayField { name, index } => {
943 if !path_so_far.is_empty() {
944 path_so_far.push('.');
945 }
946 path_so_far.push_str(name);
947 let is_optional = optional_fields.contains(&path_so_far);
948 out.push_str(nav);
949 out.push_str(&kotlin_getter(name));
950 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
951 if *index == 0 {
952 out.push_str(&format!("(){safe}.first()"));
953 } else {
954 out.push_str(&format!("(){safe}.get({index})"));
955 }
956 path_so_far.push_str("[0]");
960 prev_was_nullable = prev_was_nullable || is_optional;
961 }
962 PathSegment::MapAccess { field, key } => {
963 if !path_so_far.is_empty() {
964 path_so_far.push('.');
965 }
966 path_so_far.push_str(field);
967 let is_optional = optional_fields.contains(&path_so_far);
968 out.push_str(nav);
969 out.push_str(&kotlin_getter(field));
970 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
971 if is_numeric {
972 if prev_was_nullable || is_optional {
973 out.push_str(&format!("()?.get({key})"));
974 } else {
975 out.push_str(&format!("().get({key})"));
976 }
977 } else if prev_was_nullable || is_optional {
978 out.push_str(&format!("()?.get(\"{key}\")"));
979 } else {
980 out.push_str(&format!("().get(\"{key}\")"));
981 }
982 prev_was_nullable = prev_was_nullable || is_optional;
983 }
984 PathSegment::Length => {
985 let size_nav = if prev_was_nullable { "?" } else { "" };
988 out.push_str(&format!("{size_nav}.size"));
989 prev_was_nullable = false;
990 }
991 }
992 }
993 out
994}
995
996fn render_rust_with_optionals(
1002 segments: &[PathSegment],
1003 result_var: &str,
1004 optional_fields: &HashSet<String>,
1005 method_calls: &HashSet<String>,
1006) -> String {
1007 let mut out = result_var.to_string();
1008 let mut path_so_far = String::new();
1009 for (i, seg) in segments.iter().enumerate() {
1010 let is_leaf = i == segments.len() - 1;
1011 match seg {
1012 PathSegment::Field(f) => {
1013 if !path_so_far.is_empty() {
1014 path_so_far.push('.');
1015 }
1016 path_so_far.push_str(f);
1017 out.push('.');
1018 out.push_str(&f.to_snake_case());
1019 let is_method = method_calls.contains(&path_so_far);
1020 if is_method {
1021 out.push_str("()");
1022 if !is_leaf && optional_fields.contains(&path_so_far) {
1023 out.push_str(".as_ref().unwrap()");
1024 }
1025 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1026 out.push_str(".as_ref().unwrap()");
1027 }
1028 }
1029 PathSegment::ArrayField { name, index } => {
1030 if !path_so_far.is_empty() {
1031 path_so_far.push('.');
1032 }
1033 path_so_far.push_str(name);
1034 out.push('.');
1035 out.push_str(&name.to_snake_case());
1036 let path_with_idx = format!("{path_so_far}[0]");
1040 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1041 if is_opt {
1042 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1043 } else {
1044 out.push_str(&format!("[{index}]"));
1045 }
1046 path_so_far.push_str("[0]");
1051 }
1052 PathSegment::MapAccess { field, key } => {
1053 if !path_so_far.is_empty() {
1054 path_so_far.push('.');
1055 }
1056 path_so_far.push_str(field);
1057 out.push('.');
1058 out.push_str(&field.to_snake_case());
1059 if key.chars().all(|c| c.is_ascii_digit()) {
1060 let path_with_idx = format!("{path_so_far}[0]");
1062 let is_opt =
1063 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1064 if is_opt {
1065 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1066 } else {
1067 out.push_str(&format!("[{key}]"));
1068 }
1069 path_so_far.push_str("[0]");
1070 } else {
1071 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1072 }
1073 }
1074 PathSegment::Length => {
1075 out.push_str(".len()");
1076 }
1077 }
1078 }
1079 out
1080}
1081
1082fn render_zig_with_optionals(
1095 segments: &[PathSegment],
1096 result_var: &str,
1097 optional_fields: &HashSet<String>,
1098 method_calls: &HashSet<String>,
1099) -> String {
1100 let mut out = result_var.to_string();
1101 let mut path_so_far = String::new();
1102 for seg in segments {
1103 match seg {
1104 PathSegment::Field(f) => {
1105 if !path_so_far.is_empty() {
1106 path_so_far.push('.');
1107 }
1108 path_so_far.push_str(f);
1109 out.push('.');
1110 out.push_str(f);
1111 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1112 out.push_str(".?");
1113 }
1114 }
1115 PathSegment::ArrayField { name, index } => {
1116 if !path_so_far.is_empty() {
1117 path_so_far.push('.');
1118 }
1119 path_so_far.push_str(name);
1120 out.push('.');
1121 out.push_str(name);
1122 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1123 out.push_str(".?");
1124 }
1125 out.push_str(&format!("[{index}]"));
1126 }
1127 PathSegment::MapAccess { field, key } => {
1128 if !path_so_far.is_empty() {
1129 path_so_far.push('.');
1130 }
1131 path_so_far.push_str(field);
1132 out.push('.');
1133 out.push_str(field);
1134 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1135 out.push_str(".?");
1136 }
1137 if key.chars().all(|c| c.is_ascii_digit()) {
1138 out.push_str(&format!("[{key}]"));
1139 } else {
1140 out.push_str(&format!(".get(\"{key}\")"));
1141 }
1142 }
1143 PathSegment::Length => {
1144 out.push_str(".len");
1145 }
1146 }
1147 }
1148 out
1149}
1150
1151fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1152 let mut out = result_var.to_string();
1153 for seg in segments {
1154 match seg {
1155 PathSegment::Field(f) => {
1156 out.push('.');
1157 out.push_str(&f.to_pascal_case());
1158 }
1159 PathSegment::ArrayField { name, index } => {
1160 out.push('.');
1161 out.push_str(&name.to_pascal_case());
1162 out.push_str(&format!("[{index}]"));
1163 }
1164 PathSegment::MapAccess { field, key } => {
1165 out.push('.');
1166 out.push_str(&field.to_pascal_case());
1167 if key.chars().all(|c| c.is_ascii_digit()) {
1168 out.push_str(&format!("[{key}]"));
1169 } else {
1170 out.push_str(&format!("[\"{key}\"]"));
1171 }
1172 }
1173 PathSegment::Length => {
1174 out.push_str(".Count");
1175 }
1176 }
1177 }
1178 out
1179}
1180
1181fn render_csharp_with_optionals(
1182 segments: &[PathSegment],
1183 result_var: &str,
1184 optional_fields: &HashSet<String>,
1185) -> String {
1186 let mut out = result_var.to_string();
1187 let mut path_so_far = String::new();
1188 for (i, seg) in segments.iter().enumerate() {
1189 let is_leaf = i == segments.len() - 1;
1190 match seg {
1191 PathSegment::Field(f) => {
1192 if !path_so_far.is_empty() {
1193 path_so_far.push('.');
1194 }
1195 path_so_far.push_str(f);
1196 out.push('.');
1197 out.push_str(&f.to_pascal_case());
1198 if !is_leaf && optional_fields.contains(&path_so_far) {
1199 out.push('!');
1200 }
1201 }
1202 PathSegment::ArrayField { name, index } => {
1203 if !path_so_far.is_empty() {
1204 path_so_far.push('.');
1205 }
1206 path_so_far.push_str(name);
1207 out.push('.');
1208 out.push_str(&name.to_pascal_case());
1209 out.push_str(&format!("[{index}]"));
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_pascal_case());
1218 if key.chars().all(|c| c.is_ascii_digit()) {
1219 out.push_str(&format!("[{key}]"));
1220 } else {
1221 out.push_str(&format!("[\"{key}\"]"));
1222 }
1223 }
1224 PathSegment::Length => {
1225 out.push_str(".Count");
1226 }
1227 }
1228 }
1229 out
1230}
1231
1232fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1233 let mut out = result_var.to_string();
1234 for seg in segments {
1235 match seg {
1236 PathSegment::Field(f) => {
1237 out.push_str("->");
1238 out.push_str(&f.to_lower_camel_case());
1241 }
1242 PathSegment::ArrayField { name, index } => {
1243 out.push_str("->");
1244 out.push_str(&name.to_lower_camel_case());
1245 out.push_str(&format!("[{index}]"));
1246 }
1247 PathSegment::MapAccess { field, key } => {
1248 out.push_str("->");
1249 out.push_str(&field.to_lower_camel_case());
1250 out.push_str(&format!("[\"{key}\"]"));
1251 }
1252 PathSegment::Length => {
1253 let current = std::mem::take(&mut out);
1254 out = format!("count({current})");
1255 }
1256 }
1257 }
1258 out
1259}
1260
1261fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1262 let mut out = result_var.to_string();
1263 for seg in segments {
1264 match seg {
1265 PathSegment::Field(f) => {
1266 out.push('$');
1267 out.push_str(f);
1268 }
1269 PathSegment::ArrayField { name, index } => {
1270 out.push('$');
1271 out.push_str(name);
1272 out.push_str(&format!("[[{}]]", index + 1));
1274 }
1275 PathSegment::MapAccess { field, key } => {
1276 out.push('$');
1277 out.push_str(field);
1278 out.push_str(&format!("[[\"{key}\"]]"));
1279 }
1280 PathSegment::Length => {
1281 let current = std::mem::take(&mut out);
1282 out = format!("length({current})");
1283 }
1284 }
1285 }
1286 out
1287}
1288
1289fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1290 let mut parts = Vec::new();
1291 let mut trailing_length = false;
1292 for seg in segments {
1293 match seg {
1294 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1295 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1296 PathSegment::MapAccess { field, key } => {
1297 parts.push(field.to_snake_case());
1298 parts.push(key.clone());
1299 }
1300 PathSegment::Length => {
1301 trailing_length = true;
1302 }
1303 }
1304 }
1305 let suffix = parts.join("_");
1306 if trailing_length {
1307 format!("result_{suffix}_count({result_var})")
1308 } else {
1309 format!("result_{suffix}({result_var})")
1310 }
1311}
1312
1313fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
1320 let mut out = result_var.to_string();
1321 for seg in segments {
1322 match seg {
1323 PathSegment::Field(f) => {
1324 out.push('.');
1325 out.push_str(&f.to_lower_camel_case());
1326 }
1327 PathSegment::ArrayField { name, index } => {
1328 out.push('.');
1329 out.push_str(&name.to_lower_camel_case());
1330 out.push_str(&format!("[{index}]"));
1331 }
1332 PathSegment::MapAccess { field, key } => {
1333 out.push('.');
1334 out.push_str(&field.to_lower_camel_case());
1335 if key.chars().all(|c| c.is_ascii_digit()) {
1336 out.push_str(&format!("[{key}]"));
1337 } else {
1338 out.push_str(&format!("[\"{key}\"]"));
1339 }
1340 }
1341 PathSegment::Length => {
1342 out.push_str(".length");
1343 }
1344 }
1345 }
1346 out
1347}
1348
1349fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1355 let mut out = result_var.to_string();
1356 let mut path_so_far = String::new();
1357 let mut prev_was_nullable = false;
1358 for seg in segments {
1359 let nav = if prev_was_nullable { "?." } else { "." };
1360 match seg {
1361 PathSegment::Field(f) => {
1362 if !path_so_far.is_empty() {
1363 path_so_far.push('.');
1364 }
1365 path_so_far.push_str(f);
1366 let is_optional = optional_fields.contains(&path_so_far);
1367 out.push_str(nav);
1368 out.push_str(&f.to_lower_camel_case());
1369 prev_was_nullable = is_optional;
1370 }
1371 PathSegment::ArrayField { name, index } => {
1372 if !path_so_far.is_empty() {
1373 path_so_far.push('.');
1374 }
1375 path_so_far.push_str(name);
1376 let is_optional = optional_fields.contains(&path_so_far);
1377 out.push_str(nav);
1378 out.push_str(&name.to_lower_camel_case());
1379 if is_optional {
1383 out.push('!');
1384 }
1385 out.push_str(&format!("[{index}]"));
1386 prev_was_nullable = false;
1387 }
1388 PathSegment::MapAccess { field, key } => {
1389 if !path_so_far.is_empty() {
1390 path_so_far.push('.');
1391 }
1392 path_so_far.push_str(field);
1393 let is_optional = optional_fields.contains(&path_so_far);
1394 out.push_str(nav);
1395 out.push_str(&field.to_lower_camel_case());
1396 if key.chars().all(|c| c.is_ascii_digit()) {
1397 out.push_str(&format!("[{key}]"));
1398 } else {
1399 out.push_str(&format!("[\"{key}\"]"));
1400 }
1401 prev_was_nullable = is_optional;
1402 }
1403 PathSegment::Length => {
1404 out.push_str(nav);
1407 out.push_str("length");
1408 prev_was_nullable = false;
1409 }
1410 }
1411 }
1412 out
1413}
1414
1415#[cfg(test)]
1416mod tests {
1417 use super::*;
1418
1419 fn make_resolver() -> FieldResolver {
1420 let mut fields = HashMap::new();
1421 fields.insert("title".to_string(), "metadata.document.title".to_string());
1422 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1423 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
1424 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
1425 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
1426 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
1427 let mut optional = HashSet::new();
1428 optional.insert("metadata.document.title".to_string());
1429 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1430 }
1431
1432 fn make_resolver_with_doc_optional() -> FieldResolver {
1433 let mut fields = HashMap::new();
1434 fields.insert("title".to_string(), "metadata.document.title".to_string());
1435 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1436 let mut optional = HashSet::new();
1437 optional.insert("document".to_string());
1438 optional.insert("metadata.document.title".to_string());
1439 optional.insert("metadata.document".to_string());
1440 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1441 }
1442
1443 #[test]
1444 fn test_resolve_alias() {
1445 let r = make_resolver();
1446 assert_eq!(r.resolve("title"), "metadata.document.title");
1447 }
1448
1449 #[test]
1450 fn test_resolve_passthrough() {
1451 let r = make_resolver();
1452 assert_eq!(r.resolve("content"), "content");
1453 }
1454
1455 #[test]
1456 fn test_is_optional() {
1457 let r = make_resolver();
1458 assert!(r.is_optional("metadata.document.title"));
1459 assert!(!r.is_optional("content"));
1460 }
1461
1462 #[test]
1463 fn test_accessor_rust_struct() {
1464 let r = make_resolver();
1465 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
1466 }
1467
1468 #[test]
1469 fn test_accessor_rust_map() {
1470 let r = make_resolver();
1471 assert_eq!(
1472 r.accessor("tags", "rust", "result"),
1473 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
1474 );
1475 }
1476
1477 #[test]
1478 fn test_accessor_python() {
1479 let r = make_resolver();
1480 assert_eq!(
1481 r.accessor("title", "python", "result"),
1482 "result.metadata.document.title"
1483 );
1484 }
1485
1486 #[test]
1487 fn test_accessor_go() {
1488 let r = make_resolver();
1489 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
1490 }
1491
1492 #[test]
1493 fn test_accessor_go_initialism_fields() {
1494 let mut fields = std::collections::HashMap::new();
1495 fields.insert("content".to_string(), "html".to_string());
1496 fields.insert("link_url".to_string(), "links.url".to_string());
1497 let r = FieldResolver::new(
1498 &fields,
1499 &HashSet::new(),
1500 &HashSet::new(),
1501 &HashSet::new(),
1502 &HashSet::new(),
1503 );
1504 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
1505 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
1506 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
1507 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
1508 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
1509 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
1510 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
1511 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
1512 }
1513
1514 #[test]
1515 fn test_accessor_typescript() {
1516 let r = make_resolver();
1517 assert_eq!(
1518 r.accessor("title", "typescript", "result"),
1519 "result.metadata.document.title"
1520 );
1521 }
1522
1523 #[test]
1524 fn test_accessor_typescript_snake_to_camel() {
1525 let r = make_resolver();
1526 assert_eq!(
1527 r.accessor("og", "typescript", "result"),
1528 "result.metadata.document.openGraph"
1529 );
1530 assert_eq!(
1531 r.accessor("twitter", "typescript", "result"),
1532 "result.metadata.document.twitterCard"
1533 );
1534 assert_eq!(
1535 r.accessor("canonical", "typescript", "result"),
1536 "result.metadata.document.canonicalUrl"
1537 );
1538 }
1539
1540 #[test]
1541 fn test_accessor_typescript_map_snake_to_camel() {
1542 let r = make_resolver();
1543 assert_eq!(
1544 r.accessor("og_tag", "typescript", "result"),
1545 "result.metadata.openGraphTags[\"og_title\"]"
1546 );
1547 }
1548
1549 #[test]
1550 fn test_accessor_typescript_numeric_index_is_unquoted() {
1551 let mut fields = HashMap::new();
1555 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
1556 let r = FieldResolver::new(
1557 &fields,
1558 &HashSet::new(),
1559 &HashSet::new(),
1560 &HashSet::new(),
1561 &HashSet::new(),
1562 );
1563 assert_eq!(
1564 r.accessor("first_score", "typescript", "result"),
1565 "result.results[0].relevanceScore"
1566 );
1567 }
1568
1569 #[test]
1570 fn test_accessor_node_alias() {
1571 let r = make_resolver();
1572 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
1573 }
1574
1575 #[test]
1576 fn test_accessor_wasm_camel_case() {
1577 let r = make_resolver();
1578 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
1579 assert_eq!(
1580 r.accessor("twitter", "wasm", "result"),
1581 "result.metadata.document.twitterCard"
1582 );
1583 assert_eq!(
1584 r.accessor("canonical", "wasm", "result"),
1585 "result.metadata.document.canonicalUrl"
1586 );
1587 }
1588
1589 #[test]
1590 fn test_accessor_wasm_map_access() {
1591 let r = make_resolver();
1592 assert_eq!(
1593 r.accessor("og_tag", "wasm", "result"),
1594 "result.metadata.openGraphTags.get(\"og_title\")"
1595 );
1596 }
1597
1598 #[test]
1599 fn test_accessor_java() {
1600 let r = make_resolver();
1601 assert_eq!(
1602 r.accessor("title", "java", "result"),
1603 "result.metadata().document().title()"
1604 );
1605 }
1606
1607 #[test]
1608 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
1609 let mut fields = HashMap::new();
1610 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1611 fields.insert("node_count".to_string(), "nodes.length".to_string());
1612 let mut arrays = HashSet::new();
1613 arrays.insert("nodes".to_string());
1614 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
1615 assert_eq!(
1616 r.accessor("first_node_name", "kotlin", "result"),
1617 "result.nodes().first().name()"
1618 );
1619 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
1620 }
1621
1622 #[test]
1623 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
1624 let r = make_resolver_with_doc_optional();
1625 assert_eq!(
1626 r.accessor("title", "kotlin", "result"),
1627 "result.metadata().document()?.title()"
1628 );
1629 }
1630
1631 #[test]
1632 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
1633 let mut fields = HashMap::new();
1634 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1635 fields.insert("tag".to_string(), "tags[name]".to_string());
1636 let mut optional = HashSet::new();
1637 optional.insert("nodes".to_string());
1638 optional.insert("tags".to_string());
1639 let mut arrays = HashSet::new();
1640 arrays.insert("nodes".to_string());
1641 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
1642 assert_eq!(
1643 r.accessor("first_node_name", "kotlin", "result"),
1644 "result.nodes()?.first()?.name()"
1645 );
1646 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
1647 }
1648
1649 #[test]
1655 fn test_accessor_kotlin_optional_field_after_indexed_array() {
1656 let mut fields = HashMap::new();
1659 fields.insert(
1660 "tool_call_name".to_string(),
1661 "choices[0].message.tool_calls[0].function.name".to_string(),
1662 );
1663 let mut optional = HashSet::new();
1664 optional.insert("choices[0].message.tool_calls".to_string());
1665 let mut arrays = HashSet::new();
1666 arrays.insert("choices".to_string());
1667 arrays.insert("choices[0].message.tool_calls".to_string());
1668 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
1669 let expr = r.accessor("tool_call_name", "kotlin", "result");
1670 assert!(
1672 expr.contains("toolCalls()?.first()"),
1673 "expected toolCalls()?.first() for optional list, got: {expr}"
1674 );
1675 }
1676
1677 #[test]
1678 fn test_accessor_csharp() {
1679 let r = make_resolver();
1680 assert_eq!(
1681 r.accessor("title", "csharp", "result"),
1682 "result.Metadata.Document.Title"
1683 );
1684 }
1685
1686 #[test]
1687 fn test_accessor_php() {
1688 let r = make_resolver();
1689 assert_eq!(
1690 r.accessor("title", "php", "$result"),
1691 "$result->metadata->document->title"
1692 );
1693 }
1694
1695 #[test]
1696 fn test_accessor_r() {
1697 let r = make_resolver();
1698 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
1699 }
1700
1701 #[test]
1702 fn test_accessor_c() {
1703 let r = make_resolver();
1704 assert_eq!(
1705 r.accessor("title", "c", "result"),
1706 "result_metadata_document_title(result)"
1707 );
1708 }
1709
1710 #[test]
1711 fn test_rust_unwrap_binding() {
1712 let r = make_resolver();
1713 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
1714 assert_eq!(var, "metadata_document_title");
1715 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
1716 }
1717
1718 #[test]
1719 fn test_rust_unwrap_binding_non_optional() {
1720 let r = make_resolver();
1721 assert!(r.rust_unwrap_binding("content", "result").is_none());
1722 }
1723
1724 #[test]
1725 fn test_direct_field_no_alias() {
1726 let r = make_resolver();
1727 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1728 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
1729 }
1730
1731 #[test]
1732 fn test_accessor_rust_with_optionals() {
1733 let r = make_resolver_with_doc_optional();
1734 assert_eq!(
1735 r.accessor("title", "rust", "result"),
1736 "result.metadata.document.as_ref().unwrap().title"
1737 );
1738 }
1739
1740 #[test]
1741 fn test_accessor_csharp_with_optionals() {
1742 let r = make_resolver_with_doc_optional();
1743 assert_eq!(
1744 r.accessor("title", "csharp", "result"),
1745 "result.Metadata.Document!.Title"
1746 );
1747 }
1748
1749 #[test]
1750 fn test_accessor_rust_non_optional_field() {
1751 let r = make_resolver();
1752 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1753 }
1754
1755 #[test]
1756 fn test_accessor_csharp_non_optional_field() {
1757 let r = make_resolver();
1758 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
1759 }
1760
1761 #[test]
1762 fn test_accessor_rust_method_call() {
1763 let mut fields = HashMap::new();
1765 fields.insert(
1766 "excel_sheet_count".to_string(),
1767 "metadata.format.excel.sheet_count".to_string(),
1768 );
1769 let mut optional = HashSet::new();
1770 optional.insert("metadata.format".to_string());
1771 optional.insert("metadata.format.excel".to_string());
1772 let mut method_calls = HashSet::new();
1773 method_calls.insert("metadata.format.excel".to_string());
1774 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
1775 assert_eq!(
1776 r.accessor("excel_sheet_count", "rust", "result"),
1777 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
1778 );
1779 }
1780}