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)]
94pub struct SwiftFirstClassMap {
95 pub first_class_types: HashSet<String>,
96 pub field_types: HashMap<String, HashMap<String, String>>,
97 pub vec_field_names: HashSet<String>,
98 pub root_type: Option<String>,
99}
100
101impl SwiftFirstClassMap {
102 pub fn is_first_class(&self, type_name: Option<&str>) -> bool {
108 match type_name {
109 Some(t) => self.first_class_types.contains(t),
110 None => true,
111 }
112 }
113
114 pub fn advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
117 let owner = owner_type?;
118 self.field_types.get(owner).and_then(|m| m.get(field_name).cloned())
119 }
120
121 pub fn is_vec_field_name(&self, field_name: &str) -> bool {
126 self.vec_field_names.contains(field_name)
127 }
128
129 pub fn is_empty(&self) -> bool {
131 self.first_class_types.is_empty() && self.field_types.is_empty()
132 }
133}
134
135impl PhpGetterMap {
136 pub fn needs_getter(&self, owner_type: Option<&str>, field_name: &str) -> bool {
143 if let Some(t) = owner_type {
144 let owner_has_field = self.all_fields.get(t).is_some_and(|s| s.contains(field_name));
149 if owner_has_field {
150 if let Some(fields) = self.getters.get(t) {
151 return fields.contains(field_name);
152 }
153 }
154 }
155 self.getters.values().any(|set| set.contains(field_name))
156 }
157
158 pub fn advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
161 let owner = owner_type?;
162 self.field_types.get(owner).and_then(|m| m.get(field_name).cloned())
163 }
164
165 pub fn is_empty(&self) -> bool {
168 self.getters.is_empty()
169 }
170}
171
172#[derive(Debug, Clone)]
174enum PathSegment {
175 Field(String),
177 ArrayField { name: String, index: usize },
182 MapAccess { field: String, key: String },
184 Length,
186}
187
188impl FieldResolver {
189 pub fn new(
193 fields: &HashMap<String, String>,
194 optional: &HashSet<String>,
195 result_fields: &HashSet<String>,
196 array_fields: &HashSet<String>,
197 method_calls: &HashSet<String>,
198 ) -> Self {
199 Self {
200 aliases: fields.clone(),
201 optional_fields: optional.clone(),
202 result_fields: result_fields.clone(),
203 array_fields: array_fields.clone(),
204 method_calls: method_calls.clone(),
205 error_field_aliases: HashMap::new(),
206 php_getter_map: PhpGetterMap::default(),
207 swift_first_class_map: SwiftFirstClassMap::default(),
208 }
209 }
210
211 pub fn new_with_error_aliases(
217 fields: &HashMap<String, String>,
218 optional: &HashSet<String>,
219 result_fields: &HashSet<String>,
220 array_fields: &HashSet<String>,
221 method_calls: &HashSet<String>,
222 error_field_aliases: &HashMap<String, String>,
223 ) -> Self {
224 Self {
225 aliases: fields.clone(),
226 optional_fields: optional.clone(),
227 result_fields: result_fields.clone(),
228 array_fields: array_fields.clone(),
229 method_calls: method_calls.clone(),
230 error_field_aliases: error_field_aliases.clone(),
231 php_getter_map: PhpGetterMap::default(),
232 swift_first_class_map: SwiftFirstClassMap::default(),
233 }
234 }
235
236 pub fn new_with_php_getters(
251 fields: &HashMap<String, String>,
252 optional: &HashSet<String>,
253 result_fields: &HashSet<String>,
254 array_fields: &HashSet<String>,
255 method_calls: &HashSet<String>,
256 error_field_aliases: &HashMap<String, String>,
257 php_getter_map: PhpGetterMap,
258 ) -> Self {
259 Self {
260 aliases: fields.clone(),
261 optional_fields: optional.clone(),
262 result_fields: result_fields.clone(),
263 array_fields: array_fields.clone(),
264 method_calls: method_calls.clone(),
265 error_field_aliases: error_field_aliases.clone(),
266 php_getter_map,
267 swift_first_class_map: SwiftFirstClassMap::default(),
268 }
269 }
270
271 pub fn with_swift_root_type(&self, root_type: Option<String>) -> Self {
282 let mut clone = self.clone();
283 clone.swift_first_class_map.root_type = root_type;
284 clone
285 }
286
287 #[allow(clippy::too_many_arguments)]
291 pub fn new_with_swift_first_class(
292 fields: &HashMap<String, String>,
293 optional: &HashSet<String>,
294 result_fields: &HashSet<String>,
295 array_fields: &HashSet<String>,
296 method_calls: &HashSet<String>,
297 error_field_aliases: &HashMap<String, String>,
298 swift_first_class_map: SwiftFirstClassMap,
299 ) -> Self {
300 Self {
301 aliases: fields.clone(),
302 optional_fields: optional.clone(),
303 result_fields: result_fields.clone(),
304 array_fields: array_fields.clone(),
305 method_calls: method_calls.clone(),
306 error_field_aliases: error_field_aliases.clone(),
307 php_getter_map: PhpGetterMap::default(),
308 swift_first_class_map,
309 }
310 }
311
312 pub fn resolve<'a>(&'a self, fixture_field: &'a str) -> &'a str {
315 self.aliases
316 .get(fixture_field)
317 .map(String::as_str)
318 .unwrap_or(fixture_field)
319 }
320
321 pub fn leaf_is_vec_via_swift_map(&self, field: &str) -> bool {
328 let leaf = field.split('.').next_back().unwrap_or(field);
329 let leaf = leaf.split('[').next().unwrap_or(leaf);
330 self.swift_first_class_map.is_vec_field_name(leaf)
331 }
332
333 pub fn is_optional(&self, field: &str) -> bool {
335 if self.optional_fields.contains(field) {
336 return true;
337 }
338 let index_normalized = normalize_numeric_indices(field);
339 if index_normalized != field && self.optional_fields.contains(index_normalized.as_str()) {
340 return true;
341 }
342 let de_indexed = strip_numeric_indices(field);
345 if de_indexed != field && self.optional_fields.contains(de_indexed.as_str()) {
346 return true;
347 }
348 let normalized = field.replace("[].", ".");
349 if normalized != field && self.optional_fields.contains(normalized.as_str()) {
350 return true;
351 }
352 for af in &self.array_fields {
353 if let Some(rest) = field.strip_prefix(af.as_str()) {
354 if let Some(rest) = rest.strip_prefix('.') {
355 let with_bracket = format!("{af}[].{rest}");
356 if self.optional_fields.contains(with_bracket.as_str()) {
357 return true;
358 }
359 }
360 }
361 }
362 false
363 }
364
365 pub fn has_alias(&self, fixture_field: &str) -> bool {
367 self.aliases.contains_key(fixture_field)
368 }
369
370 pub fn has_explicit_field(&self, field_name: &str) -> bool {
376 if self.result_fields.is_empty() {
377 return false;
378 }
379 self.result_fields.contains(field_name)
380 }
381
382 pub fn is_valid_for_result(&self, fixture_field: &str) -> bool {
384 if self.result_fields.is_empty() {
385 return true;
386 }
387 let resolved = self.resolve(fixture_field);
388 let first_segment = resolved.split('.').next().unwrap_or(resolved);
389 let first_segment = first_segment.split('[').next().unwrap_or(first_segment);
390 self.result_fields.contains(first_segment)
391 }
392
393 pub fn is_array(&self, field: &str) -> bool {
395 self.array_fields.contains(field)
396 }
397
398 pub fn is_collection_root(&self, field: &str) -> bool {
411 let prefix = format!("{field}[");
412 self.array_fields.iter().any(|af| af.starts_with(&prefix))
413 || self.optional_fields.iter().any(|of| of.starts_with(&prefix))
414 }
415
416 pub fn tagged_union_split(&self, fixture_field: &str) -> Option<(String, String, String)> {
428 let resolved = self.resolve(fixture_field);
429 let segments: Vec<&str> = resolved.split('.').collect();
430 let mut path_so_far = String::new();
431 for (i, seg) in segments.iter().enumerate() {
432 if !path_so_far.is_empty() {
433 path_so_far.push('.');
434 }
435 path_so_far.push_str(seg);
436 if self.method_calls.contains(&path_so_far) {
437 let prefix = segments[..i].join(".");
439 let variant = (*seg).to_string();
440 let suffix = segments[i + 1..].join(".");
441 return Some((prefix, variant, suffix));
442 }
443 }
444 None
445 }
446
447 pub fn has_map_access(&self, fixture_field: &str) -> bool {
449 let resolved = self.resolve(fixture_field);
450 let segments = parse_path(resolved);
451 segments.iter().any(|s| {
452 if let PathSegment::MapAccess { key, .. } = s {
453 !key.chars().all(|c| c.is_ascii_digit())
454 } else {
455 false
456 }
457 })
458 }
459
460 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
462 let resolved = self.resolve(fixture_field);
463 let segments = parse_path(resolved);
464 let segments = self.inject_array_indexing(segments);
465 match language {
466 "java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
467 "kotlin" => render_kotlin_with_optionals(&segments, result_var, &self.optional_fields),
468 "kotlin_android" => render_kotlin_android_with_optionals(&segments, result_var, &self.optional_fields),
471 "rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
472 "csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
473 "zig" => render_zig_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
474 "swift" if !self.swift_first_class_map.is_empty() => render_swift_with_first_class_map(
475 &segments,
476 result_var,
477 &self.optional_fields,
478 &self.swift_first_class_map,
479 ),
480 "swift" => render_swift_with_optionals(&segments, result_var, &self.optional_fields),
481 "dart" => render_dart_with_optionals(&segments, result_var, &self.optional_fields),
482 "php" if !self.php_getter_map.is_empty() => {
483 render_php_with_getters(&segments, result_var, &self.php_getter_map)
484 }
485 _ => render_accessor(&segments, language, result_var),
486 }
487 }
488
489 pub fn accessor_for_error(&self, sub_field: &str, language: &str, err_var: &str) -> String {
503 let resolved = self
504 .error_field_aliases
505 .get(sub_field)
506 .map(String::as_str)
507 .unwrap_or(sub_field);
508 let segments = parse_path(resolved);
509 match language {
512 "rust" => render_rust_with_optionals(&segments, err_var, &self.optional_fields, &self.method_calls),
513 _ => render_accessor(&segments, language, err_var),
514 }
515 }
516
517 pub fn has_error_aliases(&self) -> bool {
524 !self.error_field_aliases.is_empty()
525 }
526
527 fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
528 if self.array_fields.is_empty() {
529 return segments;
530 }
531 let len = segments.len();
532 let mut result = Vec::with_capacity(len);
533 let mut path_so_far = String::new();
534 for i in 0..len {
535 let seg = &segments[i];
536 match seg {
537 PathSegment::Field(f) => {
538 if !path_so_far.is_empty() {
539 path_so_far.push('.');
540 }
541 path_so_far.push_str(f);
542 let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
543 if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
544 result.push(PathSegment::ArrayField {
546 name: f.clone(),
547 index: 0,
548 });
549 } else {
550 result.push(seg.clone());
551 }
552 }
553 PathSegment::ArrayField { .. } => {
556 result.push(seg.clone());
557 }
558 PathSegment::MapAccess { field, key } => {
559 if !path_so_far.is_empty() {
560 path_so_far.push('.');
561 }
562 path_so_far.push_str(field);
563 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
564 if is_numeric && self.array_fields.contains(&path_so_far) {
565 let index: usize = key.parse().unwrap_or(0);
567 result.push(PathSegment::ArrayField {
568 name: field.clone(),
569 index,
570 });
571 } else {
572 result.push(seg.clone());
573 }
574 }
575 _ => {
576 result.push(seg.clone());
577 }
578 }
579 }
580 result
581 }
582
583 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
585 let resolved = self.resolve(fixture_field);
586 if !self.is_optional(resolved) {
587 return None;
588 }
589 let segments = parse_path(resolved);
590 let segments = self.inject_array_indexing(segments);
591 let local_var = {
596 let raw = resolved.replace(['.', '['], "_").replace(']', "");
597 let mut collapsed = String::with_capacity(raw.len());
598 let mut prev_underscore = false;
599 for ch in raw.chars() {
600 if ch == '_' {
601 if !prev_underscore {
602 collapsed.push('_');
603 }
604 prev_underscore = true;
605 } else {
606 collapsed.push(ch);
607 prev_underscore = false;
608 }
609 }
610 collapsed.trim_matches('_').to_string()
611 };
612 let accessor = render_accessor(&segments, "rust", result_var);
613 let has_map_access = segments.iter().any(|s| {
614 if let PathSegment::MapAccess { key, .. } = s {
615 !key.chars().all(|c| c.is_ascii_digit())
616 } else {
617 false
618 }
619 });
620 let is_array = self.is_array(resolved);
621 let binding = if has_map_access {
622 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
623 } else if is_array {
624 format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
625 } else {
626 format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
627 };
628 Some((binding, local_var))
629 }
630}
631
632fn strip_numeric_indices(path: &str) -> String {
637 let mut result = String::with_capacity(path.len());
638 let mut chars = path.chars().peekable();
639 while let Some(c) = chars.next() {
640 if c == '[' {
641 let mut key = String::new();
642 let mut closed = false;
643 for inner in chars.by_ref() {
644 if inner == ']' {
645 closed = true;
646 break;
647 }
648 key.push(inner);
649 }
650 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
651 } else {
653 result.push('[');
654 result.push_str(&key);
655 if closed {
656 result.push(']');
657 }
658 }
659 } else {
660 result.push(c);
661 }
662 }
663 while result.contains("..") {
665 result = result.replace("..", ".");
666 }
667 if result.starts_with('.') {
668 result.remove(0);
669 }
670 result
671}
672
673fn normalize_numeric_indices(path: &str) -> String {
674 let mut result = String::with_capacity(path.len());
675 let mut chars = path.chars().peekable();
676 while let Some(c) = chars.next() {
677 if c == '[' {
678 let mut key = String::new();
679 let mut closed = false;
680 for inner in chars.by_ref() {
681 if inner == ']' {
682 closed = true;
683 break;
684 }
685 key.push(inner);
686 }
687 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
688 result.push_str("[0]");
689 } else {
690 result.push('[');
691 result.push_str(&key);
692 if closed {
693 result.push(']');
694 }
695 }
696 } else {
697 result.push(c);
698 }
699 }
700 result
701}
702
703fn parse_path(path: &str) -> Vec<PathSegment> {
704 let mut segments = Vec::new();
705 for part in path.split('.') {
706 if part == "length" || part == "count" || part == "size" {
707 segments.push(PathSegment::Length);
708 } else if let Some(bracket_pos) = part.find('[') {
709 let name = part[..bracket_pos].to_string();
710 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
711 if key.is_empty() {
712 segments.push(PathSegment::ArrayField { name, index: 0 });
714 } else if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
715 let index: usize = key.parse().unwrap_or(0);
717 segments.push(PathSegment::ArrayField { name, index });
718 } else {
719 segments.push(PathSegment::MapAccess { field: name, key });
721 }
722 } else {
723 segments.push(PathSegment::Field(part.to_string()));
724 }
725 }
726 segments
727}
728
729fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
730 match language {
731 "rust" => render_rust(segments, result_var),
732 "python" => render_dot_access(segments, result_var, "python"),
733 "typescript" | "node" => render_typescript(segments, result_var),
734 "wasm" => render_wasm(segments, result_var),
735 "go" => render_go(segments, result_var),
736 "java" => render_java(segments, result_var),
737 "kotlin" => render_kotlin(segments, result_var),
738 "kotlin_android" => render_kotlin_android(segments, result_var),
739 "csharp" => render_pascal_dot(segments, result_var),
740 "ruby" => render_dot_access(segments, result_var, "ruby"),
741 "php" => render_php(segments, result_var),
742 "elixir" => render_dot_access(segments, result_var, "elixir"),
743 "r" => render_r(segments, result_var),
744 "c" => render_c(segments, result_var),
745 "swift" => render_swift(segments, result_var),
746 "dart" => render_dart(segments, result_var),
747 _ => render_dot_access(segments, result_var, language),
748 }
749}
750
751fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
763 let mut out = result_var.to_string();
764 for seg in segments {
765 match seg {
766 PathSegment::Field(f) => {
767 out.push('.');
768 out.push_str(f);
769 }
770 PathSegment::ArrayField { name, index } => {
771 out.push('.');
772 out.push_str(name);
773 out.push_str(&format!("[{index}]"));
774 }
775 PathSegment::MapAccess { field, key } => {
776 out.push('.');
777 out.push_str(field);
778 if key.chars().all(|c| c.is_ascii_digit()) {
779 out.push_str(&format!("[{key}]"));
780 } else {
781 out.push_str(&format!("[\"{key}\"]"));
782 }
783 }
784 PathSegment::Length => {
785 out.push_str(".count");
786 }
787 }
788 }
789 out
790}
791
792fn render_swift_with_optionals(
802 segments: &[PathSegment],
803 result_var: &str,
804 optional_fields: &HashSet<String>,
805) -> String {
806 let mut out = result_var.to_string();
807 let mut path_so_far = String::new();
808 let total = segments.len();
809 for (i, seg) in segments.iter().enumerate() {
810 let is_leaf = i == total - 1;
811 match seg {
812 PathSegment::Field(f) => {
813 if !path_so_far.is_empty() {
814 path_so_far.push('.');
815 }
816 path_so_far.push_str(f);
817 out.push('.');
818 out.push_str(f);
819 if !is_leaf && optional_fields.contains(&path_so_far) {
823 out.push('?');
824 }
825 }
826 PathSegment::ArrayField { name, index } => {
827 if !path_so_far.is_empty() {
828 path_so_far.push('.');
829 }
830 path_so_far.push_str(name);
831 let is_optional = optional_fields.contains(&path_so_far);
832 out.push('.');
833 out.push_str(name);
834 if is_optional {
835 out.push_str(&format!("?[{index}]"));
837 } else {
838 out.push_str(&format!("[{index}]"));
839 }
840 path_so_far.push_str("[0]");
841 let _ = is_leaf;
842 }
843 PathSegment::MapAccess { field, key } => {
844 if !path_so_far.is_empty() {
845 path_so_far.push('.');
846 }
847 path_so_far.push_str(field);
848 out.push('.');
849 out.push_str(field);
850 if key.chars().all(|c| c.is_ascii_digit()) {
851 out.push_str(&format!("[{key}]"));
852 } else {
853 out.push_str(&format!("[\"{key}\"]"));
854 }
855 }
856 PathSegment::Length => {
857 out.push_str(".count");
858 }
859 }
860 }
861 out
862}
863
864fn render_swift_with_first_class_map(
869 segments: &[PathSegment],
870 result_var: &str,
871 optional_fields: &HashSet<String>,
872 map: &SwiftFirstClassMap,
873) -> String {
874 let mut out = result_var.to_string();
875 let mut path_so_far = String::new();
876 let mut current_type: Option<String> = map.root_type.clone();
877 let mut via_rust_vec = false;
886 let total = segments.len();
887 for (i, seg) in segments.iter().enumerate() {
888 let is_leaf = i == total - 1;
889 let property_syntax = !via_rust_vec && map.is_first_class(current_type.as_deref());
890 match seg {
891 PathSegment::Field(f) => {
892 if !path_so_far.is_empty() {
893 path_so_far.push('.');
894 }
895 path_so_far.push_str(f);
896 out.push('.');
897 out.push_str(f);
898 if !property_syntax {
899 out.push_str("()");
900 }
901 if !is_leaf && optional_fields.contains(&path_so_far) {
902 out.push('?');
903 }
904 current_type = map.advance(current_type.as_deref(), f);
905 }
906 PathSegment::ArrayField { name, index } => {
907 if !path_so_far.is_empty() {
908 path_so_far.push('.');
909 }
910 path_so_far.push_str(name);
911 let is_optional = optional_fields.contains(&path_so_far);
912 out.push('.');
913 out.push_str(name);
914 let access = if property_syntax { "" } else { "()" };
915 if is_optional {
916 out.push_str(&format!("{access}?[{index}]"));
917 } else {
918 out.push_str(&format!("{access}[{index}]"));
919 }
920 path_so_far.push_str("[0]");
921 current_type = map.advance(current_type.as_deref(), name);
924 via_rust_vec = true;
925 }
926 PathSegment::MapAccess { field, key } => {
927 if !path_so_far.is_empty() {
928 path_so_far.push('.');
929 }
930 path_so_far.push_str(field);
931 out.push('.');
932 out.push_str(field);
933 let access = if property_syntax { "" } else { "()" };
934 if key.chars().all(|c| c.is_ascii_digit()) {
935 out.push_str(&format!("{access}[{key}]"));
936 } else {
937 out.push_str(&format!("{access}[\"{key}\"]"));
938 }
939 current_type = map.advance(current_type.as_deref(), field);
940 }
941 PathSegment::Length => {
942 out.push_str(".count");
943 }
944 }
945 }
946 out
947}
948
949fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
950 let mut out = result_var.to_string();
951 for seg in segments {
952 match seg {
953 PathSegment::Field(f) => {
954 out.push('.');
955 out.push_str(&f.to_snake_case());
956 }
957 PathSegment::ArrayField { name, index } => {
958 out.push('.');
959 out.push_str(&name.to_snake_case());
960 out.push_str(&format!("[{index}]"));
961 }
962 PathSegment::MapAccess { field, key } => {
963 out.push('.');
964 out.push_str(&field.to_snake_case());
965 if key.chars().all(|c| c.is_ascii_digit()) {
966 out.push_str(&format!("[{key}]"));
967 } else {
968 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
969 }
970 }
971 PathSegment::Length => {
972 out.push_str(".len()");
973 }
974 }
975 }
976 out
977}
978
979fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
980 let mut out = result_var.to_string();
981 for seg in segments {
982 match seg {
983 PathSegment::Field(f) => {
984 out.push('.');
985 out.push_str(f);
986 }
987 PathSegment::ArrayField { name, index } => {
988 if language == "elixir" {
989 let current = std::mem::take(&mut out);
990 out = format!("Enum.at({current}.{name}, {index})");
991 } else {
992 out.push('.');
993 out.push_str(name);
994 out.push_str(&format!("[{index}]"));
995 }
996 }
997 PathSegment::MapAccess { field, key } => {
998 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
999 if is_numeric && language == "elixir" {
1000 let current = std::mem::take(&mut out);
1001 out = format!("Enum.at({current}.{field}, {key})");
1002 } else {
1003 out.push('.');
1004 out.push_str(field);
1005 if is_numeric {
1006 let idx: usize = key.parse().unwrap_or(0);
1007 out.push_str(&format!("[{idx}]"));
1008 } else if language == "elixir" || language == "ruby" {
1009 out.push_str(&format!("[\"{key}\"]"));
1012 } else {
1013 out.push_str(&format!(".get(\"{key}\")"));
1014 }
1015 }
1016 }
1017 PathSegment::Length => match language {
1018 "ruby" => out.push_str(".length"),
1019 "elixir" => {
1020 let current = std::mem::take(&mut out);
1021 out = format!("length({current})");
1022 }
1023 "gleam" => {
1024 let current = std::mem::take(&mut out);
1025 out = format!("list.length({current})");
1026 }
1027 _ => {
1028 let current = std::mem::take(&mut out);
1029 out = format!("len({current})");
1030 }
1031 },
1032 }
1033 }
1034 out
1035}
1036
1037fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
1038 let mut out = result_var.to_string();
1039 for seg in segments {
1040 match seg {
1041 PathSegment::Field(f) => {
1042 out.push('.');
1043 out.push_str(&f.to_lower_camel_case());
1044 }
1045 PathSegment::ArrayField { name, index } => {
1046 out.push('.');
1047 out.push_str(&name.to_lower_camel_case());
1048 out.push_str(&format!("[{index}]"));
1049 }
1050 PathSegment::MapAccess { field, key } => {
1051 out.push('.');
1052 out.push_str(&field.to_lower_camel_case());
1053 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
1056 out.push_str(&format!("[{key}]"));
1057 } else {
1058 out.push_str(&format!("[\"{key}\"]"));
1059 }
1060 }
1061 PathSegment::Length => {
1062 out.push_str(".length");
1063 }
1064 }
1065 }
1066 out
1067}
1068
1069fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
1070 let mut out = result_var.to_string();
1071 for seg in segments {
1072 match seg {
1073 PathSegment::Field(f) => {
1074 out.push('.');
1075 out.push_str(&f.to_lower_camel_case());
1076 }
1077 PathSegment::ArrayField { name, index } => {
1078 out.push('.');
1079 out.push_str(&name.to_lower_camel_case());
1080 out.push_str(&format!("[{index}]"));
1081 }
1082 PathSegment::MapAccess { field, key } => {
1083 out.push('.');
1084 out.push_str(&field.to_lower_camel_case());
1085 out.push_str(&format!(".get(\"{key}\")"));
1086 }
1087 PathSegment::Length => {
1088 out.push_str(".length");
1089 }
1090 }
1091 }
1092 out
1093}
1094
1095fn render_go(segments: &[PathSegment], result_var: &str) -> String {
1096 let mut out = result_var.to_string();
1097 for seg in segments {
1098 match seg {
1099 PathSegment::Field(f) => {
1100 out.push('.');
1101 out.push_str(&to_go_name(f));
1102 }
1103 PathSegment::ArrayField { name, index } => {
1104 out.push('.');
1105 out.push_str(&to_go_name(name));
1106 out.push_str(&format!("[{index}]"));
1107 }
1108 PathSegment::MapAccess { field, key } => {
1109 out.push('.');
1110 out.push_str(&to_go_name(field));
1111 if key.chars().all(|c| c.is_ascii_digit()) {
1112 out.push_str(&format!("[{key}]"));
1113 } else {
1114 out.push_str(&format!("[\"{key}\"]"));
1115 }
1116 }
1117 PathSegment::Length => {
1118 let current = std::mem::take(&mut out);
1119 out = format!("len({current})");
1120 }
1121 }
1122 }
1123 out
1124}
1125
1126fn render_java(segments: &[PathSegment], result_var: &str) -> String {
1127 let mut out = result_var.to_string();
1128 for seg in segments {
1129 match seg {
1130 PathSegment::Field(f) => {
1131 out.push('.');
1132 out.push_str(&f.to_lower_camel_case());
1133 out.push_str("()");
1134 }
1135 PathSegment::ArrayField { name, index } => {
1136 out.push('.');
1137 out.push_str(&name.to_lower_camel_case());
1138 out.push_str(&format!("().get({index})"));
1139 }
1140 PathSegment::MapAccess { field, key } => {
1141 out.push('.');
1142 out.push_str(&field.to_lower_camel_case());
1143 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1145 if is_numeric {
1146 out.push_str(&format!("().get({key})"));
1147 } else {
1148 out.push_str(&format!("().get(\"{key}\")"));
1149 }
1150 }
1151 PathSegment::Length => {
1152 out.push_str(".size()");
1153 }
1154 }
1155 }
1156 out
1157}
1158
1159fn kotlin_getter(name: &str) -> String {
1164 let camel = name.to_lower_camel_case();
1165 match camel.as_str() {
1166 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
1167 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
1168 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
1169 _ => camel,
1170 }
1171}
1172
1173fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
1174 let mut out = result_var.to_string();
1175 for seg in segments {
1176 match seg {
1177 PathSegment::Field(f) => {
1178 out.push('.');
1179 out.push_str(&kotlin_getter(f));
1180 out.push_str("()");
1181 }
1182 PathSegment::ArrayField { name, index } => {
1183 out.push('.');
1184 out.push_str(&kotlin_getter(name));
1185 if *index == 0 {
1186 out.push_str("().first()");
1187 } else {
1188 out.push_str(&format!("().get({index})"));
1189 }
1190 }
1191 PathSegment::MapAccess { field, key } => {
1192 out.push('.');
1193 out.push_str(&kotlin_getter(field));
1194 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1195 if is_numeric {
1196 out.push_str(&format!("().get({key})"));
1197 } else {
1198 out.push_str(&format!("().get(\"{key}\")"));
1199 }
1200 }
1201 PathSegment::Length => {
1202 out.push_str(".size");
1203 }
1204 }
1205 }
1206 out
1207}
1208
1209fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1210 let mut out = result_var.to_string();
1211 let mut path_so_far = String::new();
1212 for (i, seg) in segments.iter().enumerate() {
1213 let is_leaf = i == segments.len() - 1;
1214 match seg {
1215 PathSegment::Field(f) => {
1216 if !path_so_far.is_empty() {
1217 path_so_far.push('.');
1218 }
1219 path_so_far.push_str(f);
1220 out.push('.');
1221 out.push_str(&f.to_lower_camel_case());
1222 out.push_str("()");
1223 let _ = is_leaf;
1224 let _ = optional_fields;
1225 }
1226 PathSegment::ArrayField { name, index } => {
1227 if !path_so_far.is_empty() {
1228 path_so_far.push('.');
1229 }
1230 path_so_far.push_str(name);
1231 out.push('.');
1232 out.push_str(&name.to_lower_camel_case());
1233 out.push_str(&format!("().get({index})"));
1234 }
1235 PathSegment::MapAccess { field, key } => {
1236 if !path_so_far.is_empty() {
1237 path_so_far.push('.');
1238 }
1239 path_so_far.push_str(field);
1240 out.push('.');
1241 out.push_str(&field.to_lower_camel_case());
1242 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1244 if is_numeric {
1245 out.push_str(&format!("().get({key})"));
1246 } else {
1247 out.push_str(&format!("().get(\"{key}\")"));
1248 }
1249 }
1250 PathSegment::Length => {
1251 out.push_str(".size()");
1252 }
1253 }
1254 }
1255 out
1256}
1257
1258fn render_kotlin_with_optionals(
1273 segments: &[PathSegment],
1274 result_var: &str,
1275 optional_fields: &HashSet<String>,
1276) -> String {
1277 let mut out = result_var.to_string();
1278 let mut path_so_far = String::new();
1279 let mut prev_was_nullable = false;
1287 for seg in segments {
1288 let nav = if prev_was_nullable { "?." } else { "." };
1289 match seg {
1290 PathSegment::Field(f) => {
1291 if !path_so_far.is_empty() {
1292 path_so_far.push('.');
1293 }
1294 path_so_far.push_str(f);
1295 let is_optional = optional_fields.contains(&path_so_far);
1300 out.push_str(nav);
1301 out.push_str(&kotlin_getter(f));
1302 out.push_str("()");
1303 prev_was_nullable = prev_was_nullable || is_optional;
1304 }
1305 PathSegment::ArrayField { name, index } => {
1306 if !path_so_far.is_empty() {
1307 path_so_far.push('.');
1308 }
1309 path_so_far.push_str(name);
1310 let is_optional = optional_fields.contains(&path_so_far);
1311 out.push_str(nav);
1312 out.push_str(&kotlin_getter(name));
1313 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1314 if *index == 0 {
1315 out.push_str(&format!("(){safe}.first()"));
1316 } else {
1317 out.push_str(&format!("(){safe}.get({index})"));
1318 }
1319 path_so_far.push_str("[0]");
1323 prev_was_nullable = prev_was_nullable || is_optional;
1324 }
1325 PathSegment::MapAccess { field, key } => {
1326 if !path_so_far.is_empty() {
1327 path_so_far.push('.');
1328 }
1329 path_so_far.push_str(field);
1330 let is_optional = optional_fields.contains(&path_so_far);
1331 out.push_str(nav);
1332 out.push_str(&kotlin_getter(field));
1333 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1334 if is_numeric {
1335 if prev_was_nullable || is_optional {
1336 out.push_str(&format!("()?.get({key})"));
1337 } else {
1338 out.push_str(&format!("().get({key})"));
1339 }
1340 } else if prev_was_nullable || is_optional {
1341 out.push_str(&format!("()?.get(\"{key}\")"));
1342 } else {
1343 out.push_str(&format!("().get(\"{key}\")"));
1344 }
1345 prev_was_nullable = prev_was_nullable || is_optional;
1346 }
1347 PathSegment::Length => {
1348 let size_nav = if prev_was_nullable { "?" } else { "" };
1351 out.push_str(&format!("{size_nav}.size"));
1352 prev_was_nullable = false;
1353 }
1354 }
1355 }
1356 out
1357}
1358
1359fn render_kotlin_android_with_optionals(
1370 segments: &[PathSegment],
1371 result_var: &str,
1372 optional_fields: &HashSet<String>,
1373) -> String {
1374 let mut out = result_var.to_string();
1375 let mut path_so_far = String::new();
1376 let mut prev_was_nullable = false;
1377 for seg in segments {
1378 let nav = if prev_was_nullable { "?." } else { "." };
1379 match seg {
1380 PathSegment::Field(f) => {
1381 if !path_so_far.is_empty() {
1382 path_so_far.push('.');
1383 }
1384 path_so_far.push_str(f);
1385 let is_optional = optional_fields.contains(&path_so_far);
1386 out.push_str(nav);
1387 out.push_str(&kotlin_getter(f));
1389 prev_was_nullable = prev_was_nullable || is_optional;
1390 }
1391 PathSegment::ArrayField { name, index } => {
1392 if !path_so_far.is_empty() {
1393 path_so_far.push('.');
1394 }
1395 path_so_far.push_str(name);
1396 let is_optional = optional_fields.contains(&path_so_far);
1397 out.push_str(nav);
1398 out.push_str(&kotlin_getter(name));
1400 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1401 if *index == 0 {
1402 out.push_str(&format!("{safe}.first()"));
1403 } else {
1404 out.push_str(&format!("{safe}.get({index})"));
1405 }
1406 path_so_far.push_str("[0]");
1407 prev_was_nullable = prev_was_nullable || is_optional;
1408 }
1409 PathSegment::MapAccess { field, key } => {
1410 if !path_so_far.is_empty() {
1411 path_so_far.push('.');
1412 }
1413 path_so_far.push_str(field);
1414 let is_optional = optional_fields.contains(&path_so_far);
1415 out.push_str(nav);
1416 out.push_str(&kotlin_getter(field));
1418 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1419 if is_numeric {
1420 if prev_was_nullable || is_optional {
1421 out.push_str(&format!("?.get({key})"));
1422 } else {
1423 out.push_str(&format!(".get({key})"));
1424 }
1425 } else if prev_was_nullable || is_optional {
1426 out.push_str(&format!("?.get(\"{key}\")"));
1427 } else {
1428 out.push_str(&format!(".get(\"{key}\")"));
1429 }
1430 prev_was_nullable = prev_was_nullable || is_optional;
1431 }
1432 PathSegment::Length => {
1433 let size_nav = if prev_was_nullable { "?" } else { "" };
1434 out.push_str(&format!("{size_nav}.size"));
1435 prev_was_nullable = false;
1436 }
1437 }
1438 }
1439 out
1440}
1441
1442fn render_kotlin_android(segments: &[PathSegment], result_var: &str) -> String {
1446 let mut out = result_var.to_string();
1447 for seg in segments {
1448 match seg {
1449 PathSegment::Field(f) => {
1450 out.push('.');
1451 out.push_str(&kotlin_getter(f));
1452 }
1454 PathSegment::ArrayField { name, index } => {
1455 out.push('.');
1456 out.push_str(&kotlin_getter(name));
1457 if *index == 0 {
1458 out.push_str(".first()");
1459 } else {
1460 out.push_str(&format!(".get({index})"));
1461 }
1462 }
1463 PathSegment::MapAccess { field, key } => {
1464 out.push('.');
1465 out.push_str(&kotlin_getter(field));
1466 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1467 if is_numeric {
1468 out.push_str(&format!(".get({key})"));
1469 } else {
1470 out.push_str(&format!(".get(\"{key}\")"));
1471 }
1472 }
1473 PathSegment::Length => {
1474 out.push_str(".size");
1475 }
1476 }
1477 }
1478 out
1479}
1480
1481fn render_rust_with_optionals(
1487 segments: &[PathSegment],
1488 result_var: &str,
1489 optional_fields: &HashSet<String>,
1490 method_calls: &HashSet<String>,
1491) -> String {
1492 let mut out = result_var.to_string();
1493 let mut path_so_far = String::new();
1494 for (i, seg) in segments.iter().enumerate() {
1495 let is_leaf = i == segments.len() - 1;
1496 match seg {
1497 PathSegment::Field(f) => {
1498 if !path_so_far.is_empty() {
1499 path_so_far.push('.');
1500 }
1501 path_so_far.push_str(f);
1502 out.push('.');
1503 out.push_str(&f.to_snake_case());
1504 let is_method = method_calls.contains(&path_so_far);
1505 if is_method {
1506 out.push_str("()");
1507 if !is_leaf && optional_fields.contains(&path_so_far) {
1508 out.push_str(".as_ref().unwrap()");
1509 }
1510 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1511 out.push_str(".as_ref().unwrap()");
1512 }
1513 }
1514 PathSegment::ArrayField { name, index } => {
1515 if !path_so_far.is_empty() {
1516 path_so_far.push('.');
1517 }
1518 path_so_far.push_str(name);
1519 out.push('.');
1520 out.push_str(&name.to_snake_case());
1521 let path_with_idx = format!("{path_so_far}[0]");
1525 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1526 if is_opt {
1527 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1528 } else {
1529 out.push_str(&format!("[{index}]"));
1530 }
1531 path_so_far.push_str("[0]");
1536 }
1537 PathSegment::MapAccess { field, key } => {
1538 if !path_so_far.is_empty() {
1539 path_so_far.push('.');
1540 }
1541 path_so_far.push_str(field);
1542 out.push('.');
1543 out.push_str(&field.to_snake_case());
1544 if key.chars().all(|c| c.is_ascii_digit()) {
1545 let path_with_idx = format!("{path_so_far}[0]");
1547 let is_opt =
1548 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1549 if is_opt {
1550 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1551 } else {
1552 out.push_str(&format!("[{key}]"));
1553 }
1554 path_so_far.push_str("[0]");
1555 } else {
1556 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1557 }
1558 }
1559 PathSegment::Length => {
1560 out.push_str(".len()");
1561 }
1562 }
1563 }
1564 out
1565}
1566
1567fn render_zig_with_optionals(
1580 segments: &[PathSegment],
1581 result_var: &str,
1582 optional_fields: &HashSet<String>,
1583 method_calls: &HashSet<String>,
1584) -> String {
1585 let mut out = result_var.to_string();
1586 let mut path_so_far = String::new();
1587 for seg in segments {
1588 match seg {
1589 PathSegment::Field(f) => {
1590 if !path_so_far.is_empty() {
1591 path_so_far.push('.');
1592 }
1593 path_so_far.push_str(f);
1594 out.push('.');
1595 out.push_str(f);
1596 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1597 out.push_str(".?");
1598 }
1599 }
1600 PathSegment::ArrayField { name, index } => {
1601 if !path_so_far.is_empty() {
1602 path_so_far.push('.');
1603 }
1604 path_so_far.push_str(name);
1605 out.push('.');
1606 out.push_str(name);
1607 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1608 out.push_str(".?");
1609 }
1610 out.push_str(&format!("[{index}]"));
1611 }
1612 PathSegment::MapAccess { field, key } => {
1613 if !path_so_far.is_empty() {
1614 path_so_far.push('.');
1615 }
1616 path_so_far.push_str(field);
1617 out.push('.');
1618 out.push_str(field);
1619 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1620 out.push_str(".?");
1621 }
1622 if key.chars().all(|c| c.is_ascii_digit()) {
1623 out.push_str(&format!("[{key}]"));
1624 } else {
1625 out.push_str(&format!(".get(\"{key}\")"));
1626 }
1627 }
1628 PathSegment::Length => {
1629 out.push_str(".len");
1630 }
1631 }
1632 }
1633 out
1634}
1635
1636fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1637 let mut out = result_var.to_string();
1638 for seg in segments {
1639 match seg {
1640 PathSegment::Field(f) => {
1641 out.push('.');
1642 out.push_str(&f.to_pascal_case());
1643 }
1644 PathSegment::ArrayField { name, index } => {
1645 out.push('.');
1646 out.push_str(&name.to_pascal_case());
1647 out.push_str(&format!("[{index}]"));
1648 }
1649 PathSegment::MapAccess { field, key } => {
1650 out.push('.');
1651 out.push_str(&field.to_pascal_case());
1652 if key.chars().all(|c| c.is_ascii_digit()) {
1653 out.push_str(&format!("[{key}]"));
1654 } else {
1655 out.push_str(&format!("[\"{key}\"]"));
1656 }
1657 }
1658 PathSegment::Length => {
1659 out.push_str(".Count");
1660 }
1661 }
1662 }
1663 out
1664}
1665
1666fn render_csharp_with_optionals(
1667 segments: &[PathSegment],
1668 result_var: &str,
1669 optional_fields: &HashSet<String>,
1670) -> String {
1671 let mut out = result_var.to_string();
1672 let mut path_so_far = String::new();
1673 for (i, seg) in segments.iter().enumerate() {
1674 let is_leaf = i == segments.len() - 1;
1675 match seg {
1676 PathSegment::Field(f) => {
1677 if !path_so_far.is_empty() {
1678 path_so_far.push('.');
1679 }
1680 path_so_far.push_str(f);
1681 out.push('.');
1682 out.push_str(&f.to_pascal_case());
1683 if !is_leaf && optional_fields.contains(&path_so_far) {
1684 out.push('!');
1685 }
1686 }
1687 PathSegment::ArrayField { name, index } => {
1688 if !path_so_far.is_empty() {
1689 path_so_far.push('.');
1690 }
1691 path_so_far.push_str(name);
1692 out.push('.');
1693 out.push_str(&name.to_pascal_case());
1694 out.push_str(&format!("[{index}]"));
1695 }
1696 PathSegment::MapAccess { field, key } => {
1697 if !path_so_far.is_empty() {
1698 path_so_far.push('.');
1699 }
1700 path_so_far.push_str(field);
1701 out.push('.');
1702 out.push_str(&field.to_pascal_case());
1703 if key.chars().all(|c| c.is_ascii_digit()) {
1704 out.push_str(&format!("[{key}]"));
1705 } else {
1706 out.push_str(&format!("[\"{key}\"]"));
1707 }
1708 }
1709 PathSegment::Length => {
1710 out.push_str(".Count");
1711 }
1712 }
1713 }
1714 out
1715}
1716
1717fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1718 let mut out = result_var.to_string();
1719 for seg in segments {
1720 match seg {
1721 PathSegment::Field(f) => {
1722 out.push_str("->");
1723 out.push_str(&f.to_lower_camel_case());
1726 }
1727 PathSegment::ArrayField { name, index } => {
1728 out.push_str("->");
1729 out.push_str(&name.to_lower_camel_case());
1730 out.push_str(&format!("[{index}]"));
1731 }
1732 PathSegment::MapAccess { field, key } => {
1733 out.push_str("->");
1734 out.push_str(&field.to_lower_camel_case());
1735 out.push_str(&format!("[\"{key}\"]"));
1736 }
1737 PathSegment::Length => {
1738 let current = std::mem::take(&mut out);
1739 out = format!("count({current})");
1740 }
1741 }
1742 }
1743 out
1744}
1745
1746fn render_php_with_getters(segments: &[PathSegment], result_var: &str, getter_map: &PhpGetterMap) -> String {
1764 let mut out = result_var.to_string();
1765 let mut current_type: Option<String> = getter_map.root_type.clone();
1766 for seg in segments {
1767 match seg {
1768 PathSegment::Field(f) => {
1769 let camel = f.to_lower_camel_case();
1770 if getter_map.needs_getter(current_type.as_deref(), f.as_str()) {
1771 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1776 out.push_str("->");
1777 out.push_str(&getter);
1778 out.push_str("()");
1779 } else {
1780 out.push_str("->");
1781 out.push_str(&camel);
1782 }
1783 current_type = getter_map.advance(current_type.as_deref(), f.as_str());
1784 }
1785 PathSegment::ArrayField { name, index } => {
1786 let camel = name.to_lower_camel_case();
1787 if getter_map.needs_getter(current_type.as_deref(), name.as_str()) {
1788 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1789 out.push_str("->");
1790 out.push_str(&getter);
1791 out.push_str("()");
1792 } else {
1793 out.push_str("->");
1794 out.push_str(&camel);
1795 }
1796 out.push_str(&format!("[{index}]"));
1797 current_type = getter_map.advance(current_type.as_deref(), name.as_str());
1798 }
1799 PathSegment::MapAccess { field, key } => {
1800 let camel = field.to_lower_camel_case();
1801 if getter_map.needs_getter(current_type.as_deref(), field.as_str()) {
1802 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1803 out.push_str("->");
1804 out.push_str(&getter);
1805 out.push_str("()");
1806 } else {
1807 out.push_str("->");
1808 out.push_str(&camel);
1809 }
1810 out.push_str(&format!("[\"{key}\"]"));
1811 current_type = getter_map.advance(current_type.as_deref(), field.as_str());
1812 }
1813 PathSegment::Length => {
1814 let current = std::mem::take(&mut out);
1815 out = format!("count({current})");
1816 }
1817 }
1818 }
1819 out
1820}
1821
1822fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1823 let mut out = result_var.to_string();
1824 for seg in segments {
1825 match seg {
1826 PathSegment::Field(f) => {
1827 out.push('$');
1828 out.push_str(f);
1829 }
1830 PathSegment::ArrayField { name, index } => {
1831 out.push('$');
1832 out.push_str(name);
1833 out.push_str(&format!("[[{}]]", index + 1));
1835 }
1836 PathSegment::MapAccess { field, key } => {
1837 out.push('$');
1838 out.push_str(field);
1839 out.push_str(&format!("[[\"{key}\"]]"));
1840 }
1841 PathSegment::Length => {
1842 let current = std::mem::take(&mut out);
1843 out = format!("length({current})");
1844 }
1845 }
1846 }
1847 out
1848}
1849
1850fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1851 let mut parts = Vec::new();
1852 let mut trailing_length = false;
1853 for seg in segments {
1854 match seg {
1855 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1856 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1857 PathSegment::MapAccess { field, key } => {
1858 parts.push(field.to_snake_case());
1859 parts.push(key.clone());
1860 }
1861 PathSegment::Length => {
1862 trailing_length = true;
1863 }
1864 }
1865 }
1866 let suffix = parts.join("_");
1867 if trailing_length {
1868 format!("result_{suffix}_count({result_var})")
1869 } else {
1870 format!("result_{suffix}({result_var})")
1871 }
1872}
1873
1874fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
1881 let mut out = result_var.to_string();
1882 for seg in segments {
1883 match seg {
1884 PathSegment::Field(f) => {
1885 out.push('.');
1886 out.push_str(&f.to_lower_camel_case());
1887 }
1888 PathSegment::ArrayField { name, index } => {
1889 out.push('.');
1890 out.push_str(&name.to_lower_camel_case());
1891 out.push_str(&format!("[{index}]"));
1892 }
1893 PathSegment::MapAccess { field, key } => {
1894 out.push('.');
1895 out.push_str(&field.to_lower_camel_case());
1896 if key.chars().all(|c| c.is_ascii_digit()) {
1897 out.push_str(&format!("[{key}]"));
1898 } else {
1899 out.push_str(&format!("[\"{key}\"]"));
1900 }
1901 }
1902 PathSegment::Length => {
1903 out.push_str(".length");
1904 }
1905 }
1906 }
1907 out
1908}
1909
1910fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1916 let mut out = result_var.to_string();
1917 let mut path_so_far = String::new();
1918 let mut prev_was_nullable = false;
1919 for seg in segments {
1920 let nav = if prev_was_nullable { "?." } else { "." };
1921 match seg {
1922 PathSegment::Field(f) => {
1923 if !path_so_far.is_empty() {
1924 path_so_far.push('.');
1925 }
1926 path_so_far.push_str(f);
1927 let is_optional = optional_fields.contains(&path_so_far);
1928 out.push_str(nav);
1929 out.push_str(&f.to_lower_camel_case());
1930 prev_was_nullable = is_optional;
1931 }
1932 PathSegment::ArrayField { name, index } => {
1933 if !path_so_far.is_empty() {
1934 path_so_far.push('.');
1935 }
1936 path_so_far.push_str(name);
1937 let is_optional = optional_fields.contains(&path_so_far);
1938 out.push_str(nav);
1939 out.push_str(&name.to_lower_camel_case());
1940 if is_optional {
1944 out.push('!');
1945 }
1946 out.push_str(&format!("[{index}]"));
1947 prev_was_nullable = false;
1948 }
1949 PathSegment::MapAccess { field, key } => {
1950 if !path_so_far.is_empty() {
1951 path_so_far.push('.');
1952 }
1953 path_so_far.push_str(field);
1954 let is_optional = optional_fields.contains(&path_so_far);
1955 out.push_str(nav);
1956 out.push_str(&field.to_lower_camel_case());
1957 if key.chars().all(|c| c.is_ascii_digit()) {
1958 out.push_str(&format!("[{key}]"));
1959 } else {
1960 out.push_str(&format!("[\"{key}\"]"));
1961 }
1962 prev_was_nullable = is_optional;
1963 }
1964 PathSegment::Length => {
1965 out.push_str(nav);
1968 out.push_str("length");
1969 prev_was_nullable = false;
1970 }
1971 }
1972 }
1973 out
1974}
1975
1976#[cfg(test)]
1977mod tests {
1978 use super::*;
1979
1980 fn make_resolver() -> FieldResolver {
1981 let mut fields = HashMap::new();
1982 fields.insert("title".to_string(), "metadata.document.title".to_string());
1983 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1984 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
1985 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
1986 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
1987 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
1988 let mut optional = HashSet::new();
1989 optional.insert("metadata.document.title".to_string());
1990 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1991 }
1992
1993 fn make_resolver_with_doc_optional() -> FieldResolver {
1994 let mut fields = HashMap::new();
1995 fields.insert("title".to_string(), "metadata.document.title".to_string());
1996 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1997 let mut optional = HashSet::new();
1998 optional.insert("document".to_string());
1999 optional.insert("metadata.document.title".to_string());
2000 optional.insert("metadata.document".to_string());
2001 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
2002 }
2003
2004 #[test]
2005 fn test_resolve_alias() {
2006 let r = make_resolver();
2007 assert_eq!(r.resolve("title"), "metadata.document.title");
2008 }
2009
2010 #[test]
2011 fn test_resolve_passthrough() {
2012 let r = make_resolver();
2013 assert_eq!(r.resolve("content"), "content");
2014 }
2015
2016 #[test]
2017 fn test_is_optional() {
2018 let r = make_resolver();
2019 assert!(r.is_optional("metadata.document.title"));
2020 assert!(!r.is_optional("content"));
2021 }
2022
2023 #[test]
2024 fn test_accessor_rust_struct() {
2025 let r = make_resolver();
2026 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
2027 }
2028
2029 #[test]
2030 fn test_accessor_rust_map() {
2031 let r = make_resolver();
2032 assert_eq!(
2033 r.accessor("tags", "rust", "result"),
2034 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
2035 );
2036 }
2037
2038 #[test]
2039 fn test_accessor_python() {
2040 let r = make_resolver();
2041 assert_eq!(
2042 r.accessor("title", "python", "result"),
2043 "result.metadata.document.title"
2044 );
2045 }
2046
2047 #[test]
2048 fn test_accessor_go() {
2049 let r = make_resolver();
2050 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
2051 }
2052
2053 #[test]
2054 fn test_accessor_go_initialism_fields() {
2055 let mut fields = std::collections::HashMap::new();
2056 fields.insert("content".to_string(), "html".to_string());
2057 fields.insert("link_url".to_string(), "links.url".to_string());
2058 let r = FieldResolver::new(
2059 &fields,
2060 &HashSet::new(),
2061 &HashSet::new(),
2062 &HashSet::new(),
2063 &HashSet::new(),
2064 );
2065 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
2066 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
2067 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
2068 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
2069 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
2070 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
2071 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
2072 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
2073 }
2074
2075 #[test]
2076 fn test_accessor_typescript() {
2077 let r = make_resolver();
2078 assert_eq!(
2079 r.accessor("title", "typescript", "result"),
2080 "result.metadata.document.title"
2081 );
2082 }
2083
2084 #[test]
2085 fn test_accessor_typescript_snake_to_camel() {
2086 let r = make_resolver();
2087 assert_eq!(
2088 r.accessor("og", "typescript", "result"),
2089 "result.metadata.document.openGraph"
2090 );
2091 assert_eq!(
2092 r.accessor("twitter", "typescript", "result"),
2093 "result.metadata.document.twitterCard"
2094 );
2095 assert_eq!(
2096 r.accessor("canonical", "typescript", "result"),
2097 "result.metadata.document.canonicalUrl"
2098 );
2099 }
2100
2101 #[test]
2102 fn test_accessor_typescript_map_snake_to_camel() {
2103 let r = make_resolver();
2104 assert_eq!(
2105 r.accessor("og_tag", "typescript", "result"),
2106 "result.metadata.openGraphTags[\"og_title\"]"
2107 );
2108 }
2109
2110 #[test]
2111 fn test_accessor_typescript_numeric_index_is_unquoted() {
2112 let mut fields = HashMap::new();
2116 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
2117 let r = FieldResolver::new(
2118 &fields,
2119 &HashSet::new(),
2120 &HashSet::new(),
2121 &HashSet::new(),
2122 &HashSet::new(),
2123 );
2124 assert_eq!(
2125 r.accessor("first_score", "typescript", "result"),
2126 "result.results[0].relevanceScore"
2127 );
2128 }
2129
2130 #[test]
2131 fn test_accessor_node_alias() {
2132 let r = make_resolver();
2133 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
2134 }
2135
2136 #[test]
2137 fn test_accessor_wasm_camel_case() {
2138 let r = make_resolver();
2139 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
2140 assert_eq!(
2141 r.accessor("twitter", "wasm", "result"),
2142 "result.metadata.document.twitterCard"
2143 );
2144 assert_eq!(
2145 r.accessor("canonical", "wasm", "result"),
2146 "result.metadata.document.canonicalUrl"
2147 );
2148 }
2149
2150 #[test]
2151 fn test_accessor_wasm_map_access() {
2152 let r = make_resolver();
2153 assert_eq!(
2154 r.accessor("og_tag", "wasm", "result"),
2155 "result.metadata.openGraphTags.get(\"og_title\")"
2156 );
2157 }
2158
2159 #[test]
2160 fn test_accessor_java() {
2161 let r = make_resolver();
2162 assert_eq!(
2163 r.accessor("title", "java", "result"),
2164 "result.metadata().document().title()"
2165 );
2166 }
2167
2168 #[test]
2169 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
2170 let mut fields = HashMap::new();
2171 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2172 fields.insert("node_count".to_string(), "nodes.length".to_string());
2173 let mut arrays = HashSet::new();
2174 arrays.insert("nodes".to_string());
2175 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
2176 assert_eq!(
2177 r.accessor("first_node_name", "kotlin", "result"),
2178 "result.nodes().first().name()"
2179 );
2180 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
2181 }
2182
2183 #[test]
2184 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
2185 let r = make_resolver_with_doc_optional();
2186 assert_eq!(
2187 r.accessor("title", "kotlin", "result"),
2188 "result.metadata().document()?.title()"
2189 );
2190 }
2191
2192 #[test]
2193 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
2194 let mut fields = HashMap::new();
2195 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2196 fields.insert("tag".to_string(), "tags[name]".to_string());
2197 let mut optional = HashSet::new();
2198 optional.insert("nodes".to_string());
2199 optional.insert("tags".to_string());
2200 let mut arrays = HashSet::new();
2201 arrays.insert("nodes".to_string());
2202 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2203 assert_eq!(
2204 r.accessor("first_node_name", "kotlin", "result"),
2205 "result.nodes()?.first()?.name()"
2206 );
2207 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
2208 }
2209
2210 #[test]
2216 fn test_accessor_kotlin_optional_field_after_indexed_array() {
2217 let mut fields = HashMap::new();
2220 fields.insert(
2221 "tool_call_name".to_string(),
2222 "choices[0].message.tool_calls[0].function.name".to_string(),
2223 );
2224 let mut optional = HashSet::new();
2225 optional.insert("choices[0].message.tool_calls".to_string());
2226 let mut arrays = HashSet::new();
2227 arrays.insert("choices".to_string());
2228 arrays.insert("choices[0].message.tool_calls".to_string());
2229 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2230 let expr = r.accessor("tool_call_name", "kotlin", "result");
2231 assert!(
2233 expr.contains("toolCalls()?.first()"),
2234 "expected toolCalls()?.first() for optional list, got: {expr}"
2235 );
2236 }
2237
2238 #[test]
2239 fn test_accessor_csharp() {
2240 let r = make_resolver();
2241 assert_eq!(
2242 r.accessor("title", "csharp", "result"),
2243 "result.Metadata.Document.Title"
2244 );
2245 }
2246
2247 #[test]
2248 fn test_accessor_php() {
2249 let r = make_resolver();
2250 assert_eq!(
2251 r.accessor("title", "php", "$result"),
2252 "$result->metadata->document->title"
2253 );
2254 }
2255
2256 #[test]
2257 fn test_accessor_r() {
2258 let r = make_resolver();
2259 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
2260 }
2261
2262 #[test]
2263 fn test_accessor_c() {
2264 let r = make_resolver();
2265 assert_eq!(
2266 r.accessor("title", "c", "result"),
2267 "result_metadata_document_title(result)"
2268 );
2269 }
2270
2271 #[test]
2272 fn test_rust_unwrap_binding() {
2273 let r = make_resolver();
2274 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
2275 assert_eq!(var, "metadata_document_title");
2276 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
2277 }
2278
2279 #[test]
2280 fn test_rust_unwrap_binding_non_optional() {
2281 let r = make_resolver();
2282 assert!(r.rust_unwrap_binding("content", "result").is_none());
2283 }
2284
2285 #[test]
2286 fn test_rust_unwrap_binding_collapses_double_underscore() {
2287 let mut aliases = HashMap::new();
2292 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
2293 let mut optional = HashSet::new();
2294 optional.insert("json_ld[].name".to_string());
2295 let mut array = HashSet::new();
2296 array.insert("json_ld".to_string());
2297 let result_fields = HashSet::new();
2298 let method_calls = HashSet::new();
2299 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
2300 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
2301 assert_eq!(var, "json_ld_name");
2302 }
2303
2304 #[test]
2305 fn test_direct_field_no_alias() {
2306 let r = make_resolver();
2307 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2308 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
2309 }
2310
2311 #[test]
2312 fn test_accessor_rust_with_optionals() {
2313 let r = make_resolver_with_doc_optional();
2314 assert_eq!(
2315 r.accessor("title", "rust", "result"),
2316 "result.metadata.document.as_ref().unwrap().title"
2317 );
2318 }
2319
2320 #[test]
2321 fn test_accessor_csharp_with_optionals() {
2322 let r = make_resolver_with_doc_optional();
2323 assert_eq!(
2324 r.accessor("title", "csharp", "result"),
2325 "result.Metadata.Document!.Title"
2326 );
2327 }
2328
2329 #[test]
2330 fn test_accessor_rust_non_optional_field() {
2331 let r = make_resolver();
2332 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2333 }
2334
2335 #[test]
2336 fn test_accessor_csharp_non_optional_field() {
2337 let r = make_resolver();
2338 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
2339 }
2340
2341 #[test]
2342 fn test_accessor_rust_method_call() {
2343 let mut fields = HashMap::new();
2345 fields.insert(
2346 "excel_sheet_count".to_string(),
2347 "metadata.format.excel.sheet_count".to_string(),
2348 );
2349 let mut optional = HashSet::new();
2350 optional.insert("metadata.format".to_string());
2351 optional.insert("metadata.format.excel".to_string());
2352 let mut method_calls = HashSet::new();
2353 method_calls.insert("metadata.format.excel".to_string());
2354 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
2355 assert_eq!(
2356 r.accessor("excel_sheet_count", "rust", "result"),
2357 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
2358 );
2359 }
2360
2361 fn make_php_getter_resolver() -> FieldResolver {
2366 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2367 getters.insert(
2368 "Root".to_string(),
2369 ["metadata".to_string(), "links".to_string()].into_iter().collect(),
2370 );
2371 let map = PhpGetterMap {
2372 getters,
2373 field_types: HashMap::new(),
2374 root_type: Some("Root".to_string()),
2375 all_fields: HashMap::new(),
2376 };
2377 FieldResolver::new_with_php_getters(
2378 &HashMap::new(),
2379 &HashSet::new(),
2380 &HashSet::new(),
2381 &HashSet::new(),
2382 &HashSet::new(),
2383 &HashMap::new(),
2384 map,
2385 )
2386 }
2387
2388 #[test]
2389 fn render_php_uses_getter_method_for_non_scalar_field() {
2390 let r = make_php_getter_resolver();
2391 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
2392 }
2393
2394 #[test]
2395 fn render_php_uses_property_for_scalar_field() {
2396 let r = make_php_getter_resolver();
2397 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2398 }
2399
2400 #[test]
2401 fn render_php_nested_non_scalar_uses_getter_then_property() {
2402 let mut fields = HashMap::new();
2403 fields.insert("title".to_string(), "metadata.title".to_string());
2404 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2405 getters.insert("Root".to_string(), ["metadata".to_string()].into_iter().collect());
2406 getters.insert("Metadata".to_string(), HashSet::new());
2408 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2409 field_types.insert(
2410 "Root".to_string(),
2411 [("metadata".to_string(), "Metadata".to_string())].into_iter().collect(),
2412 );
2413 let map = PhpGetterMap {
2414 getters,
2415 field_types,
2416 root_type: Some("Root".to_string()),
2417 all_fields: HashMap::new(),
2418 };
2419 let r = FieldResolver::new_with_php_getters(
2420 &fields,
2421 &HashSet::new(),
2422 &HashSet::new(),
2423 &HashSet::new(),
2424 &HashSet::new(),
2425 &HashMap::new(),
2426 map,
2427 );
2428 assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
2430 }
2431
2432 #[test]
2433 fn render_php_array_field_uses_getter_when_non_scalar() {
2434 let mut fields = HashMap::new();
2435 fields.insert("first_link".to_string(), "links[0]".to_string());
2436 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2437 getters.insert("Root".to_string(), ["links".to_string()].into_iter().collect());
2438 let map = PhpGetterMap {
2439 getters,
2440 field_types: HashMap::new(),
2441 root_type: Some("Root".to_string()),
2442 all_fields: HashMap::new(),
2443 };
2444 let r = FieldResolver::new_with_php_getters(
2445 &fields,
2446 &HashSet::new(),
2447 &HashSet::new(),
2448 &HashSet::new(),
2449 &HashSet::new(),
2450 &HashMap::new(),
2451 map,
2452 );
2453 assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
2454 }
2455
2456 #[test]
2457 fn render_php_falls_back_to_property_when_getter_fields_empty() {
2458 let r = FieldResolver::new(
2461 &HashMap::new(),
2462 &HashSet::new(),
2463 &HashSet::new(),
2464 &HashSet::new(),
2465 &HashSet::new(),
2466 );
2467 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2468 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
2469 }
2470
2471 #[test]
2475 fn render_php_with_getters_distinguishes_same_field_name_on_different_types() {
2476 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2477 getters.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2479 getters.insert("B".to_string(), HashSet::new());
2481 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2484 all_fields.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2485 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2486 let map_a = PhpGetterMap {
2487 getters: getters.clone(),
2488 field_types: HashMap::new(),
2489 root_type: Some("A".to_string()),
2490 all_fields: all_fields.clone(),
2491 };
2492 let map_b = PhpGetterMap {
2493 getters,
2494 field_types: HashMap::new(),
2495 root_type: Some("B".to_string()),
2496 all_fields,
2497 };
2498 let r_a = FieldResolver::new_with_php_getters(
2499 &HashMap::new(),
2500 &HashSet::new(),
2501 &HashSet::new(),
2502 &HashSet::new(),
2503 &HashSet::new(),
2504 &HashMap::new(),
2505 map_a,
2506 );
2507 let r_b = FieldResolver::new_with_php_getters(
2508 &HashMap::new(),
2509 &HashSet::new(),
2510 &HashSet::new(),
2511 &HashSet::new(),
2512 &HashSet::new(),
2513 &HashMap::new(),
2514 map_b,
2515 );
2516 assert_eq!(r_a.accessor("content", "php", "$a"), "$a->getContent()");
2517 assert_eq!(r_b.accessor("content", "php", "$b"), "$b->content");
2518 }
2519
2520 #[test]
2524 fn render_php_with_getters_chains_through_correct_type() {
2525 let mut fields = HashMap::new();
2526 fields.insert("nested_content".to_string(), "inner.content".to_string());
2527 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2528 getters.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2530 getters.insert("B".to_string(), HashSet::new());
2532 getters.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2535 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2536 field_types.insert(
2537 "Outer".to_string(),
2538 [("inner".to_string(), "B".to_string())].into_iter().collect(),
2539 );
2540 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2541 all_fields.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2542 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2543 all_fields.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2544 let map = PhpGetterMap {
2545 getters,
2546 field_types,
2547 root_type: Some("Outer".to_string()),
2548 all_fields,
2549 };
2550 let r = FieldResolver::new_with_php_getters(
2551 &fields,
2552 &HashSet::new(),
2553 &HashSet::new(),
2554 &HashSet::new(),
2555 &HashSet::new(),
2556 &HashMap::new(),
2557 map,
2558 );
2559 assert_eq!(
2560 r.accessor("nested_content", "php", "$result"),
2561 "$result->getInner()->content"
2562 );
2563 }
2564}