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 total = segments.len();
854 for (i, seg) in segments.iter().enumerate() {
855 let is_leaf = i == total - 1;
856 let property_syntax = map.is_first_class(current_type.as_deref());
857 match seg {
858 PathSegment::Field(f) => {
859 if !path_so_far.is_empty() {
860 path_so_far.push('.');
861 }
862 path_so_far.push_str(f);
863 out.push('.');
864 out.push_str(f);
865 if !property_syntax {
866 out.push_str("()");
867 }
868 if !is_leaf && optional_fields.contains(&path_so_far) {
869 out.push('?');
870 }
871 current_type = map.advance(current_type.as_deref(), f);
872 }
873 PathSegment::ArrayField { name, index } => {
874 if !path_so_far.is_empty() {
875 path_so_far.push('.');
876 }
877 path_so_far.push_str(name);
878 let is_optional = optional_fields.contains(&path_so_far);
879 out.push('.');
880 out.push_str(name);
881 let access = if property_syntax { "" } else { "()" };
882 if is_optional {
883 out.push_str(&format!("{access}?[{index}]"));
884 } else {
885 out.push_str(&format!("{access}[{index}]"));
886 }
887 path_so_far.push_str("[0]");
888 current_type = map.advance(current_type.as_deref(), name);
890 }
891 PathSegment::MapAccess { field, key } => {
892 if !path_so_far.is_empty() {
893 path_so_far.push('.');
894 }
895 path_so_far.push_str(field);
896 out.push('.');
897 out.push_str(field);
898 let access = if property_syntax { "" } else { "()" };
899 if key.chars().all(|c| c.is_ascii_digit()) {
900 out.push_str(&format!("{access}[{key}]"));
901 } else {
902 out.push_str(&format!("{access}[\"{key}\"]"));
903 }
904 current_type = map.advance(current_type.as_deref(), field);
905 }
906 PathSegment::Length => {
907 out.push_str(".count");
908 }
909 }
910 }
911 out
912}
913
914fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
915 let mut out = result_var.to_string();
916 for seg in segments {
917 match seg {
918 PathSegment::Field(f) => {
919 out.push('.');
920 out.push_str(&f.to_snake_case());
921 }
922 PathSegment::ArrayField { name, index } => {
923 out.push('.');
924 out.push_str(&name.to_snake_case());
925 out.push_str(&format!("[{index}]"));
926 }
927 PathSegment::MapAccess { field, key } => {
928 out.push('.');
929 out.push_str(&field.to_snake_case());
930 if key.chars().all(|c| c.is_ascii_digit()) {
931 out.push_str(&format!("[{key}]"));
932 } else {
933 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
934 }
935 }
936 PathSegment::Length => {
937 out.push_str(".len()");
938 }
939 }
940 }
941 out
942}
943
944fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
945 let mut out = result_var.to_string();
946 for seg in segments {
947 match seg {
948 PathSegment::Field(f) => {
949 out.push('.');
950 out.push_str(f);
951 }
952 PathSegment::ArrayField { name, index } => {
953 if language == "elixir" {
954 let current = std::mem::take(&mut out);
955 out = format!("Enum.at({current}.{name}, {index})");
956 } else {
957 out.push('.');
958 out.push_str(name);
959 out.push_str(&format!("[{index}]"));
960 }
961 }
962 PathSegment::MapAccess { field, key } => {
963 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
964 if is_numeric && language == "elixir" {
965 let current = std::mem::take(&mut out);
966 out = format!("Enum.at({current}.{field}, {key})");
967 } else {
968 out.push('.');
969 out.push_str(field);
970 if is_numeric {
971 let idx: usize = key.parse().unwrap_or(0);
972 out.push_str(&format!("[{idx}]"));
973 } else if language == "elixir" || language == "ruby" {
974 out.push_str(&format!("[\"{key}\"]"));
977 } else {
978 out.push_str(&format!(".get(\"{key}\")"));
979 }
980 }
981 }
982 PathSegment::Length => match language {
983 "ruby" => out.push_str(".length"),
984 "elixir" => {
985 let current = std::mem::take(&mut out);
986 out = format!("length({current})");
987 }
988 "gleam" => {
989 let current = std::mem::take(&mut out);
990 out = format!("list.length({current})");
991 }
992 _ => {
993 let current = std::mem::take(&mut out);
994 out = format!("len({current})");
995 }
996 },
997 }
998 }
999 out
1000}
1001
1002fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
1003 let mut out = result_var.to_string();
1004 for seg in segments {
1005 match seg {
1006 PathSegment::Field(f) => {
1007 out.push('.');
1008 out.push_str(&f.to_lower_camel_case());
1009 }
1010 PathSegment::ArrayField { name, index } => {
1011 out.push('.');
1012 out.push_str(&name.to_lower_camel_case());
1013 out.push_str(&format!("[{index}]"));
1014 }
1015 PathSegment::MapAccess { field, key } => {
1016 out.push('.');
1017 out.push_str(&field.to_lower_camel_case());
1018 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
1021 out.push_str(&format!("[{key}]"));
1022 } else {
1023 out.push_str(&format!("[\"{key}\"]"));
1024 }
1025 }
1026 PathSegment::Length => {
1027 out.push_str(".length");
1028 }
1029 }
1030 }
1031 out
1032}
1033
1034fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
1035 let mut out = result_var.to_string();
1036 for seg in segments {
1037 match seg {
1038 PathSegment::Field(f) => {
1039 out.push('.');
1040 out.push_str(&f.to_lower_camel_case());
1041 }
1042 PathSegment::ArrayField { name, index } => {
1043 out.push('.');
1044 out.push_str(&name.to_lower_camel_case());
1045 out.push_str(&format!("[{index}]"));
1046 }
1047 PathSegment::MapAccess { field, key } => {
1048 out.push('.');
1049 out.push_str(&field.to_lower_camel_case());
1050 out.push_str(&format!(".get(\"{key}\")"));
1051 }
1052 PathSegment::Length => {
1053 out.push_str(".length");
1054 }
1055 }
1056 }
1057 out
1058}
1059
1060fn render_go(segments: &[PathSegment], result_var: &str) -> String {
1061 let mut out = result_var.to_string();
1062 for seg in segments {
1063 match seg {
1064 PathSegment::Field(f) => {
1065 out.push('.');
1066 out.push_str(&to_go_name(f));
1067 }
1068 PathSegment::ArrayField { name, index } => {
1069 out.push('.');
1070 out.push_str(&to_go_name(name));
1071 out.push_str(&format!("[{index}]"));
1072 }
1073 PathSegment::MapAccess { field, key } => {
1074 out.push('.');
1075 out.push_str(&to_go_name(field));
1076 if key.chars().all(|c| c.is_ascii_digit()) {
1077 out.push_str(&format!("[{key}]"));
1078 } else {
1079 out.push_str(&format!("[\"{key}\"]"));
1080 }
1081 }
1082 PathSegment::Length => {
1083 let current = std::mem::take(&mut out);
1084 out = format!("len({current})");
1085 }
1086 }
1087 }
1088 out
1089}
1090
1091fn render_java(segments: &[PathSegment], result_var: &str) -> String {
1092 let mut out = result_var.to_string();
1093 for seg in segments {
1094 match seg {
1095 PathSegment::Field(f) => {
1096 out.push('.');
1097 out.push_str(&f.to_lower_camel_case());
1098 out.push_str("()");
1099 }
1100 PathSegment::ArrayField { name, index } => {
1101 out.push('.');
1102 out.push_str(&name.to_lower_camel_case());
1103 out.push_str(&format!("().get({index})"));
1104 }
1105 PathSegment::MapAccess { field, key } => {
1106 out.push('.');
1107 out.push_str(&field.to_lower_camel_case());
1108 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1110 if is_numeric {
1111 out.push_str(&format!("().get({key})"));
1112 } else {
1113 out.push_str(&format!("().get(\"{key}\")"));
1114 }
1115 }
1116 PathSegment::Length => {
1117 out.push_str(".size()");
1118 }
1119 }
1120 }
1121 out
1122}
1123
1124fn kotlin_getter(name: &str) -> String {
1129 let camel = name.to_lower_camel_case();
1130 match camel.as_str() {
1131 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
1132 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
1133 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
1134 _ => camel,
1135 }
1136}
1137
1138fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
1139 let mut out = result_var.to_string();
1140 for seg in segments {
1141 match seg {
1142 PathSegment::Field(f) => {
1143 out.push('.');
1144 out.push_str(&kotlin_getter(f));
1145 out.push_str("()");
1146 }
1147 PathSegment::ArrayField { name, index } => {
1148 out.push('.');
1149 out.push_str(&kotlin_getter(name));
1150 if *index == 0 {
1151 out.push_str("().first()");
1152 } else {
1153 out.push_str(&format!("().get({index})"));
1154 }
1155 }
1156 PathSegment::MapAccess { field, key } => {
1157 out.push('.');
1158 out.push_str(&kotlin_getter(field));
1159 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1160 if is_numeric {
1161 out.push_str(&format!("().get({key})"));
1162 } else {
1163 out.push_str(&format!("().get(\"{key}\")"));
1164 }
1165 }
1166 PathSegment::Length => {
1167 out.push_str(".size");
1168 }
1169 }
1170 }
1171 out
1172}
1173
1174fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1175 let mut out = result_var.to_string();
1176 let mut path_so_far = String::new();
1177 for (i, seg) in segments.iter().enumerate() {
1178 let is_leaf = i == segments.len() - 1;
1179 match seg {
1180 PathSegment::Field(f) => {
1181 if !path_so_far.is_empty() {
1182 path_so_far.push('.');
1183 }
1184 path_so_far.push_str(f);
1185 out.push('.');
1186 out.push_str(&f.to_lower_camel_case());
1187 out.push_str("()");
1188 let _ = is_leaf;
1189 let _ = optional_fields;
1190 }
1191 PathSegment::ArrayField { name, index } => {
1192 if !path_so_far.is_empty() {
1193 path_so_far.push('.');
1194 }
1195 path_so_far.push_str(name);
1196 out.push('.');
1197 out.push_str(&name.to_lower_camel_case());
1198 out.push_str(&format!("().get({index})"));
1199 }
1200 PathSegment::MapAccess { field, key } => {
1201 if !path_so_far.is_empty() {
1202 path_so_far.push('.');
1203 }
1204 path_so_far.push_str(field);
1205 out.push('.');
1206 out.push_str(&field.to_lower_camel_case());
1207 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1209 if is_numeric {
1210 out.push_str(&format!("().get({key})"));
1211 } else {
1212 out.push_str(&format!("().get(\"{key}\")"));
1213 }
1214 }
1215 PathSegment::Length => {
1216 out.push_str(".size()");
1217 }
1218 }
1219 }
1220 out
1221}
1222
1223fn render_kotlin_with_optionals(
1238 segments: &[PathSegment],
1239 result_var: &str,
1240 optional_fields: &HashSet<String>,
1241) -> String {
1242 let mut out = result_var.to_string();
1243 let mut path_so_far = String::new();
1244 let mut prev_was_nullable = false;
1252 for seg in segments {
1253 let nav = if prev_was_nullable { "?." } else { "." };
1254 match seg {
1255 PathSegment::Field(f) => {
1256 if !path_so_far.is_empty() {
1257 path_so_far.push('.');
1258 }
1259 path_so_far.push_str(f);
1260 let is_optional = optional_fields.contains(&path_so_far);
1265 out.push_str(nav);
1266 out.push_str(&kotlin_getter(f));
1267 out.push_str("()");
1268 prev_was_nullable = prev_was_nullable || is_optional;
1269 }
1270 PathSegment::ArrayField { name, index } => {
1271 if !path_so_far.is_empty() {
1272 path_so_far.push('.');
1273 }
1274 path_so_far.push_str(name);
1275 let is_optional = optional_fields.contains(&path_so_far);
1276 out.push_str(nav);
1277 out.push_str(&kotlin_getter(name));
1278 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1279 if *index == 0 {
1280 out.push_str(&format!("(){safe}.first()"));
1281 } else {
1282 out.push_str(&format!("(){safe}.get({index})"));
1283 }
1284 path_so_far.push_str("[0]");
1288 prev_was_nullable = prev_was_nullable || is_optional;
1289 }
1290 PathSegment::MapAccess { field, key } => {
1291 if !path_so_far.is_empty() {
1292 path_so_far.push('.');
1293 }
1294 path_so_far.push_str(field);
1295 let is_optional = optional_fields.contains(&path_so_far);
1296 out.push_str(nav);
1297 out.push_str(&kotlin_getter(field));
1298 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1299 if is_numeric {
1300 if prev_was_nullable || is_optional {
1301 out.push_str(&format!("()?.get({key})"));
1302 } else {
1303 out.push_str(&format!("().get({key})"));
1304 }
1305 } else if prev_was_nullable || is_optional {
1306 out.push_str(&format!("()?.get(\"{key}\")"));
1307 } else {
1308 out.push_str(&format!("().get(\"{key}\")"));
1309 }
1310 prev_was_nullable = prev_was_nullable || is_optional;
1311 }
1312 PathSegment::Length => {
1313 let size_nav = if prev_was_nullable { "?" } else { "" };
1316 out.push_str(&format!("{size_nav}.size"));
1317 prev_was_nullable = false;
1318 }
1319 }
1320 }
1321 out
1322}
1323
1324fn render_kotlin_android_with_optionals(
1335 segments: &[PathSegment],
1336 result_var: &str,
1337 optional_fields: &HashSet<String>,
1338) -> String {
1339 let mut out = result_var.to_string();
1340 let mut path_so_far = String::new();
1341 let mut prev_was_nullable = false;
1342 for seg in segments {
1343 let nav = if prev_was_nullable { "?." } else { "." };
1344 match seg {
1345 PathSegment::Field(f) => {
1346 if !path_so_far.is_empty() {
1347 path_so_far.push('.');
1348 }
1349 path_so_far.push_str(f);
1350 let is_optional = optional_fields.contains(&path_so_far);
1351 out.push_str(nav);
1352 out.push_str(&kotlin_getter(f));
1354 prev_was_nullable = prev_was_nullable || is_optional;
1355 }
1356 PathSegment::ArrayField { name, index } => {
1357 if !path_so_far.is_empty() {
1358 path_so_far.push('.');
1359 }
1360 path_so_far.push_str(name);
1361 let is_optional = optional_fields.contains(&path_so_far);
1362 out.push_str(nav);
1363 out.push_str(&kotlin_getter(name));
1365 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1366 if *index == 0 {
1367 out.push_str(&format!("{safe}.first()"));
1368 } else {
1369 out.push_str(&format!("{safe}.get({index})"));
1370 }
1371 path_so_far.push_str("[0]");
1372 prev_was_nullable = prev_was_nullable || is_optional;
1373 }
1374 PathSegment::MapAccess { field, key } => {
1375 if !path_so_far.is_empty() {
1376 path_so_far.push('.');
1377 }
1378 path_so_far.push_str(field);
1379 let is_optional = optional_fields.contains(&path_so_far);
1380 out.push_str(nav);
1381 out.push_str(&kotlin_getter(field));
1383 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1384 if is_numeric {
1385 if prev_was_nullable || is_optional {
1386 out.push_str(&format!("?.get({key})"));
1387 } else {
1388 out.push_str(&format!(".get({key})"));
1389 }
1390 } else if prev_was_nullable || is_optional {
1391 out.push_str(&format!("?.get(\"{key}\")"));
1392 } else {
1393 out.push_str(&format!(".get(\"{key}\")"));
1394 }
1395 prev_was_nullable = prev_was_nullable || is_optional;
1396 }
1397 PathSegment::Length => {
1398 let size_nav = if prev_was_nullable { "?" } else { "" };
1399 out.push_str(&format!("{size_nav}.size"));
1400 prev_was_nullable = false;
1401 }
1402 }
1403 }
1404 out
1405}
1406
1407fn render_kotlin_android(segments: &[PathSegment], result_var: &str) -> String {
1411 let mut out = result_var.to_string();
1412 for seg in segments {
1413 match seg {
1414 PathSegment::Field(f) => {
1415 out.push('.');
1416 out.push_str(&kotlin_getter(f));
1417 }
1419 PathSegment::ArrayField { name, index } => {
1420 out.push('.');
1421 out.push_str(&kotlin_getter(name));
1422 if *index == 0 {
1423 out.push_str(".first()");
1424 } else {
1425 out.push_str(&format!(".get({index})"));
1426 }
1427 }
1428 PathSegment::MapAccess { field, key } => {
1429 out.push('.');
1430 out.push_str(&kotlin_getter(field));
1431 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1432 if is_numeric {
1433 out.push_str(&format!(".get({key})"));
1434 } else {
1435 out.push_str(&format!(".get(\"{key}\")"));
1436 }
1437 }
1438 PathSegment::Length => {
1439 out.push_str(".size");
1440 }
1441 }
1442 }
1443 out
1444}
1445
1446fn render_rust_with_optionals(
1452 segments: &[PathSegment],
1453 result_var: &str,
1454 optional_fields: &HashSet<String>,
1455 method_calls: &HashSet<String>,
1456) -> String {
1457 let mut out = result_var.to_string();
1458 let mut path_so_far = String::new();
1459 for (i, seg) in segments.iter().enumerate() {
1460 let is_leaf = i == segments.len() - 1;
1461 match seg {
1462 PathSegment::Field(f) => {
1463 if !path_so_far.is_empty() {
1464 path_so_far.push('.');
1465 }
1466 path_so_far.push_str(f);
1467 out.push('.');
1468 out.push_str(&f.to_snake_case());
1469 let is_method = method_calls.contains(&path_so_far);
1470 if is_method {
1471 out.push_str("()");
1472 if !is_leaf && optional_fields.contains(&path_so_far) {
1473 out.push_str(".as_ref().unwrap()");
1474 }
1475 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1476 out.push_str(".as_ref().unwrap()");
1477 }
1478 }
1479 PathSegment::ArrayField { name, index } => {
1480 if !path_so_far.is_empty() {
1481 path_so_far.push('.');
1482 }
1483 path_so_far.push_str(name);
1484 out.push('.');
1485 out.push_str(&name.to_snake_case());
1486 let path_with_idx = format!("{path_so_far}[0]");
1490 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1491 if is_opt {
1492 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1493 } else {
1494 out.push_str(&format!("[{index}]"));
1495 }
1496 path_so_far.push_str("[0]");
1501 }
1502 PathSegment::MapAccess { field, key } => {
1503 if !path_so_far.is_empty() {
1504 path_so_far.push('.');
1505 }
1506 path_so_far.push_str(field);
1507 out.push('.');
1508 out.push_str(&field.to_snake_case());
1509 if key.chars().all(|c| c.is_ascii_digit()) {
1510 let path_with_idx = format!("{path_so_far}[0]");
1512 let is_opt =
1513 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1514 if is_opt {
1515 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1516 } else {
1517 out.push_str(&format!("[{key}]"));
1518 }
1519 path_so_far.push_str("[0]");
1520 } else {
1521 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1522 }
1523 }
1524 PathSegment::Length => {
1525 out.push_str(".len()");
1526 }
1527 }
1528 }
1529 out
1530}
1531
1532fn render_zig_with_optionals(
1545 segments: &[PathSegment],
1546 result_var: &str,
1547 optional_fields: &HashSet<String>,
1548 method_calls: &HashSet<String>,
1549) -> String {
1550 let mut out = result_var.to_string();
1551 let mut path_so_far = String::new();
1552 for seg in segments {
1553 match seg {
1554 PathSegment::Field(f) => {
1555 if !path_so_far.is_empty() {
1556 path_so_far.push('.');
1557 }
1558 path_so_far.push_str(f);
1559 out.push('.');
1560 out.push_str(f);
1561 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1562 out.push_str(".?");
1563 }
1564 }
1565 PathSegment::ArrayField { name, index } => {
1566 if !path_so_far.is_empty() {
1567 path_so_far.push('.');
1568 }
1569 path_so_far.push_str(name);
1570 out.push('.');
1571 out.push_str(name);
1572 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1573 out.push_str(".?");
1574 }
1575 out.push_str(&format!("[{index}]"));
1576 }
1577 PathSegment::MapAccess { field, key } => {
1578 if !path_so_far.is_empty() {
1579 path_so_far.push('.');
1580 }
1581 path_so_far.push_str(field);
1582 out.push('.');
1583 out.push_str(field);
1584 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1585 out.push_str(".?");
1586 }
1587 if key.chars().all(|c| c.is_ascii_digit()) {
1588 out.push_str(&format!("[{key}]"));
1589 } else {
1590 out.push_str(&format!(".get(\"{key}\")"));
1591 }
1592 }
1593 PathSegment::Length => {
1594 out.push_str(".len");
1595 }
1596 }
1597 }
1598 out
1599}
1600
1601fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1602 let mut out = result_var.to_string();
1603 for seg in segments {
1604 match seg {
1605 PathSegment::Field(f) => {
1606 out.push('.');
1607 out.push_str(&f.to_pascal_case());
1608 }
1609 PathSegment::ArrayField { name, index } => {
1610 out.push('.');
1611 out.push_str(&name.to_pascal_case());
1612 out.push_str(&format!("[{index}]"));
1613 }
1614 PathSegment::MapAccess { field, key } => {
1615 out.push('.');
1616 out.push_str(&field.to_pascal_case());
1617 if key.chars().all(|c| c.is_ascii_digit()) {
1618 out.push_str(&format!("[{key}]"));
1619 } else {
1620 out.push_str(&format!("[\"{key}\"]"));
1621 }
1622 }
1623 PathSegment::Length => {
1624 out.push_str(".Count");
1625 }
1626 }
1627 }
1628 out
1629}
1630
1631fn render_csharp_with_optionals(
1632 segments: &[PathSegment],
1633 result_var: &str,
1634 optional_fields: &HashSet<String>,
1635) -> String {
1636 let mut out = result_var.to_string();
1637 let mut path_so_far = String::new();
1638 for (i, seg) in segments.iter().enumerate() {
1639 let is_leaf = i == segments.len() - 1;
1640 match seg {
1641 PathSegment::Field(f) => {
1642 if !path_so_far.is_empty() {
1643 path_so_far.push('.');
1644 }
1645 path_so_far.push_str(f);
1646 out.push('.');
1647 out.push_str(&f.to_pascal_case());
1648 if !is_leaf && optional_fields.contains(&path_so_far) {
1649 out.push('!');
1650 }
1651 }
1652 PathSegment::ArrayField { name, index } => {
1653 if !path_so_far.is_empty() {
1654 path_so_far.push('.');
1655 }
1656 path_so_far.push_str(name);
1657 out.push('.');
1658 out.push_str(&name.to_pascal_case());
1659 out.push_str(&format!("[{index}]"));
1660 }
1661 PathSegment::MapAccess { field, key } => {
1662 if !path_so_far.is_empty() {
1663 path_so_far.push('.');
1664 }
1665 path_so_far.push_str(field);
1666 out.push('.');
1667 out.push_str(&field.to_pascal_case());
1668 if key.chars().all(|c| c.is_ascii_digit()) {
1669 out.push_str(&format!("[{key}]"));
1670 } else {
1671 out.push_str(&format!("[\"{key}\"]"));
1672 }
1673 }
1674 PathSegment::Length => {
1675 out.push_str(".Count");
1676 }
1677 }
1678 }
1679 out
1680}
1681
1682fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1683 let mut out = result_var.to_string();
1684 for seg in segments {
1685 match seg {
1686 PathSegment::Field(f) => {
1687 out.push_str("->");
1688 out.push_str(&f.to_lower_camel_case());
1691 }
1692 PathSegment::ArrayField { name, index } => {
1693 out.push_str("->");
1694 out.push_str(&name.to_lower_camel_case());
1695 out.push_str(&format!("[{index}]"));
1696 }
1697 PathSegment::MapAccess { field, key } => {
1698 out.push_str("->");
1699 out.push_str(&field.to_lower_camel_case());
1700 out.push_str(&format!("[\"{key}\"]"));
1701 }
1702 PathSegment::Length => {
1703 let current = std::mem::take(&mut out);
1704 out = format!("count({current})");
1705 }
1706 }
1707 }
1708 out
1709}
1710
1711fn render_php_with_getters(segments: &[PathSegment], result_var: &str, getter_map: &PhpGetterMap) -> String {
1729 let mut out = result_var.to_string();
1730 let mut current_type: Option<String> = getter_map.root_type.clone();
1731 for seg in segments {
1732 match seg {
1733 PathSegment::Field(f) => {
1734 let camel = f.to_lower_camel_case();
1735 if getter_map.needs_getter(current_type.as_deref(), f.as_str()) {
1736 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1741 out.push_str("->");
1742 out.push_str(&getter);
1743 out.push_str("()");
1744 } else {
1745 out.push_str("->");
1746 out.push_str(&camel);
1747 }
1748 current_type = getter_map.advance(current_type.as_deref(), f.as_str());
1749 }
1750 PathSegment::ArrayField { name, index } => {
1751 let camel = name.to_lower_camel_case();
1752 if getter_map.needs_getter(current_type.as_deref(), name.as_str()) {
1753 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1754 out.push_str("->");
1755 out.push_str(&getter);
1756 out.push_str("()");
1757 } else {
1758 out.push_str("->");
1759 out.push_str(&camel);
1760 }
1761 out.push_str(&format!("[{index}]"));
1762 current_type = getter_map.advance(current_type.as_deref(), name.as_str());
1763 }
1764 PathSegment::MapAccess { field, key } => {
1765 let camel = field.to_lower_camel_case();
1766 if getter_map.needs_getter(current_type.as_deref(), field.as_str()) {
1767 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1768 out.push_str("->");
1769 out.push_str(&getter);
1770 out.push_str("()");
1771 } else {
1772 out.push_str("->");
1773 out.push_str(&camel);
1774 }
1775 out.push_str(&format!("[\"{key}\"]"));
1776 current_type = getter_map.advance(current_type.as_deref(), field.as_str());
1777 }
1778 PathSegment::Length => {
1779 let current = std::mem::take(&mut out);
1780 out = format!("count({current})");
1781 }
1782 }
1783 }
1784 out
1785}
1786
1787fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1788 let mut out = result_var.to_string();
1789 for seg in segments {
1790 match seg {
1791 PathSegment::Field(f) => {
1792 out.push('$');
1793 out.push_str(f);
1794 }
1795 PathSegment::ArrayField { name, index } => {
1796 out.push('$');
1797 out.push_str(name);
1798 out.push_str(&format!("[[{}]]", index + 1));
1800 }
1801 PathSegment::MapAccess { field, key } => {
1802 out.push('$');
1803 out.push_str(field);
1804 out.push_str(&format!("[[\"{key}\"]]"));
1805 }
1806 PathSegment::Length => {
1807 let current = std::mem::take(&mut out);
1808 out = format!("length({current})");
1809 }
1810 }
1811 }
1812 out
1813}
1814
1815fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1816 let mut parts = Vec::new();
1817 let mut trailing_length = false;
1818 for seg in segments {
1819 match seg {
1820 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1821 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1822 PathSegment::MapAccess { field, key } => {
1823 parts.push(field.to_snake_case());
1824 parts.push(key.clone());
1825 }
1826 PathSegment::Length => {
1827 trailing_length = true;
1828 }
1829 }
1830 }
1831 let suffix = parts.join("_");
1832 if trailing_length {
1833 format!("result_{suffix}_count({result_var})")
1834 } else {
1835 format!("result_{suffix}({result_var})")
1836 }
1837}
1838
1839fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
1846 let mut out = result_var.to_string();
1847 for seg in segments {
1848 match seg {
1849 PathSegment::Field(f) => {
1850 out.push('.');
1851 out.push_str(&f.to_lower_camel_case());
1852 }
1853 PathSegment::ArrayField { name, index } => {
1854 out.push('.');
1855 out.push_str(&name.to_lower_camel_case());
1856 out.push_str(&format!("[{index}]"));
1857 }
1858 PathSegment::MapAccess { field, key } => {
1859 out.push('.');
1860 out.push_str(&field.to_lower_camel_case());
1861 if key.chars().all(|c| c.is_ascii_digit()) {
1862 out.push_str(&format!("[{key}]"));
1863 } else {
1864 out.push_str(&format!("[\"{key}\"]"));
1865 }
1866 }
1867 PathSegment::Length => {
1868 out.push_str(".length");
1869 }
1870 }
1871 }
1872 out
1873}
1874
1875fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1881 let mut out = result_var.to_string();
1882 let mut path_so_far = String::new();
1883 let mut prev_was_nullable = false;
1884 for seg in segments {
1885 let nav = if prev_was_nullable { "?." } else { "." };
1886 match seg {
1887 PathSegment::Field(f) => {
1888 if !path_so_far.is_empty() {
1889 path_so_far.push('.');
1890 }
1891 path_so_far.push_str(f);
1892 let is_optional = optional_fields.contains(&path_so_far);
1893 out.push_str(nav);
1894 out.push_str(&f.to_lower_camel_case());
1895 prev_was_nullable = is_optional;
1896 }
1897 PathSegment::ArrayField { name, index } => {
1898 if !path_so_far.is_empty() {
1899 path_so_far.push('.');
1900 }
1901 path_so_far.push_str(name);
1902 let is_optional = optional_fields.contains(&path_so_far);
1903 out.push_str(nav);
1904 out.push_str(&name.to_lower_camel_case());
1905 if is_optional {
1909 out.push('!');
1910 }
1911 out.push_str(&format!("[{index}]"));
1912 prev_was_nullable = false;
1913 }
1914 PathSegment::MapAccess { field, key } => {
1915 if !path_so_far.is_empty() {
1916 path_so_far.push('.');
1917 }
1918 path_so_far.push_str(field);
1919 let is_optional = optional_fields.contains(&path_so_far);
1920 out.push_str(nav);
1921 out.push_str(&field.to_lower_camel_case());
1922 if key.chars().all(|c| c.is_ascii_digit()) {
1923 out.push_str(&format!("[{key}]"));
1924 } else {
1925 out.push_str(&format!("[\"{key}\"]"));
1926 }
1927 prev_was_nullable = is_optional;
1928 }
1929 PathSegment::Length => {
1930 out.push_str(nav);
1933 out.push_str("length");
1934 prev_was_nullable = false;
1935 }
1936 }
1937 }
1938 out
1939}
1940
1941#[cfg(test)]
1942mod tests {
1943 use super::*;
1944
1945 fn make_resolver() -> FieldResolver {
1946 let mut fields = HashMap::new();
1947 fields.insert("title".to_string(), "metadata.document.title".to_string());
1948 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1949 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
1950 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
1951 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
1952 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
1953 let mut optional = HashSet::new();
1954 optional.insert("metadata.document.title".to_string());
1955 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1956 }
1957
1958 fn make_resolver_with_doc_optional() -> FieldResolver {
1959 let mut fields = HashMap::new();
1960 fields.insert("title".to_string(), "metadata.document.title".to_string());
1961 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1962 let mut optional = HashSet::new();
1963 optional.insert("document".to_string());
1964 optional.insert("metadata.document.title".to_string());
1965 optional.insert("metadata.document".to_string());
1966 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1967 }
1968
1969 #[test]
1970 fn test_resolve_alias() {
1971 let r = make_resolver();
1972 assert_eq!(r.resolve("title"), "metadata.document.title");
1973 }
1974
1975 #[test]
1976 fn test_resolve_passthrough() {
1977 let r = make_resolver();
1978 assert_eq!(r.resolve("content"), "content");
1979 }
1980
1981 #[test]
1982 fn test_is_optional() {
1983 let r = make_resolver();
1984 assert!(r.is_optional("metadata.document.title"));
1985 assert!(!r.is_optional("content"));
1986 }
1987
1988 #[test]
1989 fn test_accessor_rust_struct() {
1990 let r = make_resolver();
1991 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
1992 }
1993
1994 #[test]
1995 fn test_accessor_rust_map() {
1996 let r = make_resolver();
1997 assert_eq!(
1998 r.accessor("tags", "rust", "result"),
1999 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
2000 );
2001 }
2002
2003 #[test]
2004 fn test_accessor_python() {
2005 let r = make_resolver();
2006 assert_eq!(
2007 r.accessor("title", "python", "result"),
2008 "result.metadata.document.title"
2009 );
2010 }
2011
2012 #[test]
2013 fn test_accessor_go() {
2014 let r = make_resolver();
2015 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
2016 }
2017
2018 #[test]
2019 fn test_accessor_go_initialism_fields() {
2020 let mut fields = std::collections::HashMap::new();
2021 fields.insert("content".to_string(), "html".to_string());
2022 fields.insert("link_url".to_string(), "links.url".to_string());
2023 let r = FieldResolver::new(
2024 &fields,
2025 &HashSet::new(),
2026 &HashSet::new(),
2027 &HashSet::new(),
2028 &HashSet::new(),
2029 );
2030 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
2031 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
2032 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
2033 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
2034 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
2035 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
2036 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
2037 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
2038 }
2039
2040 #[test]
2041 fn test_accessor_typescript() {
2042 let r = make_resolver();
2043 assert_eq!(
2044 r.accessor("title", "typescript", "result"),
2045 "result.metadata.document.title"
2046 );
2047 }
2048
2049 #[test]
2050 fn test_accessor_typescript_snake_to_camel() {
2051 let r = make_resolver();
2052 assert_eq!(
2053 r.accessor("og", "typescript", "result"),
2054 "result.metadata.document.openGraph"
2055 );
2056 assert_eq!(
2057 r.accessor("twitter", "typescript", "result"),
2058 "result.metadata.document.twitterCard"
2059 );
2060 assert_eq!(
2061 r.accessor("canonical", "typescript", "result"),
2062 "result.metadata.document.canonicalUrl"
2063 );
2064 }
2065
2066 #[test]
2067 fn test_accessor_typescript_map_snake_to_camel() {
2068 let r = make_resolver();
2069 assert_eq!(
2070 r.accessor("og_tag", "typescript", "result"),
2071 "result.metadata.openGraphTags[\"og_title\"]"
2072 );
2073 }
2074
2075 #[test]
2076 fn test_accessor_typescript_numeric_index_is_unquoted() {
2077 let mut fields = HashMap::new();
2081 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
2082 let r = FieldResolver::new(
2083 &fields,
2084 &HashSet::new(),
2085 &HashSet::new(),
2086 &HashSet::new(),
2087 &HashSet::new(),
2088 );
2089 assert_eq!(
2090 r.accessor("first_score", "typescript", "result"),
2091 "result.results[0].relevanceScore"
2092 );
2093 }
2094
2095 #[test]
2096 fn test_accessor_node_alias() {
2097 let r = make_resolver();
2098 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
2099 }
2100
2101 #[test]
2102 fn test_accessor_wasm_camel_case() {
2103 let r = make_resolver();
2104 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
2105 assert_eq!(
2106 r.accessor("twitter", "wasm", "result"),
2107 "result.metadata.document.twitterCard"
2108 );
2109 assert_eq!(
2110 r.accessor("canonical", "wasm", "result"),
2111 "result.metadata.document.canonicalUrl"
2112 );
2113 }
2114
2115 #[test]
2116 fn test_accessor_wasm_map_access() {
2117 let r = make_resolver();
2118 assert_eq!(
2119 r.accessor("og_tag", "wasm", "result"),
2120 "result.metadata.openGraphTags.get(\"og_title\")"
2121 );
2122 }
2123
2124 #[test]
2125 fn test_accessor_java() {
2126 let r = make_resolver();
2127 assert_eq!(
2128 r.accessor("title", "java", "result"),
2129 "result.metadata().document().title()"
2130 );
2131 }
2132
2133 #[test]
2134 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
2135 let mut fields = HashMap::new();
2136 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2137 fields.insert("node_count".to_string(), "nodes.length".to_string());
2138 let mut arrays = HashSet::new();
2139 arrays.insert("nodes".to_string());
2140 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
2141 assert_eq!(
2142 r.accessor("first_node_name", "kotlin", "result"),
2143 "result.nodes().first().name()"
2144 );
2145 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
2146 }
2147
2148 #[test]
2149 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
2150 let r = make_resolver_with_doc_optional();
2151 assert_eq!(
2152 r.accessor("title", "kotlin", "result"),
2153 "result.metadata().document()?.title()"
2154 );
2155 }
2156
2157 #[test]
2158 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
2159 let mut fields = HashMap::new();
2160 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2161 fields.insert("tag".to_string(), "tags[name]".to_string());
2162 let mut optional = HashSet::new();
2163 optional.insert("nodes".to_string());
2164 optional.insert("tags".to_string());
2165 let mut arrays = HashSet::new();
2166 arrays.insert("nodes".to_string());
2167 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2168 assert_eq!(
2169 r.accessor("first_node_name", "kotlin", "result"),
2170 "result.nodes()?.first()?.name()"
2171 );
2172 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
2173 }
2174
2175 #[test]
2181 fn test_accessor_kotlin_optional_field_after_indexed_array() {
2182 let mut fields = HashMap::new();
2185 fields.insert(
2186 "tool_call_name".to_string(),
2187 "choices[0].message.tool_calls[0].function.name".to_string(),
2188 );
2189 let mut optional = HashSet::new();
2190 optional.insert("choices[0].message.tool_calls".to_string());
2191 let mut arrays = HashSet::new();
2192 arrays.insert("choices".to_string());
2193 arrays.insert("choices[0].message.tool_calls".to_string());
2194 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2195 let expr = r.accessor("tool_call_name", "kotlin", "result");
2196 assert!(
2198 expr.contains("toolCalls()?.first()"),
2199 "expected toolCalls()?.first() for optional list, got: {expr}"
2200 );
2201 }
2202
2203 #[test]
2204 fn test_accessor_csharp() {
2205 let r = make_resolver();
2206 assert_eq!(
2207 r.accessor("title", "csharp", "result"),
2208 "result.Metadata.Document.Title"
2209 );
2210 }
2211
2212 #[test]
2213 fn test_accessor_php() {
2214 let r = make_resolver();
2215 assert_eq!(
2216 r.accessor("title", "php", "$result"),
2217 "$result->metadata->document->title"
2218 );
2219 }
2220
2221 #[test]
2222 fn test_accessor_r() {
2223 let r = make_resolver();
2224 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
2225 }
2226
2227 #[test]
2228 fn test_accessor_c() {
2229 let r = make_resolver();
2230 assert_eq!(
2231 r.accessor("title", "c", "result"),
2232 "result_metadata_document_title(result)"
2233 );
2234 }
2235
2236 #[test]
2237 fn test_rust_unwrap_binding() {
2238 let r = make_resolver();
2239 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
2240 assert_eq!(var, "metadata_document_title");
2241 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
2242 }
2243
2244 #[test]
2245 fn test_rust_unwrap_binding_non_optional() {
2246 let r = make_resolver();
2247 assert!(r.rust_unwrap_binding("content", "result").is_none());
2248 }
2249
2250 #[test]
2251 fn test_rust_unwrap_binding_collapses_double_underscore() {
2252 let mut aliases = HashMap::new();
2257 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
2258 let mut optional = HashSet::new();
2259 optional.insert("json_ld[].name".to_string());
2260 let mut array = HashSet::new();
2261 array.insert("json_ld".to_string());
2262 let result_fields = HashSet::new();
2263 let method_calls = HashSet::new();
2264 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
2265 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
2266 assert_eq!(var, "json_ld_name");
2267 }
2268
2269 #[test]
2270 fn test_direct_field_no_alias() {
2271 let r = make_resolver();
2272 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2273 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
2274 }
2275
2276 #[test]
2277 fn test_accessor_rust_with_optionals() {
2278 let r = make_resolver_with_doc_optional();
2279 assert_eq!(
2280 r.accessor("title", "rust", "result"),
2281 "result.metadata.document.as_ref().unwrap().title"
2282 );
2283 }
2284
2285 #[test]
2286 fn test_accessor_csharp_with_optionals() {
2287 let r = make_resolver_with_doc_optional();
2288 assert_eq!(
2289 r.accessor("title", "csharp", "result"),
2290 "result.Metadata.Document!.Title"
2291 );
2292 }
2293
2294 #[test]
2295 fn test_accessor_rust_non_optional_field() {
2296 let r = make_resolver();
2297 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2298 }
2299
2300 #[test]
2301 fn test_accessor_csharp_non_optional_field() {
2302 let r = make_resolver();
2303 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
2304 }
2305
2306 #[test]
2307 fn test_accessor_rust_method_call() {
2308 let mut fields = HashMap::new();
2310 fields.insert(
2311 "excel_sheet_count".to_string(),
2312 "metadata.format.excel.sheet_count".to_string(),
2313 );
2314 let mut optional = HashSet::new();
2315 optional.insert("metadata.format".to_string());
2316 optional.insert("metadata.format.excel".to_string());
2317 let mut method_calls = HashSet::new();
2318 method_calls.insert("metadata.format.excel".to_string());
2319 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
2320 assert_eq!(
2321 r.accessor("excel_sheet_count", "rust", "result"),
2322 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
2323 );
2324 }
2325
2326 fn make_php_getter_resolver() -> FieldResolver {
2331 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2332 getters.insert(
2333 "Root".to_string(),
2334 ["metadata".to_string(), "links".to_string()].into_iter().collect(),
2335 );
2336 let map = PhpGetterMap {
2337 getters,
2338 field_types: HashMap::new(),
2339 root_type: Some("Root".to_string()),
2340 all_fields: HashMap::new(),
2341 };
2342 FieldResolver::new_with_php_getters(
2343 &HashMap::new(),
2344 &HashSet::new(),
2345 &HashSet::new(),
2346 &HashSet::new(),
2347 &HashSet::new(),
2348 &HashMap::new(),
2349 map,
2350 )
2351 }
2352
2353 #[test]
2354 fn render_php_uses_getter_method_for_non_scalar_field() {
2355 let r = make_php_getter_resolver();
2356 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
2357 }
2358
2359 #[test]
2360 fn render_php_uses_property_for_scalar_field() {
2361 let r = make_php_getter_resolver();
2362 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2363 }
2364
2365 #[test]
2366 fn render_php_nested_non_scalar_uses_getter_then_property() {
2367 let mut fields = HashMap::new();
2368 fields.insert("title".to_string(), "metadata.title".to_string());
2369 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2370 getters.insert("Root".to_string(), ["metadata".to_string()].into_iter().collect());
2371 getters.insert("Metadata".to_string(), HashSet::new());
2373 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2374 field_types.insert(
2375 "Root".to_string(),
2376 [("metadata".to_string(), "Metadata".to_string())].into_iter().collect(),
2377 );
2378 let map = PhpGetterMap {
2379 getters,
2380 field_types,
2381 root_type: Some("Root".to_string()),
2382 all_fields: HashMap::new(),
2383 };
2384 let r = FieldResolver::new_with_php_getters(
2385 &fields,
2386 &HashSet::new(),
2387 &HashSet::new(),
2388 &HashSet::new(),
2389 &HashSet::new(),
2390 &HashMap::new(),
2391 map,
2392 );
2393 assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
2395 }
2396
2397 #[test]
2398 fn render_php_array_field_uses_getter_when_non_scalar() {
2399 let mut fields = HashMap::new();
2400 fields.insert("first_link".to_string(), "links[0]".to_string());
2401 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2402 getters.insert("Root".to_string(), ["links".to_string()].into_iter().collect());
2403 let map = PhpGetterMap {
2404 getters,
2405 field_types: HashMap::new(),
2406 root_type: Some("Root".to_string()),
2407 all_fields: HashMap::new(),
2408 };
2409 let r = FieldResolver::new_with_php_getters(
2410 &fields,
2411 &HashSet::new(),
2412 &HashSet::new(),
2413 &HashSet::new(),
2414 &HashSet::new(),
2415 &HashMap::new(),
2416 map,
2417 );
2418 assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
2419 }
2420
2421 #[test]
2422 fn render_php_falls_back_to_property_when_getter_fields_empty() {
2423 let r = FieldResolver::new(
2426 &HashMap::new(),
2427 &HashSet::new(),
2428 &HashSet::new(),
2429 &HashSet::new(),
2430 &HashSet::new(),
2431 );
2432 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2433 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
2434 }
2435
2436 #[test]
2440 fn render_php_with_getters_distinguishes_same_field_name_on_different_types() {
2441 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2442 getters.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2444 getters.insert("B".to_string(), HashSet::new());
2446 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2449 all_fields.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2450 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2451 let map_a = PhpGetterMap {
2452 getters: getters.clone(),
2453 field_types: HashMap::new(),
2454 root_type: Some("A".to_string()),
2455 all_fields: all_fields.clone(),
2456 };
2457 let map_b = PhpGetterMap {
2458 getters,
2459 field_types: HashMap::new(),
2460 root_type: Some("B".to_string()),
2461 all_fields,
2462 };
2463 let r_a = FieldResolver::new_with_php_getters(
2464 &HashMap::new(),
2465 &HashSet::new(),
2466 &HashSet::new(),
2467 &HashSet::new(),
2468 &HashSet::new(),
2469 &HashMap::new(),
2470 map_a,
2471 );
2472 let r_b = FieldResolver::new_with_php_getters(
2473 &HashMap::new(),
2474 &HashSet::new(),
2475 &HashSet::new(),
2476 &HashSet::new(),
2477 &HashSet::new(),
2478 &HashMap::new(),
2479 map_b,
2480 );
2481 assert_eq!(r_a.accessor("content", "php", "$a"), "$a->getContent()");
2482 assert_eq!(r_b.accessor("content", "php", "$b"), "$b->content");
2483 }
2484
2485 #[test]
2489 fn render_php_with_getters_chains_through_correct_type() {
2490 let mut fields = HashMap::new();
2491 fields.insert("nested_content".to_string(), "inner.content".to_string());
2492 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2493 getters.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2495 getters.insert("B".to_string(), HashSet::new());
2497 getters.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2500 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2501 field_types.insert(
2502 "Outer".to_string(),
2503 [("inner".to_string(), "B".to_string())].into_iter().collect(),
2504 );
2505 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2506 all_fields.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2507 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2508 all_fields.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2509 let map = PhpGetterMap {
2510 getters,
2511 field_types,
2512 root_type: Some("Outer".to_string()),
2513 all_fields,
2514 };
2515 let r = FieldResolver::new_with_php_getters(
2516 &fields,
2517 &HashSet::new(),
2518 &HashSet::new(),
2519 &HashSet::new(),
2520 &HashSet::new(),
2521 &HashMap::new(),
2522 map,
2523 );
2524 assert_eq!(
2525 r.accessor("nested_content", "php", "$result"),
2526 "$result->getInner()->content"
2527 );
2528 }
2529}