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 swift_root_type(&self) -> Option<&String> {
336 self.swift_first_class_map.root_type.as_ref()
337 }
338
339 pub fn swift_is_first_class(&self, type_name: Option<&str>) -> bool {
343 self.swift_first_class_map.is_first_class(type_name)
344 }
345
346 pub fn swift_advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
349 self.swift_first_class_map.advance(owner_type, field_name)
350 }
351
352 pub fn is_optional(&self, field: &str) -> bool {
354 if self.is_optional_direct(field) {
355 return true;
356 }
357 if let Some(suffix) = self.namespace_stripped_path(field) {
361 if self.is_optional_direct(suffix) {
362 return true;
363 }
364 }
365 false
366 }
367
368 fn is_optional_direct(&self, field: &str) -> bool {
369 if self.optional_fields.contains(field) {
370 return true;
371 }
372 let index_normalized = normalize_numeric_indices(field);
373 if index_normalized != field && self.optional_fields.contains(index_normalized.as_str()) {
374 return true;
375 }
376 let de_indexed = strip_numeric_indices(field);
379 if de_indexed != field && self.optional_fields.contains(de_indexed.as_str()) {
380 return true;
381 }
382 let normalized = field.replace("[].", ".");
383 if normalized != field && self.optional_fields.contains(normalized.as_str()) {
384 return true;
385 }
386 for af in &self.array_fields {
387 if let Some(rest) = field.strip_prefix(af.as_str()) {
388 if let Some(rest) = rest.strip_prefix('.') {
389 let with_bracket = format!("{af}[].{rest}");
390 if self.optional_fields.contains(with_bracket.as_str()) {
391 return true;
392 }
393 }
394 }
395 }
396 false
397 }
398
399 pub fn has_alias(&self, fixture_field: &str) -> bool {
401 self.aliases.contains_key(fixture_field)
402 }
403
404 pub fn has_explicit_field(&self, field_name: &str) -> bool {
410 if self.result_fields.is_empty() {
411 return false;
412 }
413 self.result_fields.contains(field_name)
414 }
415
416 pub fn is_valid_for_result(&self, fixture_field: &str) -> bool {
425 if self.result_fields.is_empty() {
426 return true;
427 }
428 let resolved = self.resolve(fixture_field);
429 let first_segment = resolved.split('.').next().unwrap_or(resolved);
430 let first_segment = first_segment.split('[').next().unwrap_or(first_segment);
431 if self.result_fields.contains(first_segment) {
432 return true;
433 }
434 if let Some(suffix) = self.namespace_stripped_path(resolved) {
440 let suffix_first = suffix.split('.').next().unwrap_or(suffix);
441 let suffix_first = suffix_first.split('[').next().unwrap_or(suffix_first);
442 return self.result_fields.contains(suffix_first);
443 }
444 false
445 }
446
447 pub fn namespace_stripped_path<'a>(&self, path: &'a str) -> Option<&'a str> {
453 let dot_pos = path.find('.')?;
454 let first = &path[..dot_pos];
455 if first.contains('[') {
458 return None;
459 }
460 if self.result_fields.contains(first) {
463 return None;
464 }
465 let suffix = &path[dot_pos + 1..];
466 if suffix.is_empty() { None } else { Some(suffix) }
467 }
468
469 pub fn is_array(&self, field: &str) -> bool {
471 self.array_fields.contains(field)
472 }
473
474 pub fn is_collection_root(&self, field: &str) -> bool {
487 let prefix = format!("{field}[");
488 self.array_fields.iter().any(|af| af.starts_with(&prefix))
489 || self.optional_fields.iter().any(|of| of.starts_with(&prefix))
490 }
491
492 pub fn tagged_union_split(&self, fixture_field: &str) -> Option<(String, String, String)> {
504 let resolved = self.resolve(fixture_field);
505 let segments: Vec<&str> = resolved.split('.').collect();
506 let mut path_so_far = String::new();
507 for (i, seg) in segments.iter().enumerate() {
508 if !path_so_far.is_empty() {
509 path_so_far.push('.');
510 }
511 path_so_far.push_str(seg);
512 if self.method_calls.contains(&path_so_far) {
513 let prefix = segments[..i].join(".");
515 let variant = (*seg).to_string();
516 let suffix = segments[i + 1..].join(".");
517 return Some((prefix, variant, suffix));
518 }
519 }
520 None
521 }
522
523 pub fn has_map_access(&self, fixture_field: &str) -> bool {
525 let resolved = self.resolve(fixture_field);
526 let segments = parse_path(resolved);
527 segments.iter().any(|s| {
528 if let PathSegment::MapAccess { key, .. } = s {
529 !key.chars().all(|c| c.is_ascii_digit())
530 } else {
531 false
532 }
533 })
534 }
535
536 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
545 let resolved = self.resolve(fixture_field);
546 let effective = if !self.result_fields.is_empty() {
550 if let Some(stripped) = self.namespace_stripped_path(resolved) {
551 let stripped_first = stripped.split('.').next().unwrap_or(stripped);
552 let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
553 if self.result_fields.contains(stripped_first) {
554 stripped
555 } else {
556 resolved
557 }
558 } else {
559 resolved
560 }
561 } else {
562 resolved
563 };
564 let segments = parse_path(effective);
565 let segments = self.inject_array_indexing(segments);
566 match language {
567 "java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
568 "kotlin" => render_kotlin_with_optionals(&segments, result_var, &self.optional_fields),
569 "kotlin_android" => render_kotlin_android_with_optionals(&segments, result_var, &self.optional_fields),
572 "rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
573 "csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
574 "zig" => render_zig_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
575 "swift" if !self.swift_first_class_map.is_empty() => render_swift_with_first_class_map(
576 &segments,
577 result_var,
578 &self.optional_fields,
579 &self.swift_first_class_map,
580 ),
581 "swift" => render_swift_with_optionals(&segments, result_var, &self.optional_fields),
582 "dart" => render_dart_with_optionals(&segments, result_var, &self.optional_fields),
583 "php" if !self.php_getter_map.is_empty() => {
584 render_php_with_getters(&segments, result_var, &self.php_getter_map)
585 }
586 _ => render_accessor(&segments, language, result_var),
587 }
588 }
589
590 pub fn accessor_for_error(&self, sub_field: &str, language: &str, err_var: &str) -> String {
604 let resolved = self
605 .error_field_aliases
606 .get(sub_field)
607 .map(String::as_str)
608 .unwrap_or(sub_field);
609 let segments = parse_path(resolved);
610 match language {
613 "rust" => render_rust_with_optionals(&segments, err_var, &self.optional_fields, &self.method_calls),
614 _ => render_accessor(&segments, language, err_var),
615 }
616 }
617
618 pub fn has_error_aliases(&self) -> bool {
625 !self.error_field_aliases.is_empty()
626 }
627
628 fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
629 if self.array_fields.is_empty() {
630 return segments;
631 }
632 let len = segments.len();
633 let mut result = Vec::with_capacity(len);
634 let mut path_so_far = String::new();
635 for i in 0..len {
636 let seg = &segments[i];
637 match seg {
638 PathSegment::Field(f) => {
639 if !path_so_far.is_empty() {
640 path_so_far.push('.');
641 }
642 path_so_far.push_str(f);
643 let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
644 if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
645 result.push(PathSegment::ArrayField {
647 name: f.clone(),
648 index: 0,
649 });
650 } else {
651 result.push(seg.clone());
652 }
653 }
654 PathSegment::ArrayField { .. } => {
657 result.push(seg.clone());
658 }
659 PathSegment::MapAccess { field, key } => {
660 if !path_so_far.is_empty() {
661 path_so_far.push('.');
662 }
663 path_so_far.push_str(field);
664 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
665 if is_numeric && self.array_fields.contains(&path_so_far) {
666 let index: usize = key.parse().unwrap_or(0);
668 result.push(PathSegment::ArrayField {
669 name: field.clone(),
670 index,
671 });
672 } else {
673 result.push(seg.clone());
674 }
675 }
676 _ => {
677 result.push(seg.clone());
678 }
679 }
680 }
681 result
682 }
683
684 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
686 let resolved = self.resolve(fixture_field);
687 if !self.is_optional(resolved) {
688 return None;
689 }
690 let effective = if !self.result_fields.is_empty() {
694 if let Some(stripped) = self.namespace_stripped_path(resolved) {
695 let stripped_first = stripped.split('.').next().unwrap_or(stripped);
696 let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
697 if self.result_fields.contains(stripped_first) {
698 stripped
699 } else {
700 resolved
701 }
702 } else {
703 resolved
704 }
705 } else {
706 resolved
707 };
708 let segments = parse_path(effective);
709 let segments = self.inject_array_indexing(segments);
710 let local_var = {
715 let raw = effective.replace(['.', '['], "_").replace(']', "");
716 let mut collapsed = String::with_capacity(raw.len());
717 let mut prev_underscore = false;
718 for ch in raw.chars() {
719 if ch == '_' {
720 if !prev_underscore {
721 collapsed.push('_');
722 }
723 prev_underscore = true;
724 } else {
725 collapsed.push(ch);
726 prev_underscore = false;
727 }
728 }
729 collapsed.trim_matches('_').to_string()
730 };
731 let accessor = render_accessor(&segments, "rust", result_var);
732 let has_map_access = segments.iter().any(|s| {
733 if let PathSegment::MapAccess { key, .. } = s {
734 !key.chars().all(|c| c.is_ascii_digit())
735 } else {
736 false
737 }
738 });
739 let is_array = self.is_array(resolved);
740 let binding = if has_map_access {
741 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
742 } else if is_array {
743 format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
744 } else {
745 format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
751 };
752 Some((binding, local_var))
753 }
754}
755
756fn strip_numeric_indices(path: &str) -> String {
761 let mut result = String::with_capacity(path.len());
762 let mut chars = path.chars().peekable();
763 while let Some(c) = chars.next() {
764 if c == '[' {
765 let mut key = String::new();
766 let mut closed = false;
767 for inner in chars.by_ref() {
768 if inner == ']' {
769 closed = true;
770 break;
771 }
772 key.push(inner);
773 }
774 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
775 } else {
777 result.push('[');
778 result.push_str(&key);
779 if closed {
780 result.push(']');
781 }
782 }
783 } else {
784 result.push(c);
785 }
786 }
787 while result.contains("..") {
789 result = result.replace("..", ".");
790 }
791 if result.starts_with('.') {
792 result.remove(0);
793 }
794 result
795}
796
797fn normalize_numeric_indices(path: &str) -> String {
798 let mut result = String::with_capacity(path.len());
799 let mut chars = path.chars().peekable();
800 while let Some(c) = chars.next() {
801 if c == '[' {
802 let mut key = String::new();
803 let mut closed = false;
804 for inner in chars.by_ref() {
805 if inner == ']' {
806 closed = true;
807 break;
808 }
809 key.push(inner);
810 }
811 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
812 result.push_str("[0]");
813 } else {
814 result.push('[');
815 result.push_str(&key);
816 if closed {
817 result.push(']');
818 }
819 }
820 } else {
821 result.push(c);
822 }
823 }
824 result
825}
826
827fn parse_path(path: &str) -> Vec<PathSegment> {
828 let mut segments = Vec::new();
829 for part in path.split('.') {
830 if part == "length" || part == "count" || part == "size" {
831 segments.push(PathSegment::Length);
832 } else if let Some(bracket_pos) = part.find('[') {
833 let name = part[..bracket_pos].to_string();
834 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
835 if key.is_empty() {
836 segments.push(PathSegment::ArrayField { name, index: 0 });
838 } else if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
839 let index: usize = key.parse().unwrap_or(0);
841 segments.push(PathSegment::ArrayField { name, index });
842 } else {
843 segments.push(PathSegment::MapAccess { field: name, key });
845 }
846 } else {
847 segments.push(PathSegment::Field(part.to_string()));
848 }
849 }
850 segments
851}
852
853fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
854 match language {
855 "rust" => render_rust(segments, result_var),
856 "python" => render_dot_access(segments, result_var, "python"),
857 "typescript" | "node" => render_typescript(segments, result_var),
858 "wasm" => render_wasm(segments, result_var),
859 "go" => render_go(segments, result_var),
860 "java" => render_java(segments, result_var),
861 "kotlin" => render_kotlin(segments, result_var),
862 "kotlin_android" => render_kotlin_android(segments, result_var),
863 "csharp" => render_pascal_dot(segments, result_var),
864 "ruby" => render_dot_access(segments, result_var, "ruby"),
865 "php" => render_php(segments, result_var),
866 "elixir" => render_dot_access(segments, result_var, "elixir"),
867 "r" => render_r(segments, result_var),
868 "c" => render_c(segments, result_var),
869 "swift" => render_swift(segments, result_var),
870 "dart" => render_dart(segments, result_var),
871 _ => render_dot_access(segments, result_var, language),
872 }
873}
874
875fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
887 let mut out = result_var.to_string();
888 for seg in segments {
889 match seg {
890 PathSegment::Field(f) => {
891 out.push('.');
892 out.push_str(&f.to_lower_camel_case());
893 }
894 PathSegment::ArrayField { name, index } => {
895 out.push('.');
896 out.push_str(&name.to_lower_camel_case());
897 out.push_str(&format!("[{index}]"));
898 }
899 PathSegment::MapAccess { field, key } => {
900 out.push('.');
901 out.push_str(&field.to_lower_camel_case());
902 if key.chars().all(|c| c.is_ascii_digit()) {
903 out.push_str(&format!("[{key}]"));
904 } else {
905 out.push_str(&format!("[\"{key}\"]"));
906 }
907 }
908 PathSegment::Length => {
909 out.push_str(".count");
910 }
911 }
912 }
913 out
914}
915
916fn render_swift_with_optionals(
926 segments: &[PathSegment],
927 result_var: &str,
928 optional_fields: &HashSet<String>,
929) -> String {
930 let mut out = result_var.to_string();
931 let mut path_so_far = String::new();
932 let total = segments.len();
933 for (i, seg) in segments.iter().enumerate() {
934 let is_leaf = i == total - 1;
935 match seg {
936 PathSegment::Field(f) => {
937 if !path_so_far.is_empty() {
938 path_so_far.push('.');
939 }
940 path_so_far.push_str(f);
941 out.push('.');
942 out.push_str(&f.to_lower_camel_case());
945 if !is_leaf && optional_fields.contains(&path_so_far) {
949 out.push('?');
950 }
951 }
952 PathSegment::ArrayField { name, index } => {
953 if !path_so_far.is_empty() {
954 path_so_far.push('.');
955 }
956 path_so_far.push_str(name);
957 let is_optional = optional_fields.contains(&path_so_far);
958 out.push('.');
959 out.push_str(&name.to_lower_camel_case());
960 if is_optional {
961 out.push_str(&format!("?[{index}]"));
963 } else {
964 out.push_str(&format!("[{index}]"));
965 }
966 path_so_far.push_str("[0]");
967 let _ = is_leaf;
968 }
969 PathSegment::MapAccess { field, key } => {
970 if !path_so_far.is_empty() {
971 path_so_far.push('.');
972 }
973 path_so_far.push_str(field);
974 out.push('.');
975 out.push_str(&field.to_lower_camel_case());
976 if key.chars().all(|c| c.is_ascii_digit()) {
977 out.push_str(&format!("[{key}]"));
978 } else {
979 out.push_str(&format!("[\"{key}\"]"));
980 }
981 }
982 PathSegment::Length => {
983 out.push_str(".count");
984 }
985 }
986 }
987 out
988}
989
990fn render_swift_with_first_class_map(
995 segments: &[PathSegment],
996 result_var: &str,
997 optional_fields: &HashSet<String>,
998 map: &SwiftFirstClassMap,
999) -> String {
1000 let mut out = result_var.to_string();
1001 let mut path_so_far = String::new();
1002 let mut current_type: Option<String> = map.root_type.clone();
1003 let mut via_rust_vec = false;
1012 let total = segments.len();
1013 for (i, seg) in segments.iter().enumerate() {
1014 let is_leaf = i == total - 1;
1015 let property_syntax = !via_rust_vec && map.is_first_class(current_type.as_deref());
1016 match seg {
1017 PathSegment::Field(f) => {
1018 if !path_so_far.is_empty() {
1019 path_so_far.push('.');
1020 }
1021 path_so_far.push_str(f);
1022 out.push('.');
1023 out.push_str(&f.to_lower_camel_case());
1026 if !property_syntax {
1027 out.push_str("()");
1028 }
1029 if !is_leaf && optional_fields.contains(&path_so_far) {
1030 out.push('?');
1031 }
1032 current_type = map.advance(current_type.as_deref(), f);
1033 }
1034 PathSegment::ArrayField { name, index } => {
1035 if !path_so_far.is_empty() {
1036 path_so_far.push('.');
1037 }
1038 path_so_far.push_str(name);
1039 let is_optional = optional_fields.contains(&path_so_far);
1040 out.push('.');
1041 out.push_str(&name.to_lower_camel_case());
1042 let access = if property_syntax { "" } else { "()" };
1043 if is_optional {
1044 out.push_str(&format!("{access}?[{index}]"));
1045 } else {
1046 out.push_str(&format!("{access}[{index}]"));
1047 }
1048 path_so_far.push_str("[0]");
1049 current_type = map.advance(current_type.as_deref(), name);
1055 if !property_syntax {
1056 via_rust_vec = true;
1057 }
1058 }
1059 PathSegment::MapAccess { field, key } => {
1060 if !path_so_far.is_empty() {
1061 path_so_far.push('.');
1062 }
1063 path_so_far.push_str(field);
1064 out.push('.');
1065 out.push_str(&field.to_lower_camel_case());
1066 let access = if property_syntax { "" } else { "()" };
1067 if key.chars().all(|c| c.is_ascii_digit()) {
1068 out.push_str(&format!("{access}[{key}]"));
1069 } else {
1070 out.push_str(&format!("{access}[\"{key}\"]"));
1071 }
1072 current_type = map.advance(current_type.as_deref(), field);
1073 }
1074 PathSegment::Length => {
1075 out.push_str(".count");
1076 }
1077 }
1078 }
1079 out
1080}
1081
1082fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
1083 let mut out = result_var.to_string();
1084 for seg in segments {
1085 match seg {
1086 PathSegment::Field(f) => {
1087 out.push('.');
1088 out.push_str(&f.to_snake_case());
1089 }
1090 PathSegment::ArrayField { name, index } => {
1091 out.push('.');
1092 out.push_str(&name.to_snake_case());
1093 out.push_str(&format!("[{index}]"));
1094 }
1095 PathSegment::MapAccess { field, key } => {
1096 out.push('.');
1097 out.push_str(&field.to_snake_case());
1098 if key.chars().all(|c| c.is_ascii_digit()) {
1099 out.push_str(&format!("[{key}]"));
1100 } else {
1101 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1102 }
1103 }
1104 PathSegment::Length => {
1105 out.push_str(".len()");
1106 }
1107 }
1108 }
1109 out
1110}
1111
1112fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
1113 let mut out = result_var.to_string();
1114 for seg in segments {
1115 match seg {
1116 PathSegment::Field(f) => {
1117 out.push('.');
1118 out.push_str(f);
1119 }
1120 PathSegment::ArrayField { name, index } => {
1121 if language == "elixir" {
1122 let current = std::mem::take(&mut out);
1123 out = format!("Enum.at({current}.{name}, {index})");
1124 } else {
1125 out.push('.');
1126 out.push_str(name);
1127 out.push_str(&format!("[{index}]"));
1128 }
1129 }
1130 PathSegment::MapAccess { field, key } => {
1131 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
1132 if is_numeric && language == "elixir" {
1133 let current = std::mem::take(&mut out);
1134 out = format!("Enum.at({current}.{field}, {key})");
1135 } else {
1136 out.push('.');
1137 out.push_str(field);
1138 if is_numeric {
1139 let idx: usize = key.parse().unwrap_or(0);
1140 out.push_str(&format!("[{idx}]"));
1141 } else if language == "elixir" || language == "ruby" {
1142 out.push_str(&format!("[\"{key}\"]"));
1145 } else {
1146 out.push_str(&format!(".get(\"{key}\")"));
1147 }
1148 }
1149 }
1150 PathSegment::Length => match language {
1151 "ruby" => out.push_str(".length"),
1152 "elixir" => {
1153 let current = std::mem::take(&mut out);
1154 out = format!("length({current})");
1155 }
1156 "gleam" => {
1157 let current = std::mem::take(&mut out);
1158 out = format!("list.length({current})");
1159 }
1160 _ => {
1161 let current = std::mem::take(&mut out);
1162 out = format!("len({current})");
1163 }
1164 },
1165 }
1166 }
1167 out
1168}
1169
1170fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
1171 let mut out = result_var.to_string();
1172 for seg in segments {
1173 match seg {
1174 PathSegment::Field(f) => {
1175 out.push('.');
1176 out.push_str(&f.to_lower_camel_case());
1177 }
1178 PathSegment::ArrayField { name, index } => {
1179 out.push('.');
1180 out.push_str(&name.to_lower_camel_case());
1181 out.push_str(&format!("[{index}]"));
1182 }
1183 PathSegment::MapAccess { field, key } => {
1184 out.push('.');
1185 out.push_str(&field.to_lower_camel_case());
1186 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
1189 out.push_str(&format!("[{key}]"));
1190 } else {
1191 out.push_str(&format!("[\"{key}\"]"));
1192 }
1193 }
1194 PathSegment::Length => {
1195 out.push_str(".length");
1196 }
1197 }
1198 }
1199 out
1200}
1201
1202fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
1203 let mut out = result_var.to_string();
1204 for seg in segments {
1205 match seg {
1206 PathSegment::Field(f) => {
1207 out.push('.');
1208 out.push_str(&f.to_lower_camel_case());
1209 }
1210 PathSegment::ArrayField { name, index } => {
1211 out.push('.');
1212 out.push_str(&name.to_lower_camel_case());
1213 out.push_str(&format!("[{index}]"));
1214 }
1215 PathSegment::MapAccess { field, key } => {
1216 out.push('.');
1217 out.push_str(&field.to_lower_camel_case());
1218 out.push_str(&format!(".get(\"{key}\")"));
1219 }
1220 PathSegment::Length => {
1221 out.push_str(".length");
1222 }
1223 }
1224 }
1225 out
1226}
1227
1228fn render_go(segments: &[PathSegment], result_var: &str) -> String {
1229 let mut out = result_var.to_string();
1230 for seg in segments {
1231 match seg {
1232 PathSegment::Field(f) => {
1233 out.push('.');
1234 out.push_str(&to_go_name(f));
1235 }
1236 PathSegment::ArrayField { name, index } => {
1237 out.push('.');
1238 out.push_str(&to_go_name(name));
1239 out.push_str(&format!("[{index}]"));
1240 }
1241 PathSegment::MapAccess { field, key } => {
1242 out.push('.');
1243 out.push_str(&to_go_name(field));
1244 if key.chars().all(|c| c.is_ascii_digit()) {
1245 out.push_str(&format!("[{key}]"));
1246 } else {
1247 out.push_str(&format!("[\"{key}\"]"));
1248 }
1249 }
1250 PathSegment::Length => {
1251 let current = std::mem::take(&mut out);
1252 out = format!("len({current})");
1253 }
1254 }
1255 }
1256 out
1257}
1258
1259fn render_java(segments: &[PathSegment], result_var: &str) -> String {
1260 let mut out = result_var.to_string();
1261 for seg in segments {
1262 match seg {
1263 PathSegment::Field(f) => {
1264 out.push('.');
1265 out.push_str(&f.to_lower_camel_case());
1266 out.push_str("()");
1267 }
1268 PathSegment::ArrayField { name, index } => {
1269 out.push('.');
1270 out.push_str(&name.to_lower_camel_case());
1271 out.push_str(&format!("().get({index})"));
1272 }
1273 PathSegment::MapAccess { field, key } => {
1274 out.push('.');
1275 out.push_str(&field.to_lower_camel_case());
1276 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1278 if is_numeric {
1279 out.push_str(&format!("().get({key})"));
1280 } else {
1281 out.push_str(&format!("().get(\"{key}\")"));
1282 }
1283 }
1284 PathSegment::Length => {
1285 out.push_str(".size()");
1286 }
1287 }
1288 }
1289 out
1290}
1291
1292fn kotlin_getter(name: &str) -> String {
1297 let camel = name.to_lower_camel_case();
1298 match camel.as_str() {
1299 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
1300 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
1301 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
1302 _ => camel,
1303 }
1304}
1305
1306fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
1307 let mut out = result_var.to_string();
1308 for seg in segments {
1309 match seg {
1310 PathSegment::Field(f) => {
1311 out.push('.');
1312 out.push_str(&kotlin_getter(f));
1313 out.push_str("()");
1314 }
1315 PathSegment::ArrayField { name, index } => {
1316 out.push('.');
1317 out.push_str(&kotlin_getter(name));
1318 if *index == 0 {
1319 out.push_str("().first()");
1320 } else {
1321 out.push_str(&format!("().get({index})"));
1322 }
1323 }
1324 PathSegment::MapAccess { field, key } => {
1325 out.push('.');
1326 out.push_str(&kotlin_getter(field));
1327 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1328 if is_numeric {
1329 out.push_str(&format!("().get({key})"));
1330 } else {
1331 out.push_str(&format!("().get(\"{key}\")"));
1332 }
1333 }
1334 PathSegment::Length => {
1335 out.push_str(".size");
1336 }
1337 }
1338 }
1339 out
1340}
1341
1342fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1343 let mut out = result_var.to_string();
1344 let mut path_so_far = String::new();
1345 for (i, seg) in segments.iter().enumerate() {
1346 let is_leaf = i == segments.len() - 1;
1347 match seg {
1348 PathSegment::Field(f) => {
1349 if !path_so_far.is_empty() {
1350 path_so_far.push('.');
1351 }
1352 path_so_far.push_str(f);
1353 out.push('.');
1354 out.push_str(&f.to_lower_camel_case());
1355 out.push_str("()");
1356 let _ = is_leaf;
1357 let _ = optional_fields;
1358 }
1359 PathSegment::ArrayField { name, index } => {
1360 if !path_so_far.is_empty() {
1361 path_so_far.push('.');
1362 }
1363 path_so_far.push_str(name);
1364 out.push('.');
1365 out.push_str(&name.to_lower_camel_case());
1366 out.push_str(&format!("().get({index})"));
1367 }
1368 PathSegment::MapAccess { field, key } => {
1369 if !path_so_far.is_empty() {
1370 path_so_far.push('.');
1371 }
1372 path_so_far.push_str(field);
1373 out.push('.');
1374 out.push_str(&field.to_lower_camel_case());
1375 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1377 if is_numeric {
1378 out.push_str(&format!("().get({key})"));
1379 } else {
1380 out.push_str(&format!("().get(\"{key}\")"));
1381 }
1382 }
1383 PathSegment::Length => {
1384 out.push_str(".size()");
1385 }
1386 }
1387 }
1388 out
1389}
1390
1391fn render_kotlin_with_optionals(
1406 segments: &[PathSegment],
1407 result_var: &str,
1408 optional_fields: &HashSet<String>,
1409) -> String {
1410 let mut out = result_var.to_string();
1411 let mut path_so_far = String::new();
1412 let mut prev_was_nullable = false;
1420 for seg in segments {
1421 let nav = if prev_was_nullable { "?." } else { "." };
1422 match seg {
1423 PathSegment::Field(f) => {
1424 if !path_so_far.is_empty() {
1425 path_so_far.push('.');
1426 }
1427 path_so_far.push_str(f);
1428 let is_optional = optional_fields.contains(&path_so_far);
1433 out.push_str(nav);
1434 out.push_str(&kotlin_getter(f));
1435 out.push_str("()");
1436 prev_was_nullable = prev_was_nullable || is_optional;
1437 }
1438 PathSegment::ArrayField { name, index } => {
1439 if !path_so_far.is_empty() {
1440 path_so_far.push('.');
1441 }
1442 path_so_far.push_str(name);
1443 let is_optional = optional_fields.contains(&path_so_far);
1444 out.push_str(nav);
1445 out.push_str(&kotlin_getter(name));
1446 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1447 if *index == 0 {
1448 out.push_str(&format!("(){safe}.first()"));
1449 } else {
1450 out.push_str(&format!("(){safe}.get({index})"));
1451 }
1452 path_so_far.push_str("[0]");
1456 prev_was_nullable = prev_was_nullable || is_optional;
1457 }
1458 PathSegment::MapAccess { field, key } => {
1459 if !path_so_far.is_empty() {
1460 path_so_far.push('.');
1461 }
1462 path_so_far.push_str(field);
1463 let is_optional = optional_fields.contains(&path_so_far);
1464 out.push_str(nav);
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 if prev_was_nullable || is_optional {
1469 out.push_str(&format!("()?.get({key})"));
1470 } else {
1471 out.push_str(&format!("().get({key})"));
1472 }
1473 } else if prev_was_nullable || is_optional {
1474 out.push_str(&format!("()?.get(\"{key}\")"));
1475 } else {
1476 out.push_str(&format!("().get(\"{key}\")"));
1477 }
1478 prev_was_nullable = prev_was_nullable || is_optional;
1479 }
1480 PathSegment::Length => {
1481 let size_nav = if prev_was_nullable { "?" } else { "" };
1484 out.push_str(&format!("{size_nav}.size"));
1485 prev_was_nullable = false;
1486 }
1487 }
1488 }
1489 out
1490}
1491
1492fn render_kotlin_android_with_optionals(
1503 segments: &[PathSegment],
1504 result_var: &str,
1505 optional_fields: &HashSet<String>,
1506) -> String {
1507 let mut out = result_var.to_string();
1508 let mut path_so_far = String::new();
1509 let mut prev_was_nullable = false;
1510 for seg in segments {
1511 let nav = if prev_was_nullable { "?." } else { "." };
1512 match seg {
1513 PathSegment::Field(f) => {
1514 if !path_so_far.is_empty() {
1515 path_so_far.push('.');
1516 }
1517 path_so_far.push_str(f);
1518 let is_optional = optional_fields.contains(&path_so_far);
1519 out.push_str(nav);
1520 out.push_str(&kotlin_getter(f));
1522 prev_was_nullable = prev_was_nullable || is_optional;
1523 }
1524 PathSegment::ArrayField { name, index } => {
1525 if !path_so_far.is_empty() {
1526 path_so_far.push('.');
1527 }
1528 path_so_far.push_str(name);
1529 let is_optional = optional_fields.contains(&path_so_far);
1530 out.push_str(nav);
1531 out.push_str(&kotlin_getter(name));
1533 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1534 if *index == 0 {
1535 out.push_str(&format!("{safe}.first()"));
1536 } else {
1537 out.push_str(&format!("{safe}.get({index})"));
1538 }
1539 path_so_far.push_str("[0]");
1540 prev_was_nullable = prev_was_nullable || is_optional;
1541 }
1542 PathSegment::MapAccess { field, key } => {
1543 if !path_so_far.is_empty() {
1544 path_so_far.push('.');
1545 }
1546 path_so_far.push_str(field);
1547 let is_optional = optional_fields.contains(&path_so_far);
1548 out.push_str(nav);
1549 out.push_str(&kotlin_getter(field));
1551 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1552 if is_numeric {
1553 if prev_was_nullable || is_optional {
1554 out.push_str(&format!("?.get({key})"));
1555 } else {
1556 out.push_str(&format!(".get({key})"));
1557 }
1558 } else if prev_was_nullable || is_optional {
1559 out.push_str(&format!("?.get(\"{key}\")"));
1560 } else {
1561 out.push_str(&format!(".get(\"{key}\")"));
1562 }
1563 prev_was_nullable = prev_was_nullable || is_optional;
1564 }
1565 PathSegment::Length => {
1566 let size_nav = if prev_was_nullable { "?" } else { "" };
1567 out.push_str(&format!("{size_nav}.size"));
1568 prev_was_nullable = false;
1569 }
1570 }
1571 }
1572 out
1573}
1574
1575fn render_kotlin_android(segments: &[PathSegment], result_var: &str) -> String {
1579 let mut out = result_var.to_string();
1580 for seg in segments {
1581 match seg {
1582 PathSegment::Field(f) => {
1583 out.push('.');
1584 out.push_str(&kotlin_getter(f));
1585 }
1587 PathSegment::ArrayField { name, index } => {
1588 out.push('.');
1589 out.push_str(&kotlin_getter(name));
1590 if *index == 0 {
1591 out.push_str(".first()");
1592 } else {
1593 out.push_str(&format!(".get({index})"));
1594 }
1595 }
1596 PathSegment::MapAccess { field, key } => {
1597 out.push('.');
1598 out.push_str(&kotlin_getter(field));
1599 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1600 if is_numeric {
1601 out.push_str(&format!(".get({key})"));
1602 } else {
1603 out.push_str(&format!(".get(\"{key}\")"));
1604 }
1605 }
1606 PathSegment::Length => {
1607 out.push_str(".size");
1608 }
1609 }
1610 }
1611 out
1612}
1613
1614fn render_rust_with_optionals(
1620 segments: &[PathSegment],
1621 result_var: &str,
1622 optional_fields: &HashSet<String>,
1623 method_calls: &HashSet<String>,
1624) -> String {
1625 let mut out = result_var.to_string();
1626 let mut path_so_far = String::new();
1627 for (i, seg) in segments.iter().enumerate() {
1628 let is_leaf = i == segments.len() - 1;
1629 match seg {
1630 PathSegment::Field(f) => {
1631 if !path_so_far.is_empty() {
1632 path_so_far.push('.');
1633 }
1634 path_so_far.push_str(f);
1635 out.push('.');
1636 out.push_str(&f.to_snake_case());
1637 let is_method = method_calls.contains(&path_so_far);
1638 if is_method {
1639 out.push_str("()");
1640 if !is_leaf && optional_fields.contains(&path_so_far) {
1641 out.push_str(".as_ref().unwrap()");
1642 }
1643 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1644 out.push_str(".as_ref().unwrap()");
1645 }
1646 }
1647 PathSegment::ArrayField { name, index } => {
1648 if !path_so_far.is_empty() {
1649 path_so_far.push('.');
1650 }
1651 path_so_far.push_str(name);
1652 out.push('.');
1653 out.push_str(&name.to_snake_case());
1654 let path_with_idx = format!("{path_so_far}[0]");
1658 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1659 if is_opt {
1660 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1661 } else {
1662 out.push_str(&format!("[{index}]"));
1663 }
1664 path_so_far.push_str("[0]");
1669 }
1670 PathSegment::MapAccess { field, key } => {
1671 if !path_so_far.is_empty() {
1672 path_so_far.push('.');
1673 }
1674 path_so_far.push_str(field);
1675 out.push('.');
1676 out.push_str(&field.to_snake_case());
1677 if key.chars().all(|c| c.is_ascii_digit()) {
1678 let path_with_idx = format!("{path_so_far}[0]");
1680 let is_opt =
1681 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1682 if is_opt {
1683 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1684 } else {
1685 out.push_str(&format!("[{key}]"));
1686 }
1687 path_so_far.push_str("[0]");
1688 } else {
1689 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1690 }
1691 }
1692 PathSegment::Length => {
1693 out.push_str(".len()");
1694 }
1695 }
1696 }
1697 out
1698}
1699
1700fn render_zig_with_optionals(
1713 segments: &[PathSegment],
1714 result_var: &str,
1715 optional_fields: &HashSet<String>,
1716 method_calls: &HashSet<String>,
1717) -> String {
1718 let mut out = result_var.to_string();
1719 let mut path_so_far = String::new();
1720 for seg in segments {
1721 match seg {
1722 PathSegment::Field(f) => {
1723 if !path_so_far.is_empty() {
1724 path_so_far.push('.');
1725 }
1726 path_so_far.push_str(f);
1727 out.push('.');
1728 out.push_str(f);
1729 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1730 out.push_str(".?");
1731 }
1732 }
1733 PathSegment::ArrayField { name, index } => {
1734 if !path_so_far.is_empty() {
1735 path_so_far.push('.');
1736 }
1737 path_so_far.push_str(name);
1738 out.push('.');
1739 out.push_str(name);
1740 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1741 out.push_str(".?");
1742 }
1743 out.push_str(&format!("[{index}]"));
1744 }
1745 PathSegment::MapAccess { field, key } => {
1746 if !path_so_far.is_empty() {
1747 path_so_far.push('.');
1748 }
1749 path_so_far.push_str(field);
1750 out.push('.');
1751 out.push_str(field);
1752 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1753 out.push_str(".?");
1754 }
1755 if key.chars().all(|c| c.is_ascii_digit()) {
1756 out.push_str(&format!("[{key}]"));
1757 } else {
1758 out.push_str(&format!(".get(\"{key}\")"));
1759 }
1760 }
1761 PathSegment::Length => {
1762 out.push_str(".len");
1763 }
1764 }
1765 }
1766 out
1767}
1768
1769fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1770 let mut out = result_var.to_string();
1771 for seg in segments {
1772 match seg {
1773 PathSegment::Field(f) => {
1774 out.push('.');
1775 out.push_str(&f.to_pascal_case());
1776 }
1777 PathSegment::ArrayField { name, index } => {
1778 out.push('.');
1779 out.push_str(&name.to_pascal_case());
1780 out.push_str(&format!("[{index}]"));
1781 }
1782 PathSegment::MapAccess { field, key } => {
1783 out.push('.');
1784 out.push_str(&field.to_pascal_case());
1785 if key.chars().all(|c| c.is_ascii_digit()) {
1786 out.push_str(&format!("[{key}]"));
1787 } else {
1788 out.push_str(&format!("[\"{key}\"]"));
1789 }
1790 }
1791 PathSegment::Length => {
1792 out.push_str(".Count");
1793 }
1794 }
1795 }
1796 out
1797}
1798
1799fn render_csharp_with_optionals(
1800 segments: &[PathSegment],
1801 result_var: &str,
1802 optional_fields: &HashSet<String>,
1803) -> String {
1804 let mut out = result_var.to_string();
1805 let mut path_so_far = String::new();
1806 for (i, seg) in segments.iter().enumerate() {
1807 let is_leaf = i == segments.len() - 1;
1808 match seg {
1809 PathSegment::Field(f) => {
1810 if !path_so_far.is_empty() {
1811 path_so_far.push('.');
1812 }
1813 path_so_far.push_str(f);
1814 out.push('.');
1815 out.push_str(&f.to_pascal_case());
1816 if !is_leaf && optional_fields.contains(&path_so_far) {
1817 out.push('!');
1818 }
1819 }
1820 PathSegment::ArrayField { name, index } => {
1821 if !path_so_far.is_empty() {
1822 path_so_far.push('.');
1823 }
1824 path_so_far.push_str(name);
1825 out.push('.');
1826 out.push_str(&name.to_pascal_case());
1827 out.push_str(&format!("[{index}]"));
1828 }
1829 PathSegment::MapAccess { field, key } => {
1830 if !path_so_far.is_empty() {
1831 path_so_far.push('.');
1832 }
1833 path_so_far.push_str(field);
1834 out.push('.');
1835 out.push_str(&field.to_pascal_case());
1836 if key.chars().all(|c| c.is_ascii_digit()) {
1837 out.push_str(&format!("[{key}]"));
1838 } else {
1839 out.push_str(&format!("[\"{key}\"]"));
1840 }
1841 }
1842 PathSegment::Length => {
1843 out.push_str(".Count");
1844 }
1845 }
1846 }
1847 out
1848}
1849
1850fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1851 let mut out = result_var.to_string();
1852 for seg in segments {
1853 match seg {
1854 PathSegment::Field(f) => {
1855 out.push_str("->");
1856 out.push_str(&f.to_lower_camel_case());
1859 }
1860 PathSegment::ArrayField { name, index } => {
1861 out.push_str("->");
1862 out.push_str(&name.to_lower_camel_case());
1863 out.push_str(&format!("[{index}]"));
1864 }
1865 PathSegment::MapAccess { field, key } => {
1866 out.push_str("->");
1867 out.push_str(&field.to_lower_camel_case());
1868 out.push_str(&format!("[\"{key}\"]"));
1869 }
1870 PathSegment::Length => {
1871 let current = std::mem::take(&mut out);
1872 out = format!("count({current})");
1873 }
1874 }
1875 }
1876 out
1877}
1878
1879fn render_php_with_getters(segments: &[PathSegment], result_var: &str, getter_map: &PhpGetterMap) -> String {
1897 let mut out = result_var.to_string();
1898 let mut current_type: Option<String> = getter_map.root_type.clone();
1899 for seg in segments {
1900 match seg {
1901 PathSegment::Field(f) => {
1902 let camel = f.to_lower_camel_case();
1903 if getter_map.needs_getter(current_type.as_deref(), f.as_str()) {
1904 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1909 out.push_str("->");
1910 out.push_str(&getter);
1911 out.push_str("()");
1912 } else {
1913 out.push_str("->");
1914 out.push_str(&camel);
1915 }
1916 current_type = getter_map.advance(current_type.as_deref(), f.as_str());
1917 }
1918 PathSegment::ArrayField { name, index } => {
1919 let camel = name.to_lower_camel_case();
1920 if getter_map.needs_getter(current_type.as_deref(), name.as_str()) {
1921 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1922 out.push_str("->");
1923 out.push_str(&getter);
1924 out.push_str("()");
1925 } else {
1926 out.push_str("->");
1927 out.push_str(&camel);
1928 }
1929 out.push_str(&format!("[{index}]"));
1930 current_type = getter_map.advance(current_type.as_deref(), name.as_str());
1931 }
1932 PathSegment::MapAccess { field, key } => {
1933 let camel = field.to_lower_camel_case();
1934 if getter_map.needs_getter(current_type.as_deref(), field.as_str()) {
1935 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1936 out.push_str("->");
1937 out.push_str(&getter);
1938 out.push_str("()");
1939 } else {
1940 out.push_str("->");
1941 out.push_str(&camel);
1942 }
1943 out.push_str(&format!("[\"{key}\"]"));
1944 current_type = getter_map.advance(current_type.as_deref(), field.as_str());
1945 }
1946 PathSegment::Length => {
1947 let current = std::mem::take(&mut out);
1948 out = format!("count({current})");
1949 }
1950 }
1951 }
1952 out
1953}
1954
1955fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1956 let mut out = result_var.to_string();
1957 for seg in segments {
1958 match seg {
1959 PathSegment::Field(f) => {
1960 out.push('$');
1961 out.push_str(f);
1962 }
1963 PathSegment::ArrayField { name, index } => {
1964 out.push('$');
1965 out.push_str(name);
1966 out.push_str(&format!("[[{}]]", index + 1));
1968 }
1969 PathSegment::MapAccess { field, key } => {
1970 out.push('$');
1971 out.push_str(field);
1972 out.push_str(&format!("[[\"{key}\"]]"));
1973 }
1974 PathSegment::Length => {
1975 let current = std::mem::take(&mut out);
1976 out = format!("length({current})");
1977 }
1978 }
1979 }
1980 out
1981}
1982
1983fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1984 let mut parts = Vec::new();
1985 let mut trailing_length = false;
1986 for seg in segments {
1987 match seg {
1988 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1989 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1990 PathSegment::MapAccess { field, key } => {
1991 parts.push(field.to_snake_case());
1992 parts.push(key.clone());
1993 }
1994 PathSegment::Length => {
1995 trailing_length = true;
1996 }
1997 }
1998 }
1999 let suffix = parts.join("_");
2000 if trailing_length {
2001 format!("result_{suffix}_count({result_var})")
2002 } else {
2003 format!("result_{suffix}({result_var})")
2004 }
2005}
2006
2007fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
2014 let mut out = result_var.to_string();
2015 for seg in segments {
2016 match seg {
2017 PathSegment::Field(f) => {
2018 out.push('.');
2019 out.push_str(&f.to_lower_camel_case());
2020 }
2021 PathSegment::ArrayField { name, index } => {
2022 out.push('.');
2023 out.push_str(&name.to_lower_camel_case());
2024 out.push_str(&format!("[{index}]"));
2025 }
2026 PathSegment::MapAccess { field, key } => {
2027 out.push('.');
2028 out.push_str(&field.to_lower_camel_case());
2029 if key.chars().all(|c| c.is_ascii_digit()) {
2030 out.push_str(&format!("[{key}]"));
2031 } else {
2032 out.push_str(&format!("[\"{key}\"]"));
2033 }
2034 }
2035 PathSegment::Length => {
2036 out.push_str(".length");
2037 }
2038 }
2039 }
2040 out
2041}
2042
2043fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
2049 let mut out = result_var.to_string();
2050 let mut path_so_far = String::new();
2051 let mut prev_was_nullable = false;
2052 for seg in segments {
2053 let nav = if prev_was_nullable { "?." } else { "." };
2054 match seg {
2055 PathSegment::Field(f) => {
2056 if !path_so_far.is_empty() {
2057 path_so_far.push('.');
2058 }
2059 path_so_far.push_str(f);
2060 let is_optional = optional_fields.contains(&path_so_far);
2061 out.push_str(nav);
2062 out.push_str(&f.to_lower_camel_case());
2063 prev_was_nullable = is_optional;
2064 }
2065 PathSegment::ArrayField { name, index } => {
2066 if !path_so_far.is_empty() {
2067 path_so_far.push('.');
2068 }
2069 path_so_far.push_str(name);
2070 let is_optional = optional_fields.contains(&path_so_far);
2071 out.push_str(nav);
2072 out.push_str(&name.to_lower_camel_case());
2073 if is_optional {
2077 out.push('!');
2078 }
2079 out.push_str(&format!("[{index}]"));
2080 prev_was_nullable = false;
2081 }
2082 PathSegment::MapAccess { field, key } => {
2083 if !path_so_far.is_empty() {
2084 path_so_far.push('.');
2085 }
2086 path_so_far.push_str(field);
2087 let is_optional = optional_fields.contains(&path_so_far);
2088 out.push_str(nav);
2089 out.push_str(&field.to_lower_camel_case());
2090 if key.chars().all(|c| c.is_ascii_digit()) {
2091 out.push_str(&format!("[{key}]"));
2092 } else {
2093 out.push_str(&format!("[\"{key}\"]"));
2094 }
2095 prev_was_nullable = is_optional;
2096 }
2097 PathSegment::Length => {
2098 out.push_str(nav);
2101 out.push_str("length");
2102 prev_was_nullable = false;
2103 }
2104 }
2105 }
2106 out
2107}
2108
2109#[cfg(test)]
2110mod tests {
2111 use super::*;
2112
2113 fn make_resolver() -> FieldResolver {
2114 let mut fields = HashMap::new();
2115 fields.insert("title".to_string(), "metadata.document.title".to_string());
2116 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
2117 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
2118 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
2119 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
2120 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
2121 let mut optional = HashSet::new();
2122 optional.insert("metadata.document.title".to_string());
2123 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
2124 }
2125
2126 fn make_resolver_with_doc_optional() -> FieldResolver {
2127 let mut fields = HashMap::new();
2128 fields.insert("title".to_string(), "metadata.document.title".to_string());
2129 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
2130 let mut optional = HashSet::new();
2131 optional.insert("document".to_string());
2132 optional.insert("metadata.document.title".to_string());
2133 optional.insert("metadata.document".to_string());
2134 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
2135 }
2136
2137 #[test]
2138 fn test_resolve_alias() {
2139 let r = make_resolver();
2140 assert_eq!(r.resolve("title"), "metadata.document.title");
2141 }
2142
2143 #[test]
2144 fn test_resolve_passthrough() {
2145 let r = make_resolver();
2146 assert_eq!(r.resolve("content"), "content");
2147 }
2148
2149 #[test]
2150 fn test_is_optional() {
2151 let r = make_resolver();
2152 assert!(r.is_optional("metadata.document.title"));
2153 assert!(!r.is_optional("content"));
2154 }
2155
2156 #[test]
2157 fn is_optional_strips_namespace_prefix() {
2158 let fields = HashMap::new();
2159 let mut optional = HashSet::new();
2160 optional.insert("action_results.data".to_string());
2161 let result_fields: HashSet<String> = ["action_results".to_string()].into_iter().collect();
2162 let r = FieldResolver::new(&fields, &optional, &result_fields, &HashSet::new(), &HashSet::new());
2163 assert!(r.is_optional("interaction.action_results[0].data"));
2165 assert!(r.is_optional("action_results[0].data"));
2167 }
2168
2169 #[test]
2170 fn test_accessor_rust_struct() {
2171 let r = make_resolver();
2172 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
2173 }
2174
2175 #[test]
2176 fn test_accessor_rust_map() {
2177 let r = make_resolver();
2178 assert_eq!(
2179 r.accessor("tags", "rust", "result"),
2180 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
2181 );
2182 }
2183
2184 #[test]
2185 fn test_accessor_python() {
2186 let r = make_resolver();
2187 assert_eq!(
2188 r.accessor("title", "python", "result"),
2189 "result.metadata.document.title"
2190 );
2191 }
2192
2193 #[test]
2194 fn test_accessor_go() {
2195 let r = make_resolver();
2196 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
2197 }
2198
2199 #[test]
2200 fn test_accessor_go_initialism_fields() {
2201 let mut fields = std::collections::HashMap::new();
2202 fields.insert("content".to_string(), "html".to_string());
2203 fields.insert("link_url".to_string(), "links.url".to_string());
2204 let r = FieldResolver::new(
2205 &fields,
2206 &HashSet::new(),
2207 &HashSet::new(),
2208 &HashSet::new(),
2209 &HashSet::new(),
2210 );
2211 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
2212 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
2213 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
2214 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
2215 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
2216 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
2217 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
2218 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
2219 }
2220
2221 #[test]
2222 fn test_accessor_typescript() {
2223 let r = make_resolver();
2224 assert_eq!(
2225 r.accessor("title", "typescript", "result"),
2226 "result.metadata.document.title"
2227 );
2228 }
2229
2230 #[test]
2231 fn test_accessor_typescript_snake_to_camel() {
2232 let r = make_resolver();
2233 assert_eq!(
2234 r.accessor("og", "typescript", "result"),
2235 "result.metadata.document.openGraph"
2236 );
2237 assert_eq!(
2238 r.accessor("twitter", "typescript", "result"),
2239 "result.metadata.document.twitterCard"
2240 );
2241 assert_eq!(
2242 r.accessor("canonical", "typescript", "result"),
2243 "result.metadata.document.canonicalUrl"
2244 );
2245 }
2246
2247 #[test]
2248 fn test_accessor_typescript_map_snake_to_camel() {
2249 let r = make_resolver();
2250 assert_eq!(
2251 r.accessor("og_tag", "typescript", "result"),
2252 "result.metadata.openGraphTags[\"og_title\"]"
2253 );
2254 }
2255
2256 #[test]
2257 fn test_accessor_typescript_numeric_index_is_unquoted() {
2258 let mut fields = HashMap::new();
2262 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
2263 let r = FieldResolver::new(
2264 &fields,
2265 &HashSet::new(),
2266 &HashSet::new(),
2267 &HashSet::new(),
2268 &HashSet::new(),
2269 );
2270 assert_eq!(
2271 r.accessor("first_score", "typescript", "result"),
2272 "result.results[0].relevanceScore"
2273 );
2274 }
2275
2276 #[test]
2277 fn test_accessor_node_alias() {
2278 let r = make_resolver();
2279 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
2280 }
2281
2282 #[test]
2283 fn test_accessor_wasm_camel_case() {
2284 let r = make_resolver();
2285 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
2286 assert_eq!(
2287 r.accessor("twitter", "wasm", "result"),
2288 "result.metadata.document.twitterCard"
2289 );
2290 assert_eq!(
2291 r.accessor("canonical", "wasm", "result"),
2292 "result.metadata.document.canonicalUrl"
2293 );
2294 }
2295
2296 #[test]
2297 fn test_accessor_wasm_map_access() {
2298 let r = make_resolver();
2299 assert_eq!(
2300 r.accessor("og_tag", "wasm", "result"),
2301 "result.metadata.openGraphTags.get(\"og_title\")"
2302 );
2303 }
2304
2305 #[test]
2306 fn test_accessor_java() {
2307 let r = make_resolver();
2308 assert_eq!(
2309 r.accessor("title", "java", "result"),
2310 "result.metadata().document().title()"
2311 );
2312 }
2313
2314 #[test]
2315 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
2316 let mut fields = HashMap::new();
2317 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2318 fields.insert("node_count".to_string(), "nodes.length".to_string());
2319 let mut arrays = HashSet::new();
2320 arrays.insert("nodes".to_string());
2321 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
2322 assert_eq!(
2323 r.accessor("first_node_name", "kotlin", "result"),
2324 "result.nodes().first().name()"
2325 );
2326 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
2327 }
2328
2329 #[test]
2330 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
2331 let r = make_resolver_with_doc_optional();
2332 assert_eq!(
2333 r.accessor("title", "kotlin", "result"),
2334 "result.metadata().document()?.title()"
2335 );
2336 }
2337
2338 #[test]
2339 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
2340 let mut fields = HashMap::new();
2341 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2342 fields.insert("tag".to_string(), "tags[name]".to_string());
2343 let mut optional = HashSet::new();
2344 optional.insert("nodes".to_string());
2345 optional.insert("tags".to_string());
2346 let mut arrays = HashSet::new();
2347 arrays.insert("nodes".to_string());
2348 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2349 assert_eq!(
2350 r.accessor("first_node_name", "kotlin", "result"),
2351 "result.nodes()?.first()?.name()"
2352 );
2353 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
2354 }
2355
2356 #[test]
2362 fn test_accessor_kotlin_optional_field_after_indexed_array() {
2363 let mut fields = HashMap::new();
2366 fields.insert(
2367 "tool_call_name".to_string(),
2368 "choices[0].message.tool_calls[0].function.name".to_string(),
2369 );
2370 let mut optional = HashSet::new();
2371 optional.insert("choices[0].message.tool_calls".to_string());
2372 let mut arrays = HashSet::new();
2373 arrays.insert("choices".to_string());
2374 arrays.insert("choices[0].message.tool_calls".to_string());
2375 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2376 let expr = r.accessor("tool_call_name", "kotlin", "result");
2377 assert!(
2379 expr.contains("toolCalls()?.first()"),
2380 "expected toolCalls()?.first() for optional list, got: {expr}"
2381 );
2382 }
2383
2384 #[test]
2385 fn test_accessor_csharp() {
2386 let r = make_resolver();
2387 assert_eq!(
2388 r.accessor("title", "csharp", "result"),
2389 "result.Metadata.Document.Title"
2390 );
2391 }
2392
2393 #[test]
2394 fn test_accessor_php() {
2395 let r = make_resolver();
2396 assert_eq!(
2397 r.accessor("title", "php", "$result"),
2398 "$result->metadata->document->title"
2399 );
2400 }
2401
2402 #[test]
2403 fn test_accessor_r() {
2404 let r = make_resolver();
2405 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
2406 }
2407
2408 #[test]
2409 fn test_accessor_c() {
2410 let r = make_resolver();
2411 assert_eq!(
2412 r.accessor("title", "c", "result"),
2413 "result_metadata_document_title(result)"
2414 );
2415 }
2416
2417 #[test]
2418 fn test_rust_unwrap_binding() {
2419 let r = make_resolver();
2420 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
2421 assert_eq!(var, "metadata_document_title");
2422 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
2425 }
2426
2427 #[test]
2428 fn test_rust_unwrap_binding_non_optional() {
2429 let r = make_resolver();
2430 assert!(r.rust_unwrap_binding("content", "result").is_none());
2431 }
2432
2433 #[test]
2434 fn test_rust_unwrap_binding_collapses_double_underscore() {
2435 let mut aliases = HashMap::new();
2440 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
2441 let mut optional = HashSet::new();
2442 optional.insert("json_ld[].name".to_string());
2443 let mut array = HashSet::new();
2444 array.insert("json_ld".to_string());
2445 let result_fields = HashSet::new();
2446 let method_calls = HashSet::new();
2447 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
2448 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
2449 assert_eq!(var, "json_ld_name");
2450 }
2451
2452 #[test]
2453 fn test_direct_field_no_alias() {
2454 let r = make_resolver();
2455 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2456 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
2457 }
2458
2459 #[test]
2460 fn test_accessor_rust_with_optionals() {
2461 let r = make_resolver_with_doc_optional();
2462 assert_eq!(
2463 r.accessor("title", "rust", "result"),
2464 "result.metadata.document.as_ref().unwrap().title"
2465 );
2466 }
2467
2468 #[test]
2469 fn test_accessor_csharp_with_optionals() {
2470 let r = make_resolver_with_doc_optional();
2471 assert_eq!(
2472 r.accessor("title", "csharp", "result"),
2473 "result.Metadata.Document!.Title"
2474 );
2475 }
2476
2477 #[test]
2478 fn test_accessor_rust_non_optional_field() {
2479 let r = make_resolver();
2480 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2481 }
2482
2483 #[test]
2484 fn test_accessor_csharp_non_optional_field() {
2485 let r = make_resolver();
2486 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
2487 }
2488
2489 #[test]
2490 fn test_accessor_rust_method_call() {
2491 let mut fields = HashMap::new();
2493 fields.insert(
2494 "excel_sheet_count".to_string(),
2495 "metadata.format.excel.sheet_count".to_string(),
2496 );
2497 let mut optional = HashSet::new();
2498 optional.insert("metadata.format".to_string());
2499 optional.insert("metadata.format.excel".to_string());
2500 let mut method_calls = HashSet::new();
2501 method_calls.insert("metadata.format.excel".to_string());
2502 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
2503 assert_eq!(
2504 r.accessor("excel_sheet_count", "rust", "result"),
2505 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
2506 );
2507 }
2508
2509 fn make_php_getter_resolver() -> FieldResolver {
2514 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2515 getters.insert(
2516 "Root".to_string(),
2517 ["metadata".to_string(), "links".to_string()].into_iter().collect(),
2518 );
2519 let map = PhpGetterMap {
2520 getters,
2521 field_types: HashMap::new(),
2522 root_type: Some("Root".to_string()),
2523 all_fields: HashMap::new(),
2524 };
2525 FieldResolver::new_with_php_getters(
2526 &HashMap::new(),
2527 &HashSet::new(),
2528 &HashSet::new(),
2529 &HashSet::new(),
2530 &HashSet::new(),
2531 &HashMap::new(),
2532 map,
2533 )
2534 }
2535
2536 #[test]
2537 fn render_php_uses_getter_method_for_non_scalar_field() {
2538 let r = make_php_getter_resolver();
2539 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
2540 }
2541
2542 #[test]
2543 fn render_php_uses_property_for_scalar_field() {
2544 let r = make_php_getter_resolver();
2545 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2546 }
2547
2548 #[test]
2549 fn render_php_nested_non_scalar_uses_getter_then_property() {
2550 let mut fields = HashMap::new();
2551 fields.insert("title".to_string(), "metadata.title".to_string());
2552 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2553 getters.insert("Root".to_string(), ["metadata".to_string()].into_iter().collect());
2554 getters.insert("Metadata".to_string(), HashSet::new());
2556 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2557 field_types.insert(
2558 "Root".to_string(),
2559 [("metadata".to_string(), "Metadata".to_string())].into_iter().collect(),
2560 );
2561 let map = PhpGetterMap {
2562 getters,
2563 field_types,
2564 root_type: Some("Root".to_string()),
2565 all_fields: HashMap::new(),
2566 };
2567 let r = FieldResolver::new_with_php_getters(
2568 &fields,
2569 &HashSet::new(),
2570 &HashSet::new(),
2571 &HashSet::new(),
2572 &HashSet::new(),
2573 &HashMap::new(),
2574 map,
2575 );
2576 assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
2578 }
2579
2580 #[test]
2581 fn render_php_array_field_uses_getter_when_non_scalar() {
2582 let mut fields = HashMap::new();
2583 fields.insert("first_link".to_string(), "links[0]".to_string());
2584 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2585 getters.insert("Root".to_string(), ["links".to_string()].into_iter().collect());
2586 let map = PhpGetterMap {
2587 getters,
2588 field_types: HashMap::new(),
2589 root_type: Some("Root".to_string()),
2590 all_fields: HashMap::new(),
2591 };
2592 let r = FieldResolver::new_with_php_getters(
2593 &fields,
2594 &HashSet::new(),
2595 &HashSet::new(),
2596 &HashSet::new(),
2597 &HashSet::new(),
2598 &HashMap::new(),
2599 map,
2600 );
2601 assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
2602 }
2603
2604 #[test]
2605 fn render_php_falls_back_to_property_when_getter_fields_empty() {
2606 let r = FieldResolver::new(
2609 &HashMap::new(),
2610 &HashSet::new(),
2611 &HashSet::new(),
2612 &HashSet::new(),
2613 &HashSet::new(),
2614 );
2615 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2616 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
2617 }
2618
2619 #[test]
2623 fn render_php_with_getters_distinguishes_same_field_name_on_different_types() {
2624 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2625 getters.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2627 getters.insert("B".to_string(), HashSet::new());
2629 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2632 all_fields.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2633 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2634 let map_a = PhpGetterMap {
2635 getters: getters.clone(),
2636 field_types: HashMap::new(),
2637 root_type: Some("A".to_string()),
2638 all_fields: all_fields.clone(),
2639 };
2640 let map_b = PhpGetterMap {
2641 getters,
2642 field_types: HashMap::new(),
2643 root_type: Some("B".to_string()),
2644 all_fields,
2645 };
2646 let r_a = FieldResolver::new_with_php_getters(
2647 &HashMap::new(),
2648 &HashSet::new(),
2649 &HashSet::new(),
2650 &HashSet::new(),
2651 &HashSet::new(),
2652 &HashMap::new(),
2653 map_a,
2654 );
2655 let r_b = FieldResolver::new_with_php_getters(
2656 &HashMap::new(),
2657 &HashSet::new(),
2658 &HashSet::new(),
2659 &HashSet::new(),
2660 &HashSet::new(),
2661 &HashMap::new(),
2662 map_b,
2663 );
2664 assert_eq!(r_a.accessor("content", "php", "$a"), "$a->getContent()");
2665 assert_eq!(r_b.accessor("content", "php", "$b"), "$b->content");
2666 }
2667
2668 #[test]
2672 fn render_php_with_getters_chains_through_correct_type() {
2673 let mut fields = HashMap::new();
2674 fields.insert("nested_content".to_string(), "inner.content".to_string());
2675 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2676 getters.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2678 getters.insert("B".to_string(), HashSet::new());
2680 getters.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2683 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2684 field_types.insert(
2685 "Outer".to_string(),
2686 [("inner".to_string(), "B".to_string())].into_iter().collect(),
2687 );
2688 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2689 all_fields.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2690 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2691 all_fields.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2692 let map = PhpGetterMap {
2693 getters,
2694 field_types,
2695 root_type: Some("Outer".to_string()),
2696 all_fields,
2697 };
2698 let r = FieldResolver::new_with_php_getters(
2699 &fields,
2700 &HashSet::new(),
2701 &HashSet::new(),
2702 &HashSet::new(),
2703 &HashSet::new(),
2704 &HashMap::new(),
2705 map,
2706 );
2707 assert_eq!(
2708 r.accessor("nested_content", "php", "$result"),
2709 "$result->getInner()->content"
2710 );
2711 }
2712
2713 fn make_resolver_with_result_fields(result_fields: &[&str]) -> FieldResolver {
2718 let rf: HashSet<String> = result_fields.iter().map(|s| s.to_string()).collect();
2719 FieldResolver::new(&HashMap::new(), &HashSet::new(), &rf, &HashSet::new(), &HashSet::new())
2720 }
2721
2722 #[test]
2725 fn is_valid_for_result_accepts_virtual_namespace_prefix() {
2726 let r = make_resolver_with_result_fields(&["browser_used", "js_render_hint", "status_code"]);
2727 assert!(
2728 r.is_valid_for_result("browser.browser_used"),
2729 "browser.browser_used should be valid via namespace-prefix stripping"
2730 );
2731 assert!(
2732 r.is_valid_for_result("browser.js_render_hint"),
2733 "browser.js_render_hint should be valid via namespace-prefix stripping"
2734 );
2735 }
2736
2737 #[test]
2740 fn is_valid_for_result_accepts_namespace_prefix_before_array_field() {
2741 let r = make_resolver_with_result_fields(&["action_results", "final_html", "final_url"]);
2742 assert!(
2743 r.is_valid_for_result("interaction.action_results[0].action_type"),
2744 "interaction. prefix should be stripped so action_results is recognised"
2745 );
2746 }
2747
2748 #[test]
2750 fn is_valid_for_result_rejects_unknown_field_even_after_namespace_strip() {
2751 let r = make_resolver_with_result_fields(&["pages", "final_url"]);
2752 assert!(
2753 !r.is_valid_for_result("browser.browser_used"),
2754 "browser_used is not in result_fields so should be rejected"
2755 );
2756 assert!(
2757 !r.is_valid_for_result("ns.unknown_field"),
2758 "unknown_field is not in result_fields so should be rejected"
2759 );
2760 }
2761
2762 #[test]
2765 fn accessor_strips_namespace_prefix_for_python() {
2766 let r = make_resolver_with_result_fields(&["browser_used", "js_render_hint"]);
2767 assert_eq!(
2768 r.accessor("browser.browser_used", "python", "result"),
2769 "result.browser_used"
2770 );
2771 assert_eq!(
2772 r.accessor("browser.js_render_hint", "python", "result"),
2773 "result.js_render_hint"
2774 );
2775 }
2776
2777 #[test]
2779 fn accessor_strips_namespace_prefix_for_csharp() {
2780 let r = make_resolver_with_result_fields(&["browser_used"]);
2781 assert_eq!(
2782 r.accessor("browser.browser_used", "csharp", "result"),
2783 "result.BrowserUsed"
2784 );
2785 }
2786
2787 #[test]
2790 fn accessor_strips_namespace_prefix_for_indexed_array_field() {
2791 let r = make_resolver_with_result_fields(&["action_results", "final_html", "final_url"]);
2792 assert_eq!(
2794 r.accessor("interaction.action_results[0].action_type", "python", "result"),
2795 "result.action_results[0].action_type"
2796 );
2797 assert_eq!(
2799 r.accessor("interaction.action_results[0].action_type", "typescript", "result"),
2800 "result.actionResults[0].actionType"
2801 );
2802 }
2803
2804 #[test]
2807 fn is_valid_for_result_is_permissive_when_result_fields_empty() {
2808 let r = make_resolver_with_result_fields(&[]);
2809 assert!(r.is_valid_for_result("browser.browser_used"));
2810 assert!(r.is_valid_for_result("anything.at.all"));
2811 }
2812
2813 #[test]
2816 fn accessor_does_not_strip_real_first_segment() {
2817 let r = make_resolver_with_result_fields(&["metadata", "status_code"]);
2818 assert_eq!(
2820 r.accessor("metadata.title", "python", "result"),
2821 "result.metadata.title"
2822 );
2823 }
2824}