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 php_getter_map: PhpGetterMap,
33 swift_first_class_map: SwiftFirstClassMap,
40}
41
42#[derive(Debug, Clone, Default)]
57pub struct PhpGetterMap {
58 pub getters: HashMap<String, HashSet<String>>,
59 pub field_types: HashMap<String, HashMap<String, String>>,
60 pub root_type: Option<String>,
61 pub all_fields: HashMap<String, HashSet<String>>,
67}
68
69#[derive(Debug, Clone, Default)]
90pub struct SwiftFirstClassMap {
91 pub first_class_types: HashSet<String>,
92 pub field_types: HashMap<String, HashMap<String, String>>,
93 pub root_type: Option<String>,
94}
95
96impl SwiftFirstClassMap {
97 pub fn is_first_class(&self, type_name: Option<&str>) -> bool {
103 match type_name {
104 Some(t) => self.first_class_types.contains(t),
105 None => true,
106 }
107 }
108
109 pub fn advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
112 let owner = owner_type?;
113 self.field_types.get(owner).and_then(|m| m.get(field_name).cloned())
114 }
115
116 pub fn is_empty(&self) -> bool {
118 self.first_class_types.is_empty() && self.field_types.is_empty()
119 }
120}
121
122impl PhpGetterMap {
123 pub fn needs_getter(&self, owner_type: Option<&str>, field_name: &str) -> bool {
130 if let Some(t) = owner_type {
131 let owner_has_field = self.all_fields.get(t).is_some_and(|s| s.contains(field_name));
136 if owner_has_field {
137 if let Some(fields) = self.getters.get(t) {
138 return fields.contains(field_name);
139 }
140 }
141 }
142 self.getters.values().any(|set| set.contains(field_name))
143 }
144
145 pub fn advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
148 let owner = owner_type?;
149 self.field_types.get(owner).and_then(|m| m.get(field_name).cloned())
150 }
151
152 pub fn is_empty(&self) -> bool {
155 self.getters.is_empty()
156 }
157}
158
159#[derive(Debug, Clone)]
161enum PathSegment {
162 Field(String),
164 ArrayField { name: String, index: usize },
169 MapAccess { field: String, key: String },
171 Length,
173}
174
175impl FieldResolver {
176 pub fn new(
180 fields: &HashMap<String, String>,
181 optional: &HashSet<String>,
182 result_fields: &HashSet<String>,
183 array_fields: &HashSet<String>,
184 method_calls: &HashSet<String>,
185 ) -> Self {
186 Self {
187 aliases: fields.clone(),
188 optional_fields: optional.clone(),
189 result_fields: result_fields.clone(),
190 array_fields: array_fields.clone(),
191 method_calls: method_calls.clone(),
192 error_field_aliases: HashMap::new(),
193 php_getter_map: PhpGetterMap::default(),
194 swift_first_class_map: SwiftFirstClassMap::default(),
195 }
196 }
197
198 pub fn new_with_error_aliases(
204 fields: &HashMap<String, String>,
205 optional: &HashSet<String>,
206 result_fields: &HashSet<String>,
207 array_fields: &HashSet<String>,
208 method_calls: &HashSet<String>,
209 error_field_aliases: &HashMap<String, String>,
210 ) -> Self {
211 Self {
212 aliases: fields.clone(),
213 optional_fields: optional.clone(),
214 result_fields: result_fields.clone(),
215 array_fields: array_fields.clone(),
216 method_calls: method_calls.clone(),
217 error_field_aliases: error_field_aliases.clone(),
218 php_getter_map: PhpGetterMap::default(),
219 swift_first_class_map: SwiftFirstClassMap::default(),
220 }
221 }
222
223 pub fn new_with_php_getters(
238 fields: &HashMap<String, String>,
239 optional: &HashSet<String>,
240 result_fields: &HashSet<String>,
241 array_fields: &HashSet<String>,
242 method_calls: &HashSet<String>,
243 error_field_aliases: &HashMap<String, String>,
244 php_getter_map: PhpGetterMap,
245 ) -> Self {
246 Self {
247 aliases: fields.clone(),
248 optional_fields: optional.clone(),
249 result_fields: result_fields.clone(),
250 array_fields: array_fields.clone(),
251 method_calls: method_calls.clone(),
252 error_field_aliases: error_field_aliases.clone(),
253 php_getter_map,
254 swift_first_class_map: SwiftFirstClassMap::default(),
255 }
256 }
257
258 #[allow(clippy::too_many_arguments)]
262 pub fn new_with_swift_first_class(
263 fields: &HashMap<String, String>,
264 optional: &HashSet<String>,
265 result_fields: &HashSet<String>,
266 array_fields: &HashSet<String>,
267 method_calls: &HashSet<String>,
268 error_field_aliases: &HashMap<String, String>,
269 swift_first_class_map: SwiftFirstClassMap,
270 ) -> Self {
271 Self {
272 aliases: fields.clone(),
273 optional_fields: optional.clone(),
274 result_fields: result_fields.clone(),
275 array_fields: array_fields.clone(),
276 method_calls: method_calls.clone(),
277 error_field_aliases: error_field_aliases.clone(),
278 php_getter_map: PhpGetterMap::default(),
279 swift_first_class_map,
280 }
281 }
282
283 pub fn resolve<'a>(&'a self, fixture_field: &'a str) -> &'a str {
286 self.aliases
287 .get(fixture_field)
288 .map(String::as_str)
289 .unwrap_or(fixture_field)
290 }
291
292 pub fn is_optional(&self, field: &str) -> bool {
294 if self.optional_fields.contains(field) {
295 return true;
296 }
297 let index_normalized = normalize_numeric_indices(field);
298 if index_normalized != field && self.optional_fields.contains(index_normalized.as_str()) {
299 return true;
300 }
301 let de_indexed = strip_numeric_indices(field);
304 if de_indexed != field && self.optional_fields.contains(de_indexed.as_str()) {
305 return true;
306 }
307 let normalized = field.replace("[].", ".");
308 if normalized != field && self.optional_fields.contains(normalized.as_str()) {
309 return true;
310 }
311 for af in &self.array_fields {
312 if let Some(rest) = field.strip_prefix(af.as_str()) {
313 if let Some(rest) = rest.strip_prefix('.') {
314 let with_bracket = format!("{af}[].{rest}");
315 if self.optional_fields.contains(with_bracket.as_str()) {
316 return true;
317 }
318 }
319 }
320 }
321 false
322 }
323
324 pub fn has_alias(&self, fixture_field: &str) -> bool {
326 self.aliases.contains_key(fixture_field)
327 }
328
329 pub fn has_explicit_field(&self, field_name: &str) -> bool {
335 if self.result_fields.is_empty() {
336 return false;
337 }
338 self.result_fields.contains(field_name)
339 }
340
341 pub fn is_valid_for_result(&self, fixture_field: &str) -> bool {
343 if self.result_fields.is_empty() {
344 return true;
345 }
346 let resolved = self.resolve(fixture_field);
347 let first_segment = resolved.split('.').next().unwrap_or(resolved);
348 let first_segment = first_segment.split('[').next().unwrap_or(first_segment);
349 self.result_fields.contains(first_segment)
350 }
351
352 pub fn is_array(&self, field: &str) -> bool {
354 self.array_fields.contains(field)
355 }
356
357 pub fn is_collection_root(&self, field: &str) -> bool {
370 let prefix = format!("{field}[");
371 self.array_fields.iter().any(|af| af.starts_with(&prefix))
372 || self.optional_fields.iter().any(|of| of.starts_with(&prefix))
373 }
374
375 pub fn tagged_union_split(&self, fixture_field: &str) -> Option<(String, String, String)> {
387 let resolved = self.resolve(fixture_field);
388 let segments: Vec<&str> = resolved.split('.').collect();
389 let mut path_so_far = String::new();
390 for (i, seg) in segments.iter().enumerate() {
391 if !path_so_far.is_empty() {
392 path_so_far.push('.');
393 }
394 path_so_far.push_str(seg);
395 if self.method_calls.contains(&path_so_far) {
396 let prefix = segments[..i].join(".");
398 let variant = (*seg).to_string();
399 let suffix = segments[i + 1..].join(".");
400 return Some((prefix, variant, suffix));
401 }
402 }
403 None
404 }
405
406 pub fn has_map_access(&self, fixture_field: &str) -> bool {
408 let resolved = self.resolve(fixture_field);
409 let segments = parse_path(resolved);
410 segments.iter().any(|s| {
411 if let PathSegment::MapAccess { key, .. } = s {
412 !key.chars().all(|c| c.is_ascii_digit())
413 } else {
414 false
415 }
416 })
417 }
418
419 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
421 let resolved = self.resolve(fixture_field);
422 let segments = parse_path(resolved);
423 let segments = self.inject_array_indexing(segments);
424 match language {
425 "java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
426 "kotlin" => render_kotlin_with_optionals(&segments, result_var, &self.optional_fields),
427 "kotlin_android" => render_kotlin_android_with_optionals(&segments, result_var, &self.optional_fields),
430 "rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
431 "csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
432 "zig" => render_zig_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
433 "swift" if !self.swift_first_class_map.is_empty() => render_swift_with_first_class_map(
434 &segments,
435 result_var,
436 &self.optional_fields,
437 &self.swift_first_class_map,
438 ),
439 "swift" => render_swift_with_optionals(&segments, result_var, &self.optional_fields),
440 "dart" => render_dart_with_optionals(&segments, result_var, &self.optional_fields),
441 "php" if !self.php_getter_map.is_empty() => {
442 render_php_with_getters(&segments, result_var, &self.php_getter_map)
443 }
444 _ => render_accessor(&segments, language, result_var),
445 }
446 }
447
448 pub fn accessor_for_error(&self, sub_field: &str, language: &str, err_var: &str) -> String {
462 let resolved = self
463 .error_field_aliases
464 .get(sub_field)
465 .map(String::as_str)
466 .unwrap_or(sub_field);
467 let segments = parse_path(resolved);
468 match language {
471 "rust" => render_rust_with_optionals(&segments, err_var, &self.optional_fields, &self.method_calls),
472 _ => render_accessor(&segments, language, err_var),
473 }
474 }
475
476 pub fn has_error_aliases(&self) -> bool {
483 !self.error_field_aliases.is_empty()
484 }
485
486 fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
487 if self.array_fields.is_empty() {
488 return segments;
489 }
490 let len = segments.len();
491 let mut result = Vec::with_capacity(len);
492 let mut path_so_far = String::new();
493 for i in 0..len {
494 let seg = &segments[i];
495 match seg {
496 PathSegment::Field(f) => {
497 if !path_so_far.is_empty() {
498 path_so_far.push('.');
499 }
500 path_so_far.push_str(f);
501 let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
502 if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
503 result.push(PathSegment::ArrayField {
505 name: f.clone(),
506 index: 0,
507 });
508 } else {
509 result.push(seg.clone());
510 }
511 }
512 PathSegment::ArrayField { .. } => {
515 result.push(seg.clone());
516 }
517 PathSegment::MapAccess { field, key } => {
518 if !path_so_far.is_empty() {
519 path_so_far.push('.');
520 }
521 path_so_far.push_str(field);
522 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
523 if is_numeric && self.array_fields.contains(&path_so_far) {
524 let index: usize = key.parse().unwrap_or(0);
526 result.push(PathSegment::ArrayField {
527 name: field.clone(),
528 index,
529 });
530 } else {
531 result.push(seg.clone());
532 }
533 }
534 _ => {
535 result.push(seg.clone());
536 }
537 }
538 }
539 result
540 }
541
542 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
544 let resolved = self.resolve(fixture_field);
545 if !self.is_optional(resolved) {
546 return None;
547 }
548 let segments = parse_path(resolved);
549 let segments = self.inject_array_indexing(segments);
550 let local_var = {
555 let raw = resolved.replace(['.', '['], "_").replace(']', "");
556 let mut collapsed = String::with_capacity(raw.len());
557 let mut prev_underscore = false;
558 for ch in raw.chars() {
559 if ch == '_' {
560 if !prev_underscore {
561 collapsed.push('_');
562 }
563 prev_underscore = true;
564 } else {
565 collapsed.push(ch);
566 prev_underscore = false;
567 }
568 }
569 collapsed.trim_matches('_').to_string()
570 };
571 let accessor = render_accessor(&segments, "rust", result_var);
572 let has_map_access = segments.iter().any(|s| {
573 if let PathSegment::MapAccess { key, .. } = s {
574 !key.chars().all(|c| c.is_ascii_digit())
575 } else {
576 false
577 }
578 });
579 let is_array = self.is_array(resolved);
580 let binding = if has_map_access {
581 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
582 } else if is_array {
583 format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
584 } else {
585 format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
586 };
587 Some((binding, local_var))
588 }
589}
590
591fn strip_numeric_indices(path: &str) -> String {
596 let mut result = String::with_capacity(path.len());
597 let mut chars = path.chars().peekable();
598 while let Some(c) = chars.next() {
599 if c == '[' {
600 let mut key = String::new();
601 let mut closed = false;
602 for inner in chars.by_ref() {
603 if inner == ']' {
604 closed = true;
605 break;
606 }
607 key.push(inner);
608 }
609 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
610 } else {
612 result.push('[');
613 result.push_str(&key);
614 if closed {
615 result.push(']');
616 }
617 }
618 } else {
619 result.push(c);
620 }
621 }
622 while result.contains("..") {
624 result = result.replace("..", ".");
625 }
626 if result.starts_with('.') {
627 result.remove(0);
628 }
629 result
630}
631
632fn normalize_numeric_indices(path: &str) -> String {
633 let mut result = String::with_capacity(path.len());
634 let mut chars = path.chars().peekable();
635 while let Some(c) = chars.next() {
636 if c == '[' {
637 let mut key = String::new();
638 let mut closed = false;
639 for inner in chars.by_ref() {
640 if inner == ']' {
641 closed = true;
642 break;
643 }
644 key.push(inner);
645 }
646 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
647 result.push_str("[0]");
648 } else {
649 result.push('[');
650 result.push_str(&key);
651 if closed {
652 result.push(']');
653 }
654 }
655 } else {
656 result.push(c);
657 }
658 }
659 result
660}
661
662fn parse_path(path: &str) -> Vec<PathSegment> {
663 let mut segments = Vec::new();
664 for part in path.split('.') {
665 if part == "length" || part == "count" || part == "size" {
666 segments.push(PathSegment::Length);
667 } else if let Some(bracket_pos) = part.find('[') {
668 let name = part[..bracket_pos].to_string();
669 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
670 if key.is_empty() {
671 segments.push(PathSegment::ArrayField { name, index: 0 });
673 } else if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
674 let index: usize = key.parse().unwrap_or(0);
676 segments.push(PathSegment::ArrayField { name, index });
677 } else {
678 segments.push(PathSegment::MapAccess { field: name, key });
680 }
681 } else {
682 segments.push(PathSegment::Field(part.to_string()));
683 }
684 }
685 segments
686}
687
688fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
689 match language {
690 "rust" => render_rust(segments, result_var),
691 "python" => render_dot_access(segments, result_var, "python"),
692 "typescript" | "node" => render_typescript(segments, result_var),
693 "wasm" => render_wasm(segments, result_var),
694 "go" => render_go(segments, result_var),
695 "java" => render_java(segments, result_var),
696 "kotlin" => render_kotlin(segments, result_var),
697 "kotlin_android" => render_kotlin_android(segments, result_var),
698 "csharp" => render_pascal_dot(segments, result_var),
699 "ruby" => render_dot_access(segments, result_var, "ruby"),
700 "php" => render_php(segments, result_var),
701 "elixir" => render_dot_access(segments, result_var, "elixir"),
702 "r" => render_r(segments, result_var),
703 "c" => render_c(segments, result_var),
704 "swift" => render_swift(segments, result_var),
705 "dart" => render_dart(segments, result_var),
706 _ => render_dot_access(segments, result_var, language),
707 }
708}
709
710fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
722 let mut out = result_var.to_string();
723 for seg in segments {
724 match seg {
725 PathSegment::Field(f) => {
726 out.push('.');
727 out.push_str(f);
728 }
729 PathSegment::ArrayField { name, index } => {
730 out.push('.');
731 out.push_str(name);
732 out.push_str(&format!("[{index}]"));
733 }
734 PathSegment::MapAccess { field, key } => {
735 out.push('.');
736 out.push_str(field);
737 if key.chars().all(|c| c.is_ascii_digit()) {
738 out.push_str(&format!("[{key}]"));
739 } else {
740 out.push_str(&format!("[\"{key}\"]"));
741 }
742 }
743 PathSegment::Length => {
744 out.push_str(".count");
745 }
746 }
747 }
748 out
749}
750
751fn render_swift_with_optionals(
761 segments: &[PathSegment],
762 result_var: &str,
763 optional_fields: &HashSet<String>,
764) -> String {
765 let mut out = result_var.to_string();
766 let mut path_so_far = String::new();
767 let total = segments.len();
768 for (i, seg) in segments.iter().enumerate() {
769 let is_leaf = i == total - 1;
770 match seg {
771 PathSegment::Field(f) => {
772 if !path_so_far.is_empty() {
773 path_so_far.push('.');
774 }
775 path_so_far.push_str(f);
776 out.push('.');
777 out.push_str(f);
778 if !is_leaf && optional_fields.contains(&path_so_far) {
782 out.push('?');
783 }
784 }
785 PathSegment::ArrayField { name, index } => {
786 if !path_so_far.is_empty() {
787 path_so_far.push('.');
788 }
789 path_so_far.push_str(name);
790 let is_optional = optional_fields.contains(&path_so_far);
791 out.push('.');
792 out.push_str(name);
793 if is_optional {
794 out.push_str(&format!("?[{index}]"));
796 } else {
797 out.push_str(&format!("[{index}]"));
798 }
799 path_so_far.push_str("[0]");
800 let _ = is_leaf;
801 }
802 PathSegment::MapAccess { field, key } => {
803 if !path_so_far.is_empty() {
804 path_so_far.push('.');
805 }
806 path_so_far.push_str(field);
807 out.push('.');
808 out.push_str(field);
809 if key.chars().all(|c| c.is_ascii_digit()) {
810 out.push_str(&format!("[{key}]"));
811 } else {
812 out.push_str(&format!("[\"{key}\"]"));
813 }
814 }
815 PathSegment::Length => {
816 out.push_str(".count");
817 }
818 }
819 }
820 out
821}
822
823fn render_swift_with_first_class_map(
828 segments: &[PathSegment],
829 result_var: &str,
830 optional_fields: &HashSet<String>,
831 map: &SwiftFirstClassMap,
832) -> String {
833 let mut out = result_var.to_string();
834 let mut path_so_far = String::new();
835 let mut current_type: Option<String> = map.root_type.clone();
836 let total = segments.len();
837 for (i, seg) in segments.iter().enumerate() {
838 let is_leaf = i == total - 1;
839 let property_syntax = map.is_first_class(current_type.as_deref());
840 match seg {
841 PathSegment::Field(f) => {
842 if !path_so_far.is_empty() {
843 path_so_far.push('.');
844 }
845 path_so_far.push_str(f);
846 out.push('.');
847 out.push_str(f);
848 if !property_syntax {
849 out.push_str("()");
850 }
851 if !is_leaf && optional_fields.contains(&path_so_far) {
852 out.push('?');
853 }
854 current_type = map.advance(current_type.as_deref(), f);
855 }
856 PathSegment::ArrayField { name, index } => {
857 if !path_so_far.is_empty() {
858 path_so_far.push('.');
859 }
860 path_so_far.push_str(name);
861 let is_optional = optional_fields.contains(&path_so_far);
862 out.push('.');
863 out.push_str(name);
864 let access = if property_syntax { "" } else { "()" };
865 if is_optional {
866 out.push_str(&format!("{access}?[{index}]"));
867 } else {
868 out.push_str(&format!("{access}[{index}]"));
869 }
870 path_so_far.push_str("[0]");
871 current_type = map.advance(current_type.as_deref(), name);
873 }
874 PathSegment::MapAccess { field, key } => {
875 if !path_so_far.is_empty() {
876 path_so_far.push('.');
877 }
878 path_so_far.push_str(field);
879 out.push('.');
880 out.push_str(field);
881 let access = if property_syntax { "" } else { "()" };
882 if key.chars().all(|c| c.is_ascii_digit()) {
883 out.push_str(&format!("{access}[{key}]"));
884 } else {
885 out.push_str(&format!("{access}[\"{key}\"]"));
886 }
887 current_type = map.advance(current_type.as_deref(), field);
888 }
889 PathSegment::Length => {
890 out.push_str(".count");
891 }
892 }
893 }
894 out
895}
896
897fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
898 let mut out = result_var.to_string();
899 for seg in segments {
900 match seg {
901 PathSegment::Field(f) => {
902 out.push('.');
903 out.push_str(&f.to_snake_case());
904 }
905 PathSegment::ArrayField { name, index } => {
906 out.push('.');
907 out.push_str(&name.to_snake_case());
908 out.push_str(&format!("[{index}]"));
909 }
910 PathSegment::MapAccess { field, key } => {
911 out.push('.');
912 out.push_str(&field.to_snake_case());
913 if key.chars().all(|c| c.is_ascii_digit()) {
914 out.push_str(&format!("[{key}]"));
915 } else {
916 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
917 }
918 }
919 PathSegment::Length => {
920 out.push_str(".len()");
921 }
922 }
923 }
924 out
925}
926
927fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
928 let mut out = result_var.to_string();
929 for seg in segments {
930 match seg {
931 PathSegment::Field(f) => {
932 out.push('.');
933 out.push_str(f);
934 }
935 PathSegment::ArrayField { name, index } => {
936 if language == "elixir" {
937 let current = std::mem::take(&mut out);
938 out = format!("Enum.at({current}.{name}, {index})");
939 } else {
940 out.push('.');
941 out.push_str(name);
942 out.push_str(&format!("[{index}]"));
943 }
944 }
945 PathSegment::MapAccess { field, key } => {
946 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
947 if is_numeric && language == "elixir" {
948 let current = std::mem::take(&mut out);
949 out = format!("Enum.at({current}.{field}, {key})");
950 } else {
951 out.push('.');
952 out.push_str(field);
953 if is_numeric {
954 let idx: usize = key.parse().unwrap_or(0);
955 out.push_str(&format!("[{idx}]"));
956 } else if language == "elixir" || language == "ruby" {
957 out.push_str(&format!("[\"{key}\"]"));
960 } else {
961 out.push_str(&format!(".get(\"{key}\")"));
962 }
963 }
964 }
965 PathSegment::Length => match language {
966 "ruby" => out.push_str(".length"),
967 "elixir" => {
968 let current = std::mem::take(&mut out);
969 out = format!("length({current})");
970 }
971 "gleam" => {
972 let current = std::mem::take(&mut out);
973 out = format!("list.length({current})");
974 }
975 _ => {
976 let current = std::mem::take(&mut out);
977 out = format!("len({current})");
978 }
979 },
980 }
981 }
982 out
983}
984
985fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
986 let mut out = result_var.to_string();
987 for seg in segments {
988 match seg {
989 PathSegment::Field(f) => {
990 out.push('.');
991 out.push_str(&f.to_lower_camel_case());
992 }
993 PathSegment::ArrayField { name, index } => {
994 out.push('.');
995 out.push_str(&name.to_lower_camel_case());
996 out.push_str(&format!("[{index}]"));
997 }
998 PathSegment::MapAccess { field, key } => {
999 out.push('.');
1000 out.push_str(&field.to_lower_camel_case());
1001 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
1004 out.push_str(&format!("[{key}]"));
1005 } else {
1006 out.push_str(&format!("[\"{key}\"]"));
1007 }
1008 }
1009 PathSegment::Length => {
1010 out.push_str(".length");
1011 }
1012 }
1013 }
1014 out
1015}
1016
1017fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
1018 let mut out = result_var.to_string();
1019 for seg in segments {
1020 match seg {
1021 PathSegment::Field(f) => {
1022 out.push('.');
1023 out.push_str(&f.to_lower_camel_case());
1024 }
1025 PathSegment::ArrayField { name, index } => {
1026 out.push('.');
1027 out.push_str(&name.to_lower_camel_case());
1028 out.push_str(&format!("[{index}]"));
1029 }
1030 PathSegment::MapAccess { field, key } => {
1031 out.push('.');
1032 out.push_str(&field.to_lower_camel_case());
1033 out.push_str(&format!(".get(\"{key}\")"));
1034 }
1035 PathSegment::Length => {
1036 out.push_str(".length");
1037 }
1038 }
1039 }
1040 out
1041}
1042
1043fn render_go(segments: &[PathSegment], result_var: &str) -> String {
1044 let mut out = result_var.to_string();
1045 for seg in segments {
1046 match seg {
1047 PathSegment::Field(f) => {
1048 out.push('.');
1049 out.push_str(&to_go_name(f));
1050 }
1051 PathSegment::ArrayField { name, index } => {
1052 out.push('.');
1053 out.push_str(&to_go_name(name));
1054 out.push_str(&format!("[{index}]"));
1055 }
1056 PathSegment::MapAccess { field, key } => {
1057 out.push('.');
1058 out.push_str(&to_go_name(field));
1059 if key.chars().all(|c| c.is_ascii_digit()) {
1060 out.push_str(&format!("[{key}]"));
1061 } else {
1062 out.push_str(&format!("[\"{key}\"]"));
1063 }
1064 }
1065 PathSegment::Length => {
1066 let current = std::mem::take(&mut out);
1067 out = format!("len({current})");
1068 }
1069 }
1070 }
1071 out
1072}
1073
1074fn render_java(segments: &[PathSegment], result_var: &str) -> String {
1075 let mut out = result_var.to_string();
1076 for seg in segments {
1077 match seg {
1078 PathSegment::Field(f) => {
1079 out.push('.');
1080 out.push_str(&f.to_lower_camel_case());
1081 out.push_str("()");
1082 }
1083 PathSegment::ArrayField { name, index } => {
1084 out.push('.');
1085 out.push_str(&name.to_lower_camel_case());
1086 out.push_str(&format!("().get({index})"));
1087 }
1088 PathSegment::MapAccess { field, key } => {
1089 out.push('.');
1090 out.push_str(&field.to_lower_camel_case());
1091 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1093 if is_numeric {
1094 out.push_str(&format!("().get({key})"));
1095 } else {
1096 out.push_str(&format!("().get(\"{key}\")"));
1097 }
1098 }
1099 PathSegment::Length => {
1100 out.push_str(".size()");
1101 }
1102 }
1103 }
1104 out
1105}
1106
1107fn kotlin_getter(name: &str) -> String {
1112 let camel = name.to_lower_camel_case();
1113 match camel.as_str() {
1114 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
1115 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
1116 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
1117 _ => camel,
1118 }
1119}
1120
1121fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
1122 let mut out = result_var.to_string();
1123 for seg in segments {
1124 match seg {
1125 PathSegment::Field(f) => {
1126 out.push('.');
1127 out.push_str(&kotlin_getter(f));
1128 out.push_str("()");
1129 }
1130 PathSegment::ArrayField { name, index } => {
1131 out.push('.');
1132 out.push_str(&kotlin_getter(name));
1133 if *index == 0 {
1134 out.push_str("().first()");
1135 } else {
1136 out.push_str(&format!("().get({index})"));
1137 }
1138 }
1139 PathSegment::MapAccess { field, key } => {
1140 out.push('.');
1141 out.push_str(&kotlin_getter(field));
1142 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1143 if is_numeric {
1144 out.push_str(&format!("().get({key})"));
1145 } else {
1146 out.push_str(&format!("().get(\"{key}\")"));
1147 }
1148 }
1149 PathSegment::Length => {
1150 out.push_str(".size");
1151 }
1152 }
1153 }
1154 out
1155}
1156
1157fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1158 let mut out = result_var.to_string();
1159 let mut path_so_far = String::new();
1160 for (i, seg) in segments.iter().enumerate() {
1161 let is_leaf = i == segments.len() - 1;
1162 match seg {
1163 PathSegment::Field(f) => {
1164 if !path_so_far.is_empty() {
1165 path_so_far.push('.');
1166 }
1167 path_so_far.push_str(f);
1168 out.push('.');
1169 out.push_str(&f.to_lower_camel_case());
1170 out.push_str("()");
1171 let _ = is_leaf;
1172 let _ = optional_fields;
1173 }
1174 PathSegment::ArrayField { name, index } => {
1175 if !path_so_far.is_empty() {
1176 path_so_far.push('.');
1177 }
1178 path_so_far.push_str(name);
1179 out.push('.');
1180 out.push_str(&name.to_lower_camel_case());
1181 out.push_str(&format!("().get({index})"));
1182 }
1183 PathSegment::MapAccess { field, key } => {
1184 if !path_so_far.is_empty() {
1185 path_so_far.push('.');
1186 }
1187 path_so_far.push_str(field);
1188 out.push('.');
1189 out.push_str(&field.to_lower_camel_case());
1190 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1192 if is_numeric {
1193 out.push_str(&format!("().get({key})"));
1194 } else {
1195 out.push_str(&format!("().get(\"{key}\")"));
1196 }
1197 }
1198 PathSegment::Length => {
1199 out.push_str(".size()");
1200 }
1201 }
1202 }
1203 out
1204}
1205
1206fn render_kotlin_with_optionals(
1221 segments: &[PathSegment],
1222 result_var: &str,
1223 optional_fields: &HashSet<String>,
1224) -> String {
1225 let mut out = result_var.to_string();
1226 let mut path_so_far = String::new();
1227 let mut prev_was_nullable = false;
1235 for seg in segments {
1236 let nav = if prev_was_nullable { "?." } else { "." };
1237 match seg {
1238 PathSegment::Field(f) => {
1239 if !path_so_far.is_empty() {
1240 path_so_far.push('.');
1241 }
1242 path_so_far.push_str(f);
1243 let is_optional = optional_fields.contains(&path_so_far);
1248 out.push_str(nav);
1249 out.push_str(&kotlin_getter(f));
1250 out.push_str("()");
1251 prev_was_nullable = prev_was_nullable || is_optional;
1252 }
1253 PathSegment::ArrayField { name, index } => {
1254 if !path_so_far.is_empty() {
1255 path_so_far.push('.');
1256 }
1257 path_so_far.push_str(name);
1258 let is_optional = optional_fields.contains(&path_so_far);
1259 out.push_str(nav);
1260 out.push_str(&kotlin_getter(name));
1261 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1262 if *index == 0 {
1263 out.push_str(&format!("(){safe}.first()"));
1264 } else {
1265 out.push_str(&format!("(){safe}.get({index})"));
1266 }
1267 path_so_far.push_str("[0]");
1271 prev_was_nullable = prev_was_nullable || is_optional;
1272 }
1273 PathSegment::MapAccess { field, key } => {
1274 if !path_so_far.is_empty() {
1275 path_so_far.push('.');
1276 }
1277 path_so_far.push_str(field);
1278 let is_optional = optional_fields.contains(&path_so_far);
1279 out.push_str(nav);
1280 out.push_str(&kotlin_getter(field));
1281 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1282 if is_numeric {
1283 if prev_was_nullable || is_optional {
1284 out.push_str(&format!("()?.get({key})"));
1285 } else {
1286 out.push_str(&format!("().get({key})"));
1287 }
1288 } else if prev_was_nullable || is_optional {
1289 out.push_str(&format!("()?.get(\"{key}\")"));
1290 } else {
1291 out.push_str(&format!("().get(\"{key}\")"));
1292 }
1293 prev_was_nullable = prev_was_nullable || is_optional;
1294 }
1295 PathSegment::Length => {
1296 let size_nav = if prev_was_nullable { "?" } else { "" };
1299 out.push_str(&format!("{size_nav}.size"));
1300 prev_was_nullable = false;
1301 }
1302 }
1303 }
1304 out
1305}
1306
1307fn render_kotlin_android_with_optionals(
1318 segments: &[PathSegment],
1319 result_var: &str,
1320 optional_fields: &HashSet<String>,
1321) -> String {
1322 let mut out = result_var.to_string();
1323 let mut path_so_far = String::new();
1324 let mut prev_was_nullable = false;
1325 for seg in segments {
1326 let nav = if prev_was_nullable { "?." } else { "." };
1327 match seg {
1328 PathSegment::Field(f) => {
1329 if !path_so_far.is_empty() {
1330 path_so_far.push('.');
1331 }
1332 path_so_far.push_str(f);
1333 let is_optional = optional_fields.contains(&path_so_far);
1334 out.push_str(nav);
1335 out.push_str(&kotlin_getter(f));
1337 prev_was_nullable = prev_was_nullable || is_optional;
1338 }
1339 PathSegment::ArrayField { name, index } => {
1340 if !path_so_far.is_empty() {
1341 path_so_far.push('.');
1342 }
1343 path_so_far.push_str(name);
1344 let is_optional = optional_fields.contains(&path_so_far);
1345 out.push_str(nav);
1346 out.push_str(&kotlin_getter(name));
1348 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1349 if *index == 0 {
1350 out.push_str(&format!("{safe}.first()"));
1351 } else {
1352 out.push_str(&format!("{safe}.get({index})"));
1353 }
1354 path_so_far.push_str("[0]");
1355 prev_was_nullable = prev_was_nullable || is_optional;
1356 }
1357 PathSegment::MapAccess { field, key } => {
1358 if !path_so_far.is_empty() {
1359 path_so_far.push('.');
1360 }
1361 path_so_far.push_str(field);
1362 let is_optional = optional_fields.contains(&path_so_far);
1363 out.push_str(nav);
1364 out.push_str(&kotlin_getter(field));
1366 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1367 if is_numeric {
1368 if prev_was_nullable || is_optional {
1369 out.push_str(&format!("?.get({key})"));
1370 } else {
1371 out.push_str(&format!(".get({key})"));
1372 }
1373 } else if prev_was_nullable || is_optional {
1374 out.push_str(&format!("?.get(\"{key}\")"));
1375 } else {
1376 out.push_str(&format!(".get(\"{key}\")"));
1377 }
1378 prev_was_nullable = prev_was_nullable || is_optional;
1379 }
1380 PathSegment::Length => {
1381 let size_nav = if prev_was_nullable { "?" } else { "" };
1382 out.push_str(&format!("{size_nav}.size"));
1383 prev_was_nullable = false;
1384 }
1385 }
1386 }
1387 out
1388}
1389
1390fn render_kotlin_android(segments: &[PathSegment], result_var: &str) -> String {
1394 let mut out = result_var.to_string();
1395 for seg in segments {
1396 match seg {
1397 PathSegment::Field(f) => {
1398 out.push('.');
1399 out.push_str(&kotlin_getter(f));
1400 }
1402 PathSegment::ArrayField { name, index } => {
1403 out.push('.');
1404 out.push_str(&kotlin_getter(name));
1405 if *index == 0 {
1406 out.push_str(".first()");
1407 } else {
1408 out.push_str(&format!(".get({index})"));
1409 }
1410 }
1411 PathSegment::MapAccess { field, key } => {
1412 out.push('.');
1413 out.push_str(&kotlin_getter(field));
1414 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1415 if is_numeric {
1416 out.push_str(&format!(".get({key})"));
1417 } else {
1418 out.push_str(&format!(".get(\"{key}\")"));
1419 }
1420 }
1421 PathSegment::Length => {
1422 out.push_str(".size");
1423 }
1424 }
1425 }
1426 out
1427}
1428
1429fn render_rust_with_optionals(
1435 segments: &[PathSegment],
1436 result_var: &str,
1437 optional_fields: &HashSet<String>,
1438 method_calls: &HashSet<String>,
1439) -> String {
1440 let mut out = result_var.to_string();
1441 let mut path_so_far = String::new();
1442 for (i, seg) in segments.iter().enumerate() {
1443 let is_leaf = i == segments.len() - 1;
1444 match seg {
1445 PathSegment::Field(f) => {
1446 if !path_so_far.is_empty() {
1447 path_so_far.push('.');
1448 }
1449 path_so_far.push_str(f);
1450 out.push('.');
1451 out.push_str(&f.to_snake_case());
1452 let is_method = method_calls.contains(&path_so_far);
1453 if is_method {
1454 out.push_str("()");
1455 if !is_leaf && optional_fields.contains(&path_so_far) {
1456 out.push_str(".as_ref().unwrap()");
1457 }
1458 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1459 out.push_str(".as_ref().unwrap()");
1460 }
1461 }
1462 PathSegment::ArrayField { name, index } => {
1463 if !path_so_far.is_empty() {
1464 path_so_far.push('.');
1465 }
1466 path_so_far.push_str(name);
1467 out.push('.');
1468 out.push_str(&name.to_snake_case());
1469 let path_with_idx = format!("{path_so_far}[0]");
1473 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1474 if is_opt {
1475 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1476 } else {
1477 out.push_str(&format!("[{index}]"));
1478 }
1479 path_so_far.push_str("[0]");
1484 }
1485 PathSegment::MapAccess { field, key } => {
1486 if !path_so_far.is_empty() {
1487 path_so_far.push('.');
1488 }
1489 path_so_far.push_str(field);
1490 out.push('.');
1491 out.push_str(&field.to_snake_case());
1492 if key.chars().all(|c| c.is_ascii_digit()) {
1493 let path_with_idx = format!("{path_so_far}[0]");
1495 let is_opt =
1496 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1497 if is_opt {
1498 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1499 } else {
1500 out.push_str(&format!("[{key}]"));
1501 }
1502 path_so_far.push_str("[0]");
1503 } else {
1504 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1505 }
1506 }
1507 PathSegment::Length => {
1508 out.push_str(".len()");
1509 }
1510 }
1511 }
1512 out
1513}
1514
1515fn render_zig_with_optionals(
1528 segments: &[PathSegment],
1529 result_var: &str,
1530 optional_fields: &HashSet<String>,
1531 method_calls: &HashSet<String>,
1532) -> String {
1533 let mut out = result_var.to_string();
1534 let mut path_so_far = String::new();
1535 for seg in segments {
1536 match seg {
1537 PathSegment::Field(f) => {
1538 if !path_so_far.is_empty() {
1539 path_so_far.push('.');
1540 }
1541 path_so_far.push_str(f);
1542 out.push('.');
1543 out.push_str(f);
1544 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1545 out.push_str(".?");
1546 }
1547 }
1548 PathSegment::ArrayField { name, index } => {
1549 if !path_so_far.is_empty() {
1550 path_so_far.push('.');
1551 }
1552 path_so_far.push_str(name);
1553 out.push('.');
1554 out.push_str(name);
1555 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1556 out.push_str(".?");
1557 }
1558 out.push_str(&format!("[{index}]"));
1559 }
1560 PathSegment::MapAccess { field, key } => {
1561 if !path_so_far.is_empty() {
1562 path_so_far.push('.');
1563 }
1564 path_so_far.push_str(field);
1565 out.push('.');
1566 out.push_str(field);
1567 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1568 out.push_str(".?");
1569 }
1570 if key.chars().all(|c| c.is_ascii_digit()) {
1571 out.push_str(&format!("[{key}]"));
1572 } else {
1573 out.push_str(&format!(".get(\"{key}\")"));
1574 }
1575 }
1576 PathSegment::Length => {
1577 out.push_str(".len");
1578 }
1579 }
1580 }
1581 out
1582}
1583
1584fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1585 let mut out = result_var.to_string();
1586 for seg in segments {
1587 match seg {
1588 PathSegment::Field(f) => {
1589 out.push('.');
1590 out.push_str(&f.to_pascal_case());
1591 }
1592 PathSegment::ArrayField { name, index } => {
1593 out.push('.');
1594 out.push_str(&name.to_pascal_case());
1595 out.push_str(&format!("[{index}]"));
1596 }
1597 PathSegment::MapAccess { field, key } => {
1598 out.push('.');
1599 out.push_str(&field.to_pascal_case());
1600 if key.chars().all(|c| c.is_ascii_digit()) {
1601 out.push_str(&format!("[{key}]"));
1602 } else {
1603 out.push_str(&format!("[\"{key}\"]"));
1604 }
1605 }
1606 PathSegment::Length => {
1607 out.push_str(".Count");
1608 }
1609 }
1610 }
1611 out
1612}
1613
1614fn render_csharp_with_optionals(
1615 segments: &[PathSegment],
1616 result_var: &str,
1617 optional_fields: &HashSet<String>,
1618) -> String {
1619 let mut out = result_var.to_string();
1620 let mut path_so_far = String::new();
1621 for (i, seg) in segments.iter().enumerate() {
1622 let is_leaf = i == segments.len() - 1;
1623 match seg {
1624 PathSegment::Field(f) => {
1625 if !path_so_far.is_empty() {
1626 path_so_far.push('.');
1627 }
1628 path_so_far.push_str(f);
1629 out.push('.');
1630 out.push_str(&f.to_pascal_case());
1631 if !is_leaf && optional_fields.contains(&path_so_far) {
1632 out.push('!');
1633 }
1634 }
1635 PathSegment::ArrayField { name, index } => {
1636 if !path_so_far.is_empty() {
1637 path_so_far.push('.');
1638 }
1639 path_so_far.push_str(name);
1640 out.push('.');
1641 out.push_str(&name.to_pascal_case());
1642 out.push_str(&format!("[{index}]"));
1643 }
1644 PathSegment::MapAccess { field, key } => {
1645 if !path_so_far.is_empty() {
1646 path_so_far.push('.');
1647 }
1648 path_so_far.push_str(field);
1649 out.push('.');
1650 out.push_str(&field.to_pascal_case());
1651 if key.chars().all(|c| c.is_ascii_digit()) {
1652 out.push_str(&format!("[{key}]"));
1653 } else {
1654 out.push_str(&format!("[\"{key}\"]"));
1655 }
1656 }
1657 PathSegment::Length => {
1658 out.push_str(".Count");
1659 }
1660 }
1661 }
1662 out
1663}
1664
1665fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1666 let mut out = result_var.to_string();
1667 for seg in segments {
1668 match seg {
1669 PathSegment::Field(f) => {
1670 out.push_str("->");
1671 out.push_str(&f.to_lower_camel_case());
1674 }
1675 PathSegment::ArrayField { name, index } => {
1676 out.push_str("->");
1677 out.push_str(&name.to_lower_camel_case());
1678 out.push_str(&format!("[{index}]"));
1679 }
1680 PathSegment::MapAccess { field, key } => {
1681 out.push_str("->");
1682 out.push_str(&field.to_lower_camel_case());
1683 out.push_str(&format!("[\"{key}\"]"));
1684 }
1685 PathSegment::Length => {
1686 let current = std::mem::take(&mut out);
1687 out = format!("count({current})");
1688 }
1689 }
1690 }
1691 out
1692}
1693
1694fn render_php_with_getters(segments: &[PathSegment], result_var: &str, getter_map: &PhpGetterMap) -> String {
1712 let mut out = result_var.to_string();
1713 let mut current_type: Option<String> = getter_map.root_type.clone();
1714 for seg in segments {
1715 match seg {
1716 PathSegment::Field(f) => {
1717 let camel = f.to_lower_camel_case();
1718 if getter_map.needs_getter(current_type.as_deref(), f.as_str()) {
1719 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1724 out.push_str("->");
1725 out.push_str(&getter);
1726 out.push_str("()");
1727 } else {
1728 out.push_str("->");
1729 out.push_str(&camel);
1730 }
1731 current_type = getter_map.advance(current_type.as_deref(), f.as_str());
1732 }
1733 PathSegment::ArrayField { name, index } => {
1734 let camel = name.to_lower_camel_case();
1735 if getter_map.needs_getter(current_type.as_deref(), name.as_str()) {
1736 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1737 out.push_str("->");
1738 out.push_str(&getter);
1739 out.push_str("()");
1740 } else {
1741 out.push_str("->");
1742 out.push_str(&camel);
1743 }
1744 out.push_str(&format!("[{index}]"));
1745 current_type = getter_map.advance(current_type.as_deref(), name.as_str());
1746 }
1747 PathSegment::MapAccess { field, key } => {
1748 let camel = field.to_lower_camel_case();
1749 if getter_map.needs_getter(current_type.as_deref(), field.as_str()) {
1750 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1751 out.push_str("->");
1752 out.push_str(&getter);
1753 out.push_str("()");
1754 } else {
1755 out.push_str("->");
1756 out.push_str(&camel);
1757 }
1758 out.push_str(&format!("[\"{key}\"]"));
1759 current_type = getter_map.advance(current_type.as_deref(), field.as_str());
1760 }
1761 PathSegment::Length => {
1762 let current = std::mem::take(&mut out);
1763 out = format!("count({current})");
1764 }
1765 }
1766 }
1767 out
1768}
1769
1770fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1771 let mut out = result_var.to_string();
1772 for seg in segments {
1773 match seg {
1774 PathSegment::Field(f) => {
1775 out.push('$');
1776 out.push_str(f);
1777 }
1778 PathSegment::ArrayField { name, index } => {
1779 out.push('$');
1780 out.push_str(name);
1781 out.push_str(&format!("[[{}]]", index + 1));
1783 }
1784 PathSegment::MapAccess { field, key } => {
1785 out.push('$');
1786 out.push_str(field);
1787 out.push_str(&format!("[[\"{key}\"]]"));
1788 }
1789 PathSegment::Length => {
1790 let current = std::mem::take(&mut out);
1791 out = format!("length({current})");
1792 }
1793 }
1794 }
1795 out
1796}
1797
1798fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1799 let mut parts = Vec::new();
1800 let mut trailing_length = false;
1801 for seg in segments {
1802 match seg {
1803 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1804 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1805 PathSegment::MapAccess { field, key } => {
1806 parts.push(field.to_snake_case());
1807 parts.push(key.clone());
1808 }
1809 PathSegment::Length => {
1810 trailing_length = true;
1811 }
1812 }
1813 }
1814 let suffix = parts.join("_");
1815 if trailing_length {
1816 format!("result_{suffix}_count({result_var})")
1817 } else {
1818 format!("result_{suffix}({result_var})")
1819 }
1820}
1821
1822fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
1829 let mut out = result_var.to_string();
1830 for seg in segments {
1831 match seg {
1832 PathSegment::Field(f) => {
1833 out.push('.');
1834 out.push_str(&f.to_lower_camel_case());
1835 }
1836 PathSegment::ArrayField { name, index } => {
1837 out.push('.');
1838 out.push_str(&name.to_lower_camel_case());
1839 out.push_str(&format!("[{index}]"));
1840 }
1841 PathSegment::MapAccess { field, key } => {
1842 out.push('.');
1843 out.push_str(&field.to_lower_camel_case());
1844 if key.chars().all(|c| c.is_ascii_digit()) {
1845 out.push_str(&format!("[{key}]"));
1846 } else {
1847 out.push_str(&format!("[\"{key}\"]"));
1848 }
1849 }
1850 PathSegment::Length => {
1851 out.push_str(".length");
1852 }
1853 }
1854 }
1855 out
1856}
1857
1858fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1864 let mut out = result_var.to_string();
1865 let mut path_so_far = String::new();
1866 let mut prev_was_nullable = false;
1867 for seg in segments {
1868 let nav = if prev_was_nullable { "?." } else { "." };
1869 match seg {
1870 PathSegment::Field(f) => {
1871 if !path_so_far.is_empty() {
1872 path_so_far.push('.');
1873 }
1874 path_so_far.push_str(f);
1875 let is_optional = optional_fields.contains(&path_so_far);
1876 out.push_str(nav);
1877 out.push_str(&f.to_lower_camel_case());
1878 prev_was_nullable = is_optional;
1879 }
1880 PathSegment::ArrayField { name, index } => {
1881 if !path_so_far.is_empty() {
1882 path_so_far.push('.');
1883 }
1884 path_so_far.push_str(name);
1885 let is_optional = optional_fields.contains(&path_so_far);
1886 out.push_str(nav);
1887 out.push_str(&name.to_lower_camel_case());
1888 if is_optional {
1892 out.push('!');
1893 }
1894 out.push_str(&format!("[{index}]"));
1895 prev_was_nullable = false;
1896 }
1897 PathSegment::MapAccess { field, key } => {
1898 if !path_so_far.is_empty() {
1899 path_so_far.push('.');
1900 }
1901 path_so_far.push_str(field);
1902 let is_optional = optional_fields.contains(&path_so_far);
1903 out.push_str(nav);
1904 out.push_str(&field.to_lower_camel_case());
1905 if key.chars().all(|c| c.is_ascii_digit()) {
1906 out.push_str(&format!("[{key}]"));
1907 } else {
1908 out.push_str(&format!("[\"{key}\"]"));
1909 }
1910 prev_was_nullable = is_optional;
1911 }
1912 PathSegment::Length => {
1913 out.push_str(nav);
1916 out.push_str("length");
1917 prev_was_nullable = false;
1918 }
1919 }
1920 }
1921 out
1922}
1923
1924#[cfg(test)]
1925mod tests {
1926 use super::*;
1927
1928 fn make_resolver() -> FieldResolver {
1929 let mut fields = HashMap::new();
1930 fields.insert("title".to_string(), "metadata.document.title".to_string());
1931 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1932 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
1933 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
1934 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
1935 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
1936 let mut optional = HashSet::new();
1937 optional.insert("metadata.document.title".to_string());
1938 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1939 }
1940
1941 fn make_resolver_with_doc_optional() -> FieldResolver {
1942 let mut fields = HashMap::new();
1943 fields.insert("title".to_string(), "metadata.document.title".to_string());
1944 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1945 let mut optional = HashSet::new();
1946 optional.insert("document".to_string());
1947 optional.insert("metadata.document.title".to_string());
1948 optional.insert("metadata.document".to_string());
1949 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1950 }
1951
1952 #[test]
1953 fn test_resolve_alias() {
1954 let r = make_resolver();
1955 assert_eq!(r.resolve("title"), "metadata.document.title");
1956 }
1957
1958 #[test]
1959 fn test_resolve_passthrough() {
1960 let r = make_resolver();
1961 assert_eq!(r.resolve("content"), "content");
1962 }
1963
1964 #[test]
1965 fn test_is_optional() {
1966 let r = make_resolver();
1967 assert!(r.is_optional("metadata.document.title"));
1968 assert!(!r.is_optional("content"));
1969 }
1970
1971 #[test]
1972 fn test_accessor_rust_struct() {
1973 let r = make_resolver();
1974 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
1975 }
1976
1977 #[test]
1978 fn test_accessor_rust_map() {
1979 let r = make_resolver();
1980 assert_eq!(
1981 r.accessor("tags", "rust", "result"),
1982 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
1983 );
1984 }
1985
1986 #[test]
1987 fn test_accessor_python() {
1988 let r = make_resolver();
1989 assert_eq!(
1990 r.accessor("title", "python", "result"),
1991 "result.metadata.document.title"
1992 );
1993 }
1994
1995 #[test]
1996 fn test_accessor_go() {
1997 let r = make_resolver();
1998 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
1999 }
2000
2001 #[test]
2002 fn test_accessor_go_initialism_fields() {
2003 let mut fields = std::collections::HashMap::new();
2004 fields.insert("content".to_string(), "html".to_string());
2005 fields.insert("link_url".to_string(), "links.url".to_string());
2006 let r = FieldResolver::new(
2007 &fields,
2008 &HashSet::new(),
2009 &HashSet::new(),
2010 &HashSet::new(),
2011 &HashSet::new(),
2012 );
2013 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
2014 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
2015 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
2016 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
2017 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
2018 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
2019 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
2020 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
2021 }
2022
2023 #[test]
2024 fn test_accessor_typescript() {
2025 let r = make_resolver();
2026 assert_eq!(
2027 r.accessor("title", "typescript", "result"),
2028 "result.metadata.document.title"
2029 );
2030 }
2031
2032 #[test]
2033 fn test_accessor_typescript_snake_to_camel() {
2034 let r = make_resolver();
2035 assert_eq!(
2036 r.accessor("og", "typescript", "result"),
2037 "result.metadata.document.openGraph"
2038 );
2039 assert_eq!(
2040 r.accessor("twitter", "typescript", "result"),
2041 "result.metadata.document.twitterCard"
2042 );
2043 assert_eq!(
2044 r.accessor("canonical", "typescript", "result"),
2045 "result.metadata.document.canonicalUrl"
2046 );
2047 }
2048
2049 #[test]
2050 fn test_accessor_typescript_map_snake_to_camel() {
2051 let r = make_resolver();
2052 assert_eq!(
2053 r.accessor("og_tag", "typescript", "result"),
2054 "result.metadata.openGraphTags[\"og_title\"]"
2055 );
2056 }
2057
2058 #[test]
2059 fn test_accessor_typescript_numeric_index_is_unquoted() {
2060 let mut fields = HashMap::new();
2064 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
2065 let r = FieldResolver::new(
2066 &fields,
2067 &HashSet::new(),
2068 &HashSet::new(),
2069 &HashSet::new(),
2070 &HashSet::new(),
2071 );
2072 assert_eq!(
2073 r.accessor("first_score", "typescript", "result"),
2074 "result.results[0].relevanceScore"
2075 );
2076 }
2077
2078 #[test]
2079 fn test_accessor_node_alias() {
2080 let r = make_resolver();
2081 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
2082 }
2083
2084 #[test]
2085 fn test_accessor_wasm_camel_case() {
2086 let r = make_resolver();
2087 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
2088 assert_eq!(
2089 r.accessor("twitter", "wasm", "result"),
2090 "result.metadata.document.twitterCard"
2091 );
2092 assert_eq!(
2093 r.accessor("canonical", "wasm", "result"),
2094 "result.metadata.document.canonicalUrl"
2095 );
2096 }
2097
2098 #[test]
2099 fn test_accessor_wasm_map_access() {
2100 let r = make_resolver();
2101 assert_eq!(
2102 r.accessor("og_tag", "wasm", "result"),
2103 "result.metadata.openGraphTags.get(\"og_title\")"
2104 );
2105 }
2106
2107 #[test]
2108 fn test_accessor_java() {
2109 let r = make_resolver();
2110 assert_eq!(
2111 r.accessor("title", "java", "result"),
2112 "result.metadata().document().title()"
2113 );
2114 }
2115
2116 #[test]
2117 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
2118 let mut fields = HashMap::new();
2119 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2120 fields.insert("node_count".to_string(), "nodes.length".to_string());
2121 let mut arrays = HashSet::new();
2122 arrays.insert("nodes".to_string());
2123 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
2124 assert_eq!(
2125 r.accessor("first_node_name", "kotlin", "result"),
2126 "result.nodes().first().name()"
2127 );
2128 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
2129 }
2130
2131 #[test]
2132 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
2133 let r = make_resolver_with_doc_optional();
2134 assert_eq!(
2135 r.accessor("title", "kotlin", "result"),
2136 "result.metadata().document()?.title()"
2137 );
2138 }
2139
2140 #[test]
2141 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
2142 let mut fields = HashMap::new();
2143 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2144 fields.insert("tag".to_string(), "tags[name]".to_string());
2145 let mut optional = HashSet::new();
2146 optional.insert("nodes".to_string());
2147 optional.insert("tags".to_string());
2148 let mut arrays = HashSet::new();
2149 arrays.insert("nodes".to_string());
2150 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2151 assert_eq!(
2152 r.accessor("first_node_name", "kotlin", "result"),
2153 "result.nodes()?.first()?.name()"
2154 );
2155 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
2156 }
2157
2158 #[test]
2164 fn test_accessor_kotlin_optional_field_after_indexed_array() {
2165 let mut fields = HashMap::new();
2168 fields.insert(
2169 "tool_call_name".to_string(),
2170 "choices[0].message.tool_calls[0].function.name".to_string(),
2171 );
2172 let mut optional = HashSet::new();
2173 optional.insert("choices[0].message.tool_calls".to_string());
2174 let mut arrays = HashSet::new();
2175 arrays.insert("choices".to_string());
2176 arrays.insert("choices[0].message.tool_calls".to_string());
2177 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2178 let expr = r.accessor("tool_call_name", "kotlin", "result");
2179 assert!(
2181 expr.contains("toolCalls()?.first()"),
2182 "expected toolCalls()?.first() for optional list, got: {expr}"
2183 );
2184 }
2185
2186 #[test]
2187 fn test_accessor_csharp() {
2188 let r = make_resolver();
2189 assert_eq!(
2190 r.accessor("title", "csharp", "result"),
2191 "result.Metadata.Document.Title"
2192 );
2193 }
2194
2195 #[test]
2196 fn test_accessor_php() {
2197 let r = make_resolver();
2198 assert_eq!(
2199 r.accessor("title", "php", "$result"),
2200 "$result->metadata->document->title"
2201 );
2202 }
2203
2204 #[test]
2205 fn test_accessor_r() {
2206 let r = make_resolver();
2207 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
2208 }
2209
2210 #[test]
2211 fn test_accessor_c() {
2212 let r = make_resolver();
2213 assert_eq!(
2214 r.accessor("title", "c", "result"),
2215 "result_metadata_document_title(result)"
2216 );
2217 }
2218
2219 #[test]
2220 fn test_rust_unwrap_binding() {
2221 let r = make_resolver();
2222 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
2223 assert_eq!(var, "metadata_document_title");
2224 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
2225 }
2226
2227 #[test]
2228 fn test_rust_unwrap_binding_non_optional() {
2229 let r = make_resolver();
2230 assert!(r.rust_unwrap_binding("content", "result").is_none());
2231 }
2232
2233 #[test]
2234 fn test_rust_unwrap_binding_collapses_double_underscore() {
2235 let mut aliases = HashMap::new();
2240 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
2241 let mut optional = HashSet::new();
2242 optional.insert("json_ld[].name".to_string());
2243 let mut array = HashSet::new();
2244 array.insert("json_ld".to_string());
2245 let result_fields = HashSet::new();
2246 let method_calls = HashSet::new();
2247 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
2248 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
2249 assert_eq!(var, "json_ld_name");
2250 }
2251
2252 #[test]
2253 fn test_direct_field_no_alias() {
2254 let r = make_resolver();
2255 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2256 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
2257 }
2258
2259 #[test]
2260 fn test_accessor_rust_with_optionals() {
2261 let r = make_resolver_with_doc_optional();
2262 assert_eq!(
2263 r.accessor("title", "rust", "result"),
2264 "result.metadata.document.as_ref().unwrap().title"
2265 );
2266 }
2267
2268 #[test]
2269 fn test_accessor_csharp_with_optionals() {
2270 let r = make_resolver_with_doc_optional();
2271 assert_eq!(
2272 r.accessor("title", "csharp", "result"),
2273 "result.Metadata.Document!.Title"
2274 );
2275 }
2276
2277 #[test]
2278 fn test_accessor_rust_non_optional_field() {
2279 let r = make_resolver();
2280 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2281 }
2282
2283 #[test]
2284 fn test_accessor_csharp_non_optional_field() {
2285 let r = make_resolver();
2286 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
2287 }
2288
2289 #[test]
2290 fn test_accessor_rust_method_call() {
2291 let mut fields = HashMap::new();
2293 fields.insert(
2294 "excel_sheet_count".to_string(),
2295 "metadata.format.excel.sheet_count".to_string(),
2296 );
2297 let mut optional = HashSet::new();
2298 optional.insert("metadata.format".to_string());
2299 optional.insert("metadata.format.excel".to_string());
2300 let mut method_calls = HashSet::new();
2301 method_calls.insert("metadata.format.excel".to_string());
2302 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
2303 assert_eq!(
2304 r.accessor("excel_sheet_count", "rust", "result"),
2305 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
2306 );
2307 }
2308
2309 fn make_php_getter_resolver() -> FieldResolver {
2314 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2315 getters.insert(
2316 "Root".to_string(),
2317 ["metadata".to_string(), "links".to_string()].into_iter().collect(),
2318 );
2319 let map = PhpGetterMap {
2320 getters,
2321 field_types: HashMap::new(),
2322 root_type: Some("Root".to_string()),
2323 all_fields: HashMap::new(),
2324 };
2325 FieldResolver::new_with_php_getters(
2326 &HashMap::new(),
2327 &HashSet::new(),
2328 &HashSet::new(),
2329 &HashSet::new(),
2330 &HashSet::new(),
2331 &HashMap::new(),
2332 map,
2333 )
2334 }
2335
2336 #[test]
2337 fn render_php_uses_getter_method_for_non_scalar_field() {
2338 let r = make_php_getter_resolver();
2339 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
2340 }
2341
2342 #[test]
2343 fn render_php_uses_property_for_scalar_field() {
2344 let r = make_php_getter_resolver();
2345 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2346 }
2347
2348 #[test]
2349 fn render_php_nested_non_scalar_uses_getter_then_property() {
2350 let mut fields = HashMap::new();
2351 fields.insert("title".to_string(), "metadata.title".to_string());
2352 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2353 getters.insert("Root".to_string(), ["metadata".to_string()].into_iter().collect());
2354 getters.insert("Metadata".to_string(), HashSet::new());
2356 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2357 field_types.insert(
2358 "Root".to_string(),
2359 [("metadata".to_string(), "Metadata".to_string())].into_iter().collect(),
2360 );
2361 let map = PhpGetterMap {
2362 getters,
2363 field_types,
2364 root_type: Some("Root".to_string()),
2365 all_fields: HashMap::new(),
2366 };
2367 let r = FieldResolver::new_with_php_getters(
2368 &fields,
2369 &HashSet::new(),
2370 &HashSet::new(),
2371 &HashSet::new(),
2372 &HashSet::new(),
2373 &HashMap::new(),
2374 map,
2375 );
2376 assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
2378 }
2379
2380 #[test]
2381 fn render_php_array_field_uses_getter_when_non_scalar() {
2382 let mut fields = HashMap::new();
2383 fields.insert("first_link".to_string(), "links[0]".to_string());
2384 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2385 getters.insert("Root".to_string(), ["links".to_string()].into_iter().collect());
2386 let map = PhpGetterMap {
2387 getters,
2388 field_types: HashMap::new(),
2389 root_type: Some("Root".to_string()),
2390 all_fields: HashMap::new(),
2391 };
2392 let r = FieldResolver::new_with_php_getters(
2393 &fields,
2394 &HashSet::new(),
2395 &HashSet::new(),
2396 &HashSet::new(),
2397 &HashSet::new(),
2398 &HashMap::new(),
2399 map,
2400 );
2401 assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
2402 }
2403
2404 #[test]
2405 fn render_php_falls_back_to_property_when_getter_fields_empty() {
2406 let r = FieldResolver::new(
2409 &HashMap::new(),
2410 &HashSet::new(),
2411 &HashSet::new(),
2412 &HashSet::new(),
2413 &HashSet::new(),
2414 );
2415 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2416 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
2417 }
2418
2419 #[test]
2423 fn render_php_with_getters_distinguishes_same_field_name_on_different_types() {
2424 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2425 getters.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2427 getters.insert("B".to_string(), HashSet::new());
2429 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2432 all_fields.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2433 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2434 let map_a = PhpGetterMap {
2435 getters: getters.clone(),
2436 field_types: HashMap::new(),
2437 root_type: Some("A".to_string()),
2438 all_fields: all_fields.clone(),
2439 };
2440 let map_b = PhpGetterMap {
2441 getters,
2442 field_types: HashMap::new(),
2443 root_type: Some("B".to_string()),
2444 all_fields,
2445 };
2446 let r_a = FieldResolver::new_with_php_getters(
2447 &HashMap::new(),
2448 &HashSet::new(),
2449 &HashSet::new(),
2450 &HashSet::new(),
2451 &HashSet::new(),
2452 &HashMap::new(),
2453 map_a,
2454 );
2455 let r_b = FieldResolver::new_with_php_getters(
2456 &HashMap::new(),
2457 &HashSet::new(),
2458 &HashSet::new(),
2459 &HashSet::new(),
2460 &HashSet::new(),
2461 &HashMap::new(),
2462 map_b,
2463 );
2464 assert_eq!(r_a.accessor("content", "php", "$a"), "$a->getContent()");
2465 assert_eq!(r_b.accessor("content", "php", "$b"), "$b->content");
2466 }
2467
2468 #[test]
2472 fn render_php_with_getters_chains_through_correct_type() {
2473 let mut fields = HashMap::new();
2474 fields.insert("nested_content".to_string(), "inner.content".to_string());
2475 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2476 getters.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2478 getters.insert("B".to_string(), HashSet::new());
2480 getters.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2483 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2484 field_types.insert(
2485 "Outer".to_string(),
2486 [("inner".to_string(), "B".to_string())].into_iter().collect(),
2487 );
2488 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2489 all_fields.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2490 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2491 all_fields.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2492 let map = PhpGetterMap {
2493 getters,
2494 field_types,
2495 root_type: Some("Outer".to_string()),
2496 all_fields,
2497 };
2498 let r = FieldResolver::new_with_php_getters(
2499 &fields,
2500 &HashSet::new(),
2501 &HashSet::new(),
2502 &HashSet::new(),
2503 &HashSet::new(),
2504 &HashMap::new(),
2505 map,
2506 );
2507 assert_eq!(
2508 r.accessor("nested_content", "php", "$result"),
2509 "$result->getInner()->content"
2510 );
2511 }
2512}