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 parts = Vec::new();
2044 let mut trailing_length = false;
2045 for seg in segments {
2046 match seg {
2047 PathSegment::Field(f) => parts.push(f.to_snake_case()),
2048 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
2049 PathSegment::MapAccess { field, key } => {
2050 parts.push(field.to_snake_case());
2051 parts.push(key.clone());
2052 }
2053 PathSegment::Length => {
2054 trailing_length = true;
2055 }
2056 }
2057 }
2058 let suffix = parts.join("_");
2059 if trailing_length {
2060 format!("result_{suffix}_count({result_var})")
2061 } else {
2062 format!("result_{suffix}({result_var})")
2063 }
2064}
2065
2066fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
2073 let mut out = result_var.to_string();
2074 for seg in segments {
2075 match seg {
2076 PathSegment::Field(f) => {
2077 out.push('.');
2078 out.push_str(&f.to_lower_camel_case());
2079 }
2080 PathSegment::ArrayField { name, index } => {
2081 out.push('.');
2082 out.push_str(&name.to_lower_camel_case());
2083 out.push_str(&format!("[{index}]"));
2084 }
2085 PathSegment::MapAccess { field, key } => {
2086 out.push('.');
2087 out.push_str(&field.to_lower_camel_case());
2088 if key.chars().all(|c| c.is_ascii_digit()) {
2089 out.push_str(&format!("[{key}]"));
2090 } else {
2091 out.push_str(&format!("[\"{key}\"]"));
2092 }
2093 }
2094 PathSegment::Length => {
2095 out.push_str(".length");
2096 }
2097 }
2098 }
2099 out
2100}
2101
2102fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
2108 let mut out = result_var.to_string();
2109 let mut path_so_far = String::new();
2117 let mut path_with_indices = String::new();
2118 let mut prev_was_nullable = false;
2119 let is_optional =
2120 |bare: &str, indexed: &str| -> bool { optional_fields.contains(bare) || optional_fields.contains(indexed) };
2121 for seg in segments {
2122 let nav = if prev_was_nullable { "?." } else { "." };
2123 match seg {
2124 PathSegment::Field(f) => {
2125 if !path_so_far.is_empty() {
2126 path_so_far.push('.');
2127 path_with_indices.push('.');
2128 }
2129 path_so_far.push_str(f);
2130 path_with_indices.push_str(f);
2131 let optional = is_optional(&path_so_far, &path_with_indices);
2132 out.push_str(nav);
2133 out.push_str(&f.to_lower_camel_case());
2134 prev_was_nullable = optional;
2135 }
2136 PathSegment::ArrayField { name, index } => {
2137 if !path_so_far.is_empty() {
2138 path_so_far.push('.');
2139 path_with_indices.push('.');
2140 }
2141 path_so_far.push_str(name);
2142 path_with_indices.push_str(name);
2143 let optional = is_optional(&path_so_far, &path_with_indices);
2144 out.push_str(nav);
2145 out.push_str(&name.to_lower_camel_case());
2146 if optional {
2150 out.push('!');
2151 }
2152 out.push_str(&format!("[{index}]"));
2153 path_with_indices.push_str(&format!("[{index}]"));
2154 prev_was_nullable = false;
2155 }
2156 PathSegment::MapAccess { field, key } => {
2157 if !path_so_far.is_empty() {
2158 path_so_far.push('.');
2159 path_with_indices.push('.');
2160 }
2161 path_so_far.push_str(field);
2162 path_with_indices.push_str(field);
2163 let optional = is_optional(&path_so_far, &path_with_indices);
2164 out.push_str(nav);
2165 out.push_str(&field.to_lower_camel_case());
2166 if key.chars().all(|c| c.is_ascii_digit()) {
2167 out.push_str(&format!("[{key}]"));
2168 path_with_indices.push_str(&format!("[{key}]"));
2169 } else {
2170 out.push_str(&format!("[\"{key}\"]"));
2171 path_with_indices.push_str(&format!("[\"{key}\"]"));
2172 }
2173 prev_was_nullable = optional;
2174 }
2175 PathSegment::Length => {
2176 out.push_str(nav);
2179 out.push_str("length");
2180 prev_was_nullable = false;
2181 }
2182 }
2183 }
2184 out
2185}
2186
2187#[cfg(test)]
2188mod tests {
2189 use super::*;
2190
2191 fn make_resolver() -> FieldResolver {
2192 let mut fields = HashMap::new();
2193 fields.insert("title".to_string(), "metadata.document.title".to_string());
2194 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
2195 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
2196 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
2197 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
2198 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
2199 let mut optional = HashSet::new();
2200 optional.insert("metadata.document.title".to_string());
2201 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
2202 }
2203
2204 fn make_resolver_with_doc_optional() -> FieldResolver {
2205 let mut fields = HashMap::new();
2206 fields.insert("title".to_string(), "metadata.document.title".to_string());
2207 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
2208 let mut optional = HashSet::new();
2209 optional.insert("document".to_string());
2210 optional.insert("metadata.document.title".to_string());
2211 optional.insert("metadata.document".to_string());
2212 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
2213 }
2214
2215 #[test]
2216 fn test_resolve_alias() {
2217 let r = make_resolver();
2218 assert_eq!(r.resolve("title"), "metadata.document.title");
2219 }
2220
2221 #[test]
2222 fn test_resolve_passthrough() {
2223 let r = make_resolver();
2224 assert_eq!(r.resolve("content"), "content");
2225 }
2226
2227 #[test]
2228 fn test_is_optional() {
2229 let r = make_resolver();
2230 assert!(r.is_optional("metadata.document.title"));
2231 assert!(!r.is_optional("content"));
2232 }
2233
2234 #[test]
2235 fn is_optional_strips_namespace_prefix() {
2236 let fields = HashMap::new();
2237 let mut optional = HashSet::new();
2238 optional.insert("action_results.data".to_string());
2239 let result_fields: HashSet<String> = ["action_results".to_string()].into_iter().collect();
2240 let r = FieldResolver::new(&fields, &optional, &result_fields, &HashSet::new(), &HashSet::new());
2241 assert!(r.is_optional("interaction.action_results[0].data"));
2243 assert!(r.is_optional("action_results[0].data"));
2245 }
2246
2247 #[test]
2248 fn test_accessor_rust_struct() {
2249 let r = make_resolver();
2250 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
2251 }
2252
2253 #[test]
2254 fn test_accessor_rust_map() {
2255 let r = make_resolver();
2256 assert_eq!(
2257 r.accessor("tags", "rust", "result"),
2258 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
2259 );
2260 }
2261
2262 #[test]
2263 fn test_accessor_python() {
2264 let r = make_resolver();
2265 assert_eq!(
2266 r.accessor("title", "python", "result"),
2267 "result.metadata.document.title"
2268 );
2269 }
2270
2271 #[test]
2272 fn test_accessor_go() {
2273 let r = make_resolver();
2274 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
2275 }
2276
2277 #[test]
2278 fn test_accessor_go_initialism_fields() {
2279 let mut fields = std::collections::HashMap::new();
2280 fields.insert("content".to_string(), "html".to_string());
2281 fields.insert("link_url".to_string(), "links.url".to_string());
2282 let r = FieldResolver::new(
2283 &fields,
2284 &HashSet::new(),
2285 &HashSet::new(),
2286 &HashSet::new(),
2287 &HashSet::new(),
2288 );
2289 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
2290 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
2291 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
2292 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
2293 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
2294 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
2295 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
2296 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
2297 }
2298
2299 #[test]
2300 fn test_accessor_typescript() {
2301 let r = make_resolver();
2302 assert_eq!(
2303 r.accessor("title", "typescript", "result"),
2304 "result.metadata.document.title"
2305 );
2306 }
2307
2308 #[test]
2309 fn test_accessor_typescript_snake_to_camel() {
2310 let r = make_resolver();
2311 assert_eq!(
2312 r.accessor("og", "typescript", "result"),
2313 "result.metadata.document.openGraph"
2314 );
2315 assert_eq!(
2316 r.accessor("twitter", "typescript", "result"),
2317 "result.metadata.document.twitterCard"
2318 );
2319 assert_eq!(
2320 r.accessor("canonical", "typescript", "result"),
2321 "result.metadata.document.canonicalUrl"
2322 );
2323 }
2324
2325 #[test]
2326 fn test_accessor_typescript_map_snake_to_camel() {
2327 let r = make_resolver();
2328 assert_eq!(
2329 r.accessor("og_tag", "typescript", "result"),
2330 "result.metadata.openGraphTags[\"og_title\"]"
2331 );
2332 }
2333
2334 #[test]
2335 fn test_accessor_typescript_numeric_index_is_unquoted() {
2336 let mut fields = HashMap::new();
2340 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
2341 let r = FieldResolver::new(
2342 &fields,
2343 &HashSet::new(),
2344 &HashSet::new(),
2345 &HashSet::new(),
2346 &HashSet::new(),
2347 );
2348 assert_eq!(
2349 r.accessor("first_score", "typescript", "result"),
2350 "result.results[0].relevanceScore"
2351 );
2352 }
2353
2354 #[test]
2355 fn test_accessor_node_alias() {
2356 let r = make_resolver();
2357 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
2358 }
2359
2360 #[test]
2361 fn test_accessor_wasm_camel_case() {
2362 let r = make_resolver();
2363 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
2364 assert_eq!(
2365 r.accessor("twitter", "wasm", "result"),
2366 "result.metadata.document.twitterCard"
2367 );
2368 assert_eq!(
2369 r.accessor("canonical", "wasm", "result"),
2370 "result.metadata.document.canonicalUrl"
2371 );
2372 }
2373
2374 #[test]
2375 fn test_accessor_wasm_map_access() {
2376 let r = make_resolver();
2377 assert_eq!(
2378 r.accessor("og_tag", "wasm", "result"),
2379 "result.metadata.openGraphTags.get(\"og_title\")"
2380 );
2381 }
2382
2383 #[test]
2384 fn test_accessor_java() {
2385 let r = make_resolver();
2386 assert_eq!(
2387 r.accessor("title", "java", "result"),
2388 "result.metadata().document().title()"
2389 );
2390 }
2391
2392 #[test]
2393 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
2394 let mut fields = HashMap::new();
2395 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2396 fields.insert("node_count".to_string(), "nodes.length".to_string());
2397 let mut arrays = HashSet::new();
2398 arrays.insert("nodes".to_string());
2399 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
2400 assert_eq!(
2401 r.accessor("first_node_name", "kotlin", "result"),
2402 "result.nodes().first().name()"
2403 );
2404 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
2405 }
2406
2407 #[test]
2408 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
2409 let r = make_resolver_with_doc_optional();
2410 assert_eq!(
2411 r.accessor("title", "kotlin", "result"),
2412 "result.metadata().document()?.title()"
2413 );
2414 }
2415
2416 #[test]
2417 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
2418 let mut fields = HashMap::new();
2419 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2420 fields.insert("tag".to_string(), "tags[name]".to_string());
2421 let mut optional = HashSet::new();
2422 optional.insert("nodes".to_string());
2423 optional.insert("tags".to_string());
2424 let mut arrays = HashSet::new();
2425 arrays.insert("nodes".to_string());
2426 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2427 assert_eq!(
2428 r.accessor("first_node_name", "kotlin", "result"),
2429 "result.nodes()?.first()?.name()"
2430 );
2431 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
2432 }
2433
2434 #[test]
2440 fn test_accessor_kotlin_optional_field_after_indexed_array() {
2441 let mut fields = HashMap::new();
2444 fields.insert(
2445 "tool_call_name".to_string(),
2446 "choices[0].message.tool_calls[0].function.name".to_string(),
2447 );
2448 let mut optional = HashSet::new();
2449 optional.insert("choices[0].message.tool_calls".to_string());
2450 let mut arrays = HashSet::new();
2451 arrays.insert("choices".to_string());
2452 arrays.insert("choices[0].message.tool_calls".to_string());
2453 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2454 let expr = r.accessor("tool_call_name", "kotlin", "result");
2455 assert!(
2457 expr.contains("toolCalls()?.first()"),
2458 "expected toolCalls()?.first() for optional list, got: {expr}"
2459 );
2460 }
2461
2462 #[test]
2463 fn test_accessor_csharp() {
2464 let r = make_resolver();
2465 assert_eq!(
2466 r.accessor("title", "csharp", "result"),
2467 "result.Metadata.Document.Title"
2468 );
2469 }
2470
2471 #[test]
2472 fn test_accessor_php() {
2473 let r = make_resolver();
2474 assert_eq!(
2475 r.accessor("title", "php", "$result"),
2476 "$result->metadata->document->title"
2477 );
2478 }
2479
2480 #[test]
2481 fn test_accessor_r() {
2482 let r = make_resolver();
2483 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
2484 }
2485
2486 #[test]
2487 fn test_accessor_c() {
2488 let r = make_resolver();
2489 assert_eq!(
2490 r.accessor("title", "c", "result"),
2491 "result_metadata_document_title(result)"
2492 );
2493 }
2494
2495 #[test]
2496 fn test_rust_unwrap_binding() {
2497 let r = make_resolver();
2498 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
2499 assert_eq!(var, "metadata_document_title");
2500 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
2503 }
2504
2505 #[test]
2506 fn test_rust_unwrap_binding_non_optional() {
2507 let r = make_resolver();
2508 assert!(r.rust_unwrap_binding("content", "result").is_none());
2509 }
2510
2511 #[test]
2512 fn test_rust_unwrap_binding_collapses_double_underscore() {
2513 let mut aliases = HashMap::new();
2518 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
2519 let mut optional = HashSet::new();
2520 optional.insert("json_ld[].name".to_string());
2521 let mut array = HashSet::new();
2522 array.insert("json_ld".to_string());
2523 let result_fields = HashSet::new();
2524 let method_calls = HashSet::new();
2525 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
2526 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
2527 assert_eq!(var, "json_ld_name");
2528 }
2529
2530 #[test]
2531 fn test_direct_field_no_alias() {
2532 let r = make_resolver();
2533 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2534 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
2535 }
2536
2537 #[test]
2538 fn test_accessor_rust_with_optionals() {
2539 let r = make_resolver_with_doc_optional();
2540 assert_eq!(
2541 r.accessor("title", "rust", "result"),
2542 "result.metadata.document.as_ref().unwrap().title"
2543 );
2544 }
2545
2546 #[test]
2547 fn test_accessor_csharp_with_optionals() {
2548 let r = make_resolver_with_doc_optional();
2549 assert_eq!(
2550 r.accessor("title", "csharp", "result"),
2551 "result.Metadata.Document!.Title"
2552 );
2553 }
2554
2555 #[test]
2556 fn test_accessor_rust_non_optional_field() {
2557 let r = make_resolver();
2558 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2559 }
2560
2561 #[test]
2562 fn test_accessor_csharp_non_optional_field() {
2563 let r = make_resolver();
2564 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
2565 }
2566
2567 #[test]
2568 fn test_accessor_rust_method_call() {
2569 let mut fields = HashMap::new();
2571 fields.insert(
2572 "excel_sheet_count".to_string(),
2573 "metadata.format.excel.sheet_count".to_string(),
2574 );
2575 let mut optional = HashSet::new();
2576 optional.insert("metadata.format".to_string());
2577 optional.insert("metadata.format.excel".to_string());
2578 let mut method_calls = HashSet::new();
2579 method_calls.insert("metadata.format.excel".to_string());
2580 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
2581 assert_eq!(
2582 r.accessor("excel_sheet_count", "rust", "result"),
2583 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
2584 );
2585 }
2586
2587 fn make_php_getter_resolver() -> FieldResolver {
2592 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2593 getters.insert(
2594 "Root".to_string(),
2595 ["metadata".to_string(), "links".to_string()].into_iter().collect(),
2596 );
2597 let map = PhpGetterMap {
2598 getters,
2599 field_types: HashMap::new(),
2600 root_type: Some("Root".to_string()),
2601 all_fields: HashMap::new(),
2602 };
2603 FieldResolver::new_with_php_getters(
2604 &HashMap::new(),
2605 &HashSet::new(),
2606 &HashSet::new(),
2607 &HashSet::new(),
2608 &HashSet::new(),
2609 &HashMap::new(),
2610 map,
2611 )
2612 }
2613
2614 #[test]
2615 fn render_php_uses_getter_method_for_non_scalar_field() {
2616 let r = make_php_getter_resolver();
2617 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
2618 }
2619
2620 #[test]
2621 fn render_php_uses_property_for_scalar_field() {
2622 let r = make_php_getter_resolver();
2623 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2624 }
2625
2626 #[test]
2627 fn render_php_nested_non_scalar_uses_getter_then_property() {
2628 let mut fields = HashMap::new();
2629 fields.insert("title".to_string(), "metadata.title".to_string());
2630 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2631 getters.insert("Root".to_string(), ["metadata".to_string()].into_iter().collect());
2632 getters.insert("Metadata".to_string(), HashSet::new());
2634 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2635 field_types.insert(
2636 "Root".to_string(),
2637 [("metadata".to_string(), "Metadata".to_string())].into_iter().collect(),
2638 );
2639 let map = PhpGetterMap {
2640 getters,
2641 field_types,
2642 root_type: Some("Root".to_string()),
2643 all_fields: HashMap::new(),
2644 };
2645 let r = FieldResolver::new_with_php_getters(
2646 &fields,
2647 &HashSet::new(),
2648 &HashSet::new(),
2649 &HashSet::new(),
2650 &HashSet::new(),
2651 &HashMap::new(),
2652 map,
2653 );
2654 assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
2656 }
2657
2658 #[test]
2659 fn render_php_array_field_uses_getter_when_non_scalar() {
2660 let mut fields = HashMap::new();
2661 fields.insert("first_link".to_string(), "links[0]".to_string());
2662 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2663 getters.insert("Root".to_string(), ["links".to_string()].into_iter().collect());
2664 let map = PhpGetterMap {
2665 getters,
2666 field_types: HashMap::new(),
2667 root_type: Some("Root".to_string()),
2668 all_fields: HashMap::new(),
2669 };
2670 let r = FieldResolver::new_with_php_getters(
2671 &fields,
2672 &HashSet::new(),
2673 &HashSet::new(),
2674 &HashSet::new(),
2675 &HashSet::new(),
2676 &HashMap::new(),
2677 map,
2678 );
2679 assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
2680 }
2681
2682 #[test]
2683 fn render_php_falls_back_to_property_when_getter_fields_empty() {
2684 let r = FieldResolver::new(
2687 &HashMap::new(),
2688 &HashSet::new(),
2689 &HashSet::new(),
2690 &HashSet::new(),
2691 &HashSet::new(),
2692 );
2693 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2694 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
2695 }
2696
2697 #[test]
2701 fn render_php_with_getters_distinguishes_same_field_name_on_different_types() {
2702 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2703 getters.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2705 getters.insert("B".to_string(), HashSet::new());
2707 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2710 all_fields.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2711 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2712 let map_a = PhpGetterMap {
2713 getters: getters.clone(),
2714 field_types: HashMap::new(),
2715 root_type: Some("A".to_string()),
2716 all_fields: all_fields.clone(),
2717 };
2718 let map_b = PhpGetterMap {
2719 getters,
2720 field_types: HashMap::new(),
2721 root_type: Some("B".to_string()),
2722 all_fields,
2723 };
2724 let r_a = FieldResolver::new_with_php_getters(
2725 &HashMap::new(),
2726 &HashSet::new(),
2727 &HashSet::new(),
2728 &HashSet::new(),
2729 &HashSet::new(),
2730 &HashMap::new(),
2731 map_a,
2732 );
2733 let r_b = FieldResolver::new_with_php_getters(
2734 &HashMap::new(),
2735 &HashSet::new(),
2736 &HashSet::new(),
2737 &HashSet::new(),
2738 &HashSet::new(),
2739 &HashMap::new(),
2740 map_b,
2741 );
2742 assert_eq!(r_a.accessor("content", "php", "$a"), "$a->getContent()");
2743 assert_eq!(r_b.accessor("content", "php", "$b"), "$b->content");
2744 }
2745
2746 #[test]
2750 fn render_php_with_getters_chains_through_correct_type() {
2751 let mut fields = HashMap::new();
2752 fields.insert("nested_content".to_string(), "inner.content".to_string());
2753 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2754 getters.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2756 getters.insert("B".to_string(), HashSet::new());
2758 getters.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2761 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2762 field_types.insert(
2763 "Outer".to_string(),
2764 [("inner".to_string(), "B".to_string())].into_iter().collect(),
2765 );
2766 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2767 all_fields.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2768 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2769 all_fields.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2770 let map = PhpGetterMap {
2771 getters,
2772 field_types,
2773 root_type: Some("Outer".to_string()),
2774 all_fields,
2775 };
2776 let r = FieldResolver::new_with_php_getters(
2777 &fields,
2778 &HashSet::new(),
2779 &HashSet::new(),
2780 &HashSet::new(),
2781 &HashSet::new(),
2782 &HashMap::new(),
2783 map,
2784 );
2785 assert_eq!(
2786 r.accessor("nested_content", "php", "$result"),
2787 "$result->getInner()->content"
2788 );
2789 }
2790
2791 fn make_resolver_with_result_fields(result_fields: &[&str]) -> FieldResolver {
2796 let rf: HashSet<String> = result_fields.iter().map(|s| s.to_string()).collect();
2797 FieldResolver::new(&HashMap::new(), &HashSet::new(), &rf, &HashSet::new(), &HashSet::new())
2798 }
2799
2800 #[test]
2803 fn is_valid_for_result_accepts_virtual_namespace_prefix() {
2804 let r = make_resolver_with_result_fields(&["browser_used", "js_render_hint", "status_code"]);
2805 assert!(
2806 r.is_valid_for_result("browser.browser_used"),
2807 "browser.browser_used should be valid via namespace-prefix stripping"
2808 );
2809 assert!(
2810 r.is_valid_for_result("browser.js_render_hint"),
2811 "browser.js_render_hint should be valid via namespace-prefix stripping"
2812 );
2813 }
2814
2815 #[test]
2818 fn is_valid_for_result_accepts_namespace_prefix_before_array_field() {
2819 let r = make_resolver_with_result_fields(&["action_results", "final_html", "final_url"]);
2820 assert!(
2821 r.is_valid_for_result("interaction.action_results[0].action_type"),
2822 "interaction. prefix should be stripped so action_results is recognised"
2823 );
2824 }
2825
2826 #[test]
2828 fn is_valid_for_result_rejects_unknown_field_even_after_namespace_strip() {
2829 let r = make_resolver_with_result_fields(&["pages", "final_url"]);
2830 assert!(
2831 !r.is_valid_for_result("browser.browser_used"),
2832 "browser_used is not in result_fields so should be rejected"
2833 );
2834 assert!(
2835 !r.is_valid_for_result("ns.unknown_field"),
2836 "unknown_field is not in result_fields so should be rejected"
2837 );
2838 }
2839
2840 #[test]
2843 fn accessor_strips_namespace_prefix_for_python() {
2844 let r = make_resolver_with_result_fields(&["browser_used", "js_render_hint"]);
2845 assert_eq!(
2846 r.accessor("browser.browser_used", "python", "result"),
2847 "result.browser_used"
2848 );
2849 assert_eq!(
2850 r.accessor("browser.js_render_hint", "python", "result"),
2851 "result.js_render_hint"
2852 );
2853 }
2854
2855 #[test]
2857 fn accessor_strips_namespace_prefix_for_csharp() {
2858 let r = make_resolver_with_result_fields(&["browser_used"]);
2859 assert_eq!(
2860 r.accessor("browser.browser_used", "csharp", "result"),
2861 "result.BrowserUsed"
2862 );
2863 }
2864
2865 #[test]
2868 fn accessor_strips_namespace_prefix_for_indexed_array_field() {
2869 let r = make_resolver_with_result_fields(&["action_results", "final_html", "final_url"]);
2870 assert_eq!(
2872 r.accessor("interaction.action_results[0].action_type", "python", "result"),
2873 "result.action_results[0].action_type"
2874 );
2875 assert_eq!(
2877 r.accessor("interaction.action_results[0].action_type", "typescript", "result"),
2878 "result.actionResults[0].actionType"
2879 );
2880 }
2881
2882 #[test]
2885 fn is_valid_for_result_is_permissive_when_result_fields_empty() {
2886 let r = make_resolver_with_result_fields(&[]);
2887 assert!(r.is_valid_for_result("browser.browser_used"));
2888 assert!(r.is_valid_for_result("anything.at.all"));
2889 }
2890
2891 #[test]
2894 fn accessor_does_not_strip_real_first_segment() {
2895 let r = make_resolver_with_result_fields(&["metadata", "status_code"]);
2896 assert_eq!(
2898 r.accessor("metadata.title", "python", "result"),
2899 "result.metadata.title"
2900 );
2901 }
2902}