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 {
819 let camel = name.to_lower_camel_case();
820 match camel.as_str() {
821 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
822 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
823 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
824 _ => camel,
825 }
826}
827
828fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
829 let mut out = result_var.to_string();
830 for seg in segments {
831 match seg {
832 PathSegment::Field(f) => {
833 out.push('.');
834 out.push_str(&kotlin_getter(f));
835 out.push_str("()");
836 }
837 PathSegment::ArrayField { name, index } => {
838 out.push('.');
839 out.push_str(&kotlin_getter(name));
840 if *index == 0 {
841 out.push_str("().first()");
842 } else {
843 out.push_str(&format!("().get({index})"));
844 }
845 }
846 PathSegment::MapAccess { field, key } => {
847 out.push('.');
848 out.push_str(&kotlin_getter(field));
849 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
850 if is_numeric {
851 out.push_str(&format!("().get({key})"));
852 } else {
853 out.push_str(&format!("().get(\"{key}\")"));
854 }
855 }
856 PathSegment::Length => {
857 out.push_str(".size");
858 }
859 }
860 }
861 out
862}
863
864fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
865 let mut out = result_var.to_string();
866 let mut path_so_far = String::new();
867 for (i, seg) in segments.iter().enumerate() {
868 let is_leaf = i == segments.len() - 1;
869 match seg {
870 PathSegment::Field(f) => {
871 if !path_so_far.is_empty() {
872 path_so_far.push('.');
873 }
874 path_so_far.push_str(f);
875 out.push('.');
876 out.push_str(&f.to_lower_camel_case());
877 out.push_str("()");
878 let _ = is_leaf;
879 let _ = optional_fields;
880 }
881 PathSegment::ArrayField { name, index } => {
882 if !path_so_far.is_empty() {
883 path_so_far.push('.');
884 }
885 path_so_far.push_str(name);
886 out.push('.');
887 out.push_str(&name.to_lower_camel_case());
888 out.push_str(&format!("().get({index})"));
889 }
890 PathSegment::MapAccess { field, key } => {
891 if !path_so_far.is_empty() {
892 path_so_far.push('.');
893 }
894 path_so_far.push_str(field);
895 out.push('.');
896 out.push_str(&field.to_lower_camel_case());
897 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
899 if is_numeric {
900 out.push_str(&format!("().get({key})"));
901 } else {
902 out.push_str(&format!("().get(\"{key}\")"));
903 }
904 }
905 PathSegment::Length => {
906 out.push_str(".size()");
907 }
908 }
909 }
910 out
911}
912
913fn render_kotlin_with_optionals(
928 segments: &[PathSegment],
929 result_var: &str,
930 optional_fields: &HashSet<String>,
931) -> String {
932 let mut out = result_var.to_string();
933 let mut path_so_far = String::new();
934 let mut prev_was_nullable = false;
942 for seg in segments {
943 let nav = if prev_was_nullable { "?." } else { "." };
944 match seg {
945 PathSegment::Field(f) => {
946 if !path_so_far.is_empty() {
947 path_so_far.push('.');
948 }
949 path_so_far.push_str(f);
950 let is_optional = optional_fields.contains(&path_so_far);
955 out.push_str(nav);
956 out.push_str(&kotlin_getter(f));
957 out.push_str("()");
958 prev_was_nullable = prev_was_nullable || is_optional;
959 }
960 PathSegment::ArrayField { name, index } => {
961 if !path_so_far.is_empty() {
962 path_so_far.push('.');
963 }
964 path_so_far.push_str(name);
965 let is_optional = optional_fields.contains(&path_so_far);
966 out.push_str(nav);
967 out.push_str(&kotlin_getter(name));
968 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
969 if *index == 0 {
970 out.push_str(&format!("(){safe}.first()"));
971 } else {
972 out.push_str(&format!("(){safe}.get({index})"));
973 }
974 path_so_far.push_str("[0]");
978 prev_was_nullable = prev_was_nullable || is_optional;
979 }
980 PathSegment::MapAccess { field, key } => {
981 if !path_so_far.is_empty() {
982 path_so_far.push('.');
983 }
984 path_so_far.push_str(field);
985 let is_optional = optional_fields.contains(&path_so_far);
986 out.push_str(nav);
987 out.push_str(&kotlin_getter(field));
988 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
989 if is_numeric {
990 if prev_was_nullable || is_optional {
991 out.push_str(&format!("()?.get({key})"));
992 } else {
993 out.push_str(&format!("().get({key})"));
994 }
995 } else if prev_was_nullable || is_optional {
996 out.push_str(&format!("()?.get(\"{key}\")"));
997 } else {
998 out.push_str(&format!("().get(\"{key}\")"));
999 }
1000 prev_was_nullable = prev_was_nullable || is_optional;
1001 }
1002 PathSegment::Length => {
1003 let size_nav = if prev_was_nullable { "?" } else { "" };
1006 out.push_str(&format!("{size_nav}.size"));
1007 prev_was_nullable = false;
1008 }
1009 }
1010 }
1011 out
1012}
1013
1014fn render_rust_with_optionals(
1020 segments: &[PathSegment],
1021 result_var: &str,
1022 optional_fields: &HashSet<String>,
1023 method_calls: &HashSet<String>,
1024) -> String {
1025 let mut out = result_var.to_string();
1026 let mut path_so_far = String::new();
1027 for (i, seg) in segments.iter().enumerate() {
1028 let is_leaf = i == segments.len() - 1;
1029 match seg {
1030 PathSegment::Field(f) => {
1031 if !path_so_far.is_empty() {
1032 path_so_far.push('.');
1033 }
1034 path_so_far.push_str(f);
1035 out.push('.');
1036 out.push_str(&f.to_snake_case());
1037 let is_method = method_calls.contains(&path_so_far);
1038 if is_method {
1039 out.push_str("()");
1040 if !is_leaf && optional_fields.contains(&path_so_far) {
1041 out.push_str(".as_ref().unwrap()");
1042 }
1043 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1044 out.push_str(".as_ref().unwrap()");
1045 }
1046 }
1047 PathSegment::ArrayField { name, index } => {
1048 if !path_so_far.is_empty() {
1049 path_so_far.push('.');
1050 }
1051 path_so_far.push_str(name);
1052 out.push('.');
1053 out.push_str(&name.to_snake_case());
1054 let path_with_idx = format!("{path_so_far}[0]");
1058 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1059 if is_opt {
1060 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1061 } else {
1062 out.push_str(&format!("[{index}]"));
1063 }
1064 path_so_far.push_str("[0]");
1069 }
1070 PathSegment::MapAccess { field, key } => {
1071 if !path_so_far.is_empty() {
1072 path_so_far.push('.');
1073 }
1074 path_so_far.push_str(field);
1075 out.push('.');
1076 out.push_str(&field.to_snake_case());
1077 if key.chars().all(|c| c.is_ascii_digit()) {
1078 let path_with_idx = format!("{path_so_far}[0]");
1080 let is_opt =
1081 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1082 if is_opt {
1083 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1084 } else {
1085 out.push_str(&format!("[{key}]"));
1086 }
1087 path_so_far.push_str("[0]");
1088 } else {
1089 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1090 }
1091 }
1092 PathSegment::Length => {
1093 out.push_str(".len()");
1094 }
1095 }
1096 }
1097 out
1098}
1099
1100fn render_zig_with_optionals(
1113 segments: &[PathSegment],
1114 result_var: &str,
1115 optional_fields: &HashSet<String>,
1116 method_calls: &HashSet<String>,
1117) -> String {
1118 let mut out = result_var.to_string();
1119 let mut path_so_far = String::new();
1120 for seg in segments {
1121 match seg {
1122 PathSegment::Field(f) => {
1123 if !path_so_far.is_empty() {
1124 path_so_far.push('.');
1125 }
1126 path_so_far.push_str(f);
1127 out.push('.');
1128 out.push_str(f);
1129 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1130 out.push_str(".?");
1131 }
1132 }
1133 PathSegment::ArrayField { name, index } => {
1134 if !path_so_far.is_empty() {
1135 path_so_far.push('.');
1136 }
1137 path_so_far.push_str(name);
1138 out.push('.');
1139 out.push_str(name);
1140 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1141 out.push_str(".?");
1142 }
1143 out.push_str(&format!("[{index}]"));
1144 }
1145 PathSegment::MapAccess { field, key } => {
1146 if !path_so_far.is_empty() {
1147 path_so_far.push('.');
1148 }
1149 path_so_far.push_str(field);
1150 out.push('.');
1151 out.push_str(field);
1152 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1153 out.push_str(".?");
1154 }
1155 if key.chars().all(|c| c.is_ascii_digit()) {
1156 out.push_str(&format!("[{key}]"));
1157 } else {
1158 out.push_str(&format!(".get(\"{key}\")"));
1159 }
1160 }
1161 PathSegment::Length => {
1162 out.push_str(".len");
1163 }
1164 }
1165 }
1166 out
1167}
1168
1169fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1170 let mut out = result_var.to_string();
1171 for seg in segments {
1172 match seg {
1173 PathSegment::Field(f) => {
1174 out.push('.');
1175 out.push_str(&f.to_pascal_case());
1176 }
1177 PathSegment::ArrayField { name, index } => {
1178 out.push('.');
1179 out.push_str(&name.to_pascal_case());
1180 out.push_str(&format!("[{index}]"));
1181 }
1182 PathSegment::MapAccess { field, key } => {
1183 out.push('.');
1184 out.push_str(&field.to_pascal_case());
1185 if key.chars().all(|c| c.is_ascii_digit()) {
1186 out.push_str(&format!("[{key}]"));
1187 } else {
1188 out.push_str(&format!("[\"{key}\"]"));
1189 }
1190 }
1191 PathSegment::Length => {
1192 out.push_str(".Count");
1193 }
1194 }
1195 }
1196 out
1197}
1198
1199fn render_csharp_with_optionals(
1200 segments: &[PathSegment],
1201 result_var: &str,
1202 optional_fields: &HashSet<String>,
1203) -> String {
1204 let mut out = result_var.to_string();
1205 let mut path_so_far = String::new();
1206 for (i, seg) in segments.iter().enumerate() {
1207 let is_leaf = i == segments.len() - 1;
1208 match seg {
1209 PathSegment::Field(f) => {
1210 if !path_so_far.is_empty() {
1211 path_so_far.push('.');
1212 }
1213 path_so_far.push_str(f);
1214 out.push('.');
1215 out.push_str(&f.to_pascal_case());
1216 if !is_leaf && optional_fields.contains(&path_so_far) {
1217 out.push('!');
1218 }
1219 }
1220 PathSegment::ArrayField { name, index } => {
1221 if !path_so_far.is_empty() {
1222 path_so_far.push('.');
1223 }
1224 path_so_far.push_str(name);
1225 out.push('.');
1226 out.push_str(&name.to_pascal_case());
1227 out.push_str(&format!("[{index}]"));
1228 }
1229 PathSegment::MapAccess { field, key } => {
1230 if !path_so_far.is_empty() {
1231 path_so_far.push('.');
1232 }
1233 path_so_far.push_str(field);
1234 out.push('.');
1235 out.push_str(&field.to_pascal_case());
1236 if key.chars().all(|c| c.is_ascii_digit()) {
1237 out.push_str(&format!("[{key}]"));
1238 } else {
1239 out.push_str(&format!("[\"{key}\"]"));
1240 }
1241 }
1242 PathSegment::Length => {
1243 out.push_str(".Count");
1244 }
1245 }
1246 }
1247 out
1248}
1249
1250fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1251 let mut out = result_var.to_string();
1252 for seg in segments {
1253 match seg {
1254 PathSegment::Field(f) => {
1255 out.push_str("->");
1256 out.push_str(&f.to_lower_camel_case());
1259 }
1260 PathSegment::ArrayField { name, index } => {
1261 out.push_str("->");
1262 out.push_str(&name.to_lower_camel_case());
1263 out.push_str(&format!("[{index}]"));
1264 }
1265 PathSegment::MapAccess { field, key } => {
1266 out.push_str("->");
1267 out.push_str(&field.to_lower_camel_case());
1268 out.push_str(&format!("[\"{key}\"]"));
1269 }
1270 PathSegment::Length => {
1271 let current = std::mem::take(&mut out);
1272 out = format!("count({current})");
1273 }
1274 }
1275 }
1276 out
1277}
1278
1279fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1280 let mut out = result_var.to_string();
1281 for seg in segments {
1282 match seg {
1283 PathSegment::Field(f) => {
1284 out.push('$');
1285 out.push_str(f);
1286 }
1287 PathSegment::ArrayField { name, index } => {
1288 out.push('$');
1289 out.push_str(name);
1290 out.push_str(&format!("[[{}]]", index + 1));
1292 }
1293 PathSegment::MapAccess { field, key } => {
1294 out.push('$');
1295 out.push_str(field);
1296 out.push_str(&format!("[[\"{key}\"]]"));
1297 }
1298 PathSegment::Length => {
1299 let current = std::mem::take(&mut out);
1300 out = format!("length({current})");
1301 }
1302 }
1303 }
1304 out
1305}
1306
1307fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1308 let mut parts = Vec::new();
1309 let mut trailing_length = false;
1310 for seg in segments {
1311 match seg {
1312 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1313 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1314 PathSegment::MapAccess { field, key } => {
1315 parts.push(field.to_snake_case());
1316 parts.push(key.clone());
1317 }
1318 PathSegment::Length => {
1319 trailing_length = true;
1320 }
1321 }
1322 }
1323 let suffix = parts.join("_");
1324 if trailing_length {
1325 format!("result_{suffix}_count({result_var})")
1326 } else {
1327 format!("result_{suffix}({result_var})")
1328 }
1329}
1330
1331fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
1338 let mut out = result_var.to_string();
1339 for seg in segments {
1340 match seg {
1341 PathSegment::Field(f) => {
1342 out.push('.');
1343 out.push_str(&f.to_lower_camel_case());
1344 }
1345 PathSegment::ArrayField { name, index } => {
1346 out.push('.');
1347 out.push_str(&name.to_lower_camel_case());
1348 out.push_str(&format!("[{index}]"));
1349 }
1350 PathSegment::MapAccess { field, key } => {
1351 out.push('.');
1352 out.push_str(&field.to_lower_camel_case());
1353 if key.chars().all(|c| c.is_ascii_digit()) {
1354 out.push_str(&format!("[{key}]"));
1355 } else {
1356 out.push_str(&format!("[\"{key}\"]"));
1357 }
1358 }
1359 PathSegment::Length => {
1360 out.push_str(".length");
1361 }
1362 }
1363 }
1364 out
1365}
1366
1367fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1373 let mut out = result_var.to_string();
1374 let mut path_so_far = String::new();
1375 let mut prev_was_nullable = false;
1376 for seg in segments {
1377 let nav = if prev_was_nullable { "?." } else { "." };
1378 match seg {
1379 PathSegment::Field(f) => {
1380 if !path_so_far.is_empty() {
1381 path_so_far.push('.');
1382 }
1383 path_so_far.push_str(f);
1384 let is_optional = optional_fields.contains(&path_so_far);
1385 out.push_str(nav);
1386 out.push_str(&f.to_lower_camel_case());
1387 prev_was_nullable = is_optional;
1388 }
1389 PathSegment::ArrayField { name, index } => {
1390 if !path_so_far.is_empty() {
1391 path_so_far.push('.');
1392 }
1393 path_so_far.push_str(name);
1394 let is_optional = optional_fields.contains(&path_so_far);
1395 out.push_str(nav);
1396 out.push_str(&name.to_lower_camel_case());
1397 if is_optional {
1401 out.push('!');
1402 }
1403 out.push_str(&format!("[{index}]"));
1404 prev_was_nullable = false;
1405 }
1406 PathSegment::MapAccess { field, key } => {
1407 if !path_so_far.is_empty() {
1408 path_so_far.push('.');
1409 }
1410 path_so_far.push_str(field);
1411 let is_optional = optional_fields.contains(&path_so_far);
1412 out.push_str(nav);
1413 out.push_str(&field.to_lower_camel_case());
1414 if key.chars().all(|c| c.is_ascii_digit()) {
1415 out.push_str(&format!("[{key}]"));
1416 } else {
1417 out.push_str(&format!("[\"{key}\"]"));
1418 }
1419 prev_was_nullable = is_optional;
1420 }
1421 PathSegment::Length => {
1422 out.push_str(nav);
1425 out.push_str("length");
1426 prev_was_nullable = false;
1427 }
1428 }
1429 }
1430 out
1431}
1432
1433#[cfg(test)]
1434mod tests {
1435 use super::*;
1436
1437 fn make_resolver() -> FieldResolver {
1438 let mut fields = HashMap::new();
1439 fields.insert("title".to_string(), "metadata.document.title".to_string());
1440 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1441 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
1442 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
1443 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
1444 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
1445 let mut optional = HashSet::new();
1446 optional.insert("metadata.document.title".to_string());
1447 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1448 }
1449
1450 fn make_resolver_with_doc_optional() -> FieldResolver {
1451 let mut fields = HashMap::new();
1452 fields.insert("title".to_string(), "metadata.document.title".to_string());
1453 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1454 let mut optional = HashSet::new();
1455 optional.insert("document".to_string());
1456 optional.insert("metadata.document.title".to_string());
1457 optional.insert("metadata.document".to_string());
1458 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1459 }
1460
1461 #[test]
1462 fn test_resolve_alias() {
1463 let r = make_resolver();
1464 assert_eq!(r.resolve("title"), "metadata.document.title");
1465 }
1466
1467 #[test]
1468 fn test_resolve_passthrough() {
1469 let r = make_resolver();
1470 assert_eq!(r.resolve("content"), "content");
1471 }
1472
1473 #[test]
1474 fn test_is_optional() {
1475 let r = make_resolver();
1476 assert!(r.is_optional("metadata.document.title"));
1477 assert!(!r.is_optional("content"));
1478 }
1479
1480 #[test]
1481 fn test_accessor_rust_struct() {
1482 let r = make_resolver();
1483 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
1484 }
1485
1486 #[test]
1487 fn test_accessor_rust_map() {
1488 let r = make_resolver();
1489 assert_eq!(
1490 r.accessor("tags", "rust", "result"),
1491 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
1492 );
1493 }
1494
1495 #[test]
1496 fn test_accessor_python() {
1497 let r = make_resolver();
1498 assert_eq!(
1499 r.accessor("title", "python", "result"),
1500 "result.metadata.document.title"
1501 );
1502 }
1503
1504 #[test]
1505 fn test_accessor_go() {
1506 let r = make_resolver();
1507 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
1508 }
1509
1510 #[test]
1511 fn test_accessor_go_initialism_fields() {
1512 let mut fields = std::collections::HashMap::new();
1513 fields.insert("content".to_string(), "html".to_string());
1514 fields.insert("link_url".to_string(), "links.url".to_string());
1515 let r = FieldResolver::new(
1516 &fields,
1517 &HashSet::new(),
1518 &HashSet::new(),
1519 &HashSet::new(),
1520 &HashSet::new(),
1521 );
1522 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
1523 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
1524 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
1525 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
1526 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
1527 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
1528 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
1529 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
1530 }
1531
1532 #[test]
1533 fn test_accessor_typescript() {
1534 let r = make_resolver();
1535 assert_eq!(
1536 r.accessor("title", "typescript", "result"),
1537 "result.metadata.document.title"
1538 );
1539 }
1540
1541 #[test]
1542 fn test_accessor_typescript_snake_to_camel() {
1543 let r = make_resolver();
1544 assert_eq!(
1545 r.accessor("og", "typescript", "result"),
1546 "result.metadata.document.openGraph"
1547 );
1548 assert_eq!(
1549 r.accessor("twitter", "typescript", "result"),
1550 "result.metadata.document.twitterCard"
1551 );
1552 assert_eq!(
1553 r.accessor("canonical", "typescript", "result"),
1554 "result.metadata.document.canonicalUrl"
1555 );
1556 }
1557
1558 #[test]
1559 fn test_accessor_typescript_map_snake_to_camel() {
1560 let r = make_resolver();
1561 assert_eq!(
1562 r.accessor("og_tag", "typescript", "result"),
1563 "result.metadata.openGraphTags[\"og_title\"]"
1564 );
1565 }
1566
1567 #[test]
1568 fn test_accessor_typescript_numeric_index_is_unquoted() {
1569 let mut fields = HashMap::new();
1573 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
1574 let r = FieldResolver::new(
1575 &fields,
1576 &HashSet::new(),
1577 &HashSet::new(),
1578 &HashSet::new(),
1579 &HashSet::new(),
1580 );
1581 assert_eq!(
1582 r.accessor("first_score", "typescript", "result"),
1583 "result.results[0].relevanceScore"
1584 );
1585 }
1586
1587 #[test]
1588 fn test_accessor_node_alias() {
1589 let r = make_resolver();
1590 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
1591 }
1592
1593 #[test]
1594 fn test_accessor_wasm_camel_case() {
1595 let r = make_resolver();
1596 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
1597 assert_eq!(
1598 r.accessor("twitter", "wasm", "result"),
1599 "result.metadata.document.twitterCard"
1600 );
1601 assert_eq!(
1602 r.accessor("canonical", "wasm", "result"),
1603 "result.metadata.document.canonicalUrl"
1604 );
1605 }
1606
1607 #[test]
1608 fn test_accessor_wasm_map_access() {
1609 let r = make_resolver();
1610 assert_eq!(
1611 r.accessor("og_tag", "wasm", "result"),
1612 "result.metadata.openGraphTags.get(\"og_title\")"
1613 );
1614 }
1615
1616 #[test]
1617 fn test_accessor_java() {
1618 let r = make_resolver();
1619 assert_eq!(
1620 r.accessor("title", "java", "result"),
1621 "result.metadata().document().title()"
1622 );
1623 }
1624
1625 #[test]
1626 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
1627 let mut fields = HashMap::new();
1628 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1629 fields.insert("node_count".to_string(), "nodes.length".to_string());
1630 let mut arrays = HashSet::new();
1631 arrays.insert("nodes".to_string());
1632 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
1633 assert_eq!(
1634 r.accessor("first_node_name", "kotlin", "result"),
1635 "result.nodes().first().name()"
1636 );
1637 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
1638 }
1639
1640 #[test]
1641 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
1642 let r = make_resolver_with_doc_optional();
1643 assert_eq!(
1644 r.accessor("title", "kotlin", "result"),
1645 "result.metadata().document()?.title()"
1646 );
1647 }
1648
1649 #[test]
1650 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
1651 let mut fields = HashMap::new();
1652 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1653 fields.insert("tag".to_string(), "tags[name]".to_string());
1654 let mut optional = HashSet::new();
1655 optional.insert("nodes".to_string());
1656 optional.insert("tags".to_string());
1657 let mut arrays = HashSet::new();
1658 arrays.insert("nodes".to_string());
1659 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
1660 assert_eq!(
1661 r.accessor("first_node_name", "kotlin", "result"),
1662 "result.nodes()?.first()?.name()"
1663 );
1664 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
1665 }
1666
1667 #[test]
1673 fn test_accessor_kotlin_optional_field_after_indexed_array() {
1674 let mut fields = HashMap::new();
1677 fields.insert(
1678 "tool_call_name".to_string(),
1679 "choices[0].message.tool_calls[0].function.name".to_string(),
1680 );
1681 let mut optional = HashSet::new();
1682 optional.insert("choices[0].message.tool_calls".to_string());
1683 let mut arrays = HashSet::new();
1684 arrays.insert("choices".to_string());
1685 arrays.insert("choices[0].message.tool_calls".to_string());
1686 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
1687 let expr = r.accessor("tool_call_name", "kotlin", "result");
1688 assert!(
1690 expr.contains("toolCalls()?.first()"),
1691 "expected toolCalls()?.first() for optional list, got: {expr}"
1692 );
1693 }
1694
1695 #[test]
1696 fn test_accessor_csharp() {
1697 let r = make_resolver();
1698 assert_eq!(
1699 r.accessor("title", "csharp", "result"),
1700 "result.Metadata.Document.Title"
1701 );
1702 }
1703
1704 #[test]
1705 fn test_accessor_php() {
1706 let r = make_resolver();
1707 assert_eq!(
1708 r.accessor("title", "php", "$result"),
1709 "$result->metadata->document->title"
1710 );
1711 }
1712
1713 #[test]
1714 fn test_accessor_r() {
1715 let r = make_resolver();
1716 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
1717 }
1718
1719 #[test]
1720 fn test_accessor_c() {
1721 let r = make_resolver();
1722 assert_eq!(
1723 r.accessor("title", "c", "result"),
1724 "result_metadata_document_title(result)"
1725 );
1726 }
1727
1728 #[test]
1729 fn test_rust_unwrap_binding() {
1730 let r = make_resolver();
1731 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
1732 assert_eq!(var, "metadata_document_title");
1733 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
1734 }
1735
1736 #[test]
1737 fn test_rust_unwrap_binding_non_optional() {
1738 let r = make_resolver();
1739 assert!(r.rust_unwrap_binding("content", "result").is_none());
1740 }
1741
1742 #[test]
1743 fn test_direct_field_no_alias() {
1744 let r = make_resolver();
1745 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1746 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
1747 }
1748
1749 #[test]
1750 fn test_accessor_rust_with_optionals() {
1751 let r = make_resolver_with_doc_optional();
1752 assert_eq!(
1753 r.accessor("title", "rust", "result"),
1754 "result.metadata.document.as_ref().unwrap().title"
1755 );
1756 }
1757
1758 #[test]
1759 fn test_accessor_csharp_with_optionals() {
1760 let r = make_resolver_with_doc_optional();
1761 assert_eq!(
1762 r.accessor("title", "csharp", "result"),
1763 "result.Metadata.Document!.Title"
1764 );
1765 }
1766
1767 #[test]
1768 fn test_accessor_rust_non_optional_field() {
1769 let r = make_resolver();
1770 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1771 }
1772
1773 #[test]
1774 fn test_accessor_csharp_non_optional_field() {
1775 let r = make_resolver();
1776 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
1777 }
1778
1779 #[test]
1780 fn test_accessor_rust_method_call() {
1781 let mut fields = HashMap::new();
1783 fields.insert(
1784 "excel_sheet_count".to_string(),
1785 "metadata.format.excel.sheet_count".to_string(),
1786 );
1787 let mut optional = HashSet::new();
1788 optional.insert("metadata.format".to_string());
1789 optional.insert("metadata.format.excel".to_string());
1790 let mut method_calls = HashSet::new();
1791 method_calls.insert("metadata.format.excel".to_string());
1792 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
1793 assert_eq!(
1794 r.accessor("excel_sheet_count", "rust", "result"),
1795 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
1796 );
1797 }
1798}