1use alef_codegen::naming::to_go_name;
8use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
9use std::collections::{HashMap, HashSet};
10
11#[derive(Clone)]
13pub struct FieldResolver {
14 aliases: HashMap<String, String>,
15 optional_fields: HashSet<String>,
16 result_fields: HashSet<String>,
17 array_fields: HashSet<String>,
18 method_calls: HashSet<String>,
19 error_field_aliases: HashMap<String, String>,
23 php_getter_map: PhpGetterMap,
34 swift_first_class_map: SwiftFirstClassMap,
41}
42
43#[derive(Debug, Clone, Default)]
58pub struct PhpGetterMap {
59 pub getters: HashMap<String, HashSet<String>>,
60 pub field_types: HashMap<String, HashMap<String, String>>,
61 pub root_type: Option<String>,
62 pub all_fields: HashMap<String, HashSet<String>>,
68}
69
70#[derive(Debug, Clone, Default)]
91pub struct SwiftFirstClassMap {
92 pub first_class_types: HashSet<String>,
93 pub field_types: HashMap<String, HashMap<String, String>>,
94 pub root_type: Option<String>,
95}
96
97impl SwiftFirstClassMap {
98 pub fn is_first_class(&self, type_name: Option<&str>) -> bool {
104 match type_name {
105 Some(t) => self.first_class_types.contains(t),
106 None => true,
107 }
108 }
109
110 pub fn advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
113 let owner = owner_type?;
114 self.field_types.get(owner).and_then(|m| m.get(field_name).cloned())
115 }
116
117 pub fn is_empty(&self) -> bool {
119 self.first_class_types.is_empty() && self.field_types.is_empty()
120 }
121}
122
123impl PhpGetterMap {
124 pub fn needs_getter(&self, owner_type: Option<&str>, field_name: &str) -> bool {
131 if let Some(t) = owner_type {
132 let owner_has_field = self.all_fields.get(t).is_some_and(|s| s.contains(field_name));
137 if owner_has_field {
138 if let Some(fields) = self.getters.get(t) {
139 return fields.contains(field_name);
140 }
141 }
142 }
143 self.getters.values().any(|set| set.contains(field_name))
144 }
145
146 pub fn advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
149 let owner = owner_type?;
150 self.field_types.get(owner).and_then(|m| m.get(field_name).cloned())
151 }
152
153 pub fn is_empty(&self) -> bool {
156 self.getters.is_empty()
157 }
158}
159
160#[derive(Debug, Clone)]
162enum PathSegment {
163 Field(String),
165 ArrayField { name: String, index: usize },
170 MapAccess { field: String, key: String },
172 Length,
174}
175
176impl FieldResolver {
177 pub fn new(
181 fields: &HashMap<String, String>,
182 optional: &HashSet<String>,
183 result_fields: &HashSet<String>,
184 array_fields: &HashSet<String>,
185 method_calls: &HashSet<String>,
186 ) -> Self {
187 Self {
188 aliases: fields.clone(),
189 optional_fields: optional.clone(),
190 result_fields: result_fields.clone(),
191 array_fields: array_fields.clone(),
192 method_calls: method_calls.clone(),
193 error_field_aliases: HashMap::new(),
194 php_getter_map: PhpGetterMap::default(),
195 swift_first_class_map: SwiftFirstClassMap::default(),
196 }
197 }
198
199 pub fn new_with_error_aliases(
205 fields: &HashMap<String, String>,
206 optional: &HashSet<String>,
207 result_fields: &HashSet<String>,
208 array_fields: &HashSet<String>,
209 method_calls: &HashSet<String>,
210 error_field_aliases: &HashMap<String, String>,
211 ) -> Self {
212 Self {
213 aliases: fields.clone(),
214 optional_fields: optional.clone(),
215 result_fields: result_fields.clone(),
216 array_fields: array_fields.clone(),
217 method_calls: method_calls.clone(),
218 error_field_aliases: error_field_aliases.clone(),
219 php_getter_map: PhpGetterMap::default(),
220 swift_first_class_map: SwiftFirstClassMap::default(),
221 }
222 }
223
224 pub fn new_with_php_getters(
239 fields: &HashMap<String, String>,
240 optional: &HashSet<String>,
241 result_fields: &HashSet<String>,
242 array_fields: &HashSet<String>,
243 method_calls: &HashSet<String>,
244 error_field_aliases: &HashMap<String, String>,
245 php_getter_map: PhpGetterMap,
246 ) -> Self {
247 Self {
248 aliases: fields.clone(),
249 optional_fields: optional.clone(),
250 result_fields: result_fields.clone(),
251 array_fields: array_fields.clone(),
252 method_calls: method_calls.clone(),
253 error_field_aliases: error_field_aliases.clone(),
254 php_getter_map,
255 swift_first_class_map: SwiftFirstClassMap::default(),
256 }
257 }
258
259 pub fn with_swift_root_type(&self, root_type: Option<String>) -> Self {
270 let mut clone = self.clone();
271 clone.swift_first_class_map.root_type = root_type;
272 clone
273 }
274
275 #[allow(clippy::too_many_arguments)]
279 pub fn new_with_swift_first_class(
280 fields: &HashMap<String, String>,
281 optional: &HashSet<String>,
282 result_fields: &HashSet<String>,
283 array_fields: &HashSet<String>,
284 method_calls: &HashSet<String>,
285 error_field_aliases: &HashMap<String, String>,
286 swift_first_class_map: SwiftFirstClassMap,
287 ) -> Self {
288 Self {
289 aliases: fields.clone(),
290 optional_fields: optional.clone(),
291 result_fields: result_fields.clone(),
292 array_fields: array_fields.clone(),
293 method_calls: method_calls.clone(),
294 error_field_aliases: error_field_aliases.clone(),
295 php_getter_map: PhpGetterMap::default(),
296 swift_first_class_map,
297 }
298 }
299
300 pub fn resolve<'a>(&'a self, fixture_field: &'a str) -> &'a str {
303 self.aliases
304 .get(fixture_field)
305 .map(String::as_str)
306 .unwrap_or(fixture_field)
307 }
308
309 pub fn is_optional(&self, field: &str) -> bool {
311 if self.optional_fields.contains(field) {
312 return true;
313 }
314 let index_normalized = normalize_numeric_indices(field);
315 if index_normalized != field && self.optional_fields.contains(index_normalized.as_str()) {
316 return true;
317 }
318 let de_indexed = strip_numeric_indices(field);
321 if de_indexed != field && self.optional_fields.contains(de_indexed.as_str()) {
322 return true;
323 }
324 let normalized = field.replace("[].", ".");
325 if normalized != field && self.optional_fields.contains(normalized.as_str()) {
326 return true;
327 }
328 for af in &self.array_fields {
329 if let Some(rest) = field.strip_prefix(af.as_str()) {
330 if let Some(rest) = rest.strip_prefix('.') {
331 let with_bracket = format!("{af}[].{rest}");
332 if self.optional_fields.contains(with_bracket.as_str()) {
333 return true;
334 }
335 }
336 }
337 }
338 false
339 }
340
341 pub fn has_alias(&self, fixture_field: &str) -> bool {
343 self.aliases.contains_key(fixture_field)
344 }
345
346 pub fn has_explicit_field(&self, field_name: &str) -> bool {
352 if self.result_fields.is_empty() {
353 return false;
354 }
355 self.result_fields.contains(field_name)
356 }
357
358 pub fn is_valid_for_result(&self, fixture_field: &str) -> bool {
360 if self.result_fields.is_empty() {
361 return true;
362 }
363 let resolved = self.resolve(fixture_field);
364 let first_segment = resolved.split('.').next().unwrap_or(resolved);
365 let first_segment = first_segment.split('[').next().unwrap_or(first_segment);
366 self.result_fields.contains(first_segment)
367 }
368
369 pub fn is_array(&self, field: &str) -> bool {
371 self.array_fields.contains(field)
372 }
373
374 pub fn is_collection_root(&self, field: &str) -> bool {
387 let prefix = format!("{field}[");
388 self.array_fields.iter().any(|af| af.starts_with(&prefix))
389 || self.optional_fields.iter().any(|of| of.starts_with(&prefix))
390 }
391
392 pub fn tagged_union_split(&self, fixture_field: &str) -> Option<(String, String, String)> {
404 let resolved = self.resolve(fixture_field);
405 let segments: Vec<&str> = resolved.split('.').collect();
406 let mut path_so_far = String::new();
407 for (i, seg) in segments.iter().enumerate() {
408 if !path_so_far.is_empty() {
409 path_so_far.push('.');
410 }
411 path_so_far.push_str(seg);
412 if self.method_calls.contains(&path_so_far) {
413 let prefix = segments[..i].join(".");
415 let variant = (*seg).to_string();
416 let suffix = segments[i + 1..].join(".");
417 return Some((prefix, variant, suffix));
418 }
419 }
420 None
421 }
422
423 pub fn has_map_access(&self, fixture_field: &str) -> bool {
425 let resolved = self.resolve(fixture_field);
426 let segments = parse_path(resolved);
427 segments.iter().any(|s| {
428 if let PathSegment::MapAccess { key, .. } = s {
429 !key.chars().all(|c| c.is_ascii_digit())
430 } else {
431 false
432 }
433 })
434 }
435
436 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
438 let resolved = self.resolve(fixture_field);
439 let segments = parse_path(resolved);
440 let segments = self.inject_array_indexing(segments);
441 match language {
442 "java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
443 "kotlin" => render_kotlin_with_optionals(&segments, result_var, &self.optional_fields),
444 "kotlin_android" => render_kotlin_android_with_optionals(&segments, result_var, &self.optional_fields),
447 "rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
448 "csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
449 "zig" => render_zig_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
450 "swift" if !self.swift_first_class_map.is_empty() => render_swift_with_first_class_map(
451 &segments,
452 result_var,
453 &self.optional_fields,
454 &self.swift_first_class_map,
455 ),
456 "swift" => render_swift_with_optionals(&segments, result_var, &self.optional_fields),
457 "dart" => render_dart_with_optionals(&segments, result_var, &self.optional_fields),
458 "php" if !self.php_getter_map.is_empty() => {
459 render_php_with_getters(&segments, result_var, &self.php_getter_map)
460 }
461 _ => render_accessor(&segments, language, result_var),
462 }
463 }
464
465 pub fn accessor_for_error(&self, sub_field: &str, language: &str, err_var: &str) -> String {
479 let resolved = self
480 .error_field_aliases
481 .get(sub_field)
482 .map(String::as_str)
483 .unwrap_or(sub_field);
484 let segments = parse_path(resolved);
485 match language {
488 "rust" => render_rust_with_optionals(&segments, err_var, &self.optional_fields, &self.method_calls),
489 _ => render_accessor(&segments, language, err_var),
490 }
491 }
492
493 pub fn has_error_aliases(&self) -> bool {
500 !self.error_field_aliases.is_empty()
501 }
502
503 fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
504 if self.array_fields.is_empty() {
505 return segments;
506 }
507 let len = segments.len();
508 let mut result = Vec::with_capacity(len);
509 let mut path_so_far = String::new();
510 for i in 0..len {
511 let seg = &segments[i];
512 match seg {
513 PathSegment::Field(f) => {
514 if !path_so_far.is_empty() {
515 path_so_far.push('.');
516 }
517 path_so_far.push_str(f);
518 let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
519 if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
520 result.push(PathSegment::ArrayField {
522 name: f.clone(),
523 index: 0,
524 });
525 } else {
526 result.push(seg.clone());
527 }
528 }
529 PathSegment::ArrayField { .. } => {
532 result.push(seg.clone());
533 }
534 PathSegment::MapAccess { field, key } => {
535 if !path_so_far.is_empty() {
536 path_so_far.push('.');
537 }
538 path_so_far.push_str(field);
539 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
540 if is_numeric && self.array_fields.contains(&path_so_far) {
541 let index: usize = key.parse().unwrap_or(0);
543 result.push(PathSegment::ArrayField {
544 name: field.clone(),
545 index,
546 });
547 } else {
548 result.push(seg.clone());
549 }
550 }
551 _ => {
552 result.push(seg.clone());
553 }
554 }
555 }
556 result
557 }
558
559 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
561 let resolved = self.resolve(fixture_field);
562 if !self.is_optional(resolved) {
563 return None;
564 }
565 let segments = parse_path(resolved);
566 let segments = self.inject_array_indexing(segments);
567 let local_var = {
572 let raw = resolved.replace(['.', '['], "_").replace(']', "");
573 let mut collapsed = String::with_capacity(raw.len());
574 let mut prev_underscore = false;
575 for ch in raw.chars() {
576 if ch == '_' {
577 if !prev_underscore {
578 collapsed.push('_');
579 }
580 prev_underscore = true;
581 } else {
582 collapsed.push(ch);
583 prev_underscore = false;
584 }
585 }
586 collapsed.trim_matches('_').to_string()
587 };
588 let accessor = render_accessor(&segments, "rust", result_var);
589 let has_map_access = segments.iter().any(|s| {
590 if let PathSegment::MapAccess { key, .. } = s {
591 !key.chars().all(|c| c.is_ascii_digit())
592 } else {
593 false
594 }
595 });
596 let is_array = self.is_array(resolved);
597 let binding = if has_map_access {
598 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
599 } else if is_array {
600 format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
601 } else {
602 format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
603 };
604 Some((binding, local_var))
605 }
606}
607
608fn strip_numeric_indices(path: &str) -> String {
613 let mut result = String::with_capacity(path.len());
614 let mut chars = path.chars().peekable();
615 while let Some(c) = chars.next() {
616 if c == '[' {
617 let mut key = String::new();
618 let mut closed = false;
619 for inner in chars.by_ref() {
620 if inner == ']' {
621 closed = true;
622 break;
623 }
624 key.push(inner);
625 }
626 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
627 } else {
629 result.push('[');
630 result.push_str(&key);
631 if closed {
632 result.push(']');
633 }
634 }
635 } else {
636 result.push(c);
637 }
638 }
639 while result.contains("..") {
641 result = result.replace("..", ".");
642 }
643 if result.starts_with('.') {
644 result.remove(0);
645 }
646 result
647}
648
649fn normalize_numeric_indices(path: &str) -> String {
650 let mut result = String::with_capacity(path.len());
651 let mut chars = path.chars().peekable();
652 while let Some(c) = chars.next() {
653 if c == '[' {
654 let mut key = String::new();
655 let mut closed = false;
656 for inner in chars.by_ref() {
657 if inner == ']' {
658 closed = true;
659 break;
660 }
661 key.push(inner);
662 }
663 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
664 result.push_str("[0]");
665 } else {
666 result.push('[');
667 result.push_str(&key);
668 if closed {
669 result.push(']');
670 }
671 }
672 } else {
673 result.push(c);
674 }
675 }
676 result
677}
678
679fn parse_path(path: &str) -> Vec<PathSegment> {
680 let mut segments = Vec::new();
681 for part in path.split('.') {
682 if part == "length" || part == "count" || part == "size" {
683 segments.push(PathSegment::Length);
684 } else if let Some(bracket_pos) = part.find('[') {
685 let name = part[..bracket_pos].to_string();
686 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
687 if key.is_empty() {
688 segments.push(PathSegment::ArrayField { name, index: 0 });
690 } else if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
691 let index: usize = key.parse().unwrap_or(0);
693 segments.push(PathSegment::ArrayField { name, index });
694 } else {
695 segments.push(PathSegment::MapAccess { field: name, key });
697 }
698 } else {
699 segments.push(PathSegment::Field(part.to_string()));
700 }
701 }
702 segments
703}
704
705fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
706 match language {
707 "rust" => render_rust(segments, result_var),
708 "python" => render_dot_access(segments, result_var, "python"),
709 "typescript" | "node" => render_typescript(segments, result_var),
710 "wasm" => render_wasm(segments, result_var),
711 "go" => render_go(segments, result_var),
712 "java" => render_java(segments, result_var),
713 "kotlin" => render_kotlin(segments, result_var),
714 "kotlin_android" => render_kotlin_android(segments, result_var),
715 "csharp" => render_pascal_dot(segments, result_var),
716 "ruby" => render_dot_access(segments, result_var, "ruby"),
717 "php" => render_php(segments, result_var),
718 "elixir" => render_dot_access(segments, result_var, "elixir"),
719 "r" => render_r(segments, result_var),
720 "c" => render_c(segments, result_var),
721 "swift" => render_swift(segments, result_var),
722 "dart" => render_dart(segments, result_var),
723 _ => render_dot_access(segments, result_var, language),
724 }
725}
726
727fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
739 let mut out = result_var.to_string();
740 for seg in segments {
741 match seg {
742 PathSegment::Field(f) => {
743 out.push('.');
744 out.push_str(f);
745 }
746 PathSegment::ArrayField { name, index } => {
747 out.push('.');
748 out.push_str(name);
749 out.push_str(&format!("[{index}]"));
750 }
751 PathSegment::MapAccess { field, key } => {
752 out.push('.');
753 out.push_str(field);
754 if key.chars().all(|c| c.is_ascii_digit()) {
755 out.push_str(&format!("[{key}]"));
756 } else {
757 out.push_str(&format!("[\"{key}\"]"));
758 }
759 }
760 PathSegment::Length => {
761 out.push_str(".count");
762 }
763 }
764 }
765 out
766}
767
768fn render_swift_with_optionals(
778 segments: &[PathSegment],
779 result_var: &str,
780 optional_fields: &HashSet<String>,
781) -> String {
782 let mut out = result_var.to_string();
783 let mut path_so_far = String::new();
784 let total = segments.len();
785 for (i, seg) in segments.iter().enumerate() {
786 let is_leaf = i == total - 1;
787 match seg {
788 PathSegment::Field(f) => {
789 if !path_so_far.is_empty() {
790 path_so_far.push('.');
791 }
792 path_so_far.push_str(f);
793 out.push('.');
794 out.push_str(f);
795 if !is_leaf && optional_fields.contains(&path_so_far) {
799 out.push('?');
800 }
801 }
802 PathSegment::ArrayField { name, index } => {
803 if !path_so_far.is_empty() {
804 path_so_far.push('.');
805 }
806 path_so_far.push_str(name);
807 let is_optional = optional_fields.contains(&path_so_far);
808 out.push('.');
809 out.push_str(name);
810 if is_optional {
811 out.push_str(&format!("?[{index}]"));
813 } else {
814 out.push_str(&format!("[{index}]"));
815 }
816 path_so_far.push_str("[0]");
817 let _ = is_leaf;
818 }
819 PathSegment::MapAccess { field, key } => {
820 if !path_so_far.is_empty() {
821 path_so_far.push('.');
822 }
823 path_so_far.push_str(field);
824 out.push('.');
825 out.push_str(field);
826 if key.chars().all(|c| c.is_ascii_digit()) {
827 out.push_str(&format!("[{key}]"));
828 } else {
829 out.push_str(&format!("[\"{key}\"]"));
830 }
831 }
832 PathSegment::Length => {
833 out.push_str(".count");
834 }
835 }
836 }
837 out
838}
839
840fn render_swift_with_first_class_map(
845 segments: &[PathSegment],
846 result_var: &str,
847 optional_fields: &HashSet<String>,
848 map: &SwiftFirstClassMap,
849) -> String {
850 let mut out = result_var.to_string();
851 let mut path_so_far = String::new();
852 let mut current_type: Option<String> = map.root_type.clone();
853 let mut via_rust_vec = false;
862 let total = segments.len();
863 for (i, seg) in segments.iter().enumerate() {
864 let is_leaf = i == total - 1;
865 let property_syntax = !via_rust_vec && map.is_first_class(current_type.as_deref());
866 match seg {
867 PathSegment::Field(f) => {
868 if !path_so_far.is_empty() {
869 path_so_far.push('.');
870 }
871 path_so_far.push_str(f);
872 out.push('.');
873 out.push_str(f);
874 if !property_syntax {
875 out.push_str("()");
876 }
877 if !is_leaf && optional_fields.contains(&path_so_far) {
878 out.push('?');
879 }
880 current_type = map.advance(current_type.as_deref(), f);
881 }
882 PathSegment::ArrayField { name, index } => {
883 if !path_so_far.is_empty() {
884 path_so_far.push('.');
885 }
886 path_so_far.push_str(name);
887 let is_optional = optional_fields.contains(&path_so_far);
888 out.push('.');
889 out.push_str(name);
890 let access = if property_syntax { "" } else { "()" };
891 if is_optional {
892 out.push_str(&format!("{access}?[{index}]"));
893 } else {
894 out.push_str(&format!("{access}[{index}]"));
895 }
896 path_so_far.push_str("[0]");
897 current_type = map.advance(current_type.as_deref(), name);
900 via_rust_vec = true;
901 }
902 PathSegment::MapAccess { field, key } => {
903 if !path_so_far.is_empty() {
904 path_so_far.push('.');
905 }
906 path_so_far.push_str(field);
907 out.push('.');
908 out.push_str(field);
909 let access = if property_syntax { "" } else { "()" };
910 if key.chars().all(|c| c.is_ascii_digit()) {
911 out.push_str(&format!("{access}[{key}]"));
912 } else {
913 out.push_str(&format!("{access}[\"{key}\"]"));
914 }
915 current_type = map.advance(current_type.as_deref(), field);
916 }
917 PathSegment::Length => {
918 out.push_str(".count");
919 }
920 }
921 }
922 out
923}
924
925fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
926 let mut out = result_var.to_string();
927 for seg in segments {
928 match seg {
929 PathSegment::Field(f) => {
930 out.push('.');
931 out.push_str(&f.to_snake_case());
932 }
933 PathSegment::ArrayField { name, index } => {
934 out.push('.');
935 out.push_str(&name.to_snake_case());
936 out.push_str(&format!("[{index}]"));
937 }
938 PathSegment::MapAccess { field, key } => {
939 out.push('.');
940 out.push_str(&field.to_snake_case());
941 if key.chars().all(|c| c.is_ascii_digit()) {
942 out.push_str(&format!("[{key}]"));
943 } else {
944 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
945 }
946 }
947 PathSegment::Length => {
948 out.push_str(".len()");
949 }
950 }
951 }
952 out
953}
954
955fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
956 let mut out = result_var.to_string();
957 for seg in segments {
958 match seg {
959 PathSegment::Field(f) => {
960 out.push('.');
961 out.push_str(f);
962 }
963 PathSegment::ArrayField { name, index } => {
964 if language == "elixir" {
965 let current = std::mem::take(&mut out);
966 out = format!("Enum.at({current}.{name}, {index})");
967 } else {
968 out.push('.');
969 out.push_str(name);
970 out.push_str(&format!("[{index}]"));
971 }
972 }
973 PathSegment::MapAccess { field, key } => {
974 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
975 if is_numeric && language == "elixir" {
976 let current = std::mem::take(&mut out);
977 out = format!("Enum.at({current}.{field}, {key})");
978 } else {
979 out.push('.');
980 out.push_str(field);
981 if is_numeric {
982 let idx: usize = key.parse().unwrap_or(0);
983 out.push_str(&format!("[{idx}]"));
984 } else if language == "elixir" || language == "ruby" {
985 out.push_str(&format!("[\"{key}\"]"));
988 } else {
989 out.push_str(&format!(".get(\"{key}\")"));
990 }
991 }
992 }
993 PathSegment::Length => match language {
994 "ruby" => out.push_str(".length"),
995 "elixir" => {
996 let current = std::mem::take(&mut out);
997 out = format!("length({current})");
998 }
999 "gleam" => {
1000 let current = std::mem::take(&mut out);
1001 out = format!("list.length({current})");
1002 }
1003 _ => {
1004 let current = std::mem::take(&mut out);
1005 out = format!("len({current})");
1006 }
1007 },
1008 }
1009 }
1010 out
1011}
1012
1013fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
1014 let mut out = result_var.to_string();
1015 for seg in segments {
1016 match seg {
1017 PathSegment::Field(f) => {
1018 out.push('.');
1019 out.push_str(&f.to_lower_camel_case());
1020 }
1021 PathSegment::ArrayField { name, index } => {
1022 out.push('.');
1023 out.push_str(&name.to_lower_camel_case());
1024 out.push_str(&format!("[{index}]"));
1025 }
1026 PathSegment::MapAccess { field, key } => {
1027 out.push('.');
1028 out.push_str(&field.to_lower_camel_case());
1029 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
1032 out.push_str(&format!("[{key}]"));
1033 } else {
1034 out.push_str(&format!("[\"{key}\"]"));
1035 }
1036 }
1037 PathSegment::Length => {
1038 out.push_str(".length");
1039 }
1040 }
1041 }
1042 out
1043}
1044
1045fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
1046 let mut out = result_var.to_string();
1047 for seg in segments {
1048 match seg {
1049 PathSegment::Field(f) => {
1050 out.push('.');
1051 out.push_str(&f.to_lower_camel_case());
1052 }
1053 PathSegment::ArrayField { name, index } => {
1054 out.push('.');
1055 out.push_str(&name.to_lower_camel_case());
1056 out.push_str(&format!("[{index}]"));
1057 }
1058 PathSegment::MapAccess { field, key } => {
1059 out.push('.');
1060 out.push_str(&field.to_lower_camel_case());
1061 out.push_str(&format!(".get(\"{key}\")"));
1062 }
1063 PathSegment::Length => {
1064 out.push_str(".length");
1065 }
1066 }
1067 }
1068 out
1069}
1070
1071fn render_go(segments: &[PathSegment], result_var: &str) -> String {
1072 let mut out = result_var.to_string();
1073 for seg in segments {
1074 match seg {
1075 PathSegment::Field(f) => {
1076 out.push('.');
1077 out.push_str(&to_go_name(f));
1078 }
1079 PathSegment::ArrayField { name, index } => {
1080 out.push('.');
1081 out.push_str(&to_go_name(name));
1082 out.push_str(&format!("[{index}]"));
1083 }
1084 PathSegment::MapAccess { field, key } => {
1085 out.push('.');
1086 out.push_str(&to_go_name(field));
1087 if key.chars().all(|c| c.is_ascii_digit()) {
1088 out.push_str(&format!("[{key}]"));
1089 } else {
1090 out.push_str(&format!("[\"{key}\"]"));
1091 }
1092 }
1093 PathSegment::Length => {
1094 let current = std::mem::take(&mut out);
1095 out = format!("len({current})");
1096 }
1097 }
1098 }
1099 out
1100}
1101
1102fn render_java(segments: &[PathSegment], result_var: &str) -> String {
1103 let mut out = result_var.to_string();
1104 for seg in segments {
1105 match seg {
1106 PathSegment::Field(f) => {
1107 out.push('.');
1108 out.push_str(&f.to_lower_camel_case());
1109 out.push_str("()");
1110 }
1111 PathSegment::ArrayField { name, index } => {
1112 out.push('.');
1113 out.push_str(&name.to_lower_camel_case());
1114 out.push_str(&format!("().get({index})"));
1115 }
1116 PathSegment::MapAccess { field, key } => {
1117 out.push('.');
1118 out.push_str(&field.to_lower_camel_case());
1119 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1121 if is_numeric {
1122 out.push_str(&format!("().get({key})"));
1123 } else {
1124 out.push_str(&format!("().get(\"{key}\")"));
1125 }
1126 }
1127 PathSegment::Length => {
1128 out.push_str(".size()");
1129 }
1130 }
1131 }
1132 out
1133}
1134
1135fn kotlin_getter(name: &str) -> String {
1140 let camel = name.to_lower_camel_case();
1141 match camel.as_str() {
1142 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
1143 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
1144 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
1145 _ => camel,
1146 }
1147}
1148
1149fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
1150 let mut out = result_var.to_string();
1151 for seg in segments {
1152 match seg {
1153 PathSegment::Field(f) => {
1154 out.push('.');
1155 out.push_str(&kotlin_getter(f));
1156 out.push_str("()");
1157 }
1158 PathSegment::ArrayField { name, index } => {
1159 out.push('.');
1160 out.push_str(&kotlin_getter(name));
1161 if *index == 0 {
1162 out.push_str("().first()");
1163 } else {
1164 out.push_str(&format!("().get({index})"));
1165 }
1166 }
1167 PathSegment::MapAccess { field, key } => {
1168 out.push('.');
1169 out.push_str(&kotlin_getter(field));
1170 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1171 if is_numeric {
1172 out.push_str(&format!("().get({key})"));
1173 } else {
1174 out.push_str(&format!("().get(\"{key}\")"));
1175 }
1176 }
1177 PathSegment::Length => {
1178 out.push_str(".size");
1179 }
1180 }
1181 }
1182 out
1183}
1184
1185fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1186 let mut out = result_var.to_string();
1187 let mut path_so_far = String::new();
1188 for (i, seg) in segments.iter().enumerate() {
1189 let is_leaf = i == segments.len() - 1;
1190 match seg {
1191 PathSegment::Field(f) => {
1192 if !path_so_far.is_empty() {
1193 path_so_far.push('.');
1194 }
1195 path_so_far.push_str(f);
1196 out.push('.');
1197 out.push_str(&f.to_lower_camel_case());
1198 out.push_str("()");
1199 let _ = is_leaf;
1200 let _ = optional_fields;
1201 }
1202 PathSegment::ArrayField { name, index } => {
1203 if !path_so_far.is_empty() {
1204 path_so_far.push('.');
1205 }
1206 path_so_far.push_str(name);
1207 out.push('.');
1208 out.push_str(&name.to_lower_camel_case());
1209 out.push_str(&format!("().get({index})"));
1210 }
1211 PathSegment::MapAccess { field, key } => {
1212 if !path_so_far.is_empty() {
1213 path_so_far.push('.');
1214 }
1215 path_so_far.push_str(field);
1216 out.push('.');
1217 out.push_str(&field.to_lower_camel_case());
1218 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1220 if is_numeric {
1221 out.push_str(&format!("().get({key})"));
1222 } else {
1223 out.push_str(&format!("().get(\"{key}\")"));
1224 }
1225 }
1226 PathSegment::Length => {
1227 out.push_str(".size()");
1228 }
1229 }
1230 }
1231 out
1232}
1233
1234fn render_kotlin_with_optionals(
1249 segments: &[PathSegment],
1250 result_var: &str,
1251 optional_fields: &HashSet<String>,
1252) -> String {
1253 let mut out = result_var.to_string();
1254 let mut path_so_far = String::new();
1255 let mut prev_was_nullable = false;
1263 for seg in segments {
1264 let nav = if prev_was_nullable { "?." } else { "." };
1265 match seg {
1266 PathSegment::Field(f) => {
1267 if !path_so_far.is_empty() {
1268 path_so_far.push('.');
1269 }
1270 path_so_far.push_str(f);
1271 let is_optional = optional_fields.contains(&path_so_far);
1276 out.push_str(nav);
1277 out.push_str(&kotlin_getter(f));
1278 out.push_str("()");
1279 prev_was_nullable = prev_was_nullable || is_optional;
1280 }
1281 PathSegment::ArrayField { name, index } => {
1282 if !path_so_far.is_empty() {
1283 path_so_far.push('.');
1284 }
1285 path_so_far.push_str(name);
1286 let is_optional = optional_fields.contains(&path_so_far);
1287 out.push_str(nav);
1288 out.push_str(&kotlin_getter(name));
1289 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1290 if *index == 0 {
1291 out.push_str(&format!("(){safe}.first()"));
1292 } else {
1293 out.push_str(&format!("(){safe}.get({index})"));
1294 }
1295 path_so_far.push_str("[0]");
1299 prev_was_nullable = prev_was_nullable || is_optional;
1300 }
1301 PathSegment::MapAccess { field, key } => {
1302 if !path_so_far.is_empty() {
1303 path_so_far.push('.');
1304 }
1305 path_so_far.push_str(field);
1306 let is_optional = optional_fields.contains(&path_so_far);
1307 out.push_str(nav);
1308 out.push_str(&kotlin_getter(field));
1309 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1310 if is_numeric {
1311 if prev_was_nullable || is_optional {
1312 out.push_str(&format!("()?.get({key})"));
1313 } else {
1314 out.push_str(&format!("().get({key})"));
1315 }
1316 } else if prev_was_nullable || is_optional {
1317 out.push_str(&format!("()?.get(\"{key}\")"));
1318 } else {
1319 out.push_str(&format!("().get(\"{key}\")"));
1320 }
1321 prev_was_nullable = prev_was_nullable || is_optional;
1322 }
1323 PathSegment::Length => {
1324 let size_nav = if prev_was_nullable { "?" } else { "" };
1327 out.push_str(&format!("{size_nav}.size"));
1328 prev_was_nullable = false;
1329 }
1330 }
1331 }
1332 out
1333}
1334
1335fn render_kotlin_android_with_optionals(
1346 segments: &[PathSegment],
1347 result_var: &str,
1348 optional_fields: &HashSet<String>,
1349) -> String {
1350 let mut out = result_var.to_string();
1351 let mut path_so_far = String::new();
1352 let mut prev_was_nullable = false;
1353 for seg in segments {
1354 let nav = if prev_was_nullable { "?." } else { "." };
1355 match seg {
1356 PathSegment::Field(f) => {
1357 if !path_so_far.is_empty() {
1358 path_so_far.push('.');
1359 }
1360 path_so_far.push_str(f);
1361 let is_optional = optional_fields.contains(&path_so_far);
1362 out.push_str(nav);
1363 out.push_str(&kotlin_getter(f));
1365 prev_was_nullable = prev_was_nullable || is_optional;
1366 }
1367 PathSegment::ArrayField { name, index } => {
1368 if !path_so_far.is_empty() {
1369 path_so_far.push('.');
1370 }
1371 path_so_far.push_str(name);
1372 let is_optional = optional_fields.contains(&path_so_far);
1373 out.push_str(nav);
1374 out.push_str(&kotlin_getter(name));
1376 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1377 if *index == 0 {
1378 out.push_str(&format!("{safe}.first()"));
1379 } else {
1380 out.push_str(&format!("{safe}.get({index})"));
1381 }
1382 path_so_far.push_str("[0]");
1383 prev_was_nullable = prev_was_nullable || is_optional;
1384 }
1385 PathSegment::MapAccess { field, key } => {
1386 if !path_so_far.is_empty() {
1387 path_so_far.push('.');
1388 }
1389 path_so_far.push_str(field);
1390 let is_optional = optional_fields.contains(&path_so_far);
1391 out.push_str(nav);
1392 out.push_str(&kotlin_getter(field));
1394 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1395 if is_numeric {
1396 if prev_was_nullable || is_optional {
1397 out.push_str(&format!("?.get({key})"));
1398 } else {
1399 out.push_str(&format!(".get({key})"));
1400 }
1401 } else if prev_was_nullable || is_optional {
1402 out.push_str(&format!("?.get(\"{key}\")"));
1403 } else {
1404 out.push_str(&format!(".get(\"{key}\")"));
1405 }
1406 prev_was_nullable = prev_was_nullable || is_optional;
1407 }
1408 PathSegment::Length => {
1409 let size_nav = if prev_was_nullable { "?" } else { "" };
1410 out.push_str(&format!("{size_nav}.size"));
1411 prev_was_nullable = false;
1412 }
1413 }
1414 }
1415 out
1416}
1417
1418fn render_kotlin_android(segments: &[PathSegment], result_var: &str) -> String {
1422 let mut out = result_var.to_string();
1423 for seg in segments {
1424 match seg {
1425 PathSegment::Field(f) => {
1426 out.push('.');
1427 out.push_str(&kotlin_getter(f));
1428 }
1430 PathSegment::ArrayField { name, index } => {
1431 out.push('.');
1432 out.push_str(&kotlin_getter(name));
1433 if *index == 0 {
1434 out.push_str(".first()");
1435 } else {
1436 out.push_str(&format!(".get({index})"));
1437 }
1438 }
1439 PathSegment::MapAccess { field, key } => {
1440 out.push('.');
1441 out.push_str(&kotlin_getter(field));
1442 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1443 if is_numeric {
1444 out.push_str(&format!(".get({key})"));
1445 } else {
1446 out.push_str(&format!(".get(\"{key}\")"));
1447 }
1448 }
1449 PathSegment::Length => {
1450 out.push_str(".size");
1451 }
1452 }
1453 }
1454 out
1455}
1456
1457fn render_rust_with_optionals(
1463 segments: &[PathSegment],
1464 result_var: &str,
1465 optional_fields: &HashSet<String>,
1466 method_calls: &HashSet<String>,
1467) -> String {
1468 let mut out = result_var.to_string();
1469 let mut path_so_far = String::new();
1470 for (i, seg) in segments.iter().enumerate() {
1471 let is_leaf = i == segments.len() - 1;
1472 match seg {
1473 PathSegment::Field(f) => {
1474 if !path_so_far.is_empty() {
1475 path_so_far.push('.');
1476 }
1477 path_so_far.push_str(f);
1478 out.push('.');
1479 out.push_str(&f.to_snake_case());
1480 let is_method = method_calls.contains(&path_so_far);
1481 if is_method {
1482 out.push_str("()");
1483 if !is_leaf && optional_fields.contains(&path_so_far) {
1484 out.push_str(".as_ref().unwrap()");
1485 }
1486 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1487 out.push_str(".as_ref().unwrap()");
1488 }
1489 }
1490 PathSegment::ArrayField { name, index } => {
1491 if !path_so_far.is_empty() {
1492 path_so_far.push('.');
1493 }
1494 path_so_far.push_str(name);
1495 out.push('.');
1496 out.push_str(&name.to_snake_case());
1497 let path_with_idx = format!("{path_so_far}[0]");
1501 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1502 if is_opt {
1503 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1504 } else {
1505 out.push_str(&format!("[{index}]"));
1506 }
1507 path_so_far.push_str("[0]");
1512 }
1513 PathSegment::MapAccess { field, key } => {
1514 if !path_so_far.is_empty() {
1515 path_so_far.push('.');
1516 }
1517 path_so_far.push_str(field);
1518 out.push('.');
1519 out.push_str(&field.to_snake_case());
1520 if key.chars().all(|c| c.is_ascii_digit()) {
1521 let path_with_idx = format!("{path_so_far}[0]");
1523 let is_opt =
1524 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1525 if is_opt {
1526 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1527 } else {
1528 out.push_str(&format!("[{key}]"));
1529 }
1530 path_so_far.push_str("[0]");
1531 } else {
1532 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1533 }
1534 }
1535 PathSegment::Length => {
1536 out.push_str(".len()");
1537 }
1538 }
1539 }
1540 out
1541}
1542
1543fn render_zig_with_optionals(
1556 segments: &[PathSegment],
1557 result_var: &str,
1558 optional_fields: &HashSet<String>,
1559 method_calls: &HashSet<String>,
1560) -> String {
1561 let mut out = result_var.to_string();
1562 let mut path_so_far = String::new();
1563 for seg in segments {
1564 match seg {
1565 PathSegment::Field(f) => {
1566 if !path_so_far.is_empty() {
1567 path_so_far.push('.');
1568 }
1569 path_so_far.push_str(f);
1570 out.push('.');
1571 out.push_str(f);
1572 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1573 out.push_str(".?");
1574 }
1575 }
1576 PathSegment::ArrayField { name, index } => {
1577 if !path_so_far.is_empty() {
1578 path_so_far.push('.');
1579 }
1580 path_so_far.push_str(name);
1581 out.push('.');
1582 out.push_str(name);
1583 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1584 out.push_str(".?");
1585 }
1586 out.push_str(&format!("[{index}]"));
1587 }
1588 PathSegment::MapAccess { field, key } => {
1589 if !path_so_far.is_empty() {
1590 path_so_far.push('.');
1591 }
1592 path_so_far.push_str(field);
1593 out.push('.');
1594 out.push_str(field);
1595 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1596 out.push_str(".?");
1597 }
1598 if key.chars().all(|c| c.is_ascii_digit()) {
1599 out.push_str(&format!("[{key}]"));
1600 } else {
1601 out.push_str(&format!(".get(\"{key}\")"));
1602 }
1603 }
1604 PathSegment::Length => {
1605 out.push_str(".len");
1606 }
1607 }
1608 }
1609 out
1610}
1611
1612fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1613 let mut out = result_var.to_string();
1614 for seg in segments {
1615 match seg {
1616 PathSegment::Field(f) => {
1617 out.push('.');
1618 out.push_str(&f.to_pascal_case());
1619 }
1620 PathSegment::ArrayField { name, index } => {
1621 out.push('.');
1622 out.push_str(&name.to_pascal_case());
1623 out.push_str(&format!("[{index}]"));
1624 }
1625 PathSegment::MapAccess { field, key } => {
1626 out.push('.');
1627 out.push_str(&field.to_pascal_case());
1628 if key.chars().all(|c| c.is_ascii_digit()) {
1629 out.push_str(&format!("[{key}]"));
1630 } else {
1631 out.push_str(&format!("[\"{key}\"]"));
1632 }
1633 }
1634 PathSegment::Length => {
1635 out.push_str(".Count");
1636 }
1637 }
1638 }
1639 out
1640}
1641
1642fn render_csharp_with_optionals(
1643 segments: &[PathSegment],
1644 result_var: &str,
1645 optional_fields: &HashSet<String>,
1646) -> String {
1647 let mut out = result_var.to_string();
1648 let mut path_so_far = String::new();
1649 for (i, seg) in segments.iter().enumerate() {
1650 let is_leaf = i == segments.len() - 1;
1651 match seg {
1652 PathSegment::Field(f) => {
1653 if !path_so_far.is_empty() {
1654 path_so_far.push('.');
1655 }
1656 path_so_far.push_str(f);
1657 out.push('.');
1658 out.push_str(&f.to_pascal_case());
1659 if !is_leaf && optional_fields.contains(&path_so_far) {
1660 out.push('!');
1661 }
1662 }
1663 PathSegment::ArrayField { name, index } => {
1664 if !path_so_far.is_empty() {
1665 path_so_far.push('.');
1666 }
1667 path_so_far.push_str(name);
1668 out.push('.');
1669 out.push_str(&name.to_pascal_case());
1670 out.push_str(&format!("[{index}]"));
1671 }
1672 PathSegment::MapAccess { field, key } => {
1673 if !path_so_far.is_empty() {
1674 path_so_far.push('.');
1675 }
1676 path_so_far.push_str(field);
1677 out.push('.');
1678 out.push_str(&field.to_pascal_case());
1679 if key.chars().all(|c| c.is_ascii_digit()) {
1680 out.push_str(&format!("[{key}]"));
1681 } else {
1682 out.push_str(&format!("[\"{key}\"]"));
1683 }
1684 }
1685 PathSegment::Length => {
1686 out.push_str(".Count");
1687 }
1688 }
1689 }
1690 out
1691}
1692
1693fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1694 let mut out = result_var.to_string();
1695 for seg in segments {
1696 match seg {
1697 PathSegment::Field(f) => {
1698 out.push_str("->");
1699 out.push_str(&f.to_lower_camel_case());
1702 }
1703 PathSegment::ArrayField { name, index } => {
1704 out.push_str("->");
1705 out.push_str(&name.to_lower_camel_case());
1706 out.push_str(&format!("[{index}]"));
1707 }
1708 PathSegment::MapAccess { field, key } => {
1709 out.push_str("->");
1710 out.push_str(&field.to_lower_camel_case());
1711 out.push_str(&format!("[\"{key}\"]"));
1712 }
1713 PathSegment::Length => {
1714 let current = std::mem::take(&mut out);
1715 out = format!("count({current})");
1716 }
1717 }
1718 }
1719 out
1720}
1721
1722fn render_php_with_getters(segments: &[PathSegment], result_var: &str, getter_map: &PhpGetterMap) -> String {
1740 let mut out = result_var.to_string();
1741 let mut current_type: Option<String> = getter_map.root_type.clone();
1742 for seg in segments {
1743 match seg {
1744 PathSegment::Field(f) => {
1745 let camel = f.to_lower_camel_case();
1746 if getter_map.needs_getter(current_type.as_deref(), f.as_str()) {
1747 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1752 out.push_str("->");
1753 out.push_str(&getter);
1754 out.push_str("()");
1755 } else {
1756 out.push_str("->");
1757 out.push_str(&camel);
1758 }
1759 current_type = getter_map.advance(current_type.as_deref(), f.as_str());
1760 }
1761 PathSegment::ArrayField { name, index } => {
1762 let camel = name.to_lower_camel_case();
1763 if getter_map.needs_getter(current_type.as_deref(), name.as_str()) {
1764 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1765 out.push_str("->");
1766 out.push_str(&getter);
1767 out.push_str("()");
1768 } else {
1769 out.push_str("->");
1770 out.push_str(&camel);
1771 }
1772 out.push_str(&format!("[{index}]"));
1773 current_type = getter_map.advance(current_type.as_deref(), name.as_str());
1774 }
1775 PathSegment::MapAccess { field, key } => {
1776 let camel = field.to_lower_camel_case();
1777 if getter_map.needs_getter(current_type.as_deref(), field.as_str()) {
1778 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1779 out.push_str("->");
1780 out.push_str(&getter);
1781 out.push_str("()");
1782 } else {
1783 out.push_str("->");
1784 out.push_str(&camel);
1785 }
1786 out.push_str(&format!("[\"{key}\"]"));
1787 current_type = getter_map.advance(current_type.as_deref(), field.as_str());
1788 }
1789 PathSegment::Length => {
1790 let current = std::mem::take(&mut out);
1791 out = format!("count({current})");
1792 }
1793 }
1794 }
1795 out
1796}
1797
1798fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1799 let mut out = result_var.to_string();
1800 for seg in segments {
1801 match seg {
1802 PathSegment::Field(f) => {
1803 out.push('$');
1804 out.push_str(f);
1805 }
1806 PathSegment::ArrayField { name, index } => {
1807 out.push('$');
1808 out.push_str(name);
1809 out.push_str(&format!("[[{}]]", index + 1));
1811 }
1812 PathSegment::MapAccess { field, key } => {
1813 out.push('$');
1814 out.push_str(field);
1815 out.push_str(&format!("[[\"{key}\"]]"));
1816 }
1817 PathSegment::Length => {
1818 let current = std::mem::take(&mut out);
1819 out = format!("length({current})");
1820 }
1821 }
1822 }
1823 out
1824}
1825
1826fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1827 let mut parts = Vec::new();
1828 let mut trailing_length = false;
1829 for seg in segments {
1830 match seg {
1831 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1832 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1833 PathSegment::MapAccess { field, key } => {
1834 parts.push(field.to_snake_case());
1835 parts.push(key.clone());
1836 }
1837 PathSegment::Length => {
1838 trailing_length = true;
1839 }
1840 }
1841 }
1842 let suffix = parts.join("_");
1843 if trailing_length {
1844 format!("result_{suffix}_count({result_var})")
1845 } else {
1846 format!("result_{suffix}({result_var})")
1847 }
1848}
1849
1850fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
1857 let mut out = result_var.to_string();
1858 for seg in segments {
1859 match seg {
1860 PathSegment::Field(f) => {
1861 out.push('.');
1862 out.push_str(&f.to_lower_camel_case());
1863 }
1864 PathSegment::ArrayField { name, index } => {
1865 out.push('.');
1866 out.push_str(&name.to_lower_camel_case());
1867 out.push_str(&format!("[{index}]"));
1868 }
1869 PathSegment::MapAccess { field, key } => {
1870 out.push('.');
1871 out.push_str(&field.to_lower_camel_case());
1872 if key.chars().all(|c| c.is_ascii_digit()) {
1873 out.push_str(&format!("[{key}]"));
1874 } else {
1875 out.push_str(&format!("[\"{key}\"]"));
1876 }
1877 }
1878 PathSegment::Length => {
1879 out.push_str(".length");
1880 }
1881 }
1882 }
1883 out
1884}
1885
1886fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1892 let mut out = result_var.to_string();
1893 let mut path_so_far = String::new();
1894 let mut prev_was_nullable = false;
1895 for seg in segments {
1896 let nav = if prev_was_nullable { "?." } else { "." };
1897 match seg {
1898 PathSegment::Field(f) => {
1899 if !path_so_far.is_empty() {
1900 path_so_far.push('.');
1901 }
1902 path_so_far.push_str(f);
1903 let is_optional = optional_fields.contains(&path_so_far);
1904 out.push_str(nav);
1905 out.push_str(&f.to_lower_camel_case());
1906 prev_was_nullable = is_optional;
1907 }
1908 PathSegment::ArrayField { name, index } => {
1909 if !path_so_far.is_empty() {
1910 path_so_far.push('.');
1911 }
1912 path_so_far.push_str(name);
1913 let is_optional = optional_fields.contains(&path_so_far);
1914 out.push_str(nav);
1915 out.push_str(&name.to_lower_camel_case());
1916 if is_optional {
1920 out.push('!');
1921 }
1922 out.push_str(&format!("[{index}]"));
1923 prev_was_nullable = false;
1924 }
1925 PathSegment::MapAccess { field, key } => {
1926 if !path_so_far.is_empty() {
1927 path_so_far.push('.');
1928 }
1929 path_so_far.push_str(field);
1930 let is_optional = optional_fields.contains(&path_so_far);
1931 out.push_str(nav);
1932 out.push_str(&field.to_lower_camel_case());
1933 if key.chars().all(|c| c.is_ascii_digit()) {
1934 out.push_str(&format!("[{key}]"));
1935 } else {
1936 out.push_str(&format!("[\"{key}\"]"));
1937 }
1938 prev_was_nullable = is_optional;
1939 }
1940 PathSegment::Length => {
1941 out.push_str(nav);
1944 out.push_str("length");
1945 prev_was_nullable = false;
1946 }
1947 }
1948 }
1949 out
1950}
1951
1952#[cfg(test)]
1953mod tests {
1954 use super::*;
1955
1956 fn make_resolver() -> FieldResolver {
1957 let mut fields = HashMap::new();
1958 fields.insert("title".to_string(), "metadata.document.title".to_string());
1959 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1960 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
1961 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
1962 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
1963 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
1964 let mut optional = HashSet::new();
1965 optional.insert("metadata.document.title".to_string());
1966 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1967 }
1968
1969 fn make_resolver_with_doc_optional() -> FieldResolver {
1970 let mut fields = HashMap::new();
1971 fields.insert("title".to_string(), "metadata.document.title".to_string());
1972 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1973 let mut optional = HashSet::new();
1974 optional.insert("document".to_string());
1975 optional.insert("metadata.document.title".to_string());
1976 optional.insert("metadata.document".to_string());
1977 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1978 }
1979
1980 #[test]
1981 fn test_resolve_alias() {
1982 let r = make_resolver();
1983 assert_eq!(r.resolve("title"), "metadata.document.title");
1984 }
1985
1986 #[test]
1987 fn test_resolve_passthrough() {
1988 let r = make_resolver();
1989 assert_eq!(r.resolve("content"), "content");
1990 }
1991
1992 #[test]
1993 fn test_is_optional() {
1994 let r = make_resolver();
1995 assert!(r.is_optional("metadata.document.title"));
1996 assert!(!r.is_optional("content"));
1997 }
1998
1999 #[test]
2000 fn test_accessor_rust_struct() {
2001 let r = make_resolver();
2002 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
2003 }
2004
2005 #[test]
2006 fn test_accessor_rust_map() {
2007 let r = make_resolver();
2008 assert_eq!(
2009 r.accessor("tags", "rust", "result"),
2010 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
2011 );
2012 }
2013
2014 #[test]
2015 fn test_accessor_python() {
2016 let r = make_resolver();
2017 assert_eq!(
2018 r.accessor("title", "python", "result"),
2019 "result.metadata.document.title"
2020 );
2021 }
2022
2023 #[test]
2024 fn test_accessor_go() {
2025 let r = make_resolver();
2026 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
2027 }
2028
2029 #[test]
2030 fn test_accessor_go_initialism_fields() {
2031 let mut fields = std::collections::HashMap::new();
2032 fields.insert("content".to_string(), "html".to_string());
2033 fields.insert("link_url".to_string(), "links.url".to_string());
2034 let r = FieldResolver::new(
2035 &fields,
2036 &HashSet::new(),
2037 &HashSet::new(),
2038 &HashSet::new(),
2039 &HashSet::new(),
2040 );
2041 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
2042 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
2043 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
2044 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
2045 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
2046 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
2047 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
2048 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
2049 }
2050
2051 #[test]
2052 fn test_accessor_typescript() {
2053 let r = make_resolver();
2054 assert_eq!(
2055 r.accessor("title", "typescript", "result"),
2056 "result.metadata.document.title"
2057 );
2058 }
2059
2060 #[test]
2061 fn test_accessor_typescript_snake_to_camel() {
2062 let r = make_resolver();
2063 assert_eq!(
2064 r.accessor("og", "typescript", "result"),
2065 "result.metadata.document.openGraph"
2066 );
2067 assert_eq!(
2068 r.accessor("twitter", "typescript", "result"),
2069 "result.metadata.document.twitterCard"
2070 );
2071 assert_eq!(
2072 r.accessor("canonical", "typescript", "result"),
2073 "result.metadata.document.canonicalUrl"
2074 );
2075 }
2076
2077 #[test]
2078 fn test_accessor_typescript_map_snake_to_camel() {
2079 let r = make_resolver();
2080 assert_eq!(
2081 r.accessor("og_tag", "typescript", "result"),
2082 "result.metadata.openGraphTags[\"og_title\"]"
2083 );
2084 }
2085
2086 #[test]
2087 fn test_accessor_typescript_numeric_index_is_unquoted() {
2088 let mut fields = HashMap::new();
2092 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
2093 let r = FieldResolver::new(
2094 &fields,
2095 &HashSet::new(),
2096 &HashSet::new(),
2097 &HashSet::new(),
2098 &HashSet::new(),
2099 );
2100 assert_eq!(
2101 r.accessor("first_score", "typescript", "result"),
2102 "result.results[0].relevanceScore"
2103 );
2104 }
2105
2106 #[test]
2107 fn test_accessor_node_alias() {
2108 let r = make_resolver();
2109 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
2110 }
2111
2112 #[test]
2113 fn test_accessor_wasm_camel_case() {
2114 let r = make_resolver();
2115 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
2116 assert_eq!(
2117 r.accessor("twitter", "wasm", "result"),
2118 "result.metadata.document.twitterCard"
2119 );
2120 assert_eq!(
2121 r.accessor("canonical", "wasm", "result"),
2122 "result.metadata.document.canonicalUrl"
2123 );
2124 }
2125
2126 #[test]
2127 fn test_accessor_wasm_map_access() {
2128 let r = make_resolver();
2129 assert_eq!(
2130 r.accessor("og_tag", "wasm", "result"),
2131 "result.metadata.openGraphTags.get(\"og_title\")"
2132 );
2133 }
2134
2135 #[test]
2136 fn test_accessor_java() {
2137 let r = make_resolver();
2138 assert_eq!(
2139 r.accessor("title", "java", "result"),
2140 "result.metadata().document().title()"
2141 );
2142 }
2143
2144 #[test]
2145 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
2146 let mut fields = HashMap::new();
2147 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2148 fields.insert("node_count".to_string(), "nodes.length".to_string());
2149 let mut arrays = HashSet::new();
2150 arrays.insert("nodes".to_string());
2151 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
2152 assert_eq!(
2153 r.accessor("first_node_name", "kotlin", "result"),
2154 "result.nodes().first().name()"
2155 );
2156 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
2157 }
2158
2159 #[test]
2160 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
2161 let r = make_resolver_with_doc_optional();
2162 assert_eq!(
2163 r.accessor("title", "kotlin", "result"),
2164 "result.metadata().document()?.title()"
2165 );
2166 }
2167
2168 #[test]
2169 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
2170 let mut fields = HashMap::new();
2171 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2172 fields.insert("tag".to_string(), "tags[name]".to_string());
2173 let mut optional = HashSet::new();
2174 optional.insert("nodes".to_string());
2175 optional.insert("tags".to_string());
2176 let mut arrays = HashSet::new();
2177 arrays.insert("nodes".to_string());
2178 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2179 assert_eq!(
2180 r.accessor("first_node_name", "kotlin", "result"),
2181 "result.nodes()?.first()?.name()"
2182 );
2183 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
2184 }
2185
2186 #[test]
2192 fn test_accessor_kotlin_optional_field_after_indexed_array() {
2193 let mut fields = HashMap::new();
2196 fields.insert(
2197 "tool_call_name".to_string(),
2198 "choices[0].message.tool_calls[0].function.name".to_string(),
2199 );
2200 let mut optional = HashSet::new();
2201 optional.insert("choices[0].message.tool_calls".to_string());
2202 let mut arrays = HashSet::new();
2203 arrays.insert("choices".to_string());
2204 arrays.insert("choices[0].message.tool_calls".to_string());
2205 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2206 let expr = r.accessor("tool_call_name", "kotlin", "result");
2207 assert!(
2209 expr.contains("toolCalls()?.first()"),
2210 "expected toolCalls()?.first() for optional list, got: {expr}"
2211 );
2212 }
2213
2214 #[test]
2215 fn test_accessor_csharp() {
2216 let r = make_resolver();
2217 assert_eq!(
2218 r.accessor("title", "csharp", "result"),
2219 "result.Metadata.Document.Title"
2220 );
2221 }
2222
2223 #[test]
2224 fn test_accessor_php() {
2225 let r = make_resolver();
2226 assert_eq!(
2227 r.accessor("title", "php", "$result"),
2228 "$result->metadata->document->title"
2229 );
2230 }
2231
2232 #[test]
2233 fn test_accessor_r() {
2234 let r = make_resolver();
2235 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
2236 }
2237
2238 #[test]
2239 fn test_accessor_c() {
2240 let r = make_resolver();
2241 assert_eq!(
2242 r.accessor("title", "c", "result"),
2243 "result_metadata_document_title(result)"
2244 );
2245 }
2246
2247 #[test]
2248 fn test_rust_unwrap_binding() {
2249 let r = make_resolver();
2250 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
2251 assert_eq!(var, "metadata_document_title");
2252 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
2253 }
2254
2255 #[test]
2256 fn test_rust_unwrap_binding_non_optional() {
2257 let r = make_resolver();
2258 assert!(r.rust_unwrap_binding("content", "result").is_none());
2259 }
2260
2261 #[test]
2262 fn test_rust_unwrap_binding_collapses_double_underscore() {
2263 let mut aliases = HashMap::new();
2268 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
2269 let mut optional = HashSet::new();
2270 optional.insert("json_ld[].name".to_string());
2271 let mut array = HashSet::new();
2272 array.insert("json_ld".to_string());
2273 let result_fields = HashSet::new();
2274 let method_calls = HashSet::new();
2275 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
2276 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
2277 assert_eq!(var, "json_ld_name");
2278 }
2279
2280 #[test]
2281 fn test_direct_field_no_alias() {
2282 let r = make_resolver();
2283 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2284 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
2285 }
2286
2287 #[test]
2288 fn test_accessor_rust_with_optionals() {
2289 let r = make_resolver_with_doc_optional();
2290 assert_eq!(
2291 r.accessor("title", "rust", "result"),
2292 "result.metadata.document.as_ref().unwrap().title"
2293 );
2294 }
2295
2296 #[test]
2297 fn test_accessor_csharp_with_optionals() {
2298 let r = make_resolver_with_doc_optional();
2299 assert_eq!(
2300 r.accessor("title", "csharp", "result"),
2301 "result.Metadata.Document!.Title"
2302 );
2303 }
2304
2305 #[test]
2306 fn test_accessor_rust_non_optional_field() {
2307 let r = make_resolver();
2308 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2309 }
2310
2311 #[test]
2312 fn test_accessor_csharp_non_optional_field() {
2313 let r = make_resolver();
2314 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
2315 }
2316
2317 #[test]
2318 fn test_accessor_rust_method_call() {
2319 let mut fields = HashMap::new();
2321 fields.insert(
2322 "excel_sheet_count".to_string(),
2323 "metadata.format.excel.sheet_count".to_string(),
2324 );
2325 let mut optional = HashSet::new();
2326 optional.insert("metadata.format".to_string());
2327 optional.insert("metadata.format.excel".to_string());
2328 let mut method_calls = HashSet::new();
2329 method_calls.insert("metadata.format.excel".to_string());
2330 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
2331 assert_eq!(
2332 r.accessor("excel_sheet_count", "rust", "result"),
2333 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
2334 );
2335 }
2336
2337 fn make_php_getter_resolver() -> FieldResolver {
2342 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2343 getters.insert(
2344 "Root".to_string(),
2345 ["metadata".to_string(), "links".to_string()].into_iter().collect(),
2346 );
2347 let map = PhpGetterMap {
2348 getters,
2349 field_types: HashMap::new(),
2350 root_type: Some("Root".to_string()),
2351 all_fields: HashMap::new(),
2352 };
2353 FieldResolver::new_with_php_getters(
2354 &HashMap::new(),
2355 &HashSet::new(),
2356 &HashSet::new(),
2357 &HashSet::new(),
2358 &HashSet::new(),
2359 &HashMap::new(),
2360 map,
2361 )
2362 }
2363
2364 #[test]
2365 fn render_php_uses_getter_method_for_non_scalar_field() {
2366 let r = make_php_getter_resolver();
2367 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
2368 }
2369
2370 #[test]
2371 fn render_php_uses_property_for_scalar_field() {
2372 let r = make_php_getter_resolver();
2373 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2374 }
2375
2376 #[test]
2377 fn render_php_nested_non_scalar_uses_getter_then_property() {
2378 let mut fields = HashMap::new();
2379 fields.insert("title".to_string(), "metadata.title".to_string());
2380 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2381 getters.insert("Root".to_string(), ["metadata".to_string()].into_iter().collect());
2382 getters.insert("Metadata".to_string(), HashSet::new());
2384 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2385 field_types.insert(
2386 "Root".to_string(),
2387 [("metadata".to_string(), "Metadata".to_string())].into_iter().collect(),
2388 );
2389 let map = PhpGetterMap {
2390 getters,
2391 field_types,
2392 root_type: Some("Root".to_string()),
2393 all_fields: HashMap::new(),
2394 };
2395 let r = FieldResolver::new_with_php_getters(
2396 &fields,
2397 &HashSet::new(),
2398 &HashSet::new(),
2399 &HashSet::new(),
2400 &HashSet::new(),
2401 &HashMap::new(),
2402 map,
2403 );
2404 assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
2406 }
2407
2408 #[test]
2409 fn render_php_array_field_uses_getter_when_non_scalar() {
2410 let mut fields = HashMap::new();
2411 fields.insert("first_link".to_string(), "links[0]".to_string());
2412 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2413 getters.insert("Root".to_string(), ["links".to_string()].into_iter().collect());
2414 let map = PhpGetterMap {
2415 getters,
2416 field_types: HashMap::new(),
2417 root_type: Some("Root".to_string()),
2418 all_fields: HashMap::new(),
2419 };
2420 let r = FieldResolver::new_with_php_getters(
2421 &fields,
2422 &HashSet::new(),
2423 &HashSet::new(),
2424 &HashSet::new(),
2425 &HashSet::new(),
2426 &HashMap::new(),
2427 map,
2428 );
2429 assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
2430 }
2431
2432 #[test]
2433 fn render_php_falls_back_to_property_when_getter_fields_empty() {
2434 let r = FieldResolver::new(
2437 &HashMap::new(),
2438 &HashSet::new(),
2439 &HashSet::new(),
2440 &HashSet::new(),
2441 &HashSet::new(),
2442 );
2443 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2444 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
2445 }
2446
2447 #[test]
2451 fn render_php_with_getters_distinguishes_same_field_name_on_different_types() {
2452 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2453 getters.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2455 getters.insert("B".to_string(), HashSet::new());
2457 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2460 all_fields.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2461 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2462 let map_a = PhpGetterMap {
2463 getters: getters.clone(),
2464 field_types: HashMap::new(),
2465 root_type: Some("A".to_string()),
2466 all_fields: all_fields.clone(),
2467 };
2468 let map_b = PhpGetterMap {
2469 getters,
2470 field_types: HashMap::new(),
2471 root_type: Some("B".to_string()),
2472 all_fields,
2473 };
2474 let r_a = FieldResolver::new_with_php_getters(
2475 &HashMap::new(),
2476 &HashSet::new(),
2477 &HashSet::new(),
2478 &HashSet::new(),
2479 &HashSet::new(),
2480 &HashMap::new(),
2481 map_a,
2482 );
2483 let r_b = FieldResolver::new_with_php_getters(
2484 &HashMap::new(),
2485 &HashSet::new(),
2486 &HashSet::new(),
2487 &HashSet::new(),
2488 &HashSet::new(),
2489 &HashMap::new(),
2490 map_b,
2491 );
2492 assert_eq!(r_a.accessor("content", "php", "$a"), "$a->getContent()");
2493 assert_eq!(r_b.accessor("content", "php", "$b"), "$b->content");
2494 }
2495
2496 #[test]
2500 fn render_php_with_getters_chains_through_correct_type() {
2501 let mut fields = HashMap::new();
2502 fields.insert("nested_content".to_string(), "inner.content".to_string());
2503 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2504 getters.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2506 getters.insert("B".to_string(), HashSet::new());
2508 getters.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2511 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2512 field_types.insert(
2513 "Outer".to_string(),
2514 [("inner".to_string(), "B".to_string())].into_iter().collect(),
2515 );
2516 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2517 all_fields.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2518 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2519 all_fields.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2520 let map = PhpGetterMap {
2521 getters,
2522 field_types,
2523 root_type: Some("Outer".to_string()),
2524 all_fields,
2525 };
2526 let r = FieldResolver::new_with_php_getters(
2527 &fields,
2528 &HashSet::new(),
2529 &HashSet::new(),
2530 &HashSet::new(),
2531 &HashSet::new(),
2532 &HashMap::new(),
2533 map,
2534 );
2535 assert_eq!(
2536 r.accessor("nested_content", "php", "$result"),
2537 "$result->getInner()->content"
2538 );
2539 }
2540}