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 mut via_opaque = false;
1023 let total = segments.len();
1024 for (i, seg) in segments.iter().enumerate() {
1025 let is_leaf = i == total - 1;
1026 let property_syntax = !via_rust_vec && !via_opaque && map.is_first_class(current_type.as_deref());
1027 if !property_syntax {
1028 via_opaque = true;
1029 }
1030 match seg {
1031 PathSegment::Field(f) => {
1032 if !path_so_far.is_empty() {
1033 path_so_far.push('.');
1034 }
1035 path_so_far.push_str(f);
1036 out.push('.');
1037 out.push_str(&f.to_lower_camel_case());
1040 if !property_syntax {
1041 out.push_str("()");
1042 }
1043 if !is_leaf && optional_fields.contains(&path_so_far) {
1044 out.push('?');
1045 }
1046 current_type = map.advance(current_type.as_deref(), f);
1047 }
1048 PathSegment::ArrayField { name, index } => {
1049 if !path_so_far.is_empty() {
1050 path_so_far.push('.');
1051 }
1052 path_so_far.push_str(name);
1053 let is_optional = optional_fields.contains(&path_so_far);
1054 out.push('.');
1055 out.push_str(&name.to_lower_camel_case());
1056 let access = if property_syntax { "" } else { "()" };
1057 if is_optional {
1058 out.push_str(&format!("{access}?[{index}]"));
1059 } else {
1060 out.push_str(&format!("{access}[{index}]"));
1061 }
1062 path_so_far.push_str("[0]");
1063 current_type = map.advance(current_type.as_deref(), name);
1069 if !property_syntax {
1070 via_rust_vec = true;
1071 }
1072 }
1073 PathSegment::MapAccess { field, key } => {
1074 if !path_so_far.is_empty() {
1075 path_so_far.push('.');
1076 }
1077 path_so_far.push_str(field);
1078 out.push('.');
1079 out.push_str(&field.to_lower_camel_case());
1080 let access = if property_syntax { "" } else { "()" };
1081 if key.chars().all(|c| c.is_ascii_digit()) {
1082 out.push_str(&format!("{access}[{key}]"));
1083 } else {
1084 out.push_str(&format!("{access}[\"{key}\"]"));
1085 }
1086 current_type = map.advance(current_type.as_deref(), field);
1087 }
1088 PathSegment::Length => {
1089 out.push_str(".count");
1090 }
1091 }
1092 }
1093 out
1094}
1095
1096fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
1097 let mut out = result_var.to_string();
1098 for seg in segments {
1099 match seg {
1100 PathSegment::Field(f) => {
1101 out.push('.');
1102 out.push_str(&f.to_snake_case());
1103 }
1104 PathSegment::ArrayField { name, index } => {
1105 out.push('.');
1106 out.push_str(&name.to_snake_case());
1107 out.push_str(&format!("[{index}]"));
1108 }
1109 PathSegment::MapAccess { field, key } => {
1110 out.push('.');
1111 out.push_str(&field.to_snake_case());
1112 if key.chars().all(|c| c.is_ascii_digit()) {
1113 out.push_str(&format!("[{key}]"));
1114 } else {
1115 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1116 }
1117 }
1118 PathSegment::Length => {
1119 out.push_str(".len()");
1120 }
1121 }
1122 }
1123 out
1124}
1125
1126fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &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);
1133 }
1134 PathSegment::ArrayField { name, index } => {
1135 if language == "elixir" {
1136 let current = std::mem::take(&mut out);
1137 out = format!("Enum.at({current}.{name}, {index})");
1138 } else {
1139 out.push('.');
1140 out.push_str(name);
1141 out.push_str(&format!("[{index}]"));
1142 }
1143 }
1144 PathSegment::MapAccess { field, key } => {
1145 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
1146 if is_numeric && language == "elixir" {
1147 let current = std::mem::take(&mut out);
1148 out = format!("Enum.at({current}.{field}, {key})");
1149 } else {
1150 out.push('.');
1151 out.push_str(field);
1152 if is_numeric {
1153 let idx: usize = key.parse().unwrap_or(0);
1154 out.push_str(&format!("[{idx}]"));
1155 } else if language == "elixir" || language == "ruby" {
1156 out.push_str(&format!("[\"{key}\"]"));
1159 } else {
1160 out.push_str(&format!(".get(\"{key}\")"));
1161 }
1162 }
1163 }
1164 PathSegment::Length => match language {
1165 "ruby" => out.push_str(".length"),
1166 "elixir" => {
1167 let current = std::mem::take(&mut out);
1168 out = format!("length({current})");
1169 }
1170 "gleam" => {
1171 let current = std::mem::take(&mut out);
1172 out = format!("list.length({current})");
1173 }
1174 _ => {
1175 let current = std::mem::take(&mut out);
1176 out = format!("len({current})");
1177 }
1178 },
1179 }
1180 }
1181 out
1182}
1183
1184fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
1185 let mut out = result_var.to_string();
1186 for seg in segments {
1187 match seg {
1188 PathSegment::Field(f) => {
1189 out.push('.');
1190 out.push_str(&f.to_lower_camel_case());
1191 }
1192 PathSegment::ArrayField { name, index } => {
1193 out.push('.');
1194 out.push_str(&name.to_lower_camel_case());
1195 out.push_str(&format!("[{index}]"));
1196 }
1197 PathSegment::MapAccess { field, key } => {
1198 out.push('.');
1199 out.push_str(&field.to_lower_camel_case());
1200 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
1203 out.push_str(&format!("[{key}]"));
1204 } else {
1205 out.push_str(&format!("[\"{key}\"]"));
1206 }
1207 }
1208 PathSegment::Length => {
1209 out.push_str(".length");
1210 }
1211 }
1212 }
1213 out
1214}
1215
1216fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
1217 let mut out = result_var.to_string();
1218 for seg in segments {
1219 match seg {
1220 PathSegment::Field(f) => {
1221 out.push('.');
1222 out.push_str(&f.to_lower_camel_case());
1223 }
1224 PathSegment::ArrayField { name, index } => {
1225 out.push('.');
1226 out.push_str(&name.to_lower_camel_case());
1227 out.push_str(&format!("[{index}]"));
1228 }
1229 PathSegment::MapAccess { field, key } => {
1230 out.push('.');
1231 out.push_str(&field.to_lower_camel_case());
1232 out.push_str(&format!(".get(\"{key}\")"));
1233 }
1234 PathSegment::Length => {
1235 out.push_str(".length");
1236 }
1237 }
1238 }
1239 out
1240}
1241
1242fn render_go(segments: &[PathSegment], result_var: &str) -> String {
1243 let mut out = result_var.to_string();
1244 for seg in segments {
1245 match seg {
1246 PathSegment::Field(f) => {
1247 out.push('.');
1248 out.push_str(&to_go_name(f));
1249 }
1250 PathSegment::ArrayField { name, index } => {
1251 out.push('.');
1252 out.push_str(&to_go_name(name));
1253 out.push_str(&format!("[{index}]"));
1254 }
1255 PathSegment::MapAccess { field, key } => {
1256 out.push('.');
1257 out.push_str(&to_go_name(field));
1258 if key.chars().all(|c| c.is_ascii_digit()) {
1259 out.push_str(&format!("[{key}]"));
1260 } else {
1261 out.push_str(&format!("[\"{key}\"]"));
1262 }
1263 }
1264 PathSegment::Length => {
1265 let current = std::mem::take(&mut out);
1266 out = format!("len({current})");
1267 }
1268 }
1269 }
1270 out
1271}
1272
1273fn render_java(segments: &[PathSegment], result_var: &str) -> String {
1274 let mut out = result_var.to_string();
1275 for seg in segments {
1276 match seg {
1277 PathSegment::Field(f) => {
1278 out.push('.');
1279 out.push_str(&f.to_lower_camel_case());
1280 out.push_str("()");
1281 }
1282 PathSegment::ArrayField { name, index } => {
1283 out.push('.');
1284 out.push_str(&name.to_lower_camel_case());
1285 out.push_str(&format!("().get({index})"));
1286 }
1287 PathSegment::MapAccess { field, key } => {
1288 out.push('.');
1289 out.push_str(&field.to_lower_camel_case());
1290 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1292 if is_numeric {
1293 out.push_str(&format!("().get({key})"));
1294 } else {
1295 out.push_str(&format!("().get(\"{key}\")"));
1296 }
1297 }
1298 PathSegment::Length => {
1299 out.push_str(".size()");
1300 }
1301 }
1302 }
1303 out
1304}
1305
1306fn kotlin_getter(name: &str) -> String {
1311 let camel = name.to_lower_camel_case();
1312 match camel.as_str() {
1313 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
1314 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
1315 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
1316 _ => camel,
1317 }
1318}
1319
1320fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
1321 let mut out = result_var.to_string();
1322 for seg in segments {
1323 match seg {
1324 PathSegment::Field(f) => {
1325 out.push('.');
1326 out.push_str(&kotlin_getter(f));
1327 out.push_str("()");
1328 }
1329 PathSegment::ArrayField { name, index } => {
1330 out.push('.');
1331 out.push_str(&kotlin_getter(name));
1332 if *index == 0 {
1333 out.push_str("().first()");
1334 } else {
1335 out.push_str(&format!("().get({index})"));
1336 }
1337 }
1338 PathSegment::MapAccess { field, key } => {
1339 out.push('.');
1340 out.push_str(&kotlin_getter(field));
1341 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1342 if is_numeric {
1343 out.push_str(&format!("().get({key})"));
1344 } else {
1345 out.push_str(&format!("().get(\"{key}\")"));
1346 }
1347 }
1348 PathSegment::Length => {
1349 out.push_str(".size");
1350 }
1351 }
1352 }
1353 out
1354}
1355
1356fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1357 let mut out = result_var.to_string();
1358 let mut path_so_far = String::new();
1359 for (i, seg) in segments.iter().enumerate() {
1360 let is_leaf = i == segments.len() - 1;
1361 match seg {
1362 PathSegment::Field(f) => {
1363 if !path_so_far.is_empty() {
1364 path_so_far.push('.');
1365 }
1366 path_so_far.push_str(f);
1367 out.push('.');
1368 out.push_str(&f.to_lower_camel_case());
1369 out.push_str("()");
1370 let _ = is_leaf;
1371 let _ = optional_fields;
1372 }
1373 PathSegment::ArrayField { name, index } => {
1374 if !path_so_far.is_empty() {
1375 path_so_far.push('.');
1376 }
1377 path_so_far.push_str(name);
1378 out.push('.');
1379 out.push_str(&name.to_lower_camel_case());
1380 out.push_str(&format!("().get({index})"));
1381 }
1382 PathSegment::MapAccess { field, key } => {
1383 if !path_so_far.is_empty() {
1384 path_so_far.push('.');
1385 }
1386 path_so_far.push_str(field);
1387 out.push('.');
1388 out.push_str(&field.to_lower_camel_case());
1389 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1391 if is_numeric {
1392 out.push_str(&format!("().get({key})"));
1393 } else {
1394 out.push_str(&format!("().get(\"{key}\")"));
1395 }
1396 }
1397 PathSegment::Length => {
1398 out.push_str(".size()");
1399 }
1400 }
1401 }
1402 out
1403}
1404
1405fn render_kotlin_with_optionals(
1420 segments: &[PathSegment],
1421 result_var: &str,
1422 optional_fields: &HashSet<String>,
1423) -> String {
1424 let mut out = result_var.to_string();
1425 let mut path_so_far = String::new();
1426 let mut prev_was_nullable = false;
1434 for seg in segments {
1435 let nav = if prev_was_nullable { "?." } else { "." };
1436 match seg {
1437 PathSegment::Field(f) => {
1438 if !path_so_far.is_empty() {
1439 path_so_far.push('.');
1440 }
1441 path_so_far.push_str(f);
1442 let is_optional = optional_fields.contains(&path_so_far);
1447 out.push_str(nav);
1448 out.push_str(&kotlin_getter(f));
1449 out.push_str("()");
1450 prev_was_nullable = prev_was_nullable || is_optional;
1451 }
1452 PathSegment::ArrayField { name, index } => {
1453 if !path_so_far.is_empty() {
1454 path_so_far.push('.');
1455 }
1456 path_so_far.push_str(name);
1457 let is_optional = optional_fields.contains(&path_so_far);
1458 out.push_str(nav);
1459 out.push_str(&kotlin_getter(name));
1460 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1461 if *index == 0 {
1462 out.push_str(&format!("(){safe}.first()"));
1463 } else {
1464 out.push_str(&format!("(){safe}.get({index})"));
1465 }
1466 path_so_far.push_str("[0]");
1470 prev_was_nullable = prev_was_nullable || is_optional;
1471 }
1472 PathSegment::MapAccess { field, key } => {
1473 if !path_so_far.is_empty() {
1474 path_so_far.push('.');
1475 }
1476 path_so_far.push_str(field);
1477 let is_optional = optional_fields.contains(&path_so_far);
1478 out.push_str(nav);
1479 out.push_str(&kotlin_getter(field));
1480 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1481 if is_numeric {
1482 if prev_was_nullable || is_optional {
1483 out.push_str(&format!("()?.get({key})"));
1484 } else {
1485 out.push_str(&format!("().get({key})"));
1486 }
1487 } else if prev_was_nullable || is_optional {
1488 out.push_str(&format!("()?.get(\"{key}\")"));
1489 } else {
1490 out.push_str(&format!("().get(\"{key}\")"));
1491 }
1492 prev_was_nullable = prev_was_nullable || is_optional;
1493 }
1494 PathSegment::Length => {
1495 let size_nav = if prev_was_nullable { "?" } else { "" };
1498 out.push_str(&format!("{size_nav}.size"));
1499 prev_was_nullable = false;
1500 }
1501 }
1502 }
1503 out
1504}
1505
1506fn render_kotlin_android_with_optionals(
1517 segments: &[PathSegment],
1518 result_var: &str,
1519 optional_fields: &HashSet<String>,
1520) -> String {
1521 let mut out = result_var.to_string();
1522 let mut path_so_far = String::new();
1523 let mut prev_was_nullable = false;
1524 for seg in segments {
1525 let nav = if prev_was_nullable { "?." } else { "." };
1526 match seg {
1527 PathSegment::Field(f) => {
1528 if !path_so_far.is_empty() {
1529 path_so_far.push('.');
1530 }
1531 path_so_far.push_str(f);
1532 let is_optional = optional_fields.contains(&path_so_far);
1533 out.push_str(nav);
1534 out.push_str(&kotlin_getter(f));
1536 prev_was_nullable = prev_was_nullable || is_optional;
1537 }
1538 PathSegment::ArrayField { name, index } => {
1539 if !path_so_far.is_empty() {
1540 path_so_far.push('.');
1541 }
1542 path_so_far.push_str(name);
1543 let is_optional = optional_fields.contains(&path_so_far);
1544 out.push_str(nav);
1545 out.push_str(&kotlin_getter(name));
1547 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1548 if *index == 0 {
1549 out.push_str(&format!("{safe}.first()"));
1550 } else {
1551 out.push_str(&format!("{safe}.get({index})"));
1552 }
1553 path_so_far.push_str("[0]");
1554 prev_was_nullable = prev_was_nullable || is_optional;
1555 }
1556 PathSegment::MapAccess { field, key } => {
1557 if !path_so_far.is_empty() {
1558 path_so_far.push('.');
1559 }
1560 path_so_far.push_str(field);
1561 let is_optional = optional_fields.contains(&path_so_far);
1562 out.push_str(nav);
1563 out.push_str(&kotlin_getter(field));
1565 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1566 if is_numeric {
1567 if prev_was_nullable || is_optional {
1568 out.push_str(&format!("?.get({key})"));
1569 } else {
1570 out.push_str(&format!(".get({key})"));
1571 }
1572 } else if prev_was_nullable || is_optional {
1573 out.push_str(&format!("?.get(\"{key}\")"));
1574 } else {
1575 out.push_str(&format!(".get(\"{key}\")"));
1576 }
1577 prev_was_nullable = prev_was_nullable || is_optional;
1578 }
1579 PathSegment::Length => {
1580 let size_nav = if prev_was_nullable { "?" } else { "" };
1581 out.push_str(&format!("{size_nav}.size"));
1582 prev_was_nullable = false;
1583 }
1584 }
1585 }
1586 out
1587}
1588
1589fn render_kotlin_android(segments: &[PathSegment], result_var: &str) -> String {
1593 let mut out = result_var.to_string();
1594 for seg in segments {
1595 match seg {
1596 PathSegment::Field(f) => {
1597 out.push('.');
1598 out.push_str(&kotlin_getter(f));
1599 }
1601 PathSegment::ArrayField { name, index } => {
1602 out.push('.');
1603 out.push_str(&kotlin_getter(name));
1604 if *index == 0 {
1605 out.push_str(".first()");
1606 } else {
1607 out.push_str(&format!(".get({index})"));
1608 }
1609 }
1610 PathSegment::MapAccess { field, key } => {
1611 out.push('.');
1612 out.push_str(&kotlin_getter(field));
1613 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1614 if is_numeric {
1615 out.push_str(&format!(".get({key})"));
1616 } else {
1617 out.push_str(&format!(".get(\"{key}\")"));
1618 }
1619 }
1620 PathSegment::Length => {
1621 out.push_str(".size");
1622 }
1623 }
1624 }
1625 out
1626}
1627
1628fn render_rust_with_optionals(
1634 segments: &[PathSegment],
1635 result_var: &str,
1636 optional_fields: &HashSet<String>,
1637 method_calls: &HashSet<String>,
1638) -> String {
1639 let mut out = result_var.to_string();
1640 let mut path_so_far = String::new();
1641 for (i, seg) in segments.iter().enumerate() {
1642 let is_leaf = i == segments.len() - 1;
1643 match seg {
1644 PathSegment::Field(f) => {
1645 if !path_so_far.is_empty() {
1646 path_so_far.push('.');
1647 }
1648 path_so_far.push_str(f);
1649 out.push('.');
1650 out.push_str(&f.to_snake_case());
1651 let is_method = method_calls.contains(&path_so_far);
1652 if is_method {
1653 out.push_str("()");
1654 if !is_leaf && optional_fields.contains(&path_so_far) {
1655 out.push_str(".as_ref().unwrap()");
1656 }
1657 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1658 out.push_str(".as_ref().unwrap()");
1659 }
1660 }
1661 PathSegment::ArrayField { name, index } => {
1662 if !path_so_far.is_empty() {
1663 path_so_far.push('.');
1664 }
1665 path_so_far.push_str(name);
1666 out.push('.');
1667 out.push_str(&name.to_snake_case());
1668 let path_with_idx = format!("{path_so_far}[0]");
1672 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1673 if is_opt {
1674 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1675 } else {
1676 out.push_str(&format!("[{index}]"));
1677 }
1678 path_so_far.push_str("[0]");
1683 }
1684 PathSegment::MapAccess { field, key } => {
1685 if !path_so_far.is_empty() {
1686 path_so_far.push('.');
1687 }
1688 path_so_far.push_str(field);
1689 out.push('.');
1690 out.push_str(&field.to_snake_case());
1691 if key.chars().all(|c| c.is_ascii_digit()) {
1692 let path_with_idx = format!("{path_so_far}[0]");
1694 let is_opt =
1695 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1696 if is_opt {
1697 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1698 } else {
1699 out.push_str(&format!("[{key}]"));
1700 }
1701 path_so_far.push_str("[0]");
1702 } else {
1703 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1704 }
1705 }
1706 PathSegment::Length => {
1707 out.push_str(".len()");
1708 }
1709 }
1710 }
1711 out
1712}
1713
1714fn render_zig_with_optionals(
1727 segments: &[PathSegment],
1728 result_var: &str,
1729 optional_fields: &HashSet<String>,
1730 method_calls: &HashSet<String>,
1731) -> String {
1732 let mut out = result_var.to_string();
1733 let mut path_so_far = String::new();
1734 for seg in segments {
1735 match seg {
1736 PathSegment::Field(f) => {
1737 if !path_so_far.is_empty() {
1738 path_so_far.push('.');
1739 }
1740 path_so_far.push_str(f);
1741 out.push('.');
1742 out.push_str(f);
1743 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1744 out.push_str(".?");
1745 }
1746 }
1747 PathSegment::ArrayField { name, index } => {
1748 if !path_so_far.is_empty() {
1749 path_so_far.push('.');
1750 }
1751 path_so_far.push_str(name);
1752 out.push('.');
1753 out.push_str(name);
1754 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1755 out.push_str(".?");
1756 }
1757 out.push_str(&format!("[{index}]"));
1758 }
1759 PathSegment::MapAccess { field, key } => {
1760 if !path_so_far.is_empty() {
1761 path_so_far.push('.');
1762 }
1763 path_so_far.push_str(field);
1764 out.push('.');
1765 out.push_str(field);
1766 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1767 out.push_str(".?");
1768 }
1769 if key.chars().all(|c| c.is_ascii_digit()) {
1770 out.push_str(&format!("[{key}]"));
1771 } else {
1772 out.push_str(&format!(".get(\"{key}\")"));
1773 }
1774 }
1775 PathSegment::Length => {
1776 out.push_str(".len");
1777 }
1778 }
1779 }
1780 out
1781}
1782
1783fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1784 let mut out = result_var.to_string();
1785 for seg in segments {
1786 match seg {
1787 PathSegment::Field(f) => {
1788 out.push('.');
1789 out.push_str(&f.to_pascal_case());
1790 }
1791 PathSegment::ArrayField { name, index } => {
1792 out.push('.');
1793 out.push_str(&name.to_pascal_case());
1794 out.push_str(&format!("[{index}]"));
1795 }
1796 PathSegment::MapAccess { field, key } => {
1797 out.push('.');
1798 out.push_str(&field.to_pascal_case());
1799 if key.chars().all(|c| c.is_ascii_digit()) {
1800 out.push_str(&format!("[{key}]"));
1801 } else {
1802 out.push_str(&format!("[\"{key}\"]"));
1803 }
1804 }
1805 PathSegment::Length => {
1806 out.push_str(".Count");
1807 }
1808 }
1809 }
1810 out
1811}
1812
1813fn render_csharp_with_optionals(
1814 segments: &[PathSegment],
1815 result_var: &str,
1816 optional_fields: &HashSet<String>,
1817) -> String {
1818 let mut out = result_var.to_string();
1819 let mut path_so_far = String::new();
1820 for (i, seg) in segments.iter().enumerate() {
1821 let is_leaf = i == segments.len() - 1;
1822 match seg {
1823 PathSegment::Field(f) => {
1824 if !path_so_far.is_empty() {
1825 path_so_far.push('.');
1826 }
1827 path_so_far.push_str(f);
1828 out.push('.');
1829 out.push_str(&f.to_pascal_case());
1830 if !is_leaf && optional_fields.contains(&path_so_far) {
1831 out.push('!');
1832 }
1833 }
1834 PathSegment::ArrayField { name, index } => {
1835 if !path_so_far.is_empty() {
1836 path_so_far.push('.');
1837 }
1838 path_so_far.push_str(name);
1839 out.push('.');
1840 out.push_str(&name.to_pascal_case());
1841 out.push_str(&format!("[{index}]"));
1842 }
1843 PathSegment::MapAccess { field, key } => {
1844 if !path_so_far.is_empty() {
1845 path_so_far.push('.');
1846 }
1847 path_so_far.push_str(field);
1848 out.push('.');
1849 out.push_str(&field.to_pascal_case());
1850 if key.chars().all(|c| c.is_ascii_digit()) {
1851 out.push_str(&format!("[{key}]"));
1852 } else {
1853 out.push_str(&format!("[\"{key}\"]"));
1854 }
1855 }
1856 PathSegment::Length => {
1857 out.push_str(".Count");
1858 }
1859 }
1860 }
1861 out
1862}
1863
1864fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1865 let mut out = result_var.to_string();
1866 for seg in segments {
1867 match seg {
1868 PathSegment::Field(f) => {
1869 out.push_str("->");
1870 out.push_str(&f.to_lower_camel_case());
1873 }
1874 PathSegment::ArrayField { name, index } => {
1875 out.push_str("->");
1876 out.push_str(&name.to_lower_camel_case());
1877 out.push_str(&format!("[{index}]"));
1878 }
1879 PathSegment::MapAccess { field, key } => {
1880 out.push_str("->");
1881 out.push_str(&field.to_lower_camel_case());
1882 out.push_str(&format!("[\"{key}\"]"));
1883 }
1884 PathSegment::Length => {
1885 let current = std::mem::take(&mut out);
1886 out = format!("count({current})");
1887 }
1888 }
1889 }
1890 out
1891}
1892
1893fn render_php_with_getters(segments: &[PathSegment], result_var: &str, getter_map: &PhpGetterMap) -> String {
1911 let mut out = result_var.to_string();
1912 let mut current_type: Option<String> = getter_map.root_type.clone();
1913 for seg in segments {
1914 match seg {
1915 PathSegment::Field(f) => {
1916 let camel = f.to_lower_camel_case();
1917 if getter_map.needs_getter(current_type.as_deref(), f.as_str()) {
1918 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1923 out.push_str("->");
1924 out.push_str(&getter);
1925 out.push_str("()");
1926 } else {
1927 out.push_str("->");
1928 out.push_str(&camel);
1929 }
1930 current_type = getter_map.advance(current_type.as_deref(), f.as_str());
1931 }
1932 PathSegment::ArrayField { name, index } => {
1933 let camel = name.to_lower_camel_case();
1934 if getter_map.needs_getter(current_type.as_deref(), name.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!("[{index}]"));
1944 current_type = getter_map.advance(current_type.as_deref(), name.as_str());
1945 }
1946 PathSegment::MapAccess { field, key } => {
1947 let camel = field.to_lower_camel_case();
1948 if getter_map.needs_getter(current_type.as_deref(), field.as_str()) {
1949 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1950 out.push_str("->");
1951 out.push_str(&getter);
1952 out.push_str("()");
1953 } else {
1954 out.push_str("->");
1955 out.push_str(&camel);
1956 }
1957 out.push_str(&format!("[\"{key}\"]"));
1958 current_type = getter_map.advance(current_type.as_deref(), field.as_str());
1959 }
1960 PathSegment::Length => {
1961 let current = std::mem::take(&mut out);
1962 out = format!("count({current})");
1963 }
1964 }
1965 }
1966 out
1967}
1968
1969fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1970 let mut out = result_var.to_string();
1971 for seg in segments {
1972 match seg {
1973 PathSegment::Field(f) => {
1974 out.push('$');
1975 out.push_str(f);
1976 }
1977 PathSegment::ArrayField { name, index } => {
1978 out.push('$');
1979 out.push_str(name);
1980 out.push_str(&format!("[[{}]]", index + 1));
1982 }
1983 PathSegment::MapAccess { field, key } => {
1984 out.push('$');
1985 out.push_str(field);
1986 out.push_str(&format!("[[\"{key}\"]]"));
1987 }
1988 PathSegment::Length => {
1989 let current = std::mem::take(&mut out);
1990 out = format!("length({current})");
1991 }
1992 }
1993 }
1994 out
1995}
1996
1997fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1998 let mut parts = Vec::new();
1999 let mut trailing_length = false;
2000 for seg in segments {
2001 match seg {
2002 PathSegment::Field(f) => parts.push(f.to_snake_case()),
2003 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
2004 PathSegment::MapAccess { field, key } => {
2005 parts.push(field.to_snake_case());
2006 parts.push(key.clone());
2007 }
2008 PathSegment::Length => {
2009 trailing_length = true;
2010 }
2011 }
2012 }
2013 let suffix = parts.join("_");
2014 if trailing_length {
2015 format!("result_{suffix}_count({result_var})")
2016 } else {
2017 format!("result_{suffix}({result_var})")
2018 }
2019}
2020
2021fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
2028 let mut out = result_var.to_string();
2029 for seg in segments {
2030 match seg {
2031 PathSegment::Field(f) => {
2032 out.push('.');
2033 out.push_str(&f.to_lower_camel_case());
2034 }
2035 PathSegment::ArrayField { name, index } => {
2036 out.push('.');
2037 out.push_str(&name.to_lower_camel_case());
2038 out.push_str(&format!("[{index}]"));
2039 }
2040 PathSegment::MapAccess { field, key } => {
2041 out.push('.');
2042 out.push_str(&field.to_lower_camel_case());
2043 if key.chars().all(|c| c.is_ascii_digit()) {
2044 out.push_str(&format!("[{key}]"));
2045 } else {
2046 out.push_str(&format!("[\"{key}\"]"));
2047 }
2048 }
2049 PathSegment::Length => {
2050 out.push_str(".length");
2051 }
2052 }
2053 }
2054 out
2055}
2056
2057fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
2063 let mut out = result_var.to_string();
2064 let mut path_so_far = String::new();
2072 let mut path_with_indices = String::new();
2073 let mut prev_was_nullable = false;
2074 let is_optional =
2075 |bare: &str, indexed: &str| -> bool { optional_fields.contains(bare) || optional_fields.contains(indexed) };
2076 for seg in segments {
2077 let nav = if prev_was_nullable { "?." } else { "." };
2078 match seg {
2079 PathSegment::Field(f) => {
2080 if !path_so_far.is_empty() {
2081 path_so_far.push('.');
2082 path_with_indices.push('.');
2083 }
2084 path_so_far.push_str(f);
2085 path_with_indices.push_str(f);
2086 let optional = is_optional(&path_so_far, &path_with_indices);
2087 out.push_str(nav);
2088 out.push_str(&f.to_lower_camel_case());
2089 prev_was_nullable = optional;
2090 }
2091 PathSegment::ArrayField { name, index } => {
2092 if !path_so_far.is_empty() {
2093 path_so_far.push('.');
2094 path_with_indices.push('.');
2095 }
2096 path_so_far.push_str(name);
2097 path_with_indices.push_str(name);
2098 let optional = is_optional(&path_so_far, &path_with_indices);
2099 out.push_str(nav);
2100 out.push_str(&name.to_lower_camel_case());
2101 if optional {
2105 out.push('!');
2106 }
2107 out.push_str(&format!("[{index}]"));
2108 path_with_indices.push_str(&format!("[{index}]"));
2109 prev_was_nullable = false;
2110 }
2111 PathSegment::MapAccess { field, key } => {
2112 if !path_so_far.is_empty() {
2113 path_so_far.push('.');
2114 path_with_indices.push('.');
2115 }
2116 path_so_far.push_str(field);
2117 path_with_indices.push_str(field);
2118 let optional = is_optional(&path_so_far, &path_with_indices);
2119 out.push_str(nav);
2120 out.push_str(&field.to_lower_camel_case());
2121 if key.chars().all(|c| c.is_ascii_digit()) {
2122 out.push_str(&format!("[{key}]"));
2123 path_with_indices.push_str(&format!("[{key}]"));
2124 } else {
2125 out.push_str(&format!("[\"{key}\"]"));
2126 path_with_indices.push_str(&format!("[\"{key}\"]"));
2127 }
2128 prev_was_nullable = optional;
2129 }
2130 PathSegment::Length => {
2131 out.push_str(nav);
2134 out.push_str("length");
2135 prev_was_nullable = false;
2136 }
2137 }
2138 }
2139 out
2140}
2141
2142#[cfg(test)]
2143mod tests {
2144 use super::*;
2145
2146 fn make_resolver() -> FieldResolver {
2147 let mut fields = HashMap::new();
2148 fields.insert("title".to_string(), "metadata.document.title".to_string());
2149 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
2150 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
2151 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
2152 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
2153 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
2154 let mut optional = HashSet::new();
2155 optional.insert("metadata.document.title".to_string());
2156 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
2157 }
2158
2159 fn make_resolver_with_doc_optional() -> FieldResolver {
2160 let mut fields = HashMap::new();
2161 fields.insert("title".to_string(), "metadata.document.title".to_string());
2162 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
2163 let mut optional = HashSet::new();
2164 optional.insert("document".to_string());
2165 optional.insert("metadata.document.title".to_string());
2166 optional.insert("metadata.document".to_string());
2167 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
2168 }
2169
2170 #[test]
2171 fn test_resolve_alias() {
2172 let r = make_resolver();
2173 assert_eq!(r.resolve("title"), "metadata.document.title");
2174 }
2175
2176 #[test]
2177 fn test_resolve_passthrough() {
2178 let r = make_resolver();
2179 assert_eq!(r.resolve("content"), "content");
2180 }
2181
2182 #[test]
2183 fn test_is_optional() {
2184 let r = make_resolver();
2185 assert!(r.is_optional("metadata.document.title"));
2186 assert!(!r.is_optional("content"));
2187 }
2188
2189 #[test]
2190 fn is_optional_strips_namespace_prefix() {
2191 let fields = HashMap::new();
2192 let mut optional = HashSet::new();
2193 optional.insert("action_results.data".to_string());
2194 let result_fields: HashSet<String> = ["action_results".to_string()].into_iter().collect();
2195 let r = FieldResolver::new(&fields, &optional, &result_fields, &HashSet::new(), &HashSet::new());
2196 assert!(r.is_optional("interaction.action_results[0].data"));
2198 assert!(r.is_optional("action_results[0].data"));
2200 }
2201
2202 #[test]
2203 fn test_accessor_rust_struct() {
2204 let r = make_resolver();
2205 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
2206 }
2207
2208 #[test]
2209 fn test_accessor_rust_map() {
2210 let r = make_resolver();
2211 assert_eq!(
2212 r.accessor("tags", "rust", "result"),
2213 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
2214 );
2215 }
2216
2217 #[test]
2218 fn test_accessor_python() {
2219 let r = make_resolver();
2220 assert_eq!(
2221 r.accessor("title", "python", "result"),
2222 "result.metadata.document.title"
2223 );
2224 }
2225
2226 #[test]
2227 fn test_accessor_go() {
2228 let r = make_resolver();
2229 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
2230 }
2231
2232 #[test]
2233 fn test_accessor_go_initialism_fields() {
2234 let mut fields = std::collections::HashMap::new();
2235 fields.insert("content".to_string(), "html".to_string());
2236 fields.insert("link_url".to_string(), "links.url".to_string());
2237 let r = FieldResolver::new(
2238 &fields,
2239 &HashSet::new(),
2240 &HashSet::new(),
2241 &HashSet::new(),
2242 &HashSet::new(),
2243 );
2244 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
2245 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
2246 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
2247 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
2248 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
2249 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
2250 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
2251 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
2252 }
2253
2254 #[test]
2255 fn test_accessor_typescript() {
2256 let r = make_resolver();
2257 assert_eq!(
2258 r.accessor("title", "typescript", "result"),
2259 "result.metadata.document.title"
2260 );
2261 }
2262
2263 #[test]
2264 fn test_accessor_typescript_snake_to_camel() {
2265 let r = make_resolver();
2266 assert_eq!(
2267 r.accessor("og", "typescript", "result"),
2268 "result.metadata.document.openGraph"
2269 );
2270 assert_eq!(
2271 r.accessor("twitter", "typescript", "result"),
2272 "result.metadata.document.twitterCard"
2273 );
2274 assert_eq!(
2275 r.accessor("canonical", "typescript", "result"),
2276 "result.metadata.document.canonicalUrl"
2277 );
2278 }
2279
2280 #[test]
2281 fn test_accessor_typescript_map_snake_to_camel() {
2282 let r = make_resolver();
2283 assert_eq!(
2284 r.accessor("og_tag", "typescript", "result"),
2285 "result.metadata.openGraphTags[\"og_title\"]"
2286 );
2287 }
2288
2289 #[test]
2290 fn test_accessor_typescript_numeric_index_is_unquoted() {
2291 let mut fields = HashMap::new();
2295 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
2296 let r = FieldResolver::new(
2297 &fields,
2298 &HashSet::new(),
2299 &HashSet::new(),
2300 &HashSet::new(),
2301 &HashSet::new(),
2302 );
2303 assert_eq!(
2304 r.accessor("first_score", "typescript", "result"),
2305 "result.results[0].relevanceScore"
2306 );
2307 }
2308
2309 #[test]
2310 fn test_accessor_node_alias() {
2311 let r = make_resolver();
2312 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
2313 }
2314
2315 #[test]
2316 fn test_accessor_wasm_camel_case() {
2317 let r = make_resolver();
2318 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
2319 assert_eq!(
2320 r.accessor("twitter", "wasm", "result"),
2321 "result.metadata.document.twitterCard"
2322 );
2323 assert_eq!(
2324 r.accessor("canonical", "wasm", "result"),
2325 "result.metadata.document.canonicalUrl"
2326 );
2327 }
2328
2329 #[test]
2330 fn test_accessor_wasm_map_access() {
2331 let r = make_resolver();
2332 assert_eq!(
2333 r.accessor("og_tag", "wasm", "result"),
2334 "result.metadata.openGraphTags.get(\"og_title\")"
2335 );
2336 }
2337
2338 #[test]
2339 fn test_accessor_java() {
2340 let r = make_resolver();
2341 assert_eq!(
2342 r.accessor("title", "java", "result"),
2343 "result.metadata().document().title()"
2344 );
2345 }
2346
2347 #[test]
2348 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
2349 let mut fields = HashMap::new();
2350 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2351 fields.insert("node_count".to_string(), "nodes.length".to_string());
2352 let mut arrays = HashSet::new();
2353 arrays.insert("nodes".to_string());
2354 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
2355 assert_eq!(
2356 r.accessor("first_node_name", "kotlin", "result"),
2357 "result.nodes().first().name()"
2358 );
2359 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
2360 }
2361
2362 #[test]
2363 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
2364 let r = make_resolver_with_doc_optional();
2365 assert_eq!(
2366 r.accessor("title", "kotlin", "result"),
2367 "result.metadata().document()?.title()"
2368 );
2369 }
2370
2371 #[test]
2372 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
2373 let mut fields = HashMap::new();
2374 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2375 fields.insert("tag".to_string(), "tags[name]".to_string());
2376 let mut optional = HashSet::new();
2377 optional.insert("nodes".to_string());
2378 optional.insert("tags".to_string());
2379 let mut arrays = HashSet::new();
2380 arrays.insert("nodes".to_string());
2381 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2382 assert_eq!(
2383 r.accessor("first_node_name", "kotlin", "result"),
2384 "result.nodes()?.first()?.name()"
2385 );
2386 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
2387 }
2388
2389 #[test]
2395 fn test_accessor_kotlin_optional_field_after_indexed_array() {
2396 let mut fields = HashMap::new();
2399 fields.insert(
2400 "tool_call_name".to_string(),
2401 "choices[0].message.tool_calls[0].function.name".to_string(),
2402 );
2403 let mut optional = HashSet::new();
2404 optional.insert("choices[0].message.tool_calls".to_string());
2405 let mut arrays = HashSet::new();
2406 arrays.insert("choices".to_string());
2407 arrays.insert("choices[0].message.tool_calls".to_string());
2408 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2409 let expr = r.accessor("tool_call_name", "kotlin", "result");
2410 assert!(
2412 expr.contains("toolCalls()?.first()"),
2413 "expected toolCalls()?.first() for optional list, got: {expr}"
2414 );
2415 }
2416
2417 #[test]
2418 fn test_accessor_csharp() {
2419 let r = make_resolver();
2420 assert_eq!(
2421 r.accessor("title", "csharp", "result"),
2422 "result.Metadata.Document.Title"
2423 );
2424 }
2425
2426 #[test]
2427 fn test_accessor_php() {
2428 let r = make_resolver();
2429 assert_eq!(
2430 r.accessor("title", "php", "$result"),
2431 "$result->metadata->document->title"
2432 );
2433 }
2434
2435 #[test]
2436 fn test_accessor_r() {
2437 let r = make_resolver();
2438 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
2439 }
2440
2441 #[test]
2442 fn test_accessor_c() {
2443 let r = make_resolver();
2444 assert_eq!(
2445 r.accessor("title", "c", "result"),
2446 "result_metadata_document_title(result)"
2447 );
2448 }
2449
2450 #[test]
2451 fn test_rust_unwrap_binding() {
2452 let r = make_resolver();
2453 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
2454 assert_eq!(var, "metadata_document_title");
2455 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
2458 }
2459
2460 #[test]
2461 fn test_rust_unwrap_binding_non_optional() {
2462 let r = make_resolver();
2463 assert!(r.rust_unwrap_binding("content", "result").is_none());
2464 }
2465
2466 #[test]
2467 fn test_rust_unwrap_binding_collapses_double_underscore() {
2468 let mut aliases = HashMap::new();
2473 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
2474 let mut optional = HashSet::new();
2475 optional.insert("json_ld[].name".to_string());
2476 let mut array = HashSet::new();
2477 array.insert("json_ld".to_string());
2478 let result_fields = HashSet::new();
2479 let method_calls = HashSet::new();
2480 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
2481 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
2482 assert_eq!(var, "json_ld_name");
2483 }
2484
2485 #[test]
2486 fn test_direct_field_no_alias() {
2487 let r = make_resolver();
2488 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2489 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
2490 }
2491
2492 #[test]
2493 fn test_accessor_rust_with_optionals() {
2494 let r = make_resolver_with_doc_optional();
2495 assert_eq!(
2496 r.accessor("title", "rust", "result"),
2497 "result.metadata.document.as_ref().unwrap().title"
2498 );
2499 }
2500
2501 #[test]
2502 fn test_accessor_csharp_with_optionals() {
2503 let r = make_resolver_with_doc_optional();
2504 assert_eq!(
2505 r.accessor("title", "csharp", "result"),
2506 "result.Metadata.Document!.Title"
2507 );
2508 }
2509
2510 #[test]
2511 fn test_accessor_rust_non_optional_field() {
2512 let r = make_resolver();
2513 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2514 }
2515
2516 #[test]
2517 fn test_accessor_csharp_non_optional_field() {
2518 let r = make_resolver();
2519 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
2520 }
2521
2522 #[test]
2523 fn test_accessor_rust_method_call() {
2524 let mut fields = HashMap::new();
2526 fields.insert(
2527 "excel_sheet_count".to_string(),
2528 "metadata.format.excel.sheet_count".to_string(),
2529 );
2530 let mut optional = HashSet::new();
2531 optional.insert("metadata.format".to_string());
2532 optional.insert("metadata.format.excel".to_string());
2533 let mut method_calls = HashSet::new();
2534 method_calls.insert("metadata.format.excel".to_string());
2535 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
2536 assert_eq!(
2537 r.accessor("excel_sheet_count", "rust", "result"),
2538 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
2539 );
2540 }
2541
2542 fn make_php_getter_resolver() -> FieldResolver {
2547 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2548 getters.insert(
2549 "Root".to_string(),
2550 ["metadata".to_string(), "links".to_string()].into_iter().collect(),
2551 );
2552 let map = PhpGetterMap {
2553 getters,
2554 field_types: HashMap::new(),
2555 root_type: Some("Root".to_string()),
2556 all_fields: HashMap::new(),
2557 };
2558 FieldResolver::new_with_php_getters(
2559 &HashMap::new(),
2560 &HashSet::new(),
2561 &HashSet::new(),
2562 &HashSet::new(),
2563 &HashSet::new(),
2564 &HashMap::new(),
2565 map,
2566 )
2567 }
2568
2569 #[test]
2570 fn render_php_uses_getter_method_for_non_scalar_field() {
2571 let r = make_php_getter_resolver();
2572 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
2573 }
2574
2575 #[test]
2576 fn render_php_uses_property_for_scalar_field() {
2577 let r = make_php_getter_resolver();
2578 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2579 }
2580
2581 #[test]
2582 fn render_php_nested_non_scalar_uses_getter_then_property() {
2583 let mut fields = HashMap::new();
2584 fields.insert("title".to_string(), "metadata.title".to_string());
2585 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2586 getters.insert("Root".to_string(), ["metadata".to_string()].into_iter().collect());
2587 getters.insert("Metadata".to_string(), HashSet::new());
2589 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2590 field_types.insert(
2591 "Root".to_string(),
2592 [("metadata".to_string(), "Metadata".to_string())].into_iter().collect(),
2593 );
2594 let map = PhpGetterMap {
2595 getters,
2596 field_types,
2597 root_type: Some("Root".to_string()),
2598 all_fields: HashMap::new(),
2599 };
2600 let r = FieldResolver::new_with_php_getters(
2601 &fields,
2602 &HashSet::new(),
2603 &HashSet::new(),
2604 &HashSet::new(),
2605 &HashSet::new(),
2606 &HashMap::new(),
2607 map,
2608 );
2609 assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
2611 }
2612
2613 #[test]
2614 fn render_php_array_field_uses_getter_when_non_scalar() {
2615 let mut fields = HashMap::new();
2616 fields.insert("first_link".to_string(), "links[0]".to_string());
2617 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2618 getters.insert("Root".to_string(), ["links".to_string()].into_iter().collect());
2619 let map = PhpGetterMap {
2620 getters,
2621 field_types: HashMap::new(),
2622 root_type: Some("Root".to_string()),
2623 all_fields: HashMap::new(),
2624 };
2625 let r = FieldResolver::new_with_php_getters(
2626 &fields,
2627 &HashSet::new(),
2628 &HashSet::new(),
2629 &HashSet::new(),
2630 &HashSet::new(),
2631 &HashMap::new(),
2632 map,
2633 );
2634 assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
2635 }
2636
2637 #[test]
2638 fn render_php_falls_back_to_property_when_getter_fields_empty() {
2639 let r = FieldResolver::new(
2642 &HashMap::new(),
2643 &HashSet::new(),
2644 &HashSet::new(),
2645 &HashSet::new(),
2646 &HashSet::new(),
2647 );
2648 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2649 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
2650 }
2651
2652 #[test]
2656 fn render_php_with_getters_distinguishes_same_field_name_on_different_types() {
2657 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2658 getters.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2660 getters.insert("B".to_string(), HashSet::new());
2662 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2665 all_fields.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2666 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2667 let map_a = PhpGetterMap {
2668 getters: getters.clone(),
2669 field_types: HashMap::new(),
2670 root_type: Some("A".to_string()),
2671 all_fields: all_fields.clone(),
2672 };
2673 let map_b = PhpGetterMap {
2674 getters,
2675 field_types: HashMap::new(),
2676 root_type: Some("B".to_string()),
2677 all_fields,
2678 };
2679 let r_a = FieldResolver::new_with_php_getters(
2680 &HashMap::new(),
2681 &HashSet::new(),
2682 &HashSet::new(),
2683 &HashSet::new(),
2684 &HashSet::new(),
2685 &HashMap::new(),
2686 map_a,
2687 );
2688 let r_b = FieldResolver::new_with_php_getters(
2689 &HashMap::new(),
2690 &HashSet::new(),
2691 &HashSet::new(),
2692 &HashSet::new(),
2693 &HashSet::new(),
2694 &HashMap::new(),
2695 map_b,
2696 );
2697 assert_eq!(r_a.accessor("content", "php", "$a"), "$a->getContent()");
2698 assert_eq!(r_b.accessor("content", "php", "$b"), "$b->content");
2699 }
2700
2701 #[test]
2705 fn render_php_with_getters_chains_through_correct_type() {
2706 let mut fields = HashMap::new();
2707 fields.insert("nested_content".to_string(), "inner.content".to_string());
2708 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2709 getters.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2711 getters.insert("B".to_string(), HashSet::new());
2713 getters.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2716 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2717 field_types.insert(
2718 "Outer".to_string(),
2719 [("inner".to_string(), "B".to_string())].into_iter().collect(),
2720 );
2721 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2722 all_fields.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2723 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2724 all_fields.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2725 let map = PhpGetterMap {
2726 getters,
2727 field_types,
2728 root_type: Some("Outer".to_string()),
2729 all_fields,
2730 };
2731 let r = FieldResolver::new_with_php_getters(
2732 &fields,
2733 &HashSet::new(),
2734 &HashSet::new(),
2735 &HashSet::new(),
2736 &HashSet::new(),
2737 &HashMap::new(),
2738 map,
2739 );
2740 assert_eq!(
2741 r.accessor("nested_content", "php", "$result"),
2742 "$result->getInner()->content"
2743 );
2744 }
2745
2746 fn make_resolver_with_result_fields(result_fields: &[&str]) -> FieldResolver {
2751 let rf: HashSet<String> = result_fields.iter().map(|s| s.to_string()).collect();
2752 FieldResolver::new(&HashMap::new(), &HashSet::new(), &rf, &HashSet::new(), &HashSet::new())
2753 }
2754
2755 #[test]
2758 fn is_valid_for_result_accepts_virtual_namespace_prefix() {
2759 let r = make_resolver_with_result_fields(&["browser_used", "js_render_hint", "status_code"]);
2760 assert!(
2761 r.is_valid_for_result("browser.browser_used"),
2762 "browser.browser_used should be valid via namespace-prefix stripping"
2763 );
2764 assert!(
2765 r.is_valid_for_result("browser.js_render_hint"),
2766 "browser.js_render_hint should be valid via namespace-prefix stripping"
2767 );
2768 }
2769
2770 #[test]
2773 fn is_valid_for_result_accepts_namespace_prefix_before_array_field() {
2774 let r = make_resolver_with_result_fields(&["action_results", "final_html", "final_url"]);
2775 assert!(
2776 r.is_valid_for_result("interaction.action_results[0].action_type"),
2777 "interaction. prefix should be stripped so action_results is recognised"
2778 );
2779 }
2780
2781 #[test]
2783 fn is_valid_for_result_rejects_unknown_field_even_after_namespace_strip() {
2784 let r = make_resolver_with_result_fields(&["pages", "final_url"]);
2785 assert!(
2786 !r.is_valid_for_result("browser.browser_used"),
2787 "browser_used is not in result_fields so should be rejected"
2788 );
2789 assert!(
2790 !r.is_valid_for_result("ns.unknown_field"),
2791 "unknown_field is not in result_fields so should be rejected"
2792 );
2793 }
2794
2795 #[test]
2798 fn accessor_strips_namespace_prefix_for_python() {
2799 let r = make_resolver_with_result_fields(&["browser_used", "js_render_hint"]);
2800 assert_eq!(
2801 r.accessor("browser.browser_used", "python", "result"),
2802 "result.browser_used"
2803 );
2804 assert_eq!(
2805 r.accessor("browser.js_render_hint", "python", "result"),
2806 "result.js_render_hint"
2807 );
2808 }
2809
2810 #[test]
2812 fn accessor_strips_namespace_prefix_for_csharp() {
2813 let r = make_resolver_with_result_fields(&["browser_used"]);
2814 assert_eq!(
2815 r.accessor("browser.browser_used", "csharp", "result"),
2816 "result.BrowserUsed"
2817 );
2818 }
2819
2820 #[test]
2823 fn accessor_strips_namespace_prefix_for_indexed_array_field() {
2824 let r = make_resolver_with_result_fields(&["action_results", "final_html", "final_url"]);
2825 assert_eq!(
2827 r.accessor("interaction.action_results[0].action_type", "python", "result"),
2828 "result.action_results[0].action_type"
2829 );
2830 assert_eq!(
2832 r.accessor("interaction.action_results[0].action_type", "typescript", "result"),
2833 "result.actionResults[0].actionType"
2834 );
2835 }
2836
2837 #[test]
2840 fn is_valid_for_result_is_permissive_when_result_fields_empty() {
2841 let r = make_resolver_with_result_fields(&[]);
2842 assert!(r.is_valid_for_result("browser.browser_used"));
2843 assert!(r.is_valid_for_result("anything.at.all"));
2844 }
2845
2846 #[test]
2849 fn accessor_does_not_strip_real_first_segment() {
2850 let r = make_resolver_with_result_fields(&["metadata", "status_code"]);
2851 assert_eq!(
2853 r.accessor("metadata.title", "python", "result"),
2854 "result.metadata.title"
2855 );
2856 }
2857}