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| format!(\"{{:?}}\", v)).unwrap_or_default();")
748 };
749 Some((binding, local_var))
750 }
751}
752
753fn strip_numeric_indices(path: &str) -> String {
758 let mut result = String::with_capacity(path.len());
759 let mut chars = path.chars().peekable();
760 while let Some(c) = chars.next() {
761 if c == '[' {
762 let mut key = String::new();
763 let mut closed = false;
764 for inner in chars.by_ref() {
765 if inner == ']' {
766 closed = true;
767 break;
768 }
769 key.push(inner);
770 }
771 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
772 } else {
774 result.push('[');
775 result.push_str(&key);
776 if closed {
777 result.push(']');
778 }
779 }
780 } else {
781 result.push(c);
782 }
783 }
784 while result.contains("..") {
786 result = result.replace("..", ".");
787 }
788 if result.starts_with('.') {
789 result.remove(0);
790 }
791 result
792}
793
794fn normalize_numeric_indices(path: &str) -> String {
795 let mut result = String::with_capacity(path.len());
796 let mut chars = path.chars().peekable();
797 while let Some(c) = chars.next() {
798 if c == '[' {
799 let mut key = String::new();
800 let mut closed = false;
801 for inner in chars.by_ref() {
802 if inner == ']' {
803 closed = true;
804 break;
805 }
806 key.push(inner);
807 }
808 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
809 result.push_str("[0]");
810 } else {
811 result.push('[');
812 result.push_str(&key);
813 if closed {
814 result.push(']');
815 }
816 }
817 } else {
818 result.push(c);
819 }
820 }
821 result
822}
823
824fn parse_path(path: &str) -> Vec<PathSegment> {
825 let mut segments = Vec::new();
826 for part in path.split('.') {
827 if part == "length" || part == "count" || part == "size" {
828 segments.push(PathSegment::Length);
829 } else if let Some(bracket_pos) = part.find('[') {
830 let name = part[..bracket_pos].to_string();
831 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
832 if key.is_empty() {
833 segments.push(PathSegment::ArrayField { name, index: 0 });
835 } else if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
836 let index: usize = key.parse().unwrap_or(0);
838 segments.push(PathSegment::ArrayField { name, index });
839 } else {
840 segments.push(PathSegment::MapAccess { field: name, key });
842 }
843 } else {
844 segments.push(PathSegment::Field(part.to_string()));
845 }
846 }
847 segments
848}
849
850fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
851 match language {
852 "rust" => render_rust(segments, result_var),
853 "python" => render_dot_access(segments, result_var, "python"),
854 "typescript" | "node" => render_typescript(segments, result_var),
855 "wasm" => render_wasm(segments, result_var),
856 "go" => render_go(segments, result_var),
857 "java" => render_java(segments, result_var),
858 "kotlin" => render_kotlin(segments, result_var),
859 "kotlin_android" => render_kotlin_android(segments, result_var),
860 "csharp" => render_pascal_dot(segments, result_var),
861 "ruby" => render_dot_access(segments, result_var, "ruby"),
862 "php" => render_php(segments, result_var),
863 "elixir" => render_dot_access(segments, result_var, "elixir"),
864 "r" => render_r(segments, result_var),
865 "c" => render_c(segments, result_var),
866 "swift" => render_swift(segments, result_var),
867 "dart" => render_dart(segments, result_var),
868 _ => render_dot_access(segments, result_var, language),
869 }
870}
871
872fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
884 let mut out = result_var.to_string();
885 for seg in segments {
886 match seg {
887 PathSegment::Field(f) => {
888 out.push('.');
889 out.push_str(&f.to_lower_camel_case());
890 }
891 PathSegment::ArrayField { name, index } => {
892 out.push('.');
893 out.push_str(&name.to_lower_camel_case());
894 out.push_str(&format!("[{index}]"));
895 }
896 PathSegment::MapAccess { field, key } => {
897 out.push('.');
898 out.push_str(&field.to_lower_camel_case());
899 if key.chars().all(|c| c.is_ascii_digit()) {
900 out.push_str(&format!("[{key}]"));
901 } else {
902 out.push_str(&format!("[\"{key}\"]"));
903 }
904 }
905 PathSegment::Length => {
906 out.push_str(".count");
907 }
908 }
909 }
910 out
911}
912
913fn render_swift_with_optionals(
923 segments: &[PathSegment],
924 result_var: &str,
925 optional_fields: &HashSet<String>,
926) -> String {
927 let mut out = result_var.to_string();
928 let mut path_so_far = String::new();
929 let total = segments.len();
930 for (i, seg) in segments.iter().enumerate() {
931 let is_leaf = i == total - 1;
932 match seg {
933 PathSegment::Field(f) => {
934 if !path_so_far.is_empty() {
935 path_so_far.push('.');
936 }
937 path_so_far.push_str(f);
938 out.push('.');
939 out.push_str(&f.to_lower_camel_case());
942 if !is_leaf && optional_fields.contains(&path_so_far) {
946 out.push('?');
947 }
948 }
949 PathSegment::ArrayField { name, index } => {
950 if !path_so_far.is_empty() {
951 path_so_far.push('.');
952 }
953 path_so_far.push_str(name);
954 let is_optional = optional_fields.contains(&path_so_far);
955 out.push('.');
956 out.push_str(&name.to_lower_camel_case());
957 if is_optional {
958 out.push_str(&format!("?[{index}]"));
960 } else {
961 out.push_str(&format!("[{index}]"));
962 }
963 path_so_far.push_str("[0]");
964 let _ = is_leaf;
965 }
966 PathSegment::MapAccess { field, key } => {
967 if !path_so_far.is_empty() {
968 path_so_far.push('.');
969 }
970 path_so_far.push_str(field);
971 out.push('.');
972 out.push_str(&field.to_lower_camel_case());
973 if key.chars().all(|c| c.is_ascii_digit()) {
974 out.push_str(&format!("[{key}]"));
975 } else {
976 out.push_str(&format!("[\"{key}\"]"));
977 }
978 }
979 PathSegment::Length => {
980 out.push_str(".count");
981 }
982 }
983 }
984 out
985}
986
987fn render_swift_with_first_class_map(
992 segments: &[PathSegment],
993 result_var: &str,
994 optional_fields: &HashSet<String>,
995 map: &SwiftFirstClassMap,
996) -> String {
997 let mut out = result_var.to_string();
998 let mut path_so_far = String::new();
999 let mut current_type: Option<String> = map.root_type.clone();
1000 let mut via_rust_vec = false;
1009 let total = segments.len();
1010 for (i, seg) in segments.iter().enumerate() {
1011 let is_leaf = i == total - 1;
1012 let property_syntax = !via_rust_vec && map.is_first_class(current_type.as_deref());
1013 match seg {
1014 PathSegment::Field(f) => {
1015 if !path_so_far.is_empty() {
1016 path_so_far.push('.');
1017 }
1018 path_so_far.push_str(f);
1019 out.push('.');
1020 out.push_str(&f.to_lower_camel_case());
1023 if !property_syntax {
1024 out.push_str("()");
1025 }
1026 if !is_leaf && optional_fields.contains(&path_so_far) {
1027 out.push('?');
1028 }
1029 current_type = map.advance(current_type.as_deref(), f);
1030 }
1031 PathSegment::ArrayField { name, index } => {
1032 if !path_so_far.is_empty() {
1033 path_so_far.push('.');
1034 }
1035 path_so_far.push_str(name);
1036 let is_optional = optional_fields.contains(&path_so_far);
1037 out.push('.');
1038 out.push_str(&name.to_lower_camel_case());
1039 let access = if property_syntax { "" } else { "()" };
1040 if is_optional {
1041 out.push_str(&format!("{access}?[{index}]"));
1042 } else {
1043 out.push_str(&format!("{access}[{index}]"));
1044 }
1045 path_so_far.push_str("[0]");
1046 current_type = map.advance(current_type.as_deref(), name);
1052 if !property_syntax {
1053 via_rust_vec = true;
1054 }
1055 }
1056 PathSegment::MapAccess { field, key } => {
1057 if !path_so_far.is_empty() {
1058 path_so_far.push('.');
1059 }
1060 path_so_far.push_str(field);
1061 out.push('.');
1062 out.push_str(&field.to_lower_camel_case());
1063 let access = if property_syntax { "" } else { "()" };
1064 if key.chars().all(|c| c.is_ascii_digit()) {
1065 out.push_str(&format!("{access}[{key}]"));
1066 } else {
1067 out.push_str(&format!("{access}[\"{key}\"]"));
1068 }
1069 current_type = map.advance(current_type.as_deref(), field);
1070 }
1071 PathSegment::Length => {
1072 out.push_str(".count");
1073 }
1074 }
1075 }
1076 out
1077}
1078
1079fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
1080 let mut out = result_var.to_string();
1081 for seg in segments {
1082 match seg {
1083 PathSegment::Field(f) => {
1084 out.push('.');
1085 out.push_str(&f.to_snake_case());
1086 }
1087 PathSegment::ArrayField { name, index } => {
1088 out.push('.');
1089 out.push_str(&name.to_snake_case());
1090 out.push_str(&format!("[{index}]"));
1091 }
1092 PathSegment::MapAccess { field, key } => {
1093 out.push('.');
1094 out.push_str(&field.to_snake_case());
1095 if key.chars().all(|c| c.is_ascii_digit()) {
1096 out.push_str(&format!("[{key}]"));
1097 } else {
1098 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1099 }
1100 }
1101 PathSegment::Length => {
1102 out.push_str(".len()");
1103 }
1104 }
1105 }
1106 out
1107}
1108
1109fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
1110 let mut out = result_var.to_string();
1111 for seg in segments {
1112 match seg {
1113 PathSegment::Field(f) => {
1114 out.push('.');
1115 out.push_str(f);
1116 }
1117 PathSegment::ArrayField { name, index } => {
1118 if language == "elixir" {
1119 let current = std::mem::take(&mut out);
1120 out = format!("Enum.at({current}.{name}, {index})");
1121 } else {
1122 out.push('.');
1123 out.push_str(name);
1124 out.push_str(&format!("[{index}]"));
1125 }
1126 }
1127 PathSegment::MapAccess { field, key } => {
1128 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
1129 if is_numeric && language == "elixir" {
1130 let current = std::mem::take(&mut out);
1131 out = format!("Enum.at({current}.{field}, {key})");
1132 } else {
1133 out.push('.');
1134 out.push_str(field);
1135 if is_numeric {
1136 let idx: usize = key.parse().unwrap_or(0);
1137 out.push_str(&format!("[{idx}]"));
1138 } else if language == "elixir" || language == "ruby" {
1139 out.push_str(&format!("[\"{key}\"]"));
1142 } else {
1143 out.push_str(&format!(".get(\"{key}\")"));
1144 }
1145 }
1146 }
1147 PathSegment::Length => match language {
1148 "ruby" => out.push_str(".length"),
1149 "elixir" => {
1150 let current = std::mem::take(&mut out);
1151 out = format!("length({current})");
1152 }
1153 "gleam" => {
1154 let current = std::mem::take(&mut out);
1155 out = format!("list.length({current})");
1156 }
1157 _ => {
1158 let current = std::mem::take(&mut out);
1159 out = format!("len({current})");
1160 }
1161 },
1162 }
1163 }
1164 out
1165}
1166
1167fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
1168 let mut out = result_var.to_string();
1169 for seg in segments {
1170 match seg {
1171 PathSegment::Field(f) => {
1172 out.push('.');
1173 out.push_str(&f.to_lower_camel_case());
1174 }
1175 PathSegment::ArrayField { name, index } => {
1176 out.push('.');
1177 out.push_str(&name.to_lower_camel_case());
1178 out.push_str(&format!("[{index}]"));
1179 }
1180 PathSegment::MapAccess { field, key } => {
1181 out.push('.');
1182 out.push_str(&field.to_lower_camel_case());
1183 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
1186 out.push_str(&format!("[{key}]"));
1187 } else {
1188 out.push_str(&format!("[\"{key}\"]"));
1189 }
1190 }
1191 PathSegment::Length => {
1192 out.push_str(".length");
1193 }
1194 }
1195 }
1196 out
1197}
1198
1199fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
1200 let mut out = result_var.to_string();
1201 for seg in segments {
1202 match seg {
1203 PathSegment::Field(f) => {
1204 out.push('.');
1205 out.push_str(&f.to_lower_camel_case());
1206 }
1207 PathSegment::ArrayField { name, index } => {
1208 out.push('.');
1209 out.push_str(&name.to_lower_camel_case());
1210 out.push_str(&format!("[{index}]"));
1211 }
1212 PathSegment::MapAccess { field, key } => {
1213 out.push('.');
1214 out.push_str(&field.to_lower_camel_case());
1215 out.push_str(&format!(".get(\"{key}\")"));
1216 }
1217 PathSegment::Length => {
1218 out.push_str(".length");
1219 }
1220 }
1221 }
1222 out
1223}
1224
1225fn render_go(segments: &[PathSegment], result_var: &str) -> String {
1226 let mut out = result_var.to_string();
1227 for seg in segments {
1228 match seg {
1229 PathSegment::Field(f) => {
1230 out.push('.');
1231 out.push_str(&to_go_name(f));
1232 }
1233 PathSegment::ArrayField { name, index } => {
1234 out.push('.');
1235 out.push_str(&to_go_name(name));
1236 out.push_str(&format!("[{index}]"));
1237 }
1238 PathSegment::MapAccess { field, key } => {
1239 out.push('.');
1240 out.push_str(&to_go_name(field));
1241 if key.chars().all(|c| c.is_ascii_digit()) {
1242 out.push_str(&format!("[{key}]"));
1243 } else {
1244 out.push_str(&format!("[\"{key}\"]"));
1245 }
1246 }
1247 PathSegment::Length => {
1248 let current = std::mem::take(&mut out);
1249 out = format!("len({current})");
1250 }
1251 }
1252 }
1253 out
1254}
1255
1256fn render_java(segments: &[PathSegment], result_var: &str) -> String {
1257 let mut out = result_var.to_string();
1258 for seg in segments {
1259 match seg {
1260 PathSegment::Field(f) => {
1261 out.push('.');
1262 out.push_str(&f.to_lower_camel_case());
1263 out.push_str("()");
1264 }
1265 PathSegment::ArrayField { name, index } => {
1266 out.push('.');
1267 out.push_str(&name.to_lower_camel_case());
1268 out.push_str(&format!("().get({index})"));
1269 }
1270 PathSegment::MapAccess { field, key } => {
1271 out.push('.');
1272 out.push_str(&field.to_lower_camel_case());
1273 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1275 if is_numeric {
1276 out.push_str(&format!("().get({key})"));
1277 } else {
1278 out.push_str(&format!("().get(\"{key}\")"));
1279 }
1280 }
1281 PathSegment::Length => {
1282 out.push_str(".size()");
1283 }
1284 }
1285 }
1286 out
1287}
1288
1289fn kotlin_getter(name: &str) -> String {
1294 let camel = name.to_lower_camel_case();
1295 match camel.as_str() {
1296 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
1297 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
1298 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
1299 _ => camel,
1300 }
1301}
1302
1303fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
1304 let mut out = result_var.to_string();
1305 for seg in segments {
1306 match seg {
1307 PathSegment::Field(f) => {
1308 out.push('.');
1309 out.push_str(&kotlin_getter(f));
1310 out.push_str("()");
1311 }
1312 PathSegment::ArrayField { name, index } => {
1313 out.push('.');
1314 out.push_str(&kotlin_getter(name));
1315 if *index == 0 {
1316 out.push_str("().first()");
1317 } else {
1318 out.push_str(&format!("().get({index})"));
1319 }
1320 }
1321 PathSegment::MapAccess { field, key } => {
1322 out.push('.');
1323 out.push_str(&kotlin_getter(field));
1324 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1325 if is_numeric {
1326 out.push_str(&format!("().get({key})"));
1327 } else {
1328 out.push_str(&format!("().get(\"{key}\")"));
1329 }
1330 }
1331 PathSegment::Length => {
1332 out.push_str(".size");
1333 }
1334 }
1335 }
1336 out
1337}
1338
1339fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1340 let mut out = result_var.to_string();
1341 let mut path_so_far = String::new();
1342 for (i, seg) in segments.iter().enumerate() {
1343 let is_leaf = i == segments.len() - 1;
1344 match seg {
1345 PathSegment::Field(f) => {
1346 if !path_so_far.is_empty() {
1347 path_so_far.push('.');
1348 }
1349 path_so_far.push_str(f);
1350 out.push('.');
1351 out.push_str(&f.to_lower_camel_case());
1352 out.push_str("()");
1353 let _ = is_leaf;
1354 let _ = optional_fields;
1355 }
1356 PathSegment::ArrayField { name, index } => {
1357 if !path_so_far.is_empty() {
1358 path_so_far.push('.');
1359 }
1360 path_so_far.push_str(name);
1361 out.push('.');
1362 out.push_str(&name.to_lower_camel_case());
1363 out.push_str(&format!("().get({index})"));
1364 }
1365 PathSegment::MapAccess { field, key } => {
1366 if !path_so_far.is_empty() {
1367 path_so_far.push('.');
1368 }
1369 path_so_far.push_str(field);
1370 out.push('.');
1371 out.push_str(&field.to_lower_camel_case());
1372 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1374 if is_numeric {
1375 out.push_str(&format!("().get({key})"));
1376 } else {
1377 out.push_str(&format!("().get(\"{key}\")"));
1378 }
1379 }
1380 PathSegment::Length => {
1381 out.push_str(".size()");
1382 }
1383 }
1384 }
1385 out
1386}
1387
1388fn render_kotlin_with_optionals(
1403 segments: &[PathSegment],
1404 result_var: &str,
1405 optional_fields: &HashSet<String>,
1406) -> String {
1407 let mut out = result_var.to_string();
1408 let mut path_so_far = String::new();
1409 let mut prev_was_nullable = false;
1417 for seg in segments {
1418 let nav = if prev_was_nullable { "?." } else { "." };
1419 match seg {
1420 PathSegment::Field(f) => {
1421 if !path_so_far.is_empty() {
1422 path_so_far.push('.');
1423 }
1424 path_so_far.push_str(f);
1425 let is_optional = optional_fields.contains(&path_so_far);
1430 out.push_str(nav);
1431 out.push_str(&kotlin_getter(f));
1432 out.push_str("()");
1433 prev_was_nullable = prev_was_nullable || is_optional;
1434 }
1435 PathSegment::ArrayField { name, index } => {
1436 if !path_so_far.is_empty() {
1437 path_so_far.push('.');
1438 }
1439 path_so_far.push_str(name);
1440 let is_optional = optional_fields.contains(&path_so_far);
1441 out.push_str(nav);
1442 out.push_str(&kotlin_getter(name));
1443 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1444 if *index == 0 {
1445 out.push_str(&format!("(){safe}.first()"));
1446 } else {
1447 out.push_str(&format!("(){safe}.get({index})"));
1448 }
1449 path_so_far.push_str("[0]");
1453 prev_was_nullable = prev_was_nullable || is_optional;
1454 }
1455 PathSegment::MapAccess { field, key } => {
1456 if !path_so_far.is_empty() {
1457 path_so_far.push('.');
1458 }
1459 path_so_far.push_str(field);
1460 let is_optional = optional_fields.contains(&path_so_far);
1461 out.push_str(nav);
1462 out.push_str(&kotlin_getter(field));
1463 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1464 if is_numeric {
1465 if prev_was_nullable || is_optional {
1466 out.push_str(&format!("()?.get({key})"));
1467 } else {
1468 out.push_str(&format!("().get({key})"));
1469 }
1470 } else if prev_was_nullable || is_optional {
1471 out.push_str(&format!("()?.get(\"{key}\")"));
1472 } else {
1473 out.push_str(&format!("().get(\"{key}\")"));
1474 }
1475 prev_was_nullable = prev_was_nullable || is_optional;
1476 }
1477 PathSegment::Length => {
1478 let size_nav = if prev_was_nullable { "?" } else { "" };
1481 out.push_str(&format!("{size_nav}.size"));
1482 prev_was_nullable = false;
1483 }
1484 }
1485 }
1486 out
1487}
1488
1489fn render_kotlin_android_with_optionals(
1500 segments: &[PathSegment],
1501 result_var: &str,
1502 optional_fields: &HashSet<String>,
1503) -> String {
1504 let mut out = result_var.to_string();
1505 let mut path_so_far = String::new();
1506 let mut prev_was_nullable = false;
1507 for seg in segments {
1508 let nav = if prev_was_nullable { "?." } else { "." };
1509 match seg {
1510 PathSegment::Field(f) => {
1511 if !path_so_far.is_empty() {
1512 path_so_far.push('.');
1513 }
1514 path_so_far.push_str(f);
1515 let is_optional = optional_fields.contains(&path_so_far);
1516 out.push_str(nav);
1517 out.push_str(&kotlin_getter(f));
1519 prev_was_nullable = prev_was_nullable || is_optional;
1520 }
1521 PathSegment::ArrayField { name, index } => {
1522 if !path_so_far.is_empty() {
1523 path_so_far.push('.');
1524 }
1525 path_so_far.push_str(name);
1526 let is_optional = optional_fields.contains(&path_so_far);
1527 out.push_str(nav);
1528 out.push_str(&kotlin_getter(name));
1530 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1531 if *index == 0 {
1532 out.push_str(&format!("{safe}.first()"));
1533 } else {
1534 out.push_str(&format!("{safe}.get({index})"));
1535 }
1536 path_so_far.push_str("[0]");
1537 prev_was_nullable = prev_was_nullable || is_optional;
1538 }
1539 PathSegment::MapAccess { field, key } => {
1540 if !path_so_far.is_empty() {
1541 path_so_far.push('.');
1542 }
1543 path_so_far.push_str(field);
1544 let is_optional = optional_fields.contains(&path_so_far);
1545 out.push_str(nav);
1546 out.push_str(&kotlin_getter(field));
1548 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1549 if is_numeric {
1550 if prev_was_nullable || is_optional {
1551 out.push_str(&format!("?.get({key})"));
1552 } else {
1553 out.push_str(&format!(".get({key})"));
1554 }
1555 } else if prev_was_nullable || is_optional {
1556 out.push_str(&format!("?.get(\"{key}\")"));
1557 } else {
1558 out.push_str(&format!(".get(\"{key}\")"));
1559 }
1560 prev_was_nullable = prev_was_nullable || is_optional;
1561 }
1562 PathSegment::Length => {
1563 let size_nav = if prev_was_nullable { "?" } else { "" };
1564 out.push_str(&format!("{size_nav}.size"));
1565 prev_was_nullable = false;
1566 }
1567 }
1568 }
1569 out
1570}
1571
1572fn render_kotlin_android(segments: &[PathSegment], result_var: &str) -> String {
1576 let mut out = result_var.to_string();
1577 for seg in segments {
1578 match seg {
1579 PathSegment::Field(f) => {
1580 out.push('.');
1581 out.push_str(&kotlin_getter(f));
1582 }
1584 PathSegment::ArrayField { name, index } => {
1585 out.push('.');
1586 out.push_str(&kotlin_getter(name));
1587 if *index == 0 {
1588 out.push_str(".first()");
1589 } else {
1590 out.push_str(&format!(".get({index})"));
1591 }
1592 }
1593 PathSegment::MapAccess { field, key } => {
1594 out.push('.');
1595 out.push_str(&kotlin_getter(field));
1596 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1597 if is_numeric {
1598 out.push_str(&format!(".get({key})"));
1599 } else {
1600 out.push_str(&format!(".get(\"{key}\")"));
1601 }
1602 }
1603 PathSegment::Length => {
1604 out.push_str(".size");
1605 }
1606 }
1607 }
1608 out
1609}
1610
1611fn render_rust_with_optionals(
1617 segments: &[PathSegment],
1618 result_var: &str,
1619 optional_fields: &HashSet<String>,
1620 method_calls: &HashSet<String>,
1621) -> String {
1622 let mut out = result_var.to_string();
1623 let mut path_so_far = String::new();
1624 for (i, seg) in segments.iter().enumerate() {
1625 let is_leaf = i == segments.len() - 1;
1626 match seg {
1627 PathSegment::Field(f) => {
1628 if !path_so_far.is_empty() {
1629 path_so_far.push('.');
1630 }
1631 path_so_far.push_str(f);
1632 out.push('.');
1633 out.push_str(&f.to_snake_case());
1634 let is_method = method_calls.contains(&path_so_far);
1635 if is_method {
1636 out.push_str("()");
1637 if !is_leaf && optional_fields.contains(&path_so_far) {
1638 out.push_str(".as_ref().unwrap()");
1639 }
1640 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1641 out.push_str(".as_ref().unwrap()");
1642 }
1643 }
1644 PathSegment::ArrayField { name, index } => {
1645 if !path_so_far.is_empty() {
1646 path_so_far.push('.');
1647 }
1648 path_so_far.push_str(name);
1649 out.push('.');
1650 out.push_str(&name.to_snake_case());
1651 let path_with_idx = format!("{path_so_far}[0]");
1655 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1656 if is_opt {
1657 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1658 } else {
1659 out.push_str(&format!("[{index}]"));
1660 }
1661 path_so_far.push_str("[0]");
1666 }
1667 PathSegment::MapAccess { field, key } => {
1668 if !path_so_far.is_empty() {
1669 path_so_far.push('.');
1670 }
1671 path_so_far.push_str(field);
1672 out.push('.');
1673 out.push_str(&field.to_snake_case());
1674 if key.chars().all(|c| c.is_ascii_digit()) {
1675 let path_with_idx = format!("{path_so_far}[0]");
1677 let is_opt =
1678 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1679 if is_opt {
1680 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1681 } else {
1682 out.push_str(&format!("[{key}]"));
1683 }
1684 path_so_far.push_str("[0]");
1685 } else {
1686 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1687 }
1688 }
1689 PathSegment::Length => {
1690 out.push_str(".len()");
1691 }
1692 }
1693 }
1694 out
1695}
1696
1697fn render_zig_with_optionals(
1710 segments: &[PathSegment],
1711 result_var: &str,
1712 optional_fields: &HashSet<String>,
1713 method_calls: &HashSet<String>,
1714) -> String {
1715 let mut out = result_var.to_string();
1716 let mut path_so_far = String::new();
1717 for seg in segments {
1718 match seg {
1719 PathSegment::Field(f) => {
1720 if !path_so_far.is_empty() {
1721 path_so_far.push('.');
1722 }
1723 path_so_far.push_str(f);
1724 out.push('.');
1725 out.push_str(f);
1726 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1727 out.push_str(".?");
1728 }
1729 }
1730 PathSegment::ArrayField { name, index } => {
1731 if !path_so_far.is_empty() {
1732 path_so_far.push('.');
1733 }
1734 path_so_far.push_str(name);
1735 out.push('.');
1736 out.push_str(name);
1737 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1738 out.push_str(".?");
1739 }
1740 out.push_str(&format!("[{index}]"));
1741 }
1742 PathSegment::MapAccess { field, key } => {
1743 if !path_so_far.is_empty() {
1744 path_so_far.push('.');
1745 }
1746 path_so_far.push_str(field);
1747 out.push('.');
1748 out.push_str(field);
1749 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1750 out.push_str(".?");
1751 }
1752 if key.chars().all(|c| c.is_ascii_digit()) {
1753 out.push_str(&format!("[{key}]"));
1754 } else {
1755 out.push_str(&format!(".get(\"{key}\")"));
1756 }
1757 }
1758 PathSegment::Length => {
1759 out.push_str(".len");
1760 }
1761 }
1762 }
1763 out
1764}
1765
1766fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1767 let mut out = result_var.to_string();
1768 for seg in segments {
1769 match seg {
1770 PathSegment::Field(f) => {
1771 out.push('.');
1772 out.push_str(&f.to_pascal_case());
1773 }
1774 PathSegment::ArrayField { name, index } => {
1775 out.push('.');
1776 out.push_str(&name.to_pascal_case());
1777 out.push_str(&format!("[{index}]"));
1778 }
1779 PathSegment::MapAccess { field, key } => {
1780 out.push('.');
1781 out.push_str(&field.to_pascal_case());
1782 if key.chars().all(|c| c.is_ascii_digit()) {
1783 out.push_str(&format!("[{key}]"));
1784 } else {
1785 out.push_str(&format!("[\"{key}\"]"));
1786 }
1787 }
1788 PathSegment::Length => {
1789 out.push_str(".Count");
1790 }
1791 }
1792 }
1793 out
1794}
1795
1796fn render_csharp_with_optionals(
1797 segments: &[PathSegment],
1798 result_var: &str,
1799 optional_fields: &HashSet<String>,
1800) -> String {
1801 let mut out = result_var.to_string();
1802 let mut path_so_far = String::new();
1803 for (i, seg) in segments.iter().enumerate() {
1804 let is_leaf = i == segments.len() - 1;
1805 match seg {
1806 PathSegment::Field(f) => {
1807 if !path_so_far.is_empty() {
1808 path_so_far.push('.');
1809 }
1810 path_so_far.push_str(f);
1811 out.push('.');
1812 out.push_str(&f.to_pascal_case());
1813 if !is_leaf && optional_fields.contains(&path_so_far) {
1814 out.push('!');
1815 }
1816 }
1817 PathSegment::ArrayField { name, index } => {
1818 if !path_so_far.is_empty() {
1819 path_so_far.push('.');
1820 }
1821 path_so_far.push_str(name);
1822 out.push('.');
1823 out.push_str(&name.to_pascal_case());
1824 out.push_str(&format!("[{index}]"));
1825 }
1826 PathSegment::MapAccess { field, key } => {
1827 if !path_so_far.is_empty() {
1828 path_so_far.push('.');
1829 }
1830 path_so_far.push_str(field);
1831 out.push('.');
1832 out.push_str(&field.to_pascal_case());
1833 if key.chars().all(|c| c.is_ascii_digit()) {
1834 out.push_str(&format!("[{key}]"));
1835 } else {
1836 out.push_str(&format!("[\"{key}\"]"));
1837 }
1838 }
1839 PathSegment::Length => {
1840 out.push_str(".Count");
1841 }
1842 }
1843 }
1844 out
1845}
1846
1847fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1848 let mut out = result_var.to_string();
1849 for seg in segments {
1850 match seg {
1851 PathSegment::Field(f) => {
1852 out.push_str("->");
1853 out.push_str(&f.to_lower_camel_case());
1856 }
1857 PathSegment::ArrayField { name, index } => {
1858 out.push_str("->");
1859 out.push_str(&name.to_lower_camel_case());
1860 out.push_str(&format!("[{index}]"));
1861 }
1862 PathSegment::MapAccess { field, key } => {
1863 out.push_str("->");
1864 out.push_str(&field.to_lower_camel_case());
1865 out.push_str(&format!("[\"{key}\"]"));
1866 }
1867 PathSegment::Length => {
1868 let current = std::mem::take(&mut out);
1869 out = format!("count({current})");
1870 }
1871 }
1872 }
1873 out
1874}
1875
1876fn render_php_with_getters(segments: &[PathSegment], result_var: &str, getter_map: &PhpGetterMap) -> String {
1894 let mut out = result_var.to_string();
1895 let mut current_type: Option<String> = getter_map.root_type.clone();
1896 for seg in segments {
1897 match seg {
1898 PathSegment::Field(f) => {
1899 let camel = f.to_lower_camel_case();
1900 if getter_map.needs_getter(current_type.as_deref(), f.as_str()) {
1901 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1906 out.push_str("->");
1907 out.push_str(&getter);
1908 out.push_str("()");
1909 } else {
1910 out.push_str("->");
1911 out.push_str(&camel);
1912 }
1913 current_type = getter_map.advance(current_type.as_deref(), f.as_str());
1914 }
1915 PathSegment::ArrayField { name, index } => {
1916 let camel = name.to_lower_camel_case();
1917 if getter_map.needs_getter(current_type.as_deref(), name.as_str()) {
1918 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1919 out.push_str("->");
1920 out.push_str(&getter);
1921 out.push_str("()");
1922 } else {
1923 out.push_str("->");
1924 out.push_str(&camel);
1925 }
1926 out.push_str(&format!("[{index}]"));
1927 current_type = getter_map.advance(current_type.as_deref(), name.as_str());
1928 }
1929 PathSegment::MapAccess { field, key } => {
1930 let camel = field.to_lower_camel_case();
1931 if getter_map.needs_getter(current_type.as_deref(), field.as_str()) {
1932 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1933 out.push_str("->");
1934 out.push_str(&getter);
1935 out.push_str("()");
1936 } else {
1937 out.push_str("->");
1938 out.push_str(&camel);
1939 }
1940 out.push_str(&format!("[\"{key}\"]"));
1941 current_type = getter_map.advance(current_type.as_deref(), field.as_str());
1942 }
1943 PathSegment::Length => {
1944 let current = std::mem::take(&mut out);
1945 out = format!("count({current})");
1946 }
1947 }
1948 }
1949 out
1950}
1951
1952fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1953 let mut out = result_var.to_string();
1954 for seg in segments {
1955 match seg {
1956 PathSegment::Field(f) => {
1957 out.push('$');
1958 out.push_str(f);
1959 }
1960 PathSegment::ArrayField { name, index } => {
1961 out.push('$');
1962 out.push_str(name);
1963 out.push_str(&format!("[[{}]]", index + 1));
1965 }
1966 PathSegment::MapAccess { field, key } => {
1967 out.push('$');
1968 out.push_str(field);
1969 out.push_str(&format!("[[\"{key}\"]]"));
1970 }
1971 PathSegment::Length => {
1972 let current = std::mem::take(&mut out);
1973 out = format!("length({current})");
1974 }
1975 }
1976 }
1977 out
1978}
1979
1980fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1981 let mut parts = Vec::new();
1982 let mut trailing_length = false;
1983 for seg in segments {
1984 match seg {
1985 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1986 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1987 PathSegment::MapAccess { field, key } => {
1988 parts.push(field.to_snake_case());
1989 parts.push(key.clone());
1990 }
1991 PathSegment::Length => {
1992 trailing_length = true;
1993 }
1994 }
1995 }
1996 let suffix = parts.join("_");
1997 if trailing_length {
1998 format!("result_{suffix}_count({result_var})")
1999 } else {
2000 format!("result_{suffix}({result_var})")
2001 }
2002}
2003
2004fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
2011 let mut out = result_var.to_string();
2012 for seg in segments {
2013 match seg {
2014 PathSegment::Field(f) => {
2015 out.push('.');
2016 out.push_str(&f.to_lower_camel_case());
2017 }
2018 PathSegment::ArrayField { name, index } => {
2019 out.push('.');
2020 out.push_str(&name.to_lower_camel_case());
2021 out.push_str(&format!("[{index}]"));
2022 }
2023 PathSegment::MapAccess { field, key } => {
2024 out.push('.');
2025 out.push_str(&field.to_lower_camel_case());
2026 if key.chars().all(|c| c.is_ascii_digit()) {
2027 out.push_str(&format!("[{key}]"));
2028 } else {
2029 out.push_str(&format!("[\"{key}\"]"));
2030 }
2031 }
2032 PathSegment::Length => {
2033 out.push_str(".length");
2034 }
2035 }
2036 }
2037 out
2038}
2039
2040fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
2046 let mut out = result_var.to_string();
2047 let mut path_so_far = String::new();
2048 let mut prev_was_nullable = false;
2049 for seg in segments {
2050 let nav = if prev_was_nullable { "?." } else { "." };
2051 match seg {
2052 PathSegment::Field(f) => {
2053 if !path_so_far.is_empty() {
2054 path_so_far.push('.');
2055 }
2056 path_so_far.push_str(f);
2057 let is_optional = optional_fields.contains(&path_so_far);
2058 out.push_str(nav);
2059 out.push_str(&f.to_lower_camel_case());
2060 prev_was_nullable = is_optional;
2061 }
2062 PathSegment::ArrayField { name, index } => {
2063 if !path_so_far.is_empty() {
2064 path_so_far.push('.');
2065 }
2066 path_so_far.push_str(name);
2067 let is_optional = optional_fields.contains(&path_so_far);
2068 out.push_str(nav);
2069 out.push_str(&name.to_lower_camel_case());
2070 if is_optional {
2074 out.push('!');
2075 }
2076 out.push_str(&format!("[{index}]"));
2077 prev_was_nullable = false;
2078 }
2079 PathSegment::MapAccess { field, key } => {
2080 if !path_so_far.is_empty() {
2081 path_so_far.push('.');
2082 }
2083 path_so_far.push_str(field);
2084 let is_optional = optional_fields.contains(&path_so_far);
2085 out.push_str(nav);
2086 out.push_str(&field.to_lower_camel_case());
2087 if key.chars().all(|c| c.is_ascii_digit()) {
2088 out.push_str(&format!("[{key}]"));
2089 } else {
2090 out.push_str(&format!("[\"{key}\"]"));
2091 }
2092 prev_was_nullable = is_optional;
2093 }
2094 PathSegment::Length => {
2095 out.push_str(nav);
2098 out.push_str("length");
2099 prev_was_nullable = false;
2100 }
2101 }
2102 }
2103 out
2104}
2105
2106#[cfg(test)]
2107mod tests {
2108 use super::*;
2109
2110 fn make_resolver() -> FieldResolver {
2111 let mut fields = HashMap::new();
2112 fields.insert("title".to_string(), "metadata.document.title".to_string());
2113 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
2114 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
2115 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
2116 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
2117 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
2118 let mut optional = HashSet::new();
2119 optional.insert("metadata.document.title".to_string());
2120 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
2121 }
2122
2123 fn make_resolver_with_doc_optional() -> FieldResolver {
2124 let mut fields = HashMap::new();
2125 fields.insert("title".to_string(), "metadata.document.title".to_string());
2126 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
2127 let mut optional = HashSet::new();
2128 optional.insert("document".to_string());
2129 optional.insert("metadata.document.title".to_string());
2130 optional.insert("metadata.document".to_string());
2131 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
2132 }
2133
2134 #[test]
2135 fn test_resolve_alias() {
2136 let r = make_resolver();
2137 assert_eq!(r.resolve("title"), "metadata.document.title");
2138 }
2139
2140 #[test]
2141 fn test_resolve_passthrough() {
2142 let r = make_resolver();
2143 assert_eq!(r.resolve("content"), "content");
2144 }
2145
2146 #[test]
2147 fn test_is_optional() {
2148 let r = make_resolver();
2149 assert!(r.is_optional("metadata.document.title"));
2150 assert!(!r.is_optional("content"));
2151 }
2152
2153 #[test]
2154 fn is_optional_strips_namespace_prefix() {
2155 let fields = HashMap::new();
2156 let mut optional = HashSet::new();
2157 optional.insert("action_results.data".to_string());
2158 let result_fields: HashSet<String> = ["action_results".to_string()].into_iter().collect();
2159 let r = FieldResolver::new(&fields, &optional, &result_fields, &HashSet::new(), &HashSet::new());
2160 assert!(r.is_optional("interaction.action_results[0].data"));
2162 assert!(r.is_optional("action_results[0].data"));
2164 }
2165
2166 #[test]
2167 fn test_accessor_rust_struct() {
2168 let r = make_resolver();
2169 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
2170 }
2171
2172 #[test]
2173 fn test_accessor_rust_map() {
2174 let r = make_resolver();
2175 assert_eq!(
2176 r.accessor("tags", "rust", "result"),
2177 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
2178 );
2179 }
2180
2181 #[test]
2182 fn test_accessor_python() {
2183 let r = make_resolver();
2184 assert_eq!(
2185 r.accessor("title", "python", "result"),
2186 "result.metadata.document.title"
2187 );
2188 }
2189
2190 #[test]
2191 fn test_accessor_go() {
2192 let r = make_resolver();
2193 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
2194 }
2195
2196 #[test]
2197 fn test_accessor_go_initialism_fields() {
2198 let mut fields = std::collections::HashMap::new();
2199 fields.insert("content".to_string(), "html".to_string());
2200 fields.insert("link_url".to_string(), "links.url".to_string());
2201 let r = FieldResolver::new(
2202 &fields,
2203 &HashSet::new(),
2204 &HashSet::new(),
2205 &HashSet::new(),
2206 &HashSet::new(),
2207 );
2208 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
2209 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
2210 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
2211 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
2212 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
2213 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
2214 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
2215 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
2216 }
2217
2218 #[test]
2219 fn test_accessor_typescript() {
2220 let r = make_resolver();
2221 assert_eq!(
2222 r.accessor("title", "typescript", "result"),
2223 "result.metadata.document.title"
2224 );
2225 }
2226
2227 #[test]
2228 fn test_accessor_typescript_snake_to_camel() {
2229 let r = make_resolver();
2230 assert_eq!(
2231 r.accessor("og", "typescript", "result"),
2232 "result.metadata.document.openGraph"
2233 );
2234 assert_eq!(
2235 r.accessor("twitter", "typescript", "result"),
2236 "result.metadata.document.twitterCard"
2237 );
2238 assert_eq!(
2239 r.accessor("canonical", "typescript", "result"),
2240 "result.metadata.document.canonicalUrl"
2241 );
2242 }
2243
2244 #[test]
2245 fn test_accessor_typescript_map_snake_to_camel() {
2246 let r = make_resolver();
2247 assert_eq!(
2248 r.accessor("og_tag", "typescript", "result"),
2249 "result.metadata.openGraphTags[\"og_title\"]"
2250 );
2251 }
2252
2253 #[test]
2254 fn test_accessor_typescript_numeric_index_is_unquoted() {
2255 let mut fields = HashMap::new();
2259 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
2260 let r = FieldResolver::new(
2261 &fields,
2262 &HashSet::new(),
2263 &HashSet::new(),
2264 &HashSet::new(),
2265 &HashSet::new(),
2266 );
2267 assert_eq!(
2268 r.accessor("first_score", "typescript", "result"),
2269 "result.results[0].relevanceScore"
2270 );
2271 }
2272
2273 #[test]
2274 fn test_accessor_node_alias() {
2275 let r = make_resolver();
2276 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
2277 }
2278
2279 #[test]
2280 fn test_accessor_wasm_camel_case() {
2281 let r = make_resolver();
2282 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
2283 assert_eq!(
2284 r.accessor("twitter", "wasm", "result"),
2285 "result.metadata.document.twitterCard"
2286 );
2287 assert_eq!(
2288 r.accessor("canonical", "wasm", "result"),
2289 "result.metadata.document.canonicalUrl"
2290 );
2291 }
2292
2293 #[test]
2294 fn test_accessor_wasm_map_access() {
2295 let r = make_resolver();
2296 assert_eq!(
2297 r.accessor("og_tag", "wasm", "result"),
2298 "result.metadata.openGraphTags.get(\"og_title\")"
2299 );
2300 }
2301
2302 #[test]
2303 fn test_accessor_java() {
2304 let r = make_resolver();
2305 assert_eq!(
2306 r.accessor("title", "java", "result"),
2307 "result.metadata().document().title()"
2308 );
2309 }
2310
2311 #[test]
2312 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
2313 let mut fields = HashMap::new();
2314 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2315 fields.insert("node_count".to_string(), "nodes.length".to_string());
2316 let mut arrays = HashSet::new();
2317 arrays.insert("nodes".to_string());
2318 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
2319 assert_eq!(
2320 r.accessor("first_node_name", "kotlin", "result"),
2321 "result.nodes().first().name()"
2322 );
2323 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
2324 }
2325
2326 #[test]
2327 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
2328 let r = make_resolver_with_doc_optional();
2329 assert_eq!(
2330 r.accessor("title", "kotlin", "result"),
2331 "result.metadata().document()?.title()"
2332 );
2333 }
2334
2335 #[test]
2336 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
2337 let mut fields = HashMap::new();
2338 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2339 fields.insert("tag".to_string(), "tags[name]".to_string());
2340 let mut optional = HashSet::new();
2341 optional.insert("nodes".to_string());
2342 optional.insert("tags".to_string());
2343 let mut arrays = HashSet::new();
2344 arrays.insert("nodes".to_string());
2345 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2346 assert_eq!(
2347 r.accessor("first_node_name", "kotlin", "result"),
2348 "result.nodes()?.first()?.name()"
2349 );
2350 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
2351 }
2352
2353 #[test]
2359 fn test_accessor_kotlin_optional_field_after_indexed_array() {
2360 let mut fields = HashMap::new();
2363 fields.insert(
2364 "tool_call_name".to_string(),
2365 "choices[0].message.tool_calls[0].function.name".to_string(),
2366 );
2367 let mut optional = HashSet::new();
2368 optional.insert("choices[0].message.tool_calls".to_string());
2369 let mut arrays = HashSet::new();
2370 arrays.insert("choices".to_string());
2371 arrays.insert("choices[0].message.tool_calls".to_string());
2372 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2373 let expr = r.accessor("tool_call_name", "kotlin", "result");
2374 assert!(
2376 expr.contains("toolCalls()?.first()"),
2377 "expected toolCalls()?.first() for optional list, got: {expr}"
2378 );
2379 }
2380
2381 #[test]
2382 fn test_accessor_csharp() {
2383 let r = make_resolver();
2384 assert_eq!(
2385 r.accessor("title", "csharp", "result"),
2386 "result.Metadata.Document.Title"
2387 );
2388 }
2389
2390 #[test]
2391 fn test_accessor_php() {
2392 let r = make_resolver();
2393 assert_eq!(
2394 r.accessor("title", "php", "$result"),
2395 "$result->metadata->document->title"
2396 );
2397 }
2398
2399 #[test]
2400 fn test_accessor_r() {
2401 let r = make_resolver();
2402 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
2403 }
2404
2405 #[test]
2406 fn test_accessor_c() {
2407 let r = make_resolver();
2408 assert_eq!(
2409 r.accessor("title", "c", "result"),
2410 "result_metadata_document_title(result)"
2411 );
2412 }
2413
2414 #[test]
2415 fn test_rust_unwrap_binding() {
2416 let r = make_resolver();
2417 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
2418 assert_eq!(var, "metadata_document_title");
2419 assert!(binding.contains("as_ref().map(|v| format!(\"{:?}\", v)).unwrap_or_default()"));
2422 }
2423
2424 #[test]
2425 fn test_rust_unwrap_binding_non_optional() {
2426 let r = make_resolver();
2427 assert!(r.rust_unwrap_binding("content", "result").is_none());
2428 }
2429
2430 #[test]
2431 fn test_rust_unwrap_binding_collapses_double_underscore() {
2432 let mut aliases = HashMap::new();
2437 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
2438 let mut optional = HashSet::new();
2439 optional.insert("json_ld[].name".to_string());
2440 let mut array = HashSet::new();
2441 array.insert("json_ld".to_string());
2442 let result_fields = HashSet::new();
2443 let method_calls = HashSet::new();
2444 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
2445 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
2446 assert_eq!(var, "json_ld_name");
2447 }
2448
2449 #[test]
2450 fn test_direct_field_no_alias() {
2451 let r = make_resolver();
2452 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2453 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
2454 }
2455
2456 #[test]
2457 fn test_accessor_rust_with_optionals() {
2458 let r = make_resolver_with_doc_optional();
2459 assert_eq!(
2460 r.accessor("title", "rust", "result"),
2461 "result.metadata.document.as_ref().unwrap().title"
2462 );
2463 }
2464
2465 #[test]
2466 fn test_accessor_csharp_with_optionals() {
2467 let r = make_resolver_with_doc_optional();
2468 assert_eq!(
2469 r.accessor("title", "csharp", "result"),
2470 "result.Metadata.Document!.Title"
2471 );
2472 }
2473
2474 #[test]
2475 fn test_accessor_rust_non_optional_field() {
2476 let r = make_resolver();
2477 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2478 }
2479
2480 #[test]
2481 fn test_accessor_csharp_non_optional_field() {
2482 let r = make_resolver();
2483 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
2484 }
2485
2486 #[test]
2487 fn test_accessor_rust_method_call() {
2488 let mut fields = HashMap::new();
2490 fields.insert(
2491 "excel_sheet_count".to_string(),
2492 "metadata.format.excel.sheet_count".to_string(),
2493 );
2494 let mut optional = HashSet::new();
2495 optional.insert("metadata.format".to_string());
2496 optional.insert("metadata.format.excel".to_string());
2497 let mut method_calls = HashSet::new();
2498 method_calls.insert("metadata.format.excel".to_string());
2499 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
2500 assert_eq!(
2501 r.accessor("excel_sheet_count", "rust", "result"),
2502 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
2503 );
2504 }
2505
2506 fn make_php_getter_resolver() -> FieldResolver {
2511 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2512 getters.insert(
2513 "Root".to_string(),
2514 ["metadata".to_string(), "links".to_string()].into_iter().collect(),
2515 );
2516 let map = PhpGetterMap {
2517 getters,
2518 field_types: HashMap::new(),
2519 root_type: Some("Root".to_string()),
2520 all_fields: HashMap::new(),
2521 };
2522 FieldResolver::new_with_php_getters(
2523 &HashMap::new(),
2524 &HashSet::new(),
2525 &HashSet::new(),
2526 &HashSet::new(),
2527 &HashSet::new(),
2528 &HashMap::new(),
2529 map,
2530 )
2531 }
2532
2533 #[test]
2534 fn render_php_uses_getter_method_for_non_scalar_field() {
2535 let r = make_php_getter_resolver();
2536 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
2537 }
2538
2539 #[test]
2540 fn render_php_uses_property_for_scalar_field() {
2541 let r = make_php_getter_resolver();
2542 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2543 }
2544
2545 #[test]
2546 fn render_php_nested_non_scalar_uses_getter_then_property() {
2547 let mut fields = HashMap::new();
2548 fields.insert("title".to_string(), "metadata.title".to_string());
2549 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2550 getters.insert("Root".to_string(), ["metadata".to_string()].into_iter().collect());
2551 getters.insert("Metadata".to_string(), HashSet::new());
2553 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2554 field_types.insert(
2555 "Root".to_string(),
2556 [("metadata".to_string(), "Metadata".to_string())].into_iter().collect(),
2557 );
2558 let map = PhpGetterMap {
2559 getters,
2560 field_types,
2561 root_type: Some("Root".to_string()),
2562 all_fields: HashMap::new(),
2563 };
2564 let r = FieldResolver::new_with_php_getters(
2565 &fields,
2566 &HashSet::new(),
2567 &HashSet::new(),
2568 &HashSet::new(),
2569 &HashSet::new(),
2570 &HashMap::new(),
2571 map,
2572 );
2573 assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
2575 }
2576
2577 #[test]
2578 fn render_php_array_field_uses_getter_when_non_scalar() {
2579 let mut fields = HashMap::new();
2580 fields.insert("first_link".to_string(), "links[0]".to_string());
2581 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2582 getters.insert("Root".to_string(), ["links".to_string()].into_iter().collect());
2583 let map = PhpGetterMap {
2584 getters,
2585 field_types: HashMap::new(),
2586 root_type: Some("Root".to_string()),
2587 all_fields: HashMap::new(),
2588 };
2589 let r = FieldResolver::new_with_php_getters(
2590 &fields,
2591 &HashSet::new(),
2592 &HashSet::new(),
2593 &HashSet::new(),
2594 &HashSet::new(),
2595 &HashMap::new(),
2596 map,
2597 );
2598 assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
2599 }
2600
2601 #[test]
2602 fn render_php_falls_back_to_property_when_getter_fields_empty() {
2603 let r = FieldResolver::new(
2606 &HashMap::new(),
2607 &HashSet::new(),
2608 &HashSet::new(),
2609 &HashSet::new(),
2610 &HashSet::new(),
2611 );
2612 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2613 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
2614 }
2615
2616 #[test]
2620 fn render_php_with_getters_distinguishes_same_field_name_on_different_types() {
2621 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2622 getters.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2624 getters.insert("B".to_string(), HashSet::new());
2626 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2629 all_fields.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2630 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2631 let map_a = PhpGetterMap {
2632 getters: getters.clone(),
2633 field_types: HashMap::new(),
2634 root_type: Some("A".to_string()),
2635 all_fields: all_fields.clone(),
2636 };
2637 let map_b = PhpGetterMap {
2638 getters,
2639 field_types: HashMap::new(),
2640 root_type: Some("B".to_string()),
2641 all_fields,
2642 };
2643 let r_a = FieldResolver::new_with_php_getters(
2644 &HashMap::new(),
2645 &HashSet::new(),
2646 &HashSet::new(),
2647 &HashSet::new(),
2648 &HashSet::new(),
2649 &HashMap::new(),
2650 map_a,
2651 );
2652 let r_b = FieldResolver::new_with_php_getters(
2653 &HashMap::new(),
2654 &HashSet::new(),
2655 &HashSet::new(),
2656 &HashSet::new(),
2657 &HashSet::new(),
2658 &HashMap::new(),
2659 map_b,
2660 );
2661 assert_eq!(r_a.accessor("content", "php", "$a"), "$a->getContent()");
2662 assert_eq!(r_b.accessor("content", "php", "$b"), "$b->content");
2663 }
2664
2665 #[test]
2669 fn render_php_with_getters_chains_through_correct_type() {
2670 let mut fields = HashMap::new();
2671 fields.insert("nested_content".to_string(), "inner.content".to_string());
2672 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2673 getters.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2675 getters.insert("B".to_string(), HashSet::new());
2677 getters.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2680 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2681 field_types.insert(
2682 "Outer".to_string(),
2683 [("inner".to_string(), "B".to_string())].into_iter().collect(),
2684 );
2685 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2686 all_fields.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2687 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2688 all_fields.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2689 let map = PhpGetterMap {
2690 getters,
2691 field_types,
2692 root_type: Some("Outer".to_string()),
2693 all_fields,
2694 };
2695 let r = FieldResolver::new_with_php_getters(
2696 &fields,
2697 &HashSet::new(),
2698 &HashSet::new(),
2699 &HashSet::new(),
2700 &HashSet::new(),
2701 &HashMap::new(),
2702 map,
2703 );
2704 assert_eq!(
2705 r.accessor("nested_content", "php", "$result"),
2706 "$result->getInner()->content"
2707 );
2708 }
2709
2710 fn make_resolver_with_result_fields(result_fields: &[&str]) -> FieldResolver {
2715 let rf: HashSet<String> = result_fields.iter().map(|s| s.to_string()).collect();
2716 FieldResolver::new(&HashMap::new(), &HashSet::new(), &rf, &HashSet::new(), &HashSet::new())
2717 }
2718
2719 #[test]
2722 fn is_valid_for_result_accepts_virtual_namespace_prefix() {
2723 let r = make_resolver_with_result_fields(&["browser_used", "js_render_hint", "status_code"]);
2724 assert!(
2725 r.is_valid_for_result("browser.browser_used"),
2726 "browser.browser_used should be valid via namespace-prefix stripping"
2727 );
2728 assert!(
2729 r.is_valid_for_result("browser.js_render_hint"),
2730 "browser.js_render_hint should be valid via namespace-prefix stripping"
2731 );
2732 }
2733
2734 #[test]
2737 fn is_valid_for_result_accepts_namespace_prefix_before_array_field() {
2738 let r = make_resolver_with_result_fields(&["action_results", "final_html", "final_url"]);
2739 assert!(
2740 r.is_valid_for_result("interaction.action_results[0].action_type"),
2741 "interaction. prefix should be stripped so action_results is recognised"
2742 );
2743 }
2744
2745 #[test]
2747 fn is_valid_for_result_rejects_unknown_field_even_after_namespace_strip() {
2748 let r = make_resolver_with_result_fields(&["pages", "final_url"]);
2749 assert!(
2750 !r.is_valid_for_result("browser.browser_used"),
2751 "browser_used is not in result_fields so should be rejected"
2752 );
2753 assert!(
2754 !r.is_valid_for_result("ns.unknown_field"),
2755 "unknown_field is not in result_fields so should be rejected"
2756 );
2757 }
2758
2759 #[test]
2762 fn accessor_strips_namespace_prefix_for_python() {
2763 let r = make_resolver_with_result_fields(&["browser_used", "js_render_hint"]);
2764 assert_eq!(
2765 r.accessor("browser.browser_used", "python", "result"),
2766 "result.browser_used"
2767 );
2768 assert_eq!(
2769 r.accessor("browser.js_render_hint", "python", "result"),
2770 "result.js_render_hint"
2771 );
2772 }
2773
2774 #[test]
2776 fn accessor_strips_namespace_prefix_for_csharp() {
2777 let r = make_resolver_with_result_fields(&["browser_used"]);
2778 assert_eq!(
2779 r.accessor("browser.browser_used", "csharp", "result"),
2780 "result.BrowserUsed"
2781 );
2782 }
2783
2784 #[test]
2787 fn accessor_strips_namespace_prefix_for_indexed_array_field() {
2788 let r = make_resolver_with_result_fields(&["action_results", "final_html", "final_url"]);
2789 assert_eq!(
2791 r.accessor("interaction.action_results[0].action_type", "python", "result"),
2792 "result.action_results[0].action_type"
2793 );
2794 assert_eq!(
2796 r.accessor("interaction.action_results[0].action_type", "typescript", "result"),
2797 "result.actionResults[0].actionType"
2798 );
2799 }
2800
2801 #[test]
2804 fn is_valid_for_result_is_permissive_when_result_fields_empty() {
2805 let r = make_resolver_with_result_fields(&[]);
2806 assert!(r.is_valid_for_result("browser.browser_used"));
2807 assert!(r.is_valid_for_result("anything.at.all"));
2808 }
2809
2810 #[test]
2813 fn accessor_does_not_strip_real_first_segment() {
2814 let r = make_resolver_with_result_fields(&["metadata", "status_code"]);
2815 assert_eq!(
2817 r.accessor("metadata.title", "python", "result"),
2818 "result.metadata.title"
2819 );
2820 }
2821}