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 = {
344 let raw = resolved.replace(['.', '['], "_").replace(']', "");
345 let mut collapsed = String::with_capacity(raw.len());
346 let mut prev_underscore = false;
347 for ch in raw.chars() {
348 if ch == '_' {
349 if !prev_underscore {
350 collapsed.push('_');
351 }
352 prev_underscore = true;
353 } else {
354 collapsed.push(ch);
355 prev_underscore = false;
356 }
357 }
358 collapsed.trim_matches('_').to_string()
359 };
360 let accessor = render_accessor(&segments, "rust", result_var);
361 let has_map_access = segments.iter().any(|s| {
362 if let PathSegment::MapAccess { key, .. } = s {
363 !key.chars().all(|c| c.is_ascii_digit())
364 } else {
365 false
366 }
367 });
368 let is_array = self.is_array(resolved);
369 let binding = if has_map_access {
370 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
371 } else if is_array {
372 format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
373 } else {
374 format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
375 };
376 Some((binding, local_var))
377 }
378}
379
380fn strip_numeric_indices(path: &str) -> String {
385 let mut result = String::with_capacity(path.len());
386 let mut chars = path.chars().peekable();
387 while let Some(c) = chars.next() {
388 if c == '[' {
389 let mut key = String::new();
390 let mut closed = false;
391 for inner in chars.by_ref() {
392 if inner == ']' {
393 closed = true;
394 break;
395 }
396 key.push(inner);
397 }
398 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
399 } else {
401 result.push('[');
402 result.push_str(&key);
403 if closed {
404 result.push(']');
405 }
406 }
407 } else {
408 result.push(c);
409 }
410 }
411 while result.contains("..") {
413 result = result.replace("..", ".");
414 }
415 if result.starts_with('.') {
416 result.remove(0);
417 }
418 result
419}
420
421fn normalize_numeric_indices(path: &str) -> String {
422 let mut result = String::with_capacity(path.len());
423 let mut chars = path.chars().peekable();
424 while let Some(c) = chars.next() {
425 if c == '[' {
426 let mut key = String::new();
427 let mut closed = false;
428 for inner in chars.by_ref() {
429 if inner == ']' {
430 closed = true;
431 break;
432 }
433 key.push(inner);
434 }
435 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
436 result.push_str("[0]");
437 } else {
438 result.push('[');
439 result.push_str(&key);
440 if closed {
441 result.push(']');
442 }
443 }
444 } else {
445 result.push(c);
446 }
447 }
448 result
449}
450
451fn parse_path(path: &str) -> Vec<PathSegment> {
452 let mut segments = Vec::new();
453 for part in path.split('.') {
454 if part == "length" || part == "count" || part == "size" {
455 segments.push(PathSegment::Length);
456 } else if let Some(bracket_pos) = part.find('[') {
457 let name = part[..bracket_pos].to_string();
458 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
459 if key.is_empty() {
460 segments.push(PathSegment::ArrayField { name, index: 0 });
462 } else if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
463 let index: usize = key.parse().unwrap_or(0);
465 segments.push(PathSegment::ArrayField { name, index });
466 } else {
467 segments.push(PathSegment::MapAccess { field: name, key });
469 }
470 } else {
471 segments.push(PathSegment::Field(part.to_string()));
472 }
473 }
474 segments
475}
476
477fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
478 match language {
479 "rust" => render_rust(segments, result_var),
480 "python" => render_dot_access(segments, result_var, "python"),
481 "typescript" | "node" => render_typescript(segments, result_var),
482 "wasm" => render_wasm(segments, result_var),
483 "go" => render_go(segments, result_var),
484 "java" => render_java(segments, result_var),
485 "kotlin" => render_kotlin(segments, result_var),
486 "csharp" => render_pascal_dot(segments, result_var),
487 "ruby" => render_dot_access(segments, result_var, "ruby"),
488 "php" => render_php(segments, result_var),
489 "elixir" => render_dot_access(segments, result_var, "elixir"),
490 "r" => render_r(segments, result_var),
491 "c" => render_c(segments, result_var),
492 "swift" => render_swift(segments, result_var),
493 "dart" => render_dart(segments, result_var),
494 _ => render_dot_access(segments, result_var, language),
495 }
496}
497
498fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
504 let mut out = result_var.to_string();
505 for seg in segments {
506 match seg {
507 PathSegment::Field(f) => {
508 out.push('.');
509 out.push_str(f);
510 out.push_str("()");
511 }
512 PathSegment::ArrayField { name, index } => {
513 out.push('.');
514 out.push_str(name);
515 out.push_str(&format!("()[{index}]"));
516 }
517 PathSegment::MapAccess { field, key } => {
518 out.push('.');
519 out.push_str(field);
520 if key.chars().all(|c| c.is_ascii_digit()) {
521 out.push_str(&format!("()[{key}]"));
522 } else {
523 out.push_str(&format!("()[\"{key}\"]"));
524 }
525 }
526 PathSegment::Length => {
527 out.push_str(".count");
528 }
529 }
530 }
531 out
532}
533
534fn render_swift_with_optionals(
544 segments: &[PathSegment],
545 result_var: &str,
546 optional_fields: &HashSet<String>,
547) -> String {
548 let mut out = result_var.to_string();
549 let mut path_so_far = String::new();
550 let total = segments.len();
551 for (i, seg) in segments.iter().enumerate() {
552 let is_leaf = i == total - 1;
553 match seg {
554 PathSegment::Field(f) => {
555 if !path_so_far.is_empty() {
556 path_so_far.push('.');
557 }
558 path_so_far.push_str(f);
559 out.push('.');
560 out.push_str(f);
561 out.push_str("()");
562 if !is_leaf && optional_fields.contains(&path_so_far) {
565 out.push('?');
566 }
567 }
568 PathSegment::ArrayField { name, index } => {
569 if !path_so_far.is_empty() {
570 path_so_far.push('.');
571 }
572 path_so_far.push_str(name);
573 let is_optional = optional_fields.contains(&path_so_far);
577 out.push('.');
578 out.push_str(name);
579 if is_optional {
580 out.push_str(&format!("()?[{index}]"));
583 } else {
584 out.push_str(&format!("()[{index}]"));
585 }
586 path_so_far.push_str("[0]");
590 let _ = is_leaf;
597 }
598 PathSegment::MapAccess { field, key } => {
599 if !path_so_far.is_empty() {
600 path_so_far.push('.');
601 }
602 path_so_far.push_str(field);
603 out.push('.');
604 out.push_str(field);
605 if key.chars().all(|c| c.is_ascii_digit()) {
606 out.push_str(&format!("()[{key}]"));
607 } else {
608 out.push_str(&format!("()[\"{key}\"]"));
609 }
610 }
611 PathSegment::Length => {
612 out.push_str(".count");
613 }
614 }
615 }
616 out
617}
618
619fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
620 let mut out = result_var.to_string();
621 for seg in segments {
622 match seg {
623 PathSegment::Field(f) => {
624 out.push('.');
625 out.push_str(&f.to_snake_case());
626 }
627 PathSegment::ArrayField { name, index } => {
628 out.push('.');
629 out.push_str(&name.to_snake_case());
630 out.push_str(&format!("[{index}]"));
631 }
632 PathSegment::MapAccess { field, key } => {
633 out.push('.');
634 out.push_str(&field.to_snake_case());
635 if key.chars().all(|c| c.is_ascii_digit()) {
636 out.push_str(&format!("[{key}]"));
637 } else {
638 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
639 }
640 }
641 PathSegment::Length => {
642 out.push_str(".len()");
643 }
644 }
645 }
646 out
647}
648
649fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
650 let mut out = result_var.to_string();
651 for seg in segments {
652 match seg {
653 PathSegment::Field(f) => {
654 out.push('.');
655 out.push_str(f);
656 }
657 PathSegment::ArrayField { name, index } => {
658 if language == "elixir" {
659 let current = std::mem::take(&mut out);
660 out = format!("Enum.at({current}.{name}, {index})");
661 } else {
662 out.push('.');
663 out.push_str(name);
664 out.push_str(&format!("[{index}]"));
665 }
666 }
667 PathSegment::MapAccess { field, key } => {
668 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
669 if is_numeric && language == "elixir" {
670 let current = std::mem::take(&mut out);
671 out = format!("Enum.at({current}.{field}, {key})");
672 } else {
673 out.push('.');
674 out.push_str(field);
675 if is_numeric {
676 let idx: usize = key.parse().unwrap_or(0);
677 out.push_str(&format!("[{idx}]"));
678 } else if language == "elixir" || language == "ruby" {
679 out.push_str(&format!("[\"{key}\"]"));
682 } else {
683 out.push_str(&format!(".get(\"{key}\")"));
684 }
685 }
686 }
687 PathSegment::Length => match language {
688 "ruby" => out.push_str(".length"),
689 "elixir" => {
690 let current = std::mem::take(&mut out);
691 out = format!("length({current})");
692 }
693 "gleam" => {
694 let current = std::mem::take(&mut out);
695 out = format!("list.length({current})");
696 }
697 _ => {
698 let current = std::mem::take(&mut out);
699 out = format!("len({current})");
700 }
701 },
702 }
703 }
704 out
705}
706
707fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
708 let mut out = result_var.to_string();
709 for seg in segments {
710 match seg {
711 PathSegment::Field(f) => {
712 out.push('.');
713 out.push_str(&f.to_lower_camel_case());
714 }
715 PathSegment::ArrayField { name, index } => {
716 out.push('.');
717 out.push_str(&name.to_lower_camel_case());
718 out.push_str(&format!("[{index}]"));
719 }
720 PathSegment::MapAccess { field, key } => {
721 out.push('.');
722 out.push_str(&field.to_lower_camel_case());
723 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
726 out.push_str(&format!("[{key}]"));
727 } else {
728 out.push_str(&format!("[\"{key}\"]"));
729 }
730 }
731 PathSegment::Length => {
732 out.push_str(".length");
733 }
734 }
735 }
736 out
737}
738
739fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
740 let mut out = result_var.to_string();
741 for seg in segments {
742 match seg {
743 PathSegment::Field(f) => {
744 out.push('.');
745 out.push_str(&f.to_lower_camel_case());
746 }
747 PathSegment::ArrayField { name, index } => {
748 out.push('.');
749 out.push_str(&name.to_lower_camel_case());
750 out.push_str(&format!("[{index}]"));
751 }
752 PathSegment::MapAccess { field, key } => {
753 out.push('.');
754 out.push_str(&field.to_lower_camel_case());
755 out.push_str(&format!(".get(\"{key}\")"));
756 }
757 PathSegment::Length => {
758 out.push_str(".length");
759 }
760 }
761 }
762 out
763}
764
765fn render_go(segments: &[PathSegment], result_var: &str) -> String {
766 let mut out = result_var.to_string();
767 for seg in segments {
768 match seg {
769 PathSegment::Field(f) => {
770 out.push('.');
771 out.push_str(&to_go_name(f));
772 }
773 PathSegment::ArrayField { name, index } => {
774 out.push('.');
775 out.push_str(&to_go_name(name));
776 out.push_str(&format!("[{index}]"));
777 }
778 PathSegment::MapAccess { field, key } => {
779 out.push('.');
780 out.push_str(&to_go_name(field));
781 if key.chars().all(|c| c.is_ascii_digit()) {
782 out.push_str(&format!("[{key}]"));
783 } else {
784 out.push_str(&format!("[\"{key}\"]"));
785 }
786 }
787 PathSegment::Length => {
788 let current = std::mem::take(&mut out);
789 out = format!("len({current})");
790 }
791 }
792 }
793 out
794}
795
796fn render_java(segments: &[PathSegment], result_var: &str) -> String {
797 let mut out = result_var.to_string();
798 for seg in segments {
799 match seg {
800 PathSegment::Field(f) => {
801 out.push('.');
802 out.push_str(&f.to_lower_camel_case());
803 out.push_str("()");
804 }
805 PathSegment::ArrayField { name, index } => {
806 out.push('.');
807 out.push_str(&name.to_lower_camel_case());
808 out.push_str(&format!("().get({index})"));
809 }
810 PathSegment::MapAccess { field, key } => {
811 out.push('.');
812 out.push_str(&field.to_lower_camel_case());
813 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
815 if is_numeric {
816 out.push_str(&format!("().get({key})"));
817 } else {
818 out.push_str(&format!("().get(\"{key}\")"));
819 }
820 }
821 PathSegment::Length => {
822 out.push_str(".size()");
823 }
824 }
825 }
826 out
827}
828
829fn kotlin_getter(name: &str) -> String {
834 let camel = name.to_lower_camel_case();
835 match camel.as_str() {
836 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
837 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
838 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
839 _ => camel,
840 }
841}
842
843fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
844 let mut out = result_var.to_string();
845 for seg in segments {
846 match seg {
847 PathSegment::Field(f) => {
848 out.push('.');
849 out.push_str(&kotlin_getter(f));
850 out.push_str("()");
851 }
852 PathSegment::ArrayField { name, index } => {
853 out.push('.');
854 out.push_str(&kotlin_getter(name));
855 if *index == 0 {
856 out.push_str("().first()");
857 } else {
858 out.push_str(&format!("().get({index})"));
859 }
860 }
861 PathSegment::MapAccess { field, key } => {
862 out.push('.');
863 out.push_str(&kotlin_getter(field));
864 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
865 if is_numeric {
866 out.push_str(&format!("().get({key})"));
867 } else {
868 out.push_str(&format!("().get(\"{key}\")"));
869 }
870 }
871 PathSegment::Length => {
872 out.push_str(".size");
873 }
874 }
875 }
876 out
877}
878
879fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
880 let mut out = result_var.to_string();
881 let mut path_so_far = String::new();
882 for (i, seg) in segments.iter().enumerate() {
883 let is_leaf = i == segments.len() - 1;
884 match seg {
885 PathSegment::Field(f) => {
886 if !path_so_far.is_empty() {
887 path_so_far.push('.');
888 }
889 path_so_far.push_str(f);
890 out.push('.');
891 out.push_str(&f.to_lower_camel_case());
892 out.push_str("()");
893 let _ = is_leaf;
894 let _ = optional_fields;
895 }
896 PathSegment::ArrayField { name, index } => {
897 if !path_so_far.is_empty() {
898 path_so_far.push('.');
899 }
900 path_so_far.push_str(name);
901 out.push('.');
902 out.push_str(&name.to_lower_camel_case());
903 out.push_str(&format!("().get({index})"));
904 }
905 PathSegment::MapAccess { field, key } => {
906 if !path_so_far.is_empty() {
907 path_so_far.push('.');
908 }
909 path_so_far.push_str(field);
910 out.push('.');
911 out.push_str(&field.to_lower_camel_case());
912 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
914 if is_numeric {
915 out.push_str(&format!("().get({key})"));
916 } else {
917 out.push_str(&format!("().get(\"{key}\")"));
918 }
919 }
920 PathSegment::Length => {
921 out.push_str(".size()");
922 }
923 }
924 }
925 out
926}
927
928fn render_kotlin_with_optionals(
943 segments: &[PathSegment],
944 result_var: &str,
945 optional_fields: &HashSet<String>,
946) -> String {
947 let mut out = result_var.to_string();
948 let mut path_so_far = String::new();
949 let mut prev_was_nullable = false;
957 for seg in segments {
958 let nav = if prev_was_nullable { "?." } else { "." };
959 match seg {
960 PathSegment::Field(f) => {
961 if !path_so_far.is_empty() {
962 path_so_far.push('.');
963 }
964 path_so_far.push_str(f);
965 let is_optional = optional_fields.contains(&path_so_far);
970 out.push_str(nav);
971 out.push_str(&kotlin_getter(f));
972 out.push_str("()");
973 prev_was_nullable = prev_was_nullable || is_optional;
974 }
975 PathSegment::ArrayField { name, index } => {
976 if !path_so_far.is_empty() {
977 path_so_far.push('.');
978 }
979 path_so_far.push_str(name);
980 let is_optional = optional_fields.contains(&path_so_far);
981 out.push_str(nav);
982 out.push_str(&kotlin_getter(name));
983 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
984 if *index == 0 {
985 out.push_str(&format!("(){safe}.first()"));
986 } else {
987 out.push_str(&format!("(){safe}.get({index})"));
988 }
989 path_so_far.push_str("[0]");
993 prev_was_nullable = prev_was_nullable || is_optional;
994 }
995 PathSegment::MapAccess { field, key } => {
996 if !path_so_far.is_empty() {
997 path_so_far.push('.');
998 }
999 path_so_far.push_str(field);
1000 let is_optional = optional_fields.contains(&path_so_far);
1001 out.push_str(nav);
1002 out.push_str(&kotlin_getter(field));
1003 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1004 if is_numeric {
1005 if prev_was_nullable || is_optional {
1006 out.push_str(&format!("()?.get({key})"));
1007 } else {
1008 out.push_str(&format!("().get({key})"));
1009 }
1010 } else if prev_was_nullable || is_optional {
1011 out.push_str(&format!("()?.get(\"{key}\")"));
1012 } else {
1013 out.push_str(&format!("().get(\"{key}\")"));
1014 }
1015 prev_was_nullable = prev_was_nullable || is_optional;
1016 }
1017 PathSegment::Length => {
1018 let size_nav = if prev_was_nullable { "?" } else { "" };
1021 out.push_str(&format!("{size_nav}.size"));
1022 prev_was_nullable = false;
1023 }
1024 }
1025 }
1026 out
1027}
1028
1029fn render_rust_with_optionals(
1035 segments: &[PathSegment],
1036 result_var: &str,
1037 optional_fields: &HashSet<String>,
1038 method_calls: &HashSet<String>,
1039) -> String {
1040 let mut out = result_var.to_string();
1041 let mut path_so_far = String::new();
1042 for (i, seg) in segments.iter().enumerate() {
1043 let is_leaf = i == segments.len() - 1;
1044 match seg {
1045 PathSegment::Field(f) => {
1046 if !path_so_far.is_empty() {
1047 path_so_far.push('.');
1048 }
1049 path_so_far.push_str(f);
1050 out.push('.');
1051 out.push_str(&f.to_snake_case());
1052 let is_method = method_calls.contains(&path_so_far);
1053 if is_method {
1054 out.push_str("()");
1055 if !is_leaf && optional_fields.contains(&path_so_far) {
1056 out.push_str(".as_ref().unwrap()");
1057 }
1058 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1059 out.push_str(".as_ref().unwrap()");
1060 }
1061 }
1062 PathSegment::ArrayField { name, index } => {
1063 if !path_so_far.is_empty() {
1064 path_so_far.push('.');
1065 }
1066 path_so_far.push_str(name);
1067 out.push('.');
1068 out.push_str(&name.to_snake_case());
1069 let path_with_idx = format!("{path_so_far}[0]");
1073 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1074 if is_opt {
1075 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1076 } else {
1077 out.push_str(&format!("[{index}]"));
1078 }
1079 path_so_far.push_str("[0]");
1084 }
1085 PathSegment::MapAccess { field, key } => {
1086 if !path_so_far.is_empty() {
1087 path_so_far.push('.');
1088 }
1089 path_so_far.push_str(field);
1090 out.push('.');
1091 out.push_str(&field.to_snake_case());
1092 if key.chars().all(|c| c.is_ascii_digit()) {
1093 let path_with_idx = format!("{path_so_far}[0]");
1095 let is_opt =
1096 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1097 if is_opt {
1098 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1099 } else {
1100 out.push_str(&format!("[{key}]"));
1101 }
1102 path_so_far.push_str("[0]");
1103 } else {
1104 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1105 }
1106 }
1107 PathSegment::Length => {
1108 out.push_str(".len()");
1109 }
1110 }
1111 }
1112 out
1113}
1114
1115fn render_zig_with_optionals(
1128 segments: &[PathSegment],
1129 result_var: &str,
1130 optional_fields: &HashSet<String>,
1131 method_calls: &HashSet<String>,
1132) -> String {
1133 let mut out = result_var.to_string();
1134 let mut path_so_far = String::new();
1135 for seg in segments {
1136 match seg {
1137 PathSegment::Field(f) => {
1138 if !path_so_far.is_empty() {
1139 path_so_far.push('.');
1140 }
1141 path_so_far.push_str(f);
1142 out.push('.');
1143 out.push_str(f);
1144 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1145 out.push_str(".?");
1146 }
1147 }
1148 PathSegment::ArrayField { name, index } => {
1149 if !path_so_far.is_empty() {
1150 path_so_far.push('.');
1151 }
1152 path_so_far.push_str(name);
1153 out.push('.');
1154 out.push_str(name);
1155 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1156 out.push_str(".?");
1157 }
1158 out.push_str(&format!("[{index}]"));
1159 }
1160 PathSegment::MapAccess { field, key } => {
1161 if !path_so_far.is_empty() {
1162 path_so_far.push('.');
1163 }
1164 path_so_far.push_str(field);
1165 out.push('.');
1166 out.push_str(field);
1167 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1168 out.push_str(".?");
1169 }
1170 if key.chars().all(|c| c.is_ascii_digit()) {
1171 out.push_str(&format!("[{key}]"));
1172 } else {
1173 out.push_str(&format!(".get(\"{key}\")"));
1174 }
1175 }
1176 PathSegment::Length => {
1177 out.push_str(".len");
1178 }
1179 }
1180 }
1181 out
1182}
1183
1184fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1185 let mut out = result_var.to_string();
1186 for seg in segments {
1187 match seg {
1188 PathSegment::Field(f) => {
1189 out.push('.');
1190 out.push_str(&f.to_pascal_case());
1191 }
1192 PathSegment::ArrayField { name, index } => {
1193 out.push('.');
1194 out.push_str(&name.to_pascal_case());
1195 out.push_str(&format!("[{index}]"));
1196 }
1197 PathSegment::MapAccess { field, key } => {
1198 out.push('.');
1199 out.push_str(&field.to_pascal_case());
1200 if key.chars().all(|c| c.is_ascii_digit()) {
1201 out.push_str(&format!("[{key}]"));
1202 } else {
1203 out.push_str(&format!("[\"{key}\"]"));
1204 }
1205 }
1206 PathSegment::Length => {
1207 out.push_str(".Count");
1208 }
1209 }
1210 }
1211 out
1212}
1213
1214fn render_csharp_with_optionals(
1215 segments: &[PathSegment],
1216 result_var: &str,
1217 optional_fields: &HashSet<String>,
1218) -> String {
1219 let mut out = result_var.to_string();
1220 let mut path_so_far = String::new();
1221 for (i, seg) in segments.iter().enumerate() {
1222 let is_leaf = i == segments.len() - 1;
1223 match seg {
1224 PathSegment::Field(f) => {
1225 if !path_so_far.is_empty() {
1226 path_so_far.push('.');
1227 }
1228 path_so_far.push_str(f);
1229 out.push('.');
1230 out.push_str(&f.to_pascal_case());
1231 if !is_leaf && optional_fields.contains(&path_so_far) {
1232 out.push('!');
1233 }
1234 }
1235 PathSegment::ArrayField { name, index } => {
1236 if !path_so_far.is_empty() {
1237 path_so_far.push('.');
1238 }
1239 path_so_far.push_str(name);
1240 out.push('.');
1241 out.push_str(&name.to_pascal_case());
1242 out.push_str(&format!("[{index}]"));
1243 }
1244 PathSegment::MapAccess { field, key } => {
1245 if !path_so_far.is_empty() {
1246 path_so_far.push('.');
1247 }
1248 path_so_far.push_str(field);
1249 out.push('.');
1250 out.push_str(&field.to_pascal_case());
1251 if key.chars().all(|c| c.is_ascii_digit()) {
1252 out.push_str(&format!("[{key}]"));
1253 } else {
1254 out.push_str(&format!("[\"{key}\"]"));
1255 }
1256 }
1257 PathSegment::Length => {
1258 out.push_str(".Count");
1259 }
1260 }
1261 }
1262 out
1263}
1264
1265fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1266 let mut out = result_var.to_string();
1267 for seg in segments {
1268 match seg {
1269 PathSegment::Field(f) => {
1270 out.push_str("->");
1271 out.push_str(&f.to_lower_camel_case());
1274 }
1275 PathSegment::ArrayField { name, index } => {
1276 out.push_str("->");
1277 out.push_str(&name.to_lower_camel_case());
1278 out.push_str(&format!("[{index}]"));
1279 }
1280 PathSegment::MapAccess { field, key } => {
1281 out.push_str("->");
1282 out.push_str(&field.to_lower_camel_case());
1283 out.push_str(&format!("[\"{key}\"]"));
1284 }
1285 PathSegment::Length => {
1286 let current = std::mem::take(&mut out);
1287 out = format!("count({current})");
1288 }
1289 }
1290 }
1291 out
1292}
1293
1294fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1295 let mut out = result_var.to_string();
1296 for seg in segments {
1297 match seg {
1298 PathSegment::Field(f) => {
1299 out.push('$');
1300 out.push_str(f);
1301 }
1302 PathSegment::ArrayField { name, index } => {
1303 out.push('$');
1304 out.push_str(name);
1305 out.push_str(&format!("[[{}]]", index + 1));
1307 }
1308 PathSegment::MapAccess { field, key } => {
1309 out.push('$');
1310 out.push_str(field);
1311 out.push_str(&format!("[[\"{key}\"]]"));
1312 }
1313 PathSegment::Length => {
1314 let current = std::mem::take(&mut out);
1315 out = format!("length({current})");
1316 }
1317 }
1318 }
1319 out
1320}
1321
1322fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1323 let mut parts = Vec::new();
1324 let mut trailing_length = false;
1325 for seg in segments {
1326 match seg {
1327 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1328 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1329 PathSegment::MapAccess { field, key } => {
1330 parts.push(field.to_snake_case());
1331 parts.push(key.clone());
1332 }
1333 PathSegment::Length => {
1334 trailing_length = true;
1335 }
1336 }
1337 }
1338 let suffix = parts.join("_");
1339 if trailing_length {
1340 format!("result_{suffix}_count({result_var})")
1341 } else {
1342 format!("result_{suffix}({result_var})")
1343 }
1344}
1345
1346fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
1353 let mut out = result_var.to_string();
1354 for seg in segments {
1355 match seg {
1356 PathSegment::Field(f) => {
1357 out.push('.');
1358 out.push_str(&f.to_lower_camel_case());
1359 }
1360 PathSegment::ArrayField { name, index } => {
1361 out.push('.');
1362 out.push_str(&name.to_lower_camel_case());
1363 out.push_str(&format!("[{index}]"));
1364 }
1365 PathSegment::MapAccess { field, key } => {
1366 out.push('.');
1367 out.push_str(&field.to_lower_camel_case());
1368 if key.chars().all(|c| c.is_ascii_digit()) {
1369 out.push_str(&format!("[{key}]"));
1370 } else {
1371 out.push_str(&format!("[\"{key}\"]"));
1372 }
1373 }
1374 PathSegment::Length => {
1375 out.push_str(".length");
1376 }
1377 }
1378 }
1379 out
1380}
1381
1382fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1388 let mut out = result_var.to_string();
1389 let mut path_so_far = String::new();
1390 let mut prev_was_nullable = false;
1391 for seg in segments {
1392 let nav = if prev_was_nullable { "?." } else { "." };
1393 match seg {
1394 PathSegment::Field(f) => {
1395 if !path_so_far.is_empty() {
1396 path_so_far.push('.');
1397 }
1398 path_so_far.push_str(f);
1399 let is_optional = optional_fields.contains(&path_so_far);
1400 out.push_str(nav);
1401 out.push_str(&f.to_lower_camel_case());
1402 prev_was_nullable = is_optional;
1403 }
1404 PathSegment::ArrayField { name, index } => {
1405 if !path_so_far.is_empty() {
1406 path_so_far.push('.');
1407 }
1408 path_so_far.push_str(name);
1409 let is_optional = optional_fields.contains(&path_so_far);
1410 out.push_str(nav);
1411 out.push_str(&name.to_lower_camel_case());
1412 if is_optional {
1416 out.push('!');
1417 }
1418 out.push_str(&format!("[{index}]"));
1419 prev_was_nullable = false;
1420 }
1421 PathSegment::MapAccess { field, key } => {
1422 if !path_so_far.is_empty() {
1423 path_so_far.push('.');
1424 }
1425 path_so_far.push_str(field);
1426 let is_optional = optional_fields.contains(&path_so_far);
1427 out.push_str(nav);
1428 out.push_str(&field.to_lower_camel_case());
1429 if key.chars().all(|c| c.is_ascii_digit()) {
1430 out.push_str(&format!("[{key}]"));
1431 } else {
1432 out.push_str(&format!("[\"{key}\"]"));
1433 }
1434 prev_was_nullable = is_optional;
1435 }
1436 PathSegment::Length => {
1437 out.push_str(nav);
1440 out.push_str("length");
1441 prev_was_nullable = false;
1442 }
1443 }
1444 }
1445 out
1446}
1447
1448#[cfg(test)]
1449mod tests {
1450 use super::*;
1451
1452 fn make_resolver() -> FieldResolver {
1453 let mut fields = HashMap::new();
1454 fields.insert("title".to_string(), "metadata.document.title".to_string());
1455 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1456 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
1457 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
1458 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
1459 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
1460 let mut optional = HashSet::new();
1461 optional.insert("metadata.document.title".to_string());
1462 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1463 }
1464
1465 fn make_resolver_with_doc_optional() -> FieldResolver {
1466 let mut fields = HashMap::new();
1467 fields.insert("title".to_string(), "metadata.document.title".to_string());
1468 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1469 let mut optional = HashSet::new();
1470 optional.insert("document".to_string());
1471 optional.insert("metadata.document.title".to_string());
1472 optional.insert("metadata.document".to_string());
1473 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1474 }
1475
1476 #[test]
1477 fn test_resolve_alias() {
1478 let r = make_resolver();
1479 assert_eq!(r.resolve("title"), "metadata.document.title");
1480 }
1481
1482 #[test]
1483 fn test_resolve_passthrough() {
1484 let r = make_resolver();
1485 assert_eq!(r.resolve("content"), "content");
1486 }
1487
1488 #[test]
1489 fn test_is_optional() {
1490 let r = make_resolver();
1491 assert!(r.is_optional("metadata.document.title"));
1492 assert!(!r.is_optional("content"));
1493 }
1494
1495 #[test]
1496 fn test_accessor_rust_struct() {
1497 let r = make_resolver();
1498 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
1499 }
1500
1501 #[test]
1502 fn test_accessor_rust_map() {
1503 let r = make_resolver();
1504 assert_eq!(
1505 r.accessor("tags", "rust", "result"),
1506 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
1507 );
1508 }
1509
1510 #[test]
1511 fn test_accessor_python() {
1512 let r = make_resolver();
1513 assert_eq!(
1514 r.accessor("title", "python", "result"),
1515 "result.metadata.document.title"
1516 );
1517 }
1518
1519 #[test]
1520 fn test_accessor_go() {
1521 let r = make_resolver();
1522 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
1523 }
1524
1525 #[test]
1526 fn test_accessor_go_initialism_fields() {
1527 let mut fields = std::collections::HashMap::new();
1528 fields.insert("content".to_string(), "html".to_string());
1529 fields.insert("link_url".to_string(), "links.url".to_string());
1530 let r = FieldResolver::new(
1531 &fields,
1532 &HashSet::new(),
1533 &HashSet::new(),
1534 &HashSet::new(),
1535 &HashSet::new(),
1536 );
1537 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
1538 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
1539 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
1540 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
1541 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
1542 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
1543 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
1544 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
1545 }
1546
1547 #[test]
1548 fn test_accessor_typescript() {
1549 let r = make_resolver();
1550 assert_eq!(
1551 r.accessor("title", "typescript", "result"),
1552 "result.metadata.document.title"
1553 );
1554 }
1555
1556 #[test]
1557 fn test_accessor_typescript_snake_to_camel() {
1558 let r = make_resolver();
1559 assert_eq!(
1560 r.accessor("og", "typescript", "result"),
1561 "result.metadata.document.openGraph"
1562 );
1563 assert_eq!(
1564 r.accessor("twitter", "typescript", "result"),
1565 "result.metadata.document.twitterCard"
1566 );
1567 assert_eq!(
1568 r.accessor("canonical", "typescript", "result"),
1569 "result.metadata.document.canonicalUrl"
1570 );
1571 }
1572
1573 #[test]
1574 fn test_accessor_typescript_map_snake_to_camel() {
1575 let r = make_resolver();
1576 assert_eq!(
1577 r.accessor("og_tag", "typescript", "result"),
1578 "result.metadata.openGraphTags[\"og_title\"]"
1579 );
1580 }
1581
1582 #[test]
1583 fn test_accessor_typescript_numeric_index_is_unquoted() {
1584 let mut fields = HashMap::new();
1588 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
1589 let r = FieldResolver::new(
1590 &fields,
1591 &HashSet::new(),
1592 &HashSet::new(),
1593 &HashSet::new(),
1594 &HashSet::new(),
1595 );
1596 assert_eq!(
1597 r.accessor("first_score", "typescript", "result"),
1598 "result.results[0].relevanceScore"
1599 );
1600 }
1601
1602 #[test]
1603 fn test_accessor_node_alias() {
1604 let r = make_resolver();
1605 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
1606 }
1607
1608 #[test]
1609 fn test_accessor_wasm_camel_case() {
1610 let r = make_resolver();
1611 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
1612 assert_eq!(
1613 r.accessor("twitter", "wasm", "result"),
1614 "result.metadata.document.twitterCard"
1615 );
1616 assert_eq!(
1617 r.accessor("canonical", "wasm", "result"),
1618 "result.metadata.document.canonicalUrl"
1619 );
1620 }
1621
1622 #[test]
1623 fn test_accessor_wasm_map_access() {
1624 let r = make_resolver();
1625 assert_eq!(
1626 r.accessor("og_tag", "wasm", "result"),
1627 "result.metadata.openGraphTags.get(\"og_title\")"
1628 );
1629 }
1630
1631 #[test]
1632 fn test_accessor_java() {
1633 let r = make_resolver();
1634 assert_eq!(
1635 r.accessor("title", "java", "result"),
1636 "result.metadata().document().title()"
1637 );
1638 }
1639
1640 #[test]
1641 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
1642 let mut fields = HashMap::new();
1643 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1644 fields.insert("node_count".to_string(), "nodes.length".to_string());
1645 let mut arrays = HashSet::new();
1646 arrays.insert("nodes".to_string());
1647 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
1648 assert_eq!(
1649 r.accessor("first_node_name", "kotlin", "result"),
1650 "result.nodes().first().name()"
1651 );
1652 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
1653 }
1654
1655 #[test]
1656 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
1657 let r = make_resolver_with_doc_optional();
1658 assert_eq!(
1659 r.accessor("title", "kotlin", "result"),
1660 "result.metadata().document()?.title()"
1661 );
1662 }
1663
1664 #[test]
1665 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
1666 let mut fields = HashMap::new();
1667 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1668 fields.insert("tag".to_string(), "tags[name]".to_string());
1669 let mut optional = HashSet::new();
1670 optional.insert("nodes".to_string());
1671 optional.insert("tags".to_string());
1672 let mut arrays = HashSet::new();
1673 arrays.insert("nodes".to_string());
1674 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
1675 assert_eq!(
1676 r.accessor("first_node_name", "kotlin", "result"),
1677 "result.nodes()?.first()?.name()"
1678 );
1679 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
1680 }
1681
1682 #[test]
1688 fn test_accessor_kotlin_optional_field_after_indexed_array() {
1689 let mut fields = HashMap::new();
1692 fields.insert(
1693 "tool_call_name".to_string(),
1694 "choices[0].message.tool_calls[0].function.name".to_string(),
1695 );
1696 let mut optional = HashSet::new();
1697 optional.insert("choices[0].message.tool_calls".to_string());
1698 let mut arrays = HashSet::new();
1699 arrays.insert("choices".to_string());
1700 arrays.insert("choices[0].message.tool_calls".to_string());
1701 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
1702 let expr = r.accessor("tool_call_name", "kotlin", "result");
1703 assert!(
1705 expr.contains("toolCalls()?.first()"),
1706 "expected toolCalls()?.first() for optional list, got: {expr}"
1707 );
1708 }
1709
1710 #[test]
1711 fn test_accessor_csharp() {
1712 let r = make_resolver();
1713 assert_eq!(
1714 r.accessor("title", "csharp", "result"),
1715 "result.Metadata.Document.Title"
1716 );
1717 }
1718
1719 #[test]
1720 fn test_accessor_php() {
1721 let r = make_resolver();
1722 assert_eq!(
1723 r.accessor("title", "php", "$result"),
1724 "$result->metadata->document->title"
1725 );
1726 }
1727
1728 #[test]
1729 fn test_accessor_r() {
1730 let r = make_resolver();
1731 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
1732 }
1733
1734 #[test]
1735 fn test_accessor_c() {
1736 let r = make_resolver();
1737 assert_eq!(
1738 r.accessor("title", "c", "result"),
1739 "result_metadata_document_title(result)"
1740 );
1741 }
1742
1743 #[test]
1744 fn test_rust_unwrap_binding() {
1745 let r = make_resolver();
1746 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
1747 assert_eq!(var, "metadata_document_title");
1748 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
1749 }
1750
1751 #[test]
1752 fn test_rust_unwrap_binding_non_optional() {
1753 let r = make_resolver();
1754 assert!(r.rust_unwrap_binding("content", "result").is_none());
1755 }
1756
1757 #[test]
1758 fn test_rust_unwrap_binding_collapses_double_underscore() {
1759 let mut aliases = HashMap::new();
1764 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
1765 let mut optional = HashSet::new();
1766 optional.insert("json_ld[].name".to_string());
1767 let mut array = HashSet::new();
1768 array.insert("json_ld".to_string());
1769 let result_fields = HashSet::new();
1770 let method_calls = HashSet::new();
1771 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
1772 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
1773 assert_eq!(var, "json_ld_name");
1774 }
1775
1776 #[test]
1777 fn test_direct_field_no_alias() {
1778 let r = make_resolver();
1779 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1780 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
1781 }
1782
1783 #[test]
1784 fn test_accessor_rust_with_optionals() {
1785 let r = make_resolver_with_doc_optional();
1786 assert_eq!(
1787 r.accessor("title", "rust", "result"),
1788 "result.metadata.document.as_ref().unwrap().title"
1789 );
1790 }
1791
1792 #[test]
1793 fn test_accessor_csharp_with_optionals() {
1794 let r = make_resolver_with_doc_optional();
1795 assert_eq!(
1796 r.accessor("title", "csharp", "result"),
1797 "result.Metadata.Document!.Title"
1798 );
1799 }
1800
1801 #[test]
1802 fn test_accessor_rust_non_optional_field() {
1803 let r = make_resolver();
1804 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1805 }
1806
1807 #[test]
1808 fn test_accessor_csharp_non_optional_field() {
1809 let r = make_resolver();
1810 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
1811 }
1812
1813 #[test]
1814 fn test_accessor_rust_method_call() {
1815 let mut fields = HashMap::new();
1817 fields.insert(
1818 "excel_sheet_count".to_string(),
1819 "metadata.format.excel.sheet_count".to_string(),
1820 );
1821 let mut optional = HashSet::new();
1822 optional.insert("metadata.format".to_string());
1823 optional.insert("metadata.format.excel".to_string());
1824 let mut method_calls = HashSet::new();
1825 method_calls.insert("metadata.format.excel".to_string());
1826 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
1827 assert_eq!(
1828 r.accessor("excel_sheet_count", "rust", "result"),
1829 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
1830 );
1831 }
1832}