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