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, Copy, PartialEq, Eq)]
100pub enum StringyFieldKind {
101 Plain,
103 Optional,
105 Vec,
108}
109
110#[derive(Debug, Clone)]
114pub struct StringyField {
115 pub name: String,
116 pub kind: StringyFieldKind,
117}
118
119#[derive(Debug, Clone, Default)]
120pub struct SwiftFirstClassMap {
121 pub first_class_types: HashSet<String>,
122 pub field_types: HashMap<String, HashMap<String, String>>,
123 pub vec_field_names: HashSet<String>,
124 pub root_type: Option<String>,
125 pub stringy_fields_by_type: HashMap<String, Vec<StringyField>>,
131}
132
133impl SwiftFirstClassMap {
134 pub fn is_first_class(&self, type_name: Option<&str>) -> bool {
140 match type_name {
141 Some(t) => self.first_class_types.contains(t),
142 None => true,
143 }
144 }
145
146 pub fn advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
149 let owner = owner_type?;
150 self.field_types.get(owner).and_then(|m| m.get(field_name).cloned())
151 }
152
153 pub fn is_vec_field_name(&self, field_name: &str) -> bool {
158 self.vec_field_names.contains(field_name)
159 }
160
161 pub fn is_empty(&self) -> bool {
163 self.first_class_types.is_empty() && self.field_types.is_empty()
164 }
165
166 pub fn stringy_fields(&self, type_name: &str) -> Option<&[StringyField]> {
169 self.stringy_fields_by_type.get(type_name).map(Vec::as_slice)
170 }
171}
172
173impl PhpGetterMap {
174 pub fn needs_getter(&self, owner_type: Option<&str>, field_name: &str) -> bool {
181 if let Some(t) = owner_type {
182 let owner_has_field = self.all_fields.get(t).is_some_and(|s| s.contains(field_name));
187 if owner_has_field {
188 if let Some(fields) = self.getters.get(t) {
189 return fields.contains(field_name);
190 }
191 }
192 }
193 self.getters.values().any(|set| set.contains(field_name))
194 }
195
196 pub fn advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
199 let owner = owner_type?;
200 self.field_types.get(owner).and_then(|m| m.get(field_name).cloned())
201 }
202
203 pub fn is_empty(&self) -> bool {
206 self.getters.is_empty()
207 }
208}
209
210#[derive(Debug, Clone)]
212enum PathSegment {
213 Field(String),
215 ArrayField { name: String, index: usize },
220 MapAccess { field: String, key: String },
222 Length,
224}
225
226impl FieldResolver {
227 pub fn new(
231 fields: &HashMap<String, String>,
232 optional: &HashSet<String>,
233 result_fields: &HashSet<String>,
234 array_fields: &HashSet<String>,
235 method_calls: &HashSet<String>,
236 ) -> Self {
237 Self {
238 aliases: fields.clone(),
239 optional_fields: optional.clone(),
240 result_fields: result_fields.clone(),
241 array_fields: array_fields.clone(),
242 method_calls: method_calls.clone(),
243 error_field_aliases: HashMap::new(),
244 php_getter_map: PhpGetterMap::default(),
245 swift_first_class_map: SwiftFirstClassMap::default(),
246 }
247 }
248
249 pub fn new_with_error_aliases(
255 fields: &HashMap<String, String>,
256 optional: &HashSet<String>,
257 result_fields: &HashSet<String>,
258 array_fields: &HashSet<String>,
259 method_calls: &HashSet<String>,
260 error_field_aliases: &HashMap<String, String>,
261 ) -> Self {
262 Self {
263 aliases: fields.clone(),
264 optional_fields: optional.clone(),
265 result_fields: result_fields.clone(),
266 array_fields: array_fields.clone(),
267 method_calls: method_calls.clone(),
268 error_field_aliases: error_field_aliases.clone(),
269 php_getter_map: PhpGetterMap::default(),
270 swift_first_class_map: SwiftFirstClassMap::default(),
271 }
272 }
273
274 pub fn new_with_php_getters(
289 fields: &HashMap<String, String>,
290 optional: &HashSet<String>,
291 result_fields: &HashSet<String>,
292 array_fields: &HashSet<String>,
293 method_calls: &HashSet<String>,
294 error_field_aliases: &HashMap<String, String>,
295 php_getter_map: PhpGetterMap,
296 ) -> Self {
297 Self {
298 aliases: fields.clone(),
299 optional_fields: optional.clone(),
300 result_fields: result_fields.clone(),
301 array_fields: array_fields.clone(),
302 method_calls: method_calls.clone(),
303 error_field_aliases: error_field_aliases.clone(),
304 php_getter_map,
305 swift_first_class_map: SwiftFirstClassMap::default(),
306 }
307 }
308
309 pub fn with_swift_root_type(&self, root_type: Option<String>) -> Self {
320 let mut clone = self.clone();
321 clone.swift_first_class_map.root_type = root_type;
322 clone
323 }
324
325 #[allow(clippy::too_many_arguments)]
329 pub fn new_with_swift_first_class(
330 fields: &HashMap<String, String>,
331 optional: &HashSet<String>,
332 result_fields: &HashSet<String>,
333 array_fields: &HashSet<String>,
334 method_calls: &HashSet<String>,
335 error_field_aliases: &HashMap<String, String>,
336 swift_first_class_map: SwiftFirstClassMap,
337 ) -> Self {
338 Self {
339 aliases: fields.clone(),
340 optional_fields: optional.clone(),
341 result_fields: result_fields.clone(),
342 array_fields: array_fields.clone(),
343 method_calls: method_calls.clone(),
344 error_field_aliases: error_field_aliases.clone(),
345 php_getter_map: PhpGetterMap::default(),
346 swift_first_class_map,
347 }
348 }
349
350 pub fn resolve<'a>(&'a self, fixture_field: &'a str) -> &'a str {
353 self.aliases
354 .get(fixture_field)
355 .map(String::as_str)
356 .unwrap_or(fixture_field)
357 }
358
359 pub fn leaf_is_vec_via_swift_map(&self, field: &str) -> bool {
366 let leaf = field.split('.').next_back().unwrap_or(field);
367 let leaf = leaf.split('[').next().unwrap_or(leaf);
368 self.swift_first_class_map.is_vec_field_name(leaf)
369 }
370
371 pub fn swift_root_type(&self) -> Option<&String> {
374 self.swift_first_class_map.root_type.as_ref()
375 }
376
377 pub fn swift_is_first_class(&self, type_name: Option<&str>) -> bool {
381 self.swift_first_class_map.is_first_class(type_name)
382 }
383
384 pub fn swift_advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
387 self.swift_first_class_map.advance(owner_type, field_name)
388 }
389
390 pub fn swift_stringy_fields(&self, type_name: &str) -> Option<&[StringyField]> {
394 self.swift_first_class_map.stringy_fields(type_name)
395 }
396
397 pub fn is_optional(&self, field: &str) -> bool {
399 if self.is_optional_direct(field) {
400 return true;
401 }
402 if let Some(suffix) = self.namespace_stripped_path(field) {
406 if self.is_optional_direct(suffix) {
407 return true;
408 }
409 }
410 false
411 }
412
413 fn is_optional_direct(&self, field: &str) -> bool {
414 if self.optional_fields.contains(field) {
415 return true;
416 }
417 let index_normalized = normalize_numeric_indices(field);
418 if index_normalized != field && self.optional_fields.contains(index_normalized.as_str()) {
419 return true;
420 }
421 let de_indexed = strip_numeric_indices(field);
424 if de_indexed != field && self.optional_fields.contains(de_indexed.as_str()) {
425 return true;
426 }
427 let normalized = field.replace("[].", ".");
428 if normalized != field && self.optional_fields.contains(normalized.as_str()) {
429 return true;
430 }
431 for af in &self.array_fields {
432 if let Some(rest) = field.strip_prefix(af.as_str()) {
433 if let Some(rest) = rest.strip_prefix('.') {
434 let with_bracket = format!("{af}[].{rest}");
435 if self.optional_fields.contains(with_bracket.as_str()) {
436 return true;
437 }
438 }
439 }
440 }
441 false
442 }
443
444 pub fn has_alias(&self, fixture_field: &str) -> bool {
446 self.aliases.contains_key(fixture_field)
447 }
448
449 pub fn has_explicit_field(&self, field_name: &str) -> bool {
455 if self.result_fields.is_empty() {
456 return false;
457 }
458 self.result_fields.contains(field_name)
459 }
460
461 pub fn is_valid_for_result(&self, fixture_field: &str) -> bool {
470 if self.result_fields.is_empty() {
471 return true;
472 }
473 let resolved = self.resolve(fixture_field);
474 let first_segment = resolved.split('.').next().unwrap_or(resolved);
475 let first_segment = first_segment.split('[').next().unwrap_or(first_segment);
476 if self.result_fields.contains(first_segment) {
477 return true;
478 }
479 if let Some(suffix) = self.namespace_stripped_path(resolved) {
485 let suffix_first = suffix.split('.').next().unwrap_or(suffix);
486 let suffix_first = suffix_first.split('[').next().unwrap_or(suffix_first);
487 return self.result_fields.contains(suffix_first);
488 }
489 false
490 }
491
492 pub fn namespace_stripped_path<'a>(&self, path: &'a str) -> Option<&'a str> {
498 let dot_pos = path.find('.')?;
499 let first = &path[..dot_pos];
500 if first.contains('[') {
503 return None;
504 }
505 if self.result_fields.contains(first) {
508 return None;
509 }
510 let suffix = &path[dot_pos + 1..];
511 if suffix.is_empty() { None } else { Some(suffix) }
512 }
513
514 pub fn is_array(&self, field: &str) -> bool {
516 self.array_fields.contains(field)
517 }
518
519 pub fn is_collection_root(&self, field: &str) -> bool {
532 let prefix = format!("{field}[");
533 self.array_fields.iter().any(|af| af.starts_with(&prefix))
534 || self.optional_fields.iter().any(|of| of.starts_with(&prefix))
535 }
536
537 pub fn tagged_union_split(&self, fixture_field: &str) -> Option<(String, String, String)> {
549 let resolved = self.resolve(fixture_field);
550 let segments: Vec<&str> = resolved.split('.').collect();
551 let mut path_so_far = String::new();
552 for (i, seg) in segments.iter().enumerate() {
553 if !path_so_far.is_empty() {
554 path_so_far.push('.');
555 }
556 path_so_far.push_str(seg);
557 if self.method_calls.contains(&path_so_far) {
558 let prefix = segments[..i].join(".");
560 let variant = (*seg).to_string();
561 let suffix = segments[i + 1..].join(".");
562 return Some((prefix, variant, suffix));
563 }
564 }
565 None
566 }
567
568 pub fn has_map_access(&self, fixture_field: &str) -> bool {
570 let resolved = self.resolve(fixture_field);
571 let segments = parse_path(resolved);
572 segments.iter().any(|s| {
573 if let PathSegment::MapAccess { key, .. } = s {
574 !key.chars().all(|c| c.is_ascii_digit())
575 } else {
576 false
577 }
578 })
579 }
580
581 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
590 let resolved = self.resolve(fixture_field);
591 let effective = if !self.result_fields.is_empty() {
595 if let Some(stripped) = self.namespace_stripped_path(resolved) {
596 let stripped_first = stripped.split('.').next().unwrap_or(stripped);
597 let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
598 if self.result_fields.contains(stripped_first) {
599 stripped
600 } else {
601 resolved
602 }
603 } else {
604 resolved
605 }
606 } else {
607 resolved
608 };
609 let segments = parse_path(effective);
610 let segments = self.inject_array_indexing(segments);
611 match language {
612 "java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
613 "kotlin" => render_kotlin_with_optionals(&segments, result_var, &self.optional_fields),
614 "kotlin_android" => render_kotlin_android_with_optionals(&segments, result_var, &self.optional_fields),
617 "rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
618 "csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
619 "zig" => render_zig_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
620 "swift" if !self.swift_first_class_map.is_empty() => render_swift_with_first_class_map(
621 &segments,
622 result_var,
623 &self.optional_fields,
624 &self.swift_first_class_map,
625 ),
626 "swift" => render_swift_with_optionals(&segments, result_var, &self.optional_fields),
627 "dart" => render_dart_with_optionals(&segments, result_var, &self.optional_fields),
628 "php" if !self.php_getter_map.is_empty() => {
629 render_php_with_getters(&segments, result_var, &self.php_getter_map)
630 }
631 _ => render_accessor(&segments, language, result_var),
632 }
633 }
634
635 pub fn accessor_for_error(&self, sub_field: &str, language: &str, err_var: &str) -> String {
649 let resolved = self
650 .error_field_aliases
651 .get(sub_field)
652 .map(String::as_str)
653 .unwrap_or(sub_field);
654 let segments = parse_path(resolved);
655 match language {
658 "rust" => render_rust_with_optionals(&segments, err_var, &self.optional_fields, &self.method_calls),
659 _ => render_accessor(&segments, language, err_var),
660 }
661 }
662
663 pub fn has_error_aliases(&self) -> bool {
670 !self.error_field_aliases.is_empty()
671 }
672
673 fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
674 if self.array_fields.is_empty() {
675 return segments;
676 }
677 let len = segments.len();
678 let mut result = Vec::with_capacity(len);
679 let mut path_so_far = String::new();
680 for i in 0..len {
681 let seg = &segments[i];
682 match seg {
683 PathSegment::Field(f) => {
684 if !path_so_far.is_empty() {
685 path_so_far.push('.');
686 }
687 path_so_far.push_str(f);
688 let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
689 if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
690 result.push(PathSegment::ArrayField {
692 name: f.clone(),
693 index: 0,
694 });
695 } else {
696 result.push(seg.clone());
697 }
698 }
699 PathSegment::ArrayField { .. } => {
702 result.push(seg.clone());
703 }
704 PathSegment::MapAccess { field, key } => {
705 if !path_so_far.is_empty() {
706 path_so_far.push('.');
707 }
708 path_so_far.push_str(field);
709 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
710 if is_numeric && self.array_fields.contains(&path_so_far) {
711 let index: usize = key.parse().unwrap_or(0);
713 result.push(PathSegment::ArrayField {
714 name: field.clone(),
715 index,
716 });
717 } else {
718 result.push(seg.clone());
719 }
720 }
721 _ => {
722 result.push(seg.clone());
723 }
724 }
725 }
726 result
727 }
728
729 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
731 let resolved = self.resolve(fixture_field);
732 if !self.is_optional(resolved) {
733 return None;
734 }
735 let effective = if !self.result_fields.is_empty() {
739 if let Some(stripped) = self.namespace_stripped_path(resolved) {
740 let stripped_first = stripped.split('.').next().unwrap_or(stripped);
741 let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
742 if self.result_fields.contains(stripped_first) {
743 stripped
744 } else {
745 resolved
746 }
747 } else {
748 resolved
749 }
750 } else {
751 resolved
752 };
753 let segments = parse_path(effective);
754 let segments = self.inject_array_indexing(segments);
755 let local_var = {
760 let raw = effective.replace(['.', '['], "_").replace(']', "");
761 let mut collapsed = String::with_capacity(raw.len());
762 let mut prev_underscore = false;
763 for ch in raw.chars() {
764 if ch == '_' {
765 if !prev_underscore {
766 collapsed.push('_');
767 }
768 prev_underscore = true;
769 } else {
770 collapsed.push(ch);
771 prev_underscore = false;
772 }
773 }
774 collapsed.trim_matches('_').to_string()
775 };
776 let accessor = render_accessor(&segments, "rust", result_var);
777 let has_map_access = segments.iter().any(|s| {
778 if let PathSegment::MapAccess { key, .. } = s {
779 !key.chars().all(|c| c.is_ascii_digit())
780 } else {
781 false
782 }
783 });
784 let is_array = self.is_array(resolved);
785 let binding = if has_map_access {
786 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
787 } else if is_array {
788 format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
789 } else {
790 format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
796 };
797 Some((binding, local_var))
798 }
799}
800
801fn strip_numeric_indices(path: &str) -> String {
806 let mut result = String::with_capacity(path.len());
807 let mut chars = path.chars().peekable();
808 while let Some(c) = chars.next() {
809 if c == '[' {
810 let mut key = String::new();
811 let mut closed = false;
812 for inner in chars.by_ref() {
813 if inner == ']' {
814 closed = true;
815 break;
816 }
817 key.push(inner);
818 }
819 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
820 } else {
822 result.push('[');
823 result.push_str(&key);
824 if closed {
825 result.push(']');
826 }
827 }
828 } else {
829 result.push(c);
830 }
831 }
832 while result.contains("..") {
834 result = result.replace("..", ".");
835 }
836 if result.starts_with('.') {
837 result.remove(0);
838 }
839 result
840}
841
842fn normalize_numeric_indices(path: &str) -> String {
843 let mut result = String::with_capacity(path.len());
844 let mut chars = path.chars().peekable();
845 while let Some(c) = chars.next() {
846 if c == '[' {
847 let mut key = String::new();
848 let mut closed = false;
849 for inner in chars.by_ref() {
850 if inner == ']' {
851 closed = true;
852 break;
853 }
854 key.push(inner);
855 }
856 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
857 result.push_str("[0]");
858 } else {
859 result.push('[');
860 result.push_str(&key);
861 if closed {
862 result.push(']');
863 }
864 }
865 } else {
866 result.push(c);
867 }
868 }
869 result
870}
871
872fn parse_path(path: &str) -> Vec<PathSegment> {
873 let mut segments = Vec::new();
874 for part in path.split('.') {
875 if part == "length" || part == "count" || part == "size" {
876 segments.push(PathSegment::Length);
877 } else if let Some(bracket_pos) = part.find('[') {
878 let name = part[..bracket_pos].to_string();
879 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
880 if key.is_empty() {
881 segments.push(PathSegment::ArrayField { name, index: 0 });
883 } else if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
884 let index: usize = key.parse().unwrap_or(0);
886 segments.push(PathSegment::ArrayField { name, index });
887 } else {
888 segments.push(PathSegment::MapAccess { field: name, key });
890 }
891 } else {
892 segments.push(PathSegment::Field(part.to_string()));
893 }
894 }
895 segments
896}
897
898fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
899 match language {
900 "rust" => render_rust(segments, result_var),
901 "python" => render_dot_access(segments, result_var, "python"),
902 "typescript" | "node" => render_typescript(segments, result_var),
903 "wasm" => render_wasm(segments, result_var),
904 "go" => render_go(segments, result_var),
905 "java" => render_java(segments, result_var),
906 "kotlin" => render_kotlin(segments, result_var),
907 "kotlin_android" => render_kotlin_android(segments, result_var),
908 "csharp" => render_pascal_dot(segments, result_var),
909 "ruby" => render_dot_access(segments, result_var, "ruby"),
910 "php" => render_php(segments, result_var),
911 "elixir" => render_dot_access(segments, result_var, "elixir"),
912 "r" => render_r(segments, result_var),
913 "c" => render_c(segments, result_var),
914 "swift" => render_swift(segments, result_var),
915 "dart" => render_dart(segments, result_var),
916 _ => render_dot_access(segments, result_var, language),
917 }
918}
919
920fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
932 let mut out = result_var.to_string();
933 for seg in segments {
934 match seg {
935 PathSegment::Field(f) => {
936 out.push('.');
937 out.push_str(&f.to_lower_camel_case());
938 }
939 PathSegment::ArrayField { name, index } => {
940 out.push('.');
941 out.push_str(&name.to_lower_camel_case());
942 out.push_str(&format!("[{index}]"));
943 }
944 PathSegment::MapAccess { field, key } => {
945 out.push('.');
946 out.push_str(&field.to_lower_camel_case());
947 if key.chars().all(|c| c.is_ascii_digit()) {
948 out.push_str(&format!("[{key}]"));
949 } else {
950 out.push_str(&format!("[\"{key}\"]"));
951 }
952 }
953 PathSegment::Length => {
954 out.push_str(".count");
955 }
956 }
957 }
958 out
959}
960
961fn render_swift_with_optionals(
971 segments: &[PathSegment],
972 result_var: &str,
973 optional_fields: &HashSet<String>,
974) -> String {
975 let mut out = result_var.to_string();
976 let mut path_so_far = String::new();
977 let total = segments.len();
978 for (i, seg) in segments.iter().enumerate() {
979 let is_leaf = i == total - 1;
980 match seg {
981 PathSegment::Field(f) => {
982 if !path_so_far.is_empty() {
983 path_so_far.push('.');
984 }
985 path_so_far.push_str(f);
986 out.push('.');
987 out.push_str(&f.to_lower_camel_case());
990 if !is_leaf && optional_fields.contains(&path_so_far) {
994 out.push('?');
995 }
996 }
997 PathSegment::ArrayField { name, index } => {
998 if !path_so_far.is_empty() {
999 path_so_far.push('.');
1000 }
1001 path_so_far.push_str(name);
1002 let is_optional = optional_fields.contains(&path_so_far);
1003 out.push('.');
1004 out.push_str(&name.to_lower_camel_case());
1005 if is_optional {
1006 out.push_str(&format!("?[{index}]"));
1008 } else {
1009 out.push_str(&format!("[{index}]"));
1010 }
1011 path_so_far.push_str("[0]");
1012 let _ = is_leaf;
1013 }
1014 PathSegment::MapAccess { field, key } => {
1015 if !path_so_far.is_empty() {
1016 path_so_far.push('.');
1017 }
1018 path_so_far.push_str(field);
1019 out.push('.');
1020 out.push_str(&field.to_lower_camel_case());
1021 if key.chars().all(|c| c.is_ascii_digit()) {
1022 out.push_str(&format!("[{key}]"));
1023 } else {
1024 out.push_str(&format!("[\"{key}\"]"));
1025 }
1026 }
1027 PathSegment::Length => {
1028 out.push_str(".count");
1029 }
1030 }
1031 }
1032 out
1033}
1034
1035fn render_swift_with_first_class_map(
1040 segments: &[PathSegment],
1041 result_var: &str,
1042 optional_fields: &HashSet<String>,
1043 map: &SwiftFirstClassMap,
1044) -> String {
1045 let mut out = result_var.to_string();
1046 let mut path_so_far = String::new();
1047 let mut current_type: Option<String> = map.root_type.clone();
1048 let mut via_rust_vec = false;
1057 let mut via_opaque = false;
1068 let total = segments.len();
1069 for (i, seg) in segments.iter().enumerate() {
1070 let is_leaf = i == total - 1;
1071 let property_syntax = !via_rust_vec && !via_opaque && map.is_first_class(current_type.as_deref());
1072 if !property_syntax {
1073 via_opaque = true;
1074 }
1075 match seg {
1076 PathSegment::Field(f) => {
1077 if !path_so_far.is_empty() {
1078 path_so_far.push('.');
1079 }
1080 path_so_far.push_str(f);
1081 out.push('.');
1082 out.push_str(&f.to_lower_camel_case());
1085 if !property_syntax {
1086 out.push_str("()");
1087 }
1088 if !is_leaf && optional_fields.contains(&path_so_far) {
1089 out.push('?');
1090 }
1091 current_type = map.advance(current_type.as_deref(), f);
1092 }
1093 PathSegment::ArrayField { name, index } => {
1094 if !path_so_far.is_empty() {
1095 path_so_far.push('.');
1096 }
1097 path_so_far.push_str(name);
1098 let is_optional = optional_fields.contains(&path_so_far);
1099 out.push('.');
1100 out.push_str(&name.to_lower_camel_case());
1101 let access = if property_syntax { "" } else { "()" };
1102 if is_optional {
1103 out.push_str(&format!("{access}?[{index}]"));
1104 } else {
1105 out.push_str(&format!("{access}[{index}]"));
1106 }
1107 path_so_far.push_str("[0]");
1108 current_type = map.advance(current_type.as_deref(), name);
1114 if !property_syntax {
1115 via_rust_vec = true;
1116 }
1117 }
1118 PathSegment::MapAccess { field, key } => {
1119 if !path_so_far.is_empty() {
1120 path_so_far.push('.');
1121 }
1122 path_so_far.push_str(field);
1123 out.push('.');
1124 out.push_str(&field.to_lower_camel_case());
1125 let access = if property_syntax { "" } else { "()" };
1126 if key.chars().all(|c| c.is_ascii_digit()) {
1127 out.push_str(&format!("{access}[{key}]"));
1128 } else {
1129 out.push_str(&format!("{access}[\"{key}\"]"));
1130 }
1131 current_type = map.advance(current_type.as_deref(), field);
1132 }
1133 PathSegment::Length => {
1134 out.push_str(".count");
1135 }
1136 }
1137 }
1138 out
1139}
1140
1141fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
1142 let mut out = result_var.to_string();
1143 for seg in segments {
1144 match seg {
1145 PathSegment::Field(f) => {
1146 out.push('.');
1147 out.push_str(&f.to_snake_case());
1148 }
1149 PathSegment::ArrayField { name, index } => {
1150 out.push('.');
1151 out.push_str(&name.to_snake_case());
1152 out.push_str(&format!("[{index}]"));
1153 }
1154 PathSegment::MapAccess { field, key } => {
1155 out.push('.');
1156 out.push_str(&field.to_snake_case());
1157 if key.chars().all(|c| c.is_ascii_digit()) {
1158 out.push_str(&format!("[{key}]"));
1159 } else {
1160 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1161 }
1162 }
1163 PathSegment::Length => {
1164 out.push_str(".len()");
1165 }
1166 }
1167 }
1168 out
1169}
1170
1171fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
1172 let mut out = result_var.to_string();
1173 for seg in segments {
1174 match seg {
1175 PathSegment::Field(f) => {
1176 out.push('.');
1177 out.push_str(f);
1178 }
1179 PathSegment::ArrayField { name, index } => {
1180 if language == "elixir" {
1181 let current = std::mem::take(&mut out);
1182 out = format!("Enum.at({current}.{name}, {index})");
1183 } else {
1184 out.push('.');
1185 out.push_str(name);
1186 out.push_str(&format!("[{index}]"));
1187 }
1188 }
1189 PathSegment::MapAccess { field, key } => {
1190 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
1191 if is_numeric && language == "elixir" {
1192 let current = std::mem::take(&mut out);
1193 out = format!("Enum.at({current}.{field}, {key})");
1194 } else {
1195 out.push('.');
1196 out.push_str(field);
1197 if is_numeric {
1198 let idx: usize = key.parse().unwrap_or(0);
1199 out.push_str(&format!("[{idx}]"));
1200 } else if language == "elixir" || language == "ruby" {
1201 out.push_str(&format!("[\"{key}\"]"));
1204 } else {
1205 out.push_str(&format!(".get(\"{key}\")"));
1206 }
1207 }
1208 }
1209 PathSegment::Length => match language {
1210 "ruby" => out.push_str(".length"),
1211 "elixir" => {
1212 let current = std::mem::take(&mut out);
1213 out = format!("length({current})");
1214 }
1215 "gleam" => {
1216 let current = std::mem::take(&mut out);
1217 out = format!("list.length({current})");
1218 }
1219 _ => {
1220 let current = std::mem::take(&mut out);
1221 out = format!("len({current})");
1222 }
1223 },
1224 }
1225 }
1226 out
1227}
1228
1229fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
1230 let mut out = result_var.to_string();
1231 for seg in segments {
1232 match seg {
1233 PathSegment::Field(f) => {
1234 out.push('.');
1235 out.push_str(&f.to_lower_camel_case());
1236 }
1237 PathSegment::ArrayField { name, index } => {
1238 out.push('.');
1239 out.push_str(&name.to_lower_camel_case());
1240 out.push_str(&format!("[{index}]"));
1241 }
1242 PathSegment::MapAccess { field, key } => {
1243 out.push('.');
1244 out.push_str(&field.to_lower_camel_case());
1245 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
1248 out.push_str(&format!("[{key}]"));
1249 } else {
1250 out.push_str(&format!("[\"{key}\"]"));
1251 }
1252 }
1253 PathSegment::Length => {
1254 out.push_str(".length");
1255 }
1256 }
1257 }
1258 out
1259}
1260
1261fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
1262 let mut out = result_var.to_string();
1263 for seg in segments {
1264 match seg {
1265 PathSegment::Field(f) => {
1266 out.push('.');
1267 out.push_str(&f.to_lower_camel_case());
1268 }
1269 PathSegment::ArrayField { name, index } => {
1270 out.push('.');
1271 out.push_str(&name.to_lower_camel_case());
1272 out.push_str(&format!("[{index}]"));
1273 }
1274 PathSegment::MapAccess { field, key } => {
1275 out.push('.');
1276 out.push_str(&field.to_lower_camel_case());
1277 out.push_str(&format!(".get(\"{key}\")"));
1278 }
1279 PathSegment::Length => {
1280 out.push_str(".length");
1281 }
1282 }
1283 }
1284 out
1285}
1286
1287fn render_go(segments: &[PathSegment], result_var: &str) -> String {
1288 let mut out = result_var.to_string();
1289 for seg in segments {
1290 match seg {
1291 PathSegment::Field(f) => {
1292 out.push('.');
1293 out.push_str(&to_go_name(f));
1294 }
1295 PathSegment::ArrayField { name, index } => {
1296 out.push('.');
1297 out.push_str(&to_go_name(name));
1298 out.push_str(&format!("[{index}]"));
1299 }
1300 PathSegment::MapAccess { field, key } => {
1301 out.push('.');
1302 out.push_str(&to_go_name(field));
1303 if key.chars().all(|c| c.is_ascii_digit()) {
1304 out.push_str(&format!("[{key}]"));
1305 } else {
1306 out.push_str(&format!("[\"{key}\"]"));
1307 }
1308 }
1309 PathSegment::Length => {
1310 let current = std::mem::take(&mut out);
1311 out = format!("len({current})");
1312 }
1313 }
1314 }
1315 out
1316}
1317
1318fn render_java(segments: &[PathSegment], result_var: &str) -> String {
1319 let mut out = result_var.to_string();
1320 for seg in segments {
1321 match seg {
1322 PathSegment::Field(f) => {
1323 out.push('.');
1324 out.push_str(&f.to_lower_camel_case());
1325 out.push_str("()");
1326 }
1327 PathSegment::ArrayField { name, index } => {
1328 out.push('.');
1329 out.push_str(&name.to_lower_camel_case());
1330 out.push_str(&format!("().get({index})"));
1331 }
1332 PathSegment::MapAccess { field, key } => {
1333 out.push('.');
1334 out.push_str(&field.to_lower_camel_case());
1335 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1337 if is_numeric {
1338 out.push_str(&format!("().get({key})"));
1339 } else {
1340 out.push_str(&format!("().get(\"{key}\")"));
1341 }
1342 }
1343 PathSegment::Length => {
1344 out.push_str(".size()");
1345 }
1346 }
1347 }
1348 out
1349}
1350
1351fn kotlin_getter(name: &str) -> String {
1356 let camel = name.to_lower_camel_case();
1357 match camel.as_str() {
1358 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
1359 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
1360 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
1361 _ => camel,
1362 }
1363}
1364
1365fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
1366 let mut out = result_var.to_string();
1367 for seg in segments {
1368 match seg {
1369 PathSegment::Field(f) => {
1370 out.push('.');
1371 out.push_str(&kotlin_getter(f));
1372 out.push_str("()");
1373 }
1374 PathSegment::ArrayField { name, index } => {
1375 out.push('.');
1376 out.push_str(&kotlin_getter(name));
1377 if *index == 0 {
1378 out.push_str("().first()");
1379 } else {
1380 out.push_str(&format!("().get({index})"));
1381 }
1382 }
1383 PathSegment::MapAccess { field, key } => {
1384 out.push('.');
1385 out.push_str(&kotlin_getter(field));
1386 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1387 if is_numeric {
1388 out.push_str(&format!("().get({key})"));
1389 } else {
1390 out.push_str(&format!("().get(\"{key}\")"));
1391 }
1392 }
1393 PathSegment::Length => {
1394 out.push_str(".size");
1395 }
1396 }
1397 }
1398 out
1399}
1400
1401fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1402 let mut out = result_var.to_string();
1403 let mut path_so_far = String::new();
1404 for (i, seg) in segments.iter().enumerate() {
1405 let is_leaf = i == segments.len() - 1;
1406 match seg {
1407 PathSegment::Field(f) => {
1408 if !path_so_far.is_empty() {
1409 path_so_far.push('.');
1410 }
1411 path_so_far.push_str(f);
1412 out.push('.');
1413 out.push_str(&f.to_lower_camel_case());
1414 out.push_str("()");
1415 let _ = is_leaf;
1416 let _ = optional_fields;
1417 }
1418 PathSegment::ArrayField { name, index } => {
1419 if !path_so_far.is_empty() {
1420 path_so_far.push('.');
1421 }
1422 path_so_far.push_str(name);
1423 out.push('.');
1424 out.push_str(&name.to_lower_camel_case());
1425 out.push_str(&format!("().get({index})"));
1426 }
1427 PathSegment::MapAccess { field, key } => {
1428 if !path_so_far.is_empty() {
1429 path_so_far.push('.');
1430 }
1431 path_so_far.push_str(field);
1432 out.push('.');
1433 out.push_str(&field.to_lower_camel_case());
1434 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1436 if is_numeric {
1437 out.push_str(&format!("().get({key})"));
1438 } else {
1439 out.push_str(&format!("().get(\"{key}\")"));
1440 }
1441 }
1442 PathSegment::Length => {
1443 out.push_str(".size()");
1444 }
1445 }
1446 }
1447 out
1448}
1449
1450fn render_kotlin_with_optionals(
1465 segments: &[PathSegment],
1466 result_var: &str,
1467 optional_fields: &HashSet<String>,
1468) -> String {
1469 let mut out = result_var.to_string();
1470 let mut path_so_far = String::new();
1471 let mut prev_was_nullable = false;
1479 for seg in segments {
1480 let nav = if prev_was_nullable { "?." } else { "." };
1481 match seg {
1482 PathSegment::Field(f) => {
1483 if !path_so_far.is_empty() {
1484 path_so_far.push('.');
1485 }
1486 path_so_far.push_str(f);
1487 let is_optional = optional_fields.contains(&path_so_far);
1492 out.push_str(nav);
1493 out.push_str(&kotlin_getter(f));
1494 out.push_str("()");
1495 prev_was_nullable = prev_was_nullable || is_optional;
1496 }
1497 PathSegment::ArrayField { name, index } => {
1498 if !path_so_far.is_empty() {
1499 path_so_far.push('.');
1500 }
1501 path_so_far.push_str(name);
1502 let is_optional = optional_fields.contains(&path_so_far);
1503 out.push_str(nav);
1504 out.push_str(&kotlin_getter(name));
1505 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1506 if *index == 0 {
1507 out.push_str(&format!("(){safe}.first()"));
1508 } else {
1509 out.push_str(&format!("(){safe}.get({index})"));
1510 }
1511 path_so_far.push_str("[0]");
1515 prev_was_nullable = prev_was_nullable || is_optional;
1516 }
1517 PathSegment::MapAccess { field, key } => {
1518 if !path_so_far.is_empty() {
1519 path_so_far.push('.');
1520 }
1521 path_so_far.push_str(field);
1522 let is_optional = optional_fields.contains(&path_so_far);
1523 out.push_str(nav);
1524 out.push_str(&kotlin_getter(field));
1525 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1526 if is_numeric {
1527 if prev_was_nullable || is_optional {
1528 out.push_str(&format!("()?.get({key})"));
1529 } else {
1530 out.push_str(&format!("().get({key})"));
1531 }
1532 } else if prev_was_nullable || is_optional {
1533 out.push_str(&format!("()?.get(\"{key}\")"));
1534 } else {
1535 out.push_str(&format!("().get(\"{key}\")"));
1536 }
1537 prev_was_nullable = prev_was_nullable || is_optional;
1538 }
1539 PathSegment::Length => {
1540 let size_nav = if prev_was_nullable { "?" } else { "" };
1543 out.push_str(&format!("{size_nav}.size"));
1544 prev_was_nullable = false;
1545 }
1546 }
1547 }
1548 out
1549}
1550
1551fn render_kotlin_android_with_optionals(
1562 segments: &[PathSegment],
1563 result_var: &str,
1564 optional_fields: &HashSet<String>,
1565) -> String {
1566 let mut out = result_var.to_string();
1567 let mut path_so_far = String::new();
1568 let mut prev_was_nullable = false;
1569 for seg in segments {
1570 let nav = if prev_was_nullable { "?." } else { "." };
1571 match seg {
1572 PathSegment::Field(f) => {
1573 if !path_so_far.is_empty() {
1574 path_so_far.push('.');
1575 }
1576 path_so_far.push_str(f);
1577 let is_optional = optional_fields.contains(&path_so_far);
1578 out.push_str(nav);
1579 out.push_str(&kotlin_getter(f));
1581 prev_was_nullable = prev_was_nullable || is_optional;
1582 }
1583 PathSegment::ArrayField { name, index } => {
1584 if !path_so_far.is_empty() {
1585 path_so_far.push('.');
1586 }
1587 path_so_far.push_str(name);
1588 let is_optional = optional_fields.contains(&path_so_far);
1589 out.push_str(nav);
1590 out.push_str(&kotlin_getter(name));
1592 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1593 if *index == 0 {
1594 out.push_str(&format!("{safe}.first()"));
1595 } else {
1596 out.push_str(&format!("{safe}.get({index})"));
1597 }
1598 path_so_far.push_str("[0]");
1599 prev_was_nullable = prev_was_nullable || is_optional;
1600 }
1601 PathSegment::MapAccess { field, key } => {
1602 if !path_so_far.is_empty() {
1603 path_so_far.push('.');
1604 }
1605 path_so_far.push_str(field);
1606 let is_optional = optional_fields.contains(&path_so_far);
1607 out.push_str(nav);
1608 out.push_str(&kotlin_getter(field));
1610 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1611 if is_numeric {
1612 if prev_was_nullable || is_optional {
1613 out.push_str(&format!("?.get({key})"));
1614 } else {
1615 out.push_str(&format!(".get({key})"));
1616 }
1617 } else if prev_was_nullable || is_optional {
1618 out.push_str(&format!("?.get(\"{key}\")"));
1619 } else {
1620 out.push_str(&format!(".get(\"{key}\")"));
1621 }
1622 prev_was_nullable = prev_was_nullable || is_optional;
1623 }
1624 PathSegment::Length => {
1625 let size_nav = if prev_was_nullable { "?" } else { "" };
1626 out.push_str(&format!("{size_nav}.size"));
1627 prev_was_nullable = false;
1628 }
1629 }
1630 }
1631 out
1632}
1633
1634fn render_kotlin_android(segments: &[PathSegment], result_var: &str) -> String {
1638 let mut out = result_var.to_string();
1639 for seg in segments {
1640 match seg {
1641 PathSegment::Field(f) => {
1642 out.push('.');
1643 out.push_str(&kotlin_getter(f));
1644 }
1646 PathSegment::ArrayField { name, index } => {
1647 out.push('.');
1648 out.push_str(&kotlin_getter(name));
1649 if *index == 0 {
1650 out.push_str(".first()");
1651 } else {
1652 out.push_str(&format!(".get({index})"));
1653 }
1654 }
1655 PathSegment::MapAccess { field, key } => {
1656 out.push('.');
1657 out.push_str(&kotlin_getter(field));
1658 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1659 if is_numeric {
1660 out.push_str(&format!(".get({key})"));
1661 } else {
1662 out.push_str(&format!(".get(\"{key}\")"));
1663 }
1664 }
1665 PathSegment::Length => {
1666 out.push_str(".size");
1667 }
1668 }
1669 }
1670 out
1671}
1672
1673fn render_rust_with_optionals(
1679 segments: &[PathSegment],
1680 result_var: &str,
1681 optional_fields: &HashSet<String>,
1682 method_calls: &HashSet<String>,
1683) -> String {
1684 let mut out = result_var.to_string();
1685 let mut path_so_far = String::new();
1686 for (i, seg) in segments.iter().enumerate() {
1687 let is_leaf = i == segments.len() - 1;
1688 match seg {
1689 PathSegment::Field(f) => {
1690 if !path_so_far.is_empty() {
1691 path_so_far.push('.');
1692 }
1693 path_so_far.push_str(f);
1694 out.push('.');
1695 out.push_str(&f.to_snake_case());
1696 let is_method = method_calls.contains(&path_so_far);
1697 if is_method {
1698 out.push_str("()");
1699 if !is_leaf && optional_fields.contains(&path_so_far) {
1700 out.push_str(".as_ref().unwrap()");
1701 }
1702 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1703 out.push_str(".as_ref().unwrap()");
1704 }
1705 }
1706 PathSegment::ArrayField { name, index } => {
1707 if !path_so_far.is_empty() {
1708 path_so_far.push('.');
1709 }
1710 path_so_far.push_str(name);
1711 out.push('.');
1712 out.push_str(&name.to_snake_case());
1713 let path_with_idx = format!("{path_so_far}[0]");
1717 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1718 if is_opt {
1719 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1720 } else {
1721 out.push_str(&format!("[{index}]"));
1722 }
1723 path_so_far.push_str("[0]");
1728 }
1729 PathSegment::MapAccess { field, key } => {
1730 if !path_so_far.is_empty() {
1731 path_so_far.push('.');
1732 }
1733 path_so_far.push_str(field);
1734 out.push('.');
1735 out.push_str(&field.to_snake_case());
1736 if key.chars().all(|c| c.is_ascii_digit()) {
1737 let path_with_idx = format!("{path_so_far}[0]");
1739 let is_opt =
1740 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1741 if is_opt {
1742 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1743 } else {
1744 out.push_str(&format!("[{key}]"));
1745 }
1746 path_so_far.push_str("[0]");
1747 } else {
1748 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1749 }
1750 }
1751 PathSegment::Length => {
1752 out.push_str(".len()");
1753 }
1754 }
1755 }
1756 out
1757}
1758
1759fn render_zig_with_optionals(
1772 segments: &[PathSegment],
1773 result_var: &str,
1774 optional_fields: &HashSet<String>,
1775 method_calls: &HashSet<String>,
1776) -> String {
1777 let mut out = result_var.to_string();
1778 let mut path_so_far = String::new();
1779 for seg in segments {
1780 match seg {
1781 PathSegment::Field(f) => {
1782 if !path_so_far.is_empty() {
1783 path_so_far.push('.');
1784 }
1785 path_so_far.push_str(f);
1786 out.push('.');
1787 out.push_str(f);
1788 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1789 out.push_str(".?");
1790 }
1791 }
1792 PathSegment::ArrayField { name, index } => {
1793 if !path_so_far.is_empty() {
1794 path_so_far.push('.');
1795 }
1796 path_so_far.push_str(name);
1797 out.push('.');
1798 out.push_str(name);
1799 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1800 out.push_str(".?");
1801 }
1802 out.push_str(&format!("[{index}]"));
1803 }
1804 PathSegment::MapAccess { field, key } => {
1805 if !path_so_far.is_empty() {
1806 path_so_far.push('.');
1807 }
1808 path_so_far.push_str(field);
1809 out.push('.');
1810 out.push_str(field);
1811 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1812 out.push_str(".?");
1813 }
1814 if key.chars().all(|c| c.is_ascii_digit()) {
1815 out.push_str(&format!("[{key}]"));
1816 } else {
1817 out.push_str(&format!(".get(\"{key}\")"));
1818 }
1819 }
1820 PathSegment::Length => {
1821 out.push_str(".len");
1822 }
1823 }
1824 }
1825 out
1826}
1827
1828fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1829 let mut out = result_var.to_string();
1830 for seg in segments {
1831 match seg {
1832 PathSegment::Field(f) => {
1833 out.push('.');
1834 out.push_str(&f.to_pascal_case());
1835 }
1836 PathSegment::ArrayField { name, index } => {
1837 out.push('.');
1838 out.push_str(&name.to_pascal_case());
1839 out.push_str(&format!("[{index}]"));
1840 }
1841 PathSegment::MapAccess { field, key } => {
1842 out.push('.');
1843 out.push_str(&field.to_pascal_case());
1844 if key.chars().all(|c| c.is_ascii_digit()) {
1845 out.push_str(&format!("[{key}]"));
1846 } else {
1847 out.push_str(&format!("[\"{key}\"]"));
1848 }
1849 }
1850 PathSegment::Length => {
1851 out.push_str(".Count");
1852 }
1853 }
1854 }
1855 out
1856}
1857
1858fn render_csharp_with_optionals(
1859 segments: &[PathSegment],
1860 result_var: &str,
1861 optional_fields: &HashSet<String>,
1862) -> String {
1863 let mut out = result_var.to_string();
1864 let mut path_so_far = String::new();
1865 for (i, seg) in segments.iter().enumerate() {
1866 let is_leaf = i == segments.len() - 1;
1867 match seg {
1868 PathSegment::Field(f) => {
1869 if !path_so_far.is_empty() {
1870 path_so_far.push('.');
1871 }
1872 path_so_far.push_str(f);
1873 out.push('.');
1874 out.push_str(&f.to_pascal_case());
1875 if !is_leaf && optional_fields.contains(&path_so_far) {
1876 out.push('!');
1877 }
1878 }
1879 PathSegment::ArrayField { name, index } => {
1880 if !path_so_far.is_empty() {
1881 path_so_far.push('.');
1882 }
1883 path_so_far.push_str(name);
1884 out.push('.');
1885 out.push_str(&name.to_pascal_case());
1886 out.push_str(&format!("[{index}]"));
1887 }
1888 PathSegment::MapAccess { field, key } => {
1889 if !path_so_far.is_empty() {
1890 path_so_far.push('.');
1891 }
1892 path_so_far.push_str(field);
1893 out.push('.');
1894 out.push_str(&field.to_pascal_case());
1895 if key.chars().all(|c| c.is_ascii_digit()) {
1896 out.push_str(&format!("[{key}]"));
1897 } else {
1898 out.push_str(&format!("[\"{key}\"]"));
1899 }
1900 }
1901 PathSegment::Length => {
1902 out.push_str(".Count");
1903 }
1904 }
1905 }
1906 out
1907}
1908
1909fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1910 let mut out = result_var.to_string();
1911 for seg in segments {
1912 match seg {
1913 PathSegment::Field(f) => {
1914 out.push_str("->");
1915 out.push_str(&f.to_lower_camel_case());
1918 }
1919 PathSegment::ArrayField { name, index } => {
1920 out.push_str("->");
1921 out.push_str(&name.to_lower_camel_case());
1922 out.push_str(&format!("[{index}]"));
1923 }
1924 PathSegment::MapAccess { field, key } => {
1925 out.push_str("->");
1926 out.push_str(&field.to_lower_camel_case());
1927 out.push_str(&format!("[\"{key}\"]"));
1928 }
1929 PathSegment::Length => {
1930 let current = std::mem::take(&mut out);
1931 out = format!("count({current})");
1932 }
1933 }
1934 }
1935 out
1936}
1937
1938fn render_php_with_getters(segments: &[PathSegment], result_var: &str, getter_map: &PhpGetterMap) -> String {
1956 let mut out = result_var.to_string();
1957 let mut current_type: Option<String> = getter_map.root_type.clone();
1958 for seg in segments {
1959 match seg {
1960 PathSegment::Field(f) => {
1961 let camel = f.to_lower_camel_case();
1962 if getter_map.needs_getter(current_type.as_deref(), f.as_str()) {
1963 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1968 out.push_str("->");
1969 out.push_str(&getter);
1970 out.push_str("()");
1971 } else {
1972 out.push_str("->");
1973 out.push_str(&camel);
1974 }
1975 current_type = getter_map.advance(current_type.as_deref(), f.as_str());
1976 }
1977 PathSegment::ArrayField { name, index } => {
1978 let camel = name.to_lower_camel_case();
1979 if getter_map.needs_getter(current_type.as_deref(), name.as_str()) {
1980 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1981 out.push_str("->");
1982 out.push_str(&getter);
1983 out.push_str("()");
1984 } else {
1985 out.push_str("->");
1986 out.push_str(&camel);
1987 }
1988 out.push_str(&format!("[{index}]"));
1989 current_type = getter_map.advance(current_type.as_deref(), name.as_str());
1990 }
1991 PathSegment::MapAccess { field, key } => {
1992 let camel = field.to_lower_camel_case();
1993 if getter_map.needs_getter(current_type.as_deref(), field.as_str()) {
1994 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1995 out.push_str("->");
1996 out.push_str(&getter);
1997 out.push_str("()");
1998 } else {
1999 out.push_str("->");
2000 out.push_str(&camel);
2001 }
2002 out.push_str(&format!("[\"{key}\"]"));
2003 current_type = getter_map.advance(current_type.as_deref(), field.as_str());
2004 }
2005 PathSegment::Length => {
2006 let current = std::mem::take(&mut out);
2007 out = format!("count({current})");
2008 }
2009 }
2010 }
2011 out
2012}
2013
2014fn render_r(segments: &[PathSegment], result_var: &str) -> String {
2015 let mut out = result_var.to_string();
2016 for seg in segments {
2017 match seg {
2018 PathSegment::Field(f) => {
2019 out.push('$');
2020 out.push_str(f);
2021 }
2022 PathSegment::ArrayField { name, index } => {
2023 out.push('$');
2024 out.push_str(name);
2025 out.push_str(&format!("[[{}]]", index + 1));
2027 }
2028 PathSegment::MapAccess { field, key } => {
2029 out.push('$');
2030 out.push_str(field);
2031 out.push_str(&format!("[[\"{key}\"]]"));
2032 }
2033 PathSegment::Length => {
2034 let current = std::mem::take(&mut out);
2035 out = format!("length({current})");
2036 }
2037 }
2038 }
2039 out
2040}
2041
2042fn render_c(segments: &[PathSegment], result_var: &str) -> String {
2043 let mut out = result_var.to_string();
2044 for seg in segments {
2045 match seg {
2046 PathSegment::Field(f) => {
2047 let snake = f.to_snake_case();
2048 let current = std::mem::take(&mut out);
2049 out = format!("result_{snake}({current})");
2051 }
2052 PathSegment::ArrayField { name, index } => {
2053 let snake = name.to_snake_case();
2054 let current = std::mem::take(&mut out);
2055 out = format!("result_{snake}({current})[{index}]");
2056 }
2057 PathSegment::MapAccess { field, key } => {
2058 let snake = field.to_snake_case();
2059 let current = std::mem::take(&mut out);
2060 out = format!("result_{snake}({current})[\"{key}\"]");
2061 }
2062 PathSegment::Length => {
2063 let current = std::mem::take(&mut out);
2064 out = format!("result_{current}_count()");
2065 }
2066 }
2067 }
2068 out
2069}
2070
2071fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
2078 let mut out = result_var.to_string();
2079 for seg in segments {
2080 match seg {
2081 PathSegment::Field(f) => {
2082 out.push('.');
2083 out.push_str(&f.to_lower_camel_case());
2084 }
2085 PathSegment::ArrayField { name, index } => {
2086 out.push('.');
2087 out.push_str(&name.to_lower_camel_case());
2088 out.push_str(&format!("[{index}]"));
2089 }
2090 PathSegment::MapAccess { field, key } => {
2091 out.push('.');
2092 out.push_str(&field.to_lower_camel_case());
2093 if key.chars().all(|c| c.is_ascii_digit()) {
2094 out.push_str(&format!("[{key}]"));
2095 } else {
2096 out.push_str(&format!("[\"{key}\"]"));
2097 }
2098 }
2099 PathSegment::Length => {
2100 out.push_str(".length");
2101 }
2102 }
2103 }
2104 out
2105}
2106
2107fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
2113 let mut out = result_var.to_string();
2114 let mut path_so_far = String::new();
2122 let mut path_with_indices = String::new();
2123 let mut prev_was_nullable = false;
2124 let is_optional =
2125 |bare: &str, indexed: &str| -> bool { optional_fields.contains(bare) || optional_fields.contains(indexed) };
2126 for seg in segments {
2127 let nav = if prev_was_nullable { "?." } else { "." };
2128 match seg {
2129 PathSegment::Field(f) => {
2130 if !path_so_far.is_empty() {
2131 path_so_far.push('.');
2132 path_with_indices.push('.');
2133 }
2134 path_so_far.push_str(f);
2135 path_with_indices.push_str(f);
2136 let optional = is_optional(&path_so_far, &path_with_indices);
2137 out.push_str(nav);
2138 out.push_str(&f.to_lower_camel_case());
2139 prev_was_nullable = optional;
2140 }
2141 PathSegment::ArrayField { name, index } => {
2142 if !path_so_far.is_empty() {
2143 path_so_far.push('.');
2144 path_with_indices.push('.');
2145 }
2146 path_so_far.push_str(name);
2147 path_with_indices.push_str(name);
2148 let optional = is_optional(&path_so_far, &path_with_indices);
2149 out.push_str(nav);
2150 out.push_str(&name.to_lower_camel_case());
2151 if optional {
2155 out.push('!');
2156 }
2157 out.push_str(&format!("[{index}]"));
2158 path_with_indices.push_str(&format!("[{index}]"));
2159 prev_was_nullable = false;
2160 }
2161 PathSegment::MapAccess { field, key } => {
2162 if !path_so_far.is_empty() {
2163 path_so_far.push('.');
2164 path_with_indices.push('.');
2165 }
2166 path_so_far.push_str(field);
2167 path_with_indices.push_str(field);
2168 let optional = is_optional(&path_so_far, &path_with_indices);
2169 out.push_str(nav);
2170 out.push_str(&field.to_lower_camel_case());
2171 if key.chars().all(|c| c.is_ascii_digit()) {
2172 out.push_str(&format!("[{key}]"));
2173 path_with_indices.push_str(&format!("[{key}]"));
2174 } else {
2175 out.push_str(&format!("[\"{key}\"]"));
2176 path_with_indices.push_str(&format!("[\"{key}\"]"));
2177 }
2178 prev_was_nullable = optional;
2179 }
2180 PathSegment::Length => {
2181 out.push_str(nav);
2184 out.push_str("length");
2185 prev_was_nullable = false;
2186 }
2187 }
2188 }
2189 out
2190}
2191
2192#[cfg(test)]
2193mod tests {
2194 use super::*;
2195
2196 fn make_resolver() -> FieldResolver {
2197 let mut fields = HashMap::new();
2198 fields.insert("title".to_string(), "metadata.document.title".to_string());
2199 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
2200 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
2201 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
2202 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
2203 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
2204 let mut optional = HashSet::new();
2205 optional.insert("metadata.document.title".to_string());
2206 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
2207 }
2208
2209 fn make_resolver_with_doc_optional() -> FieldResolver {
2210 let mut fields = HashMap::new();
2211 fields.insert("title".to_string(), "metadata.document.title".to_string());
2212 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
2213 let mut optional = HashSet::new();
2214 optional.insert("document".to_string());
2215 optional.insert("metadata.document.title".to_string());
2216 optional.insert("metadata.document".to_string());
2217 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
2218 }
2219
2220 #[test]
2221 fn test_resolve_alias() {
2222 let r = make_resolver();
2223 assert_eq!(r.resolve("title"), "metadata.document.title");
2224 }
2225
2226 #[test]
2227 fn test_resolve_passthrough() {
2228 let r = make_resolver();
2229 assert_eq!(r.resolve("content"), "content");
2230 }
2231
2232 #[test]
2233 fn test_is_optional() {
2234 let r = make_resolver();
2235 assert!(r.is_optional("metadata.document.title"));
2236 assert!(!r.is_optional("content"));
2237 }
2238
2239 #[test]
2240 fn is_optional_strips_namespace_prefix() {
2241 let fields = HashMap::new();
2242 let mut optional = HashSet::new();
2243 optional.insert("action_results.data".to_string());
2244 let result_fields: HashSet<String> = ["action_results".to_string()].into_iter().collect();
2245 let r = FieldResolver::new(&fields, &optional, &result_fields, &HashSet::new(), &HashSet::new());
2246 assert!(r.is_optional("interaction.action_results[0].data"));
2248 assert!(r.is_optional("action_results[0].data"));
2250 }
2251
2252 #[test]
2253 fn test_accessor_rust_struct() {
2254 let r = make_resolver();
2255 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
2256 }
2257
2258 #[test]
2259 fn test_accessor_rust_map() {
2260 let r = make_resolver();
2261 assert_eq!(
2262 r.accessor("tags", "rust", "result"),
2263 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
2264 );
2265 }
2266
2267 #[test]
2268 fn test_accessor_python() {
2269 let r = make_resolver();
2270 assert_eq!(
2271 r.accessor("title", "python", "result"),
2272 "result.metadata.document.title"
2273 );
2274 }
2275
2276 #[test]
2277 fn test_accessor_go() {
2278 let r = make_resolver();
2279 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
2280 }
2281
2282 #[test]
2283 fn test_accessor_go_initialism_fields() {
2284 let mut fields = std::collections::HashMap::new();
2285 fields.insert("content".to_string(), "html".to_string());
2286 fields.insert("link_url".to_string(), "links.url".to_string());
2287 let r = FieldResolver::new(
2288 &fields,
2289 &HashSet::new(),
2290 &HashSet::new(),
2291 &HashSet::new(),
2292 &HashSet::new(),
2293 );
2294 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
2295 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
2296 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
2297 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
2298 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
2299 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
2300 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
2301 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
2302 }
2303
2304 #[test]
2305 fn test_accessor_typescript() {
2306 let r = make_resolver();
2307 assert_eq!(
2308 r.accessor("title", "typescript", "result"),
2309 "result.metadata.document.title"
2310 );
2311 }
2312
2313 #[test]
2314 fn test_accessor_typescript_snake_to_camel() {
2315 let r = make_resolver();
2316 assert_eq!(
2317 r.accessor("og", "typescript", "result"),
2318 "result.metadata.document.openGraph"
2319 );
2320 assert_eq!(
2321 r.accessor("twitter", "typescript", "result"),
2322 "result.metadata.document.twitterCard"
2323 );
2324 assert_eq!(
2325 r.accessor("canonical", "typescript", "result"),
2326 "result.metadata.document.canonicalUrl"
2327 );
2328 }
2329
2330 #[test]
2331 fn test_accessor_typescript_map_snake_to_camel() {
2332 let r = make_resolver();
2333 assert_eq!(
2334 r.accessor("og_tag", "typescript", "result"),
2335 "result.metadata.openGraphTags[\"og_title\"]"
2336 );
2337 }
2338
2339 #[test]
2340 fn test_accessor_typescript_numeric_index_is_unquoted() {
2341 let mut fields = HashMap::new();
2345 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
2346 let r = FieldResolver::new(
2347 &fields,
2348 &HashSet::new(),
2349 &HashSet::new(),
2350 &HashSet::new(),
2351 &HashSet::new(),
2352 );
2353 assert_eq!(
2354 r.accessor("first_score", "typescript", "result"),
2355 "result.results[0].relevanceScore"
2356 );
2357 }
2358
2359 #[test]
2360 fn test_accessor_node_alias() {
2361 let r = make_resolver();
2362 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
2363 }
2364
2365 #[test]
2366 fn test_accessor_wasm_camel_case() {
2367 let r = make_resolver();
2368 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
2369 assert_eq!(
2370 r.accessor("twitter", "wasm", "result"),
2371 "result.metadata.document.twitterCard"
2372 );
2373 assert_eq!(
2374 r.accessor("canonical", "wasm", "result"),
2375 "result.metadata.document.canonicalUrl"
2376 );
2377 }
2378
2379 #[test]
2380 fn test_accessor_wasm_map_access() {
2381 let r = make_resolver();
2382 assert_eq!(
2383 r.accessor("og_tag", "wasm", "result"),
2384 "result.metadata.openGraphTags.get(\"og_title\")"
2385 );
2386 }
2387
2388 #[test]
2389 fn test_accessor_java() {
2390 let r = make_resolver();
2391 assert_eq!(
2392 r.accessor("title", "java", "result"),
2393 "result.metadata().document().title()"
2394 );
2395 }
2396
2397 #[test]
2398 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
2399 let mut fields = HashMap::new();
2400 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2401 fields.insert("node_count".to_string(), "nodes.length".to_string());
2402 let mut arrays = HashSet::new();
2403 arrays.insert("nodes".to_string());
2404 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
2405 assert_eq!(
2406 r.accessor("first_node_name", "kotlin", "result"),
2407 "result.nodes().first().name()"
2408 );
2409 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
2410 }
2411
2412 #[test]
2413 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
2414 let r = make_resolver_with_doc_optional();
2415 assert_eq!(
2416 r.accessor("title", "kotlin", "result"),
2417 "result.metadata().document()?.title()"
2418 );
2419 }
2420
2421 #[test]
2422 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
2423 let mut fields = HashMap::new();
2424 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2425 fields.insert("tag".to_string(), "tags[name]".to_string());
2426 let mut optional = HashSet::new();
2427 optional.insert("nodes".to_string());
2428 optional.insert("tags".to_string());
2429 let mut arrays = HashSet::new();
2430 arrays.insert("nodes".to_string());
2431 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2432 assert_eq!(
2433 r.accessor("first_node_name", "kotlin", "result"),
2434 "result.nodes()?.first()?.name()"
2435 );
2436 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
2437 }
2438
2439 #[test]
2445 fn test_accessor_kotlin_optional_field_after_indexed_array() {
2446 let mut fields = HashMap::new();
2449 fields.insert(
2450 "tool_call_name".to_string(),
2451 "choices[0].message.tool_calls[0].function.name".to_string(),
2452 );
2453 let mut optional = HashSet::new();
2454 optional.insert("choices[0].message.tool_calls".to_string());
2455 let mut arrays = HashSet::new();
2456 arrays.insert("choices".to_string());
2457 arrays.insert("choices[0].message.tool_calls".to_string());
2458 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2459 let expr = r.accessor("tool_call_name", "kotlin", "result");
2460 assert!(
2462 expr.contains("toolCalls()?.first()"),
2463 "expected toolCalls()?.first() for optional list, got: {expr}"
2464 );
2465 }
2466
2467 #[test]
2468 fn test_accessor_csharp() {
2469 let r = make_resolver();
2470 assert_eq!(
2471 r.accessor("title", "csharp", "result"),
2472 "result.Metadata.Document.Title"
2473 );
2474 }
2475
2476 #[test]
2477 fn test_accessor_php() {
2478 let r = make_resolver();
2479 assert_eq!(
2480 r.accessor("title", "php", "$result"),
2481 "$result->metadata->document->title"
2482 );
2483 }
2484
2485 #[test]
2486 fn test_accessor_r() {
2487 let r = make_resolver();
2488 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
2489 }
2490
2491 #[test]
2492 fn test_accessor_c() {
2493 let r = make_resolver();
2494 assert_eq!(
2495 r.accessor("title", "c", "result"),
2496 "result_title(result_document(result_metadata(result)))"
2497 );
2498 }
2499
2500 #[test]
2501 fn test_rust_unwrap_binding() {
2502 let r = make_resolver();
2503 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
2504 assert_eq!(var, "metadata_document_title");
2505 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
2508 }
2509
2510 #[test]
2511 fn test_rust_unwrap_binding_non_optional() {
2512 let r = make_resolver();
2513 assert!(r.rust_unwrap_binding("content", "result").is_none());
2514 }
2515
2516 #[test]
2517 fn test_rust_unwrap_binding_collapses_double_underscore() {
2518 let mut aliases = HashMap::new();
2523 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
2524 let mut optional = HashSet::new();
2525 optional.insert("json_ld[].name".to_string());
2526 let mut array = HashSet::new();
2527 array.insert("json_ld".to_string());
2528 let result_fields = HashSet::new();
2529 let method_calls = HashSet::new();
2530 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
2531 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
2532 assert_eq!(var, "json_ld_name");
2533 }
2534
2535 #[test]
2536 fn test_direct_field_no_alias() {
2537 let r = make_resolver();
2538 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2539 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
2540 }
2541
2542 #[test]
2543 fn test_accessor_rust_with_optionals() {
2544 let r = make_resolver_with_doc_optional();
2545 assert_eq!(
2546 r.accessor("title", "rust", "result"),
2547 "result.metadata.document.as_ref().unwrap().title"
2548 );
2549 }
2550
2551 #[test]
2552 fn test_accessor_csharp_with_optionals() {
2553 let r = make_resolver_with_doc_optional();
2554 assert_eq!(
2555 r.accessor("title", "csharp", "result"),
2556 "result.Metadata.Document!.Title"
2557 );
2558 }
2559
2560 #[test]
2561 fn test_accessor_rust_non_optional_field() {
2562 let r = make_resolver();
2563 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2564 }
2565
2566 #[test]
2567 fn test_accessor_csharp_non_optional_field() {
2568 let r = make_resolver();
2569 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
2570 }
2571
2572 #[test]
2573 fn test_accessor_rust_method_call() {
2574 let mut fields = HashMap::new();
2576 fields.insert(
2577 "excel_sheet_count".to_string(),
2578 "metadata.format.excel.sheet_count".to_string(),
2579 );
2580 let mut optional = HashSet::new();
2581 optional.insert("metadata.format".to_string());
2582 optional.insert("metadata.format.excel".to_string());
2583 let mut method_calls = HashSet::new();
2584 method_calls.insert("metadata.format.excel".to_string());
2585 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
2586 assert_eq!(
2587 r.accessor("excel_sheet_count", "rust", "result"),
2588 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
2589 );
2590 }
2591
2592 fn make_php_getter_resolver() -> FieldResolver {
2597 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2598 getters.insert(
2599 "Root".to_string(),
2600 ["metadata".to_string(), "links".to_string()].into_iter().collect(),
2601 );
2602 let map = PhpGetterMap {
2603 getters,
2604 field_types: HashMap::new(),
2605 root_type: Some("Root".to_string()),
2606 all_fields: HashMap::new(),
2607 };
2608 FieldResolver::new_with_php_getters(
2609 &HashMap::new(),
2610 &HashSet::new(),
2611 &HashSet::new(),
2612 &HashSet::new(),
2613 &HashSet::new(),
2614 &HashMap::new(),
2615 map,
2616 )
2617 }
2618
2619 #[test]
2620 fn render_php_uses_getter_method_for_non_scalar_field() {
2621 let r = make_php_getter_resolver();
2622 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
2623 }
2624
2625 #[test]
2626 fn render_php_uses_property_for_scalar_field() {
2627 let r = make_php_getter_resolver();
2628 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2629 }
2630
2631 #[test]
2632 fn render_php_nested_non_scalar_uses_getter_then_property() {
2633 let mut fields = HashMap::new();
2634 fields.insert("title".to_string(), "metadata.title".to_string());
2635 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2636 getters.insert("Root".to_string(), ["metadata".to_string()].into_iter().collect());
2637 getters.insert("Metadata".to_string(), HashSet::new());
2639 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2640 field_types.insert(
2641 "Root".to_string(),
2642 [("metadata".to_string(), "Metadata".to_string())].into_iter().collect(),
2643 );
2644 let map = PhpGetterMap {
2645 getters,
2646 field_types,
2647 root_type: Some("Root".to_string()),
2648 all_fields: HashMap::new(),
2649 };
2650 let r = FieldResolver::new_with_php_getters(
2651 &fields,
2652 &HashSet::new(),
2653 &HashSet::new(),
2654 &HashSet::new(),
2655 &HashSet::new(),
2656 &HashMap::new(),
2657 map,
2658 );
2659 assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
2661 }
2662
2663 #[test]
2664 fn render_php_array_field_uses_getter_when_non_scalar() {
2665 let mut fields = HashMap::new();
2666 fields.insert("first_link".to_string(), "links[0]".to_string());
2667 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2668 getters.insert("Root".to_string(), ["links".to_string()].into_iter().collect());
2669 let map = PhpGetterMap {
2670 getters,
2671 field_types: HashMap::new(),
2672 root_type: Some("Root".to_string()),
2673 all_fields: HashMap::new(),
2674 };
2675 let r = FieldResolver::new_with_php_getters(
2676 &fields,
2677 &HashSet::new(),
2678 &HashSet::new(),
2679 &HashSet::new(),
2680 &HashSet::new(),
2681 &HashMap::new(),
2682 map,
2683 );
2684 assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
2685 }
2686
2687 #[test]
2688 fn render_php_falls_back_to_property_when_getter_fields_empty() {
2689 let r = FieldResolver::new(
2692 &HashMap::new(),
2693 &HashSet::new(),
2694 &HashSet::new(),
2695 &HashSet::new(),
2696 &HashSet::new(),
2697 );
2698 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2699 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
2700 }
2701
2702 #[test]
2706 fn render_php_with_getters_distinguishes_same_field_name_on_different_types() {
2707 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2708 getters.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2710 getters.insert("B".to_string(), HashSet::new());
2712 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2715 all_fields.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2716 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2717 let map_a = PhpGetterMap {
2718 getters: getters.clone(),
2719 field_types: HashMap::new(),
2720 root_type: Some("A".to_string()),
2721 all_fields: all_fields.clone(),
2722 };
2723 let map_b = PhpGetterMap {
2724 getters,
2725 field_types: HashMap::new(),
2726 root_type: Some("B".to_string()),
2727 all_fields,
2728 };
2729 let r_a = FieldResolver::new_with_php_getters(
2730 &HashMap::new(),
2731 &HashSet::new(),
2732 &HashSet::new(),
2733 &HashSet::new(),
2734 &HashSet::new(),
2735 &HashMap::new(),
2736 map_a,
2737 );
2738 let r_b = FieldResolver::new_with_php_getters(
2739 &HashMap::new(),
2740 &HashSet::new(),
2741 &HashSet::new(),
2742 &HashSet::new(),
2743 &HashSet::new(),
2744 &HashMap::new(),
2745 map_b,
2746 );
2747 assert_eq!(r_a.accessor("content", "php", "$a"), "$a->getContent()");
2748 assert_eq!(r_b.accessor("content", "php", "$b"), "$b->content");
2749 }
2750
2751 #[test]
2755 fn render_php_with_getters_chains_through_correct_type() {
2756 let mut fields = HashMap::new();
2757 fields.insert("nested_content".to_string(), "inner.content".to_string());
2758 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2759 getters.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2761 getters.insert("B".to_string(), HashSet::new());
2763 getters.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2766 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2767 field_types.insert(
2768 "Outer".to_string(),
2769 [("inner".to_string(), "B".to_string())].into_iter().collect(),
2770 );
2771 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2772 all_fields.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2773 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2774 all_fields.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2775 let map = PhpGetterMap {
2776 getters,
2777 field_types,
2778 root_type: Some("Outer".to_string()),
2779 all_fields,
2780 };
2781 let r = FieldResolver::new_with_php_getters(
2782 &fields,
2783 &HashSet::new(),
2784 &HashSet::new(),
2785 &HashSet::new(),
2786 &HashSet::new(),
2787 &HashMap::new(),
2788 map,
2789 );
2790 assert_eq!(
2791 r.accessor("nested_content", "php", "$result"),
2792 "$result->getInner()->content"
2793 );
2794 }
2795
2796 fn make_resolver_with_result_fields(result_fields: &[&str]) -> FieldResolver {
2801 let rf: HashSet<String> = result_fields.iter().map(|s| s.to_string()).collect();
2802 FieldResolver::new(&HashMap::new(), &HashSet::new(), &rf, &HashSet::new(), &HashSet::new())
2803 }
2804
2805 #[test]
2808 fn is_valid_for_result_accepts_virtual_namespace_prefix() {
2809 let r = make_resolver_with_result_fields(&["browser_used", "js_render_hint", "status_code"]);
2810 assert!(
2811 r.is_valid_for_result("browser.browser_used"),
2812 "browser.browser_used should be valid via namespace-prefix stripping"
2813 );
2814 assert!(
2815 r.is_valid_for_result("browser.js_render_hint"),
2816 "browser.js_render_hint should be valid via namespace-prefix stripping"
2817 );
2818 }
2819
2820 #[test]
2823 fn is_valid_for_result_accepts_namespace_prefix_before_array_field() {
2824 let r = make_resolver_with_result_fields(&["action_results", "final_html", "final_url"]);
2825 assert!(
2826 r.is_valid_for_result("interaction.action_results[0].action_type"),
2827 "interaction. prefix should be stripped so action_results is recognised"
2828 );
2829 }
2830
2831 #[test]
2833 fn is_valid_for_result_rejects_unknown_field_even_after_namespace_strip() {
2834 let r = make_resolver_with_result_fields(&["pages", "final_url"]);
2835 assert!(
2836 !r.is_valid_for_result("browser.browser_used"),
2837 "browser_used is not in result_fields so should be rejected"
2838 );
2839 assert!(
2840 !r.is_valid_for_result("ns.unknown_field"),
2841 "unknown_field is not in result_fields so should be rejected"
2842 );
2843 }
2844
2845 #[test]
2848 fn accessor_strips_namespace_prefix_for_python() {
2849 let r = make_resolver_with_result_fields(&["browser_used", "js_render_hint"]);
2850 assert_eq!(
2851 r.accessor("browser.browser_used", "python", "result"),
2852 "result.browser_used"
2853 );
2854 assert_eq!(
2855 r.accessor("browser.js_render_hint", "python", "result"),
2856 "result.js_render_hint"
2857 );
2858 }
2859
2860 #[test]
2862 fn accessor_strips_namespace_prefix_for_csharp() {
2863 let r = make_resolver_with_result_fields(&["browser_used"]);
2864 assert_eq!(
2865 r.accessor("browser.browser_used", "csharp", "result"),
2866 "result.BrowserUsed"
2867 );
2868 }
2869
2870 #[test]
2873 fn accessor_strips_namespace_prefix_for_indexed_array_field() {
2874 let r = make_resolver_with_result_fields(&["action_results", "final_html", "final_url"]);
2875 assert_eq!(
2877 r.accessor("interaction.action_results[0].action_type", "python", "result"),
2878 "result.action_results[0].action_type"
2879 );
2880 assert_eq!(
2882 r.accessor("interaction.action_results[0].action_type", "typescript", "result"),
2883 "result.actionResults[0].actionType"
2884 );
2885 }
2886
2887 #[test]
2890 fn is_valid_for_result_is_permissive_when_result_fields_empty() {
2891 let r = make_resolver_with_result_fields(&[]);
2892 assert!(r.is_valid_for_result("browser.browser_used"));
2893 assert!(r.is_valid_for_result("anything.at.all"));
2894 }
2895
2896 #[test]
2899 fn accessor_does_not_strip_real_first_segment() {
2900 let r = make_resolver_with_result_fields(&["metadata", "status_code"]);
2901 assert_eq!(
2903 r.accessor("metadata.title", "python", "result"),
2904 "result.metadata.title"
2905 );
2906 }
2907}