1use alef_codegen::naming::to_go_name;
8use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
9use std::collections::{HashMap, HashSet};
10
11#[derive(Clone)]
13pub struct FieldResolver {
14 aliases: HashMap<String, String>,
15 optional_fields: HashSet<String>,
16 result_fields: HashSet<String>,
17 array_fields: HashSet<String>,
18 method_calls: HashSet<String>,
19 error_field_aliases: HashMap<String, String>,
23 php_getter_map: PhpGetterMap,
34 swift_first_class_map: SwiftFirstClassMap,
41}
42
43#[derive(Debug, Clone, Default)]
58pub struct PhpGetterMap {
59 pub getters: HashMap<String, HashSet<String>>,
60 pub field_types: HashMap<String, HashMap<String, String>>,
61 pub root_type: Option<String>,
62 pub all_fields: HashMap<String, HashSet<String>>,
68}
69
70#[derive(Debug, Clone, Default)]
94pub struct SwiftFirstClassMap {
95 pub first_class_types: HashSet<String>,
96 pub field_types: HashMap<String, HashMap<String, String>>,
97 pub vec_field_names: HashSet<String>,
98 pub root_type: Option<String>,
99}
100
101impl SwiftFirstClassMap {
102 pub fn is_first_class(&self, type_name: Option<&str>) -> bool {
108 match type_name {
109 Some(t) => self.first_class_types.contains(t),
110 None => true,
111 }
112 }
113
114 pub fn advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
117 let owner = owner_type?;
118 self.field_types.get(owner).and_then(|m| m.get(field_name).cloned())
119 }
120
121 pub fn is_vec_field_name(&self, field_name: &str) -> bool {
126 self.vec_field_names.contains(field_name)
127 }
128
129 pub fn is_empty(&self) -> bool {
131 self.first_class_types.is_empty() && self.field_types.is_empty()
132 }
133}
134
135impl PhpGetterMap {
136 pub fn needs_getter(&self, owner_type: Option<&str>, field_name: &str) -> bool {
143 if let Some(t) = owner_type {
144 let owner_has_field = self.all_fields.get(t).is_some_and(|s| s.contains(field_name));
149 if owner_has_field {
150 if let Some(fields) = self.getters.get(t) {
151 return fields.contains(field_name);
152 }
153 }
154 }
155 self.getters.values().any(|set| set.contains(field_name))
156 }
157
158 pub fn advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
161 let owner = owner_type?;
162 self.field_types.get(owner).and_then(|m| m.get(field_name).cloned())
163 }
164
165 pub fn is_empty(&self) -> bool {
168 self.getters.is_empty()
169 }
170}
171
172#[derive(Debug, Clone)]
174enum PathSegment {
175 Field(String),
177 ArrayField { name: String, index: usize },
182 MapAccess { field: String, key: String },
184 Length,
186}
187
188impl FieldResolver {
189 pub fn new(
193 fields: &HashMap<String, String>,
194 optional: &HashSet<String>,
195 result_fields: &HashSet<String>,
196 array_fields: &HashSet<String>,
197 method_calls: &HashSet<String>,
198 ) -> Self {
199 Self {
200 aliases: fields.clone(),
201 optional_fields: optional.clone(),
202 result_fields: result_fields.clone(),
203 array_fields: array_fields.clone(),
204 method_calls: method_calls.clone(),
205 error_field_aliases: HashMap::new(),
206 php_getter_map: PhpGetterMap::default(),
207 swift_first_class_map: SwiftFirstClassMap::default(),
208 }
209 }
210
211 pub fn new_with_error_aliases(
217 fields: &HashMap<String, String>,
218 optional: &HashSet<String>,
219 result_fields: &HashSet<String>,
220 array_fields: &HashSet<String>,
221 method_calls: &HashSet<String>,
222 error_field_aliases: &HashMap<String, String>,
223 ) -> Self {
224 Self {
225 aliases: fields.clone(),
226 optional_fields: optional.clone(),
227 result_fields: result_fields.clone(),
228 array_fields: array_fields.clone(),
229 method_calls: method_calls.clone(),
230 error_field_aliases: error_field_aliases.clone(),
231 php_getter_map: PhpGetterMap::default(),
232 swift_first_class_map: SwiftFirstClassMap::default(),
233 }
234 }
235
236 pub fn new_with_php_getters(
251 fields: &HashMap<String, String>,
252 optional: &HashSet<String>,
253 result_fields: &HashSet<String>,
254 array_fields: &HashSet<String>,
255 method_calls: &HashSet<String>,
256 error_field_aliases: &HashMap<String, String>,
257 php_getter_map: PhpGetterMap,
258 ) -> Self {
259 Self {
260 aliases: fields.clone(),
261 optional_fields: optional.clone(),
262 result_fields: result_fields.clone(),
263 array_fields: array_fields.clone(),
264 method_calls: method_calls.clone(),
265 error_field_aliases: error_field_aliases.clone(),
266 php_getter_map,
267 swift_first_class_map: SwiftFirstClassMap::default(),
268 }
269 }
270
271 pub fn with_swift_root_type(&self, root_type: Option<String>) -> Self {
282 let mut clone = self.clone();
283 clone.swift_first_class_map.root_type = root_type;
284 clone
285 }
286
287 #[allow(clippy::too_many_arguments)]
291 pub fn new_with_swift_first_class(
292 fields: &HashMap<String, String>,
293 optional: &HashSet<String>,
294 result_fields: &HashSet<String>,
295 array_fields: &HashSet<String>,
296 method_calls: &HashSet<String>,
297 error_field_aliases: &HashMap<String, String>,
298 swift_first_class_map: SwiftFirstClassMap,
299 ) -> Self {
300 Self {
301 aliases: fields.clone(),
302 optional_fields: optional.clone(),
303 result_fields: result_fields.clone(),
304 array_fields: array_fields.clone(),
305 method_calls: method_calls.clone(),
306 error_field_aliases: error_field_aliases.clone(),
307 php_getter_map: PhpGetterMap::default(),
308 swift_first_class_map,
309 }
310 }
311
312 pub fn resolve<'a>(&'a self, fixture_field: &'a str) -> &'a str {
315 self.aliases
316 .get(fixture_field)
317 .map(String::as_str)
318 .unwrap_or(fixture_field)
319 }
320
321 pub fn leaf_is_vec_via_swift_map(&self, field: &str) -> bool {
328 let leaf = field.split('.').next_back().unwrap_or(field);
329 let leaf = leaf.split('[').next().unwrap_or(leaf);
330 self.swift_first_class_map.is_vec_field_name(leaf)
331 }
332
333 pub fn is_optional(&self, field: &str) -> bool {
335 if self.optional_fields.contains(field) {
336 return true;
337 }
338 let index_normalized = normalize_numeric_indices(field);
339 if index_normalized != field && self.optional_fields.contains(index_normalized.as_str()) {
340 return true;
341 }
342 let de_indexed = strip_numeric_indices(field);
345 if de_indexed != field && self.optional_fields.contains(de_indexed.as_str()) {
346 return true;
347 }
348 let normalized = field.replace("[].", ".");
349 if normalized != field && self.optional_fields.contains(normalized.as_str()) {
350 return true;
351 }
352 for af in &self.array_fields {
353 if let Some(rest) = field.strip_prefix(af.as_str()) {
354 if let Some(rest) = rest.strip_prefix('.') {
355 let with_bracket = format!("{af}[].{rest}");
356 if self.optional_fields.contains(with_bracket.as_str()) {
357 return true;
358 }
359 }
360 }
361 }
362 false
363 }
364
365 pub fn has_alias(&self, fixture_field: &str) -> bool {
367 self.aliases.contains_key(fixture_field)
368 }
369
370 pub fn has_explicit_field(&self, field_name: &str) -> bool {
376 if self.result_fields.is_empty() {
377 return false;
378 }
379 self.result_fields.contains(field_name)
380 }
381
382 pub fn is_valid_for_result(&self, fixture_field: &str) -> bool {
391 if self.result_fields.is_empty() {
392 return true;
393 }
394 let resolved = self.resolve(fixture_field);
395 let first_segment = resolved.split('.').next().unwrap_or(resolved);
396 let first_segment = first_segment.split('[').next().unwrap_or(first_segment);
397 if self.result_fields.contains(first_segment) {
398 return true;
399 }
400 if let Some(suffix) = self.namespace_stripped_path(resolved) {
406 let suffix_first = suffix.split('.').next().unwrap_or(suffix);
407 let suffix_first = suffix_first.split('[').next().unwrap_or(suffix_first);
408 return self.result_fields.contains(suffix_first);
409 }
410 false
411 }
412
413 fn namespace_stripped_path<'a>(&self, path: &'a str) -> Option<&'a str> {
419 let dot_pos = path.find('.')?;
420 let first = &path[..dot_pos];
421 if first.contains('[') {
424 return None;
425 }
426 if self.result_fields.contains(first) {
429 return None;
430 }
431 let suffix = &path[dot_pos + 1..];
432 if suffix.is_empty() { None } else { Some(suffix) }
433 }
434
435 pub fn is_array(&self, field: &str) -> bool {
437 self.array_fields.contains(field)
438 }
439
440 pub fn is_collection_root(&self, field: &str) -> bool {
453 let prefix = format!("{field}[");
454 self.array_fields.iter().any(|af| af.starts_with(&prefix))
455 || self.optional_fields.iter().any(|of| of.starts_with(&prefix))
456 }
457
458 pub fn tagged_union_split(&self, fixture_field: &str) -> Option<(String, String, String)> {
470 let resolved = self.resolve(fixture_field);
471 let segments: Vec<&str> = resolved.split('.').collect();
472 let mut path_so_far = String::new();
473 for (i, seg) in segments.iter().enumerate() {
474 if !path_so_far.is_empty() {
475 path_so_far.push('.');
476 }
477 path_so_far.push_str(seg);
478 if self.method_calls.contains(&path_so_far) {
479 let prefix = segments[..i].join(".");
481 let variant = (*seg).to_string();
482 let suffix = segments[i + 1..].join(".");
483 return Some((prefix, variant, suffix));
484 }
485 }
486 None
487 }
488
489 pub fn has_map_access(&self, fixture_field: &str) -> bool {
491 let resolved = self.resolve(fixture_field);
492 let segments = parse_path(resolved);
493 segments.iter().any(|s| {
494 if let PathSegment::MapAccess { key, .. } = s {
495 !key.chars().all(|c| c.is_ascii_digit())
496 } else {
497 false
498 }
499 })
500 }
501
502 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
511 let resolved = self.resolve(fixture_field);
512 let effective = if !self.result_fields.is_empty() {
516 if let Some(stripped) = self.namespace_stripped_path(resolved) {
517 let stripped_first = stripped.split('.').next().unwrap_or(stripped);
518 let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
519 if self.result_fields.contains(stripped_first) {
520 stripped
521 } else {
522 resolved
523 }
524 } else {
525 resolved
526 }
527 } else {
528 resolved
529 };
530 let segments = parse_path(effective);
531 let segments = self.inject_array_indexing(segments);
532 match language {
533 "java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
534 "kotlin" => render_kotlin_with_optionals(&segments, result_var, &self.optional_fields),
535 "kotlin_android" => render_kotlin_android_with_optionals(&segments, result_var, &self.optional_fields),
538 "rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
539 "csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
540 "zig" => render_zig_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
541 "swift" if !self.swift_first_class_map.is_empty() => render_swift_with_first_class_map(
542 &segments,
543 result_var,
544 &self.optional_fields,
545 &self.swift_first_class_map,
546 ),
547 "swift" => render_swift_with_optionals(&segments, result_var, &self.optional_fields),
548 "dart" => render_dart_with_optionals(&segments, result_var, &self.optional_fields),
549 "php" if !self.php_getter_map.is_empty() => {
550 render_php_with_getters(&segments, result_var, &self.php_getter_map)
551 }
552 _ => render_accessor(&segments, language, result_var),
553 }
554 }
555
556 pub fn accessor_for_error(&self, sub_field: &str, language: &str, err_var: &str) -> String {
570 let resolved = self
571 .error_field_aliases
572 .get(sub_field)
573 .map(String::as_str)
574 .unwrap_or(sub_field);
575 let segments = parse_path(resolved);
576 match language {
579 "rust" => render_rust_with_optionals(&segments, err_var, &self.optional_fields, &self.method_calls),
580 _ => render_accessor(&segments, language, err_var),
581 }
582 }
583
584 pub fn has_error_aliases(&self) -> bool {
591 !self.error_field_aliases.is_empty()
592 }
593
594 fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
595 if self.array_fields.is_empty() {
596 return segments;
597 }
598 let len = segments.len();
599 let mut result = Vec::with_capacity(len);
600 let mut path_so_far = String::new();
601 for i in 0..len {
602 let seg = &segments[i];
603 match seg {
604 PathSegment::Field(f) => {
605 if !path_so_far.is_empty() {
606 path_so_far.push('.');
607 }
608 path_so_far.push_str(f);
609 let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
610 if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
611 result.push(PathSegment::ArrayField {
613 name: f.clone(),
614 index: 0,
615 });
616 } else {
617 result.push(seg.clone());
618 }
619 }
620 PathSegment::ArrayField { .. } => {
623 result.push(seg.clone());
624 }
625 PathSegment::MapAccess { field, key } => {
626 if !path_so_far.is_empty() {
627 path_so_far.push('.');
628 }
629 path_so_far.push_str(field);
630 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
631 if is_numeric && self.array_fields.contains(&path_so_far) {
632 let index: usize = key.parse().unwrap_or(0);
634 result.push(PathSegment::ArrayField {
635 name: field.clone(),
636 index,
637 });
638 } else {
639 result.push(seg.clone());
640 }
641 }
642 _ => {
643 result.push(seg.clone());
644 }
645 }
646 }
647 result
648 }
649
650 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
652 let resolved = self.resolve(fixture_field);
653 if !self.is_optional(resolved) {
654 return None;
655 }
656 let segments = parse_path(resolved);
657 let segments = self.inject_array_indexing(segments);
658 let local_var = {
663 let raw = resolved.replace(['.', '['], "_").replace(']', "");
664 let mut collapsed = String::with_capacity(raw.len());
665 let mut prev_underscore = false;
666 for ch in raw.chars() {
667 if ch == '_' {
668 if !prev_underscore {
669 collapsed.push('_');
670 }
671 prev_underscore = true;
672 } else {
673 collapsed.push(ch);
674 prev_underscore = false;
675 }
676 }
677 collapsed.trim_matches('_').to_string()
678 };
679 let accessor = render_accessor(&segments, "rust", result_var);
680 let has_map_access = segments.iter().any(|s| {
681 if let PathSegment::MapAccess { key, .. } = s {
682 !key.chars().all(|c| c.is_ascii_digit())
683 } else {
684 false
685 }
686 });
687 let is_array = self.is_array(resolved);
688 let binding = if has_map_access {
689 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
690 } else if is_array {
691 format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
692 } else {
693 format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
694 };
695 Some((binding, local_var))
696 }
697}
698
699fn strip_numeric_indices(path: &str) -> String {
704 let mut result = String::with_capacity(path.len());
705 let mut chars = path.chars().peekable();
706 while let Some(c) = chars.next() {
707 if c == '[' {
708 let mut key = String::new();
709 let mut closed = false;
710 for inner in chars.by_ref() {
711 if inner == ']' {
712 closed = true;
713 break;
714 }
715 key.push(inner);
716 }
717 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
718 } else {
720 result.push('[');
721 result.push_str(&key);
722 if closed {
723 result.push(']');
724 }
725 }
726 } else {
727 result.push(c);
728 }
729 }
730 while result.contains("..") {
732 result = result.replace("..", ".");
733 }
734 if result.starts_with('.') {
735 result.remove(0);
736 }
737 result
738}
739
740fn normalize_numeric_indices(path: &str) -> String {
741 let mut result = String::with_capacity(path.len());
742 let mut chars = path.chars().peekable();
743 while let Some(c) = chars.next() {
744 if c == '[' {
745 let mut key = String::new();
746 let mut closed = false;
747 for inner in chars.by_ref() {
748 if inner == ']' {
749 closed = true;
750 break;
751 }
752 key.push(inner);
753 }
754 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
755 result.push_str("[0]");
756 } else {
757 result.push('[');
758 result.push_str(&key);
759 if closed {
760 result.push(']');
761 }
762 }
763 } else {
764 result.push(c);
765 }
766 }
767 result
768}
769
770fn parse_path(path: &str) -> Vec<PathSegment> {
771 let mut segments = Vec::new();
772 for part in path.split('.') {
773 if part == "length" || part == "count" || part == "size" {
774 segments.push(PathSegment::Length);
775 } else if let Some(bracket_pos) = part.find('[') {
776 let name = part[..bracket_pos].to_string();
777 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
778 if key.is_empty() {
779 segments.push(PathSegment::ArrayField { name, index: 0 });
781 } else if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
782 let index: usize = key.parse().unwrap_or(0);
784 segments.push(PathSegment::ArrayField { name, index });
785 } else {
786 segments.push(PathSegment::MapAccess { field: name, key });
788 }
789 } else {
790 segments.push(PathSegment::Field(part.to_string()));
791 }
792 }
793 segments
794}
795
796fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
797 match language {
798 "rust" => render_rust(segments, result_var),
799 "python" => render_dot_access(segments, result_var, "python"),
800 "typescript" | "node" => render_typescript(segments, result_var),
801 "wasm" => render_wasm(segments, result_var),
802 "go" => render_go(segments, result_var),
803 "java" => render_java(segments, result_var),
804 "kotlin" => render_kotlin(segments, result_var),
805 "kotlin_android" => render_kotlin_android(segments, result_var),
806 "csharp" => render_pascal_dot(segments, result_var),
807 "ruby" => render_dot_access(segments, result_var, "ruby"),
808 "php" => render_php(segments, result_var),
809 "elixir" => render_dot_access(segments, result_var, "elixir"),
810 "r" => render_r(segments, result_var),
811 "c" => render_c(segments, result_var),
812 "swift" => render_swift(segments, result_var),
813 "dart" => render_dart(segments, result_var),
814 _ => render_dot_access(segments, result_var, language),
815 }
816}
817
818fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
830 let mut out = result_var.to_string();
831 for seg in segments {
832 match seg {
833 PathSegment::Field(f) => {
834 out.push('.');
835 out.push_str(f);
836 }
837 PathSegment::ArrayField { name, index } => {
838 out.push('.');
839 out.push_str(name);
840 out.push_str(&format!("[{index}]"));
841 }
842 PathSegment::MapAccess { field, key } => {
843 out.push('.');
844 out.push_str(field);
845 if key.chars().all(|c| c.is_ascii_digit()) {
846 out.push_str(&format!("[{key}]"));
847 } else {
848 out.push_str(&format!("[\"{key}\"]"));
849 }
850 }
851 PathSegment::Length => {
852 out.push_str(".count");
853 }
854 }
855 }
856 out
857}
858
859fn render_swift_with_optionals(
869 segments: &[PathSegment],
870 result_var: &str,
871 optional_fields: &HashSet<String>,
872) -> String {
873 let mut out = result_var.to_string();
874 let mut path_so_far = String::new();
875 let total = segments.len();
876 for (i, seg) in segments.iter().enumerate() {
877 let is_leaf = i == total - 1;
878 match seg {
879 PathSegment::Field(f) => {
880 if !path_so_far.is_empty() {
881 path_so_far.push('.');
882 }
883 path_so_far.push_str(f);
884 out.push('.');
885 out.push_str(f);
886 if !is_leaf && optional_fields.contains(&path_so_far) {
890 out.push('?');
891 }
892 }
893 PathSegment::ArrayField { name, index } => {
894 if !path_so_far.is_empty() {
895 path_so_far.push('.');
896 }
897 path_so_far.push_str(name);
898 let is_optional = optional_fields.contains(&path_so_far);
899 out.push('.');
900 out.push_str(name);
901 if is_optional {
902 out.push_str(&format!("?[{index}]"));
904 } else {
905 out.push_str(&format!("[{index}]"));
906 }
907 path_so_far.push_str("[0]");
908 let _ = is_leaf;
909 }
910 PathSegment::MapAccess { field, key } => {
911 if !path_so_far.is_empty() {
912 path_so_far.push('.');
913 }
914 path_so_far.push_str(field);
915 out.push('.');
916 out.push_str(field);
917 if key.chars().all(|c| c.is_ascii_digit()) {
918 out.push_str(&format!("[{key}]"));
919 } else {
920 out.push_str(&format!("[\"{key}\"]"));
921 }
922 }
923 PathSegment::Length => {
924 out.push_str(".count");
925 }
926 }
927 }
928 out
929}
930
931fn render_swift_with_first_class_map(
936 segments: &[PathSegment],
937 result_var: &str,
938 optional_fields: &HashSet<String>,
939 map: &SwiftFirstClassMap,
940) -> String {
941 let mut out = result_var.to_string();
942 let mut path_so_far = String::new();
943 let mut current_type: Option<String> = map.root_type.clone();
944 let mut via_rust_vec = false;
953 let total = segments.len();
954 for (i, seg) in segments.iter().enumerate() {
955 let is_leaf = i == total - 1;
956 let property_syntax = !via_rust_vec && map.is_first_class(current_type.as_deref());
957 match seg {
958 PathSegment::Field(f) => {
959 if !path_so_far.is_empty() {
960 path_so_far.push('.');
961 }
962 path_so_far.push_str(f);
963 out.push('.');
964 out.push_str(f);
965 if !property_syntax {
966 out.push_str("()");
967 }
968 if !is_leaf && optional_fields.contains(&path_so_far) {
969 out.push('?');
970 }
971 current_type = map.advance(current_type.as_deref(), f);
972 }
973 PathSegment::ArrayField { name, index } => {
974 if !path_so_far.is_empty() {
975 path_so_far.push('.');
976 }
977 path_so_far.push_str(name);
978 let is_optional = optional_fields.contains(&path_so_far);
979 out.push('.');
980 out.push_str(name);
981 let access = if property_syntax { "" } else { "()" };
982 if is_optional {
983 out.push_str(&format!("{access}?[{index}]"));
984 } else {
985 out.push_str(&format!("{access}[{index}]"));
986 }
987 path_so_far.push_str("[0]");
988 current_type = map.advance(current_type.as_deref(), name);
991 via_rust_vec = true;
992 }
993 PathSegment::MapAccess { field, key } => {
994 if !path_so_far.is_empty() {
995 path_so_far.push('.');
996 }
997 path_so_far.push_str(field);
998 out.push('.');
999 out.push_str(field);
1000 let access = if property_syntax { "" } else { "()" };
1001 if key.chars().all(|c| c.is_ascii_digit()) {
1002 out.push_str(&format!("{access}[{key}]"));
1003 } else {
1004 out.push_str(&format!("{access}[\"{key}\"]"));
1005 }
1006 current_type = map.advance(current_type.as_deref(), field);
1007 }
1008 PathSegment::Length => {
1009 out.push_str(".count");
1010 }
1011 }
1012 }
1013 out
1014}
1015
1016fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
1017 let mut out = result_var.to_string();
1018 for seg in segments {
1019 match seg {
1020 PathSegment::Field(f) => {
1021 out.push('.');
1022 out.push_str(&f.to_snake_case());
1023 }
1024 PathSegment::ArrayField { name, index } => {
1025 out.push('.');
1026 out.push_str(&name.to_snake_case());
1027 out.push_str(&format!("[{index}]"));
1028 }
1029 PathSegment::MapAccess { field, key } => {
1030 out.push('.');
1031 out.push_str(&field.to_snake_case());
1032 if key.chars().all(|c| c.is_ascii_digit()) {
1033 out.push_str(&format!("[{key}]"));
1034 } else {
1035 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1036 }
1037 }
1038 PathSegment::Length => {
1039 out.push_str(".len()");
1040 }
1041 }
1042 }
1043 out
1044}
1045
1046fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
1047 let mut out = result_var.to_string();
1048 for seg in segments {
1049 match seg {
1050 PathSegment::Field(f) => {
1051 out.push('.');
1052 out.push_str(f);
1053 }
1054 PathSegment::ArrayField { name, index } => {
1055 if language == "elixir" {
1056 let current = std::mem::take(&mut out);
1057 out = format!("Enum.at({current}.{name}, {index})");
1058 } else {
1059 out.push('.');
1060 out.push_str(name);
1061 out.push_str(&format!("[{index}]"));
1062 }
1063 }
1064 PathSegment::MapAccess { field, key } => {
1065 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
1066 if is_numeric && language == "elixir" {
1067 let current = std::mem::take(&mut out);
1068 out = format!("Enum.at({current}.{field}, {key})");
1069 } else {
1070 out.push('.');
1071 out.push_str(field);
1072 if is_numeric {
1073 let idx: usize = key.parse().unwrap_or(0);
1074 out.push_str(&format!("[{idx}]"));
1075 } else if language == "elixir" || language == "ruby" {
1076 out.push_str(&format!("[\"{key}\"]"));
1079 } else {
1080 out.push_str(&format!(".get(\"{key}\")"));
1081 }
1082 }
1083 }
1084 PathSegment::Length => match language {
1085 "ruby" => out.push_str(".length"),
1086 "elixir" => {
1087 let current = std::mem::take(&mut out);
1088 out = format!("length({current})");
1089 }
1090 "gleam" => {
1091 let current = std::mem::take(&mut out);
1092 out = format!("list.length({current})");
1093 }
1094 _ => {
1095 let current = std::mem::take(&mut out);
1096 out = format!("len({current})");
1097 }
1098 },
1099 }
1100 }
1101 out
1102}
1103
1104fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
1105 let mut out = result_var.to_string();
1106 for seg in segments {
1107 match seg {
1108 PathSegment::Field(f) => {
1109 out.push('.');
1110 out.push_str(&f.to_lower_camel_case());
1111 }
1112 PathSegment::ArrayField { name, index } => {
1113 out.push('.');
1114 out.push_str(&name.to_lower_camel_case());
1115 out.push_str(&format!("[{index}]"));
1116 }
1117 PathSegment::MapAccess { field, key } => {
1118 out.push('.');
1119 out.push_str(&field.to_lower_camel_case());
1120 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
1123 out.push_str(&format!("[{key}]"));
1124 } else {
1125 out.push_str(&format!("[\"{key}\"]"));
1126 }
1127 }
1128 PathSegment::Length => {
1129 out.push_str(".length");
1130 }
1131 }
1132 }
1133 out
1134}
1135
1136fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
1137 let mut out = result_var.to_string();
1138 for seg in segments {
1139 match seg {
1140 PathSegment::Field(f) => {
1141 out.push('.');
1142 out.push_str(&f.to_lower_camel_case());
1143 }
1144 PathSegment::ArrayField { name, index } => {
1145 out.push('.');
1146 out.push_str(&name.to_lower_camel_case());
1147 out.push_str(&format!("[{index}]"));
1148 }
1149 PathSegment::MapAccess { field, key } => {
1150 out.push('.');
1151 out.push_str(&field.to_lower_camel_case());
1152 out.push_str(&format!(".get(\"{key}\")"));
1153 }
1154 PathSegment::Length => {
1155 out.push_str(".length");
1156 }
1157 }
1158 }
1159 out
1160}
1161
1162fn render_go(segments: &[PathSegment], result_var: &str) -> String {
1163 let mut out = result_var.to_string();
1164 for seg in segments {
1165 match seg {
1166 PathSegment::Field(f) => {
1167 out.push('.');
1168 out.push_str(&to_go_name(f));
1169 }
1170 PathSegment::ArrayField { name, index } => {
1171 out.push('.');
1172 out.push_str(&to_go_name(name));
1173 out.push_str(&format!("[{index}]"));
1174 }
1175 PathSegment::MapAccess { field, key } => {
1176 out.push('.');
1177 out.push_str(&to_go_name(field));
1178 if key.chars().all(|c| c.is_ascii_digit()) {
1179 out.push_str(&format!("[{key}]"));
1180 } else {
1181 out.push_str(&format!("[\"{key}\"]"));
1182 }
1183 }
1184 PathSegment::Length => {
1185 let current = std::mem::take(&mut out);
1186 out = format!("len({current})");
1187 }
1188 }
1189 }
1190 out
1191}
1192
1193fn render_java(segments: &[PathSegment], result_var: &str) -> String {
1194 let mut out = result_var.to_string();
1195 for seg in segments {
1196 match seg {
1197 PathSegment::Field(f) => {
1198 out.push('.');
1199 out.push_str(&f.to_lower_camel_case());
1200 out.push_str("()");
1201 }
1202 PathSegment::ArrayField { name, index } => {
1203 out.push('.');
1204 out.push_str(&name.to_lower_camel_case());
1205 out.push_str(&format!("().get({index})"));
1206 }
1207 PathSegment::MapAccess { field, key } => {
1208 out.push('.');
1209 out.push_str(&field.to_lower_camel_case());
1210 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1212 if is_numeric {
1213 out.push_str(&format!("().get({key})"));
1214 } else {
1215 out.push_str(&format!("().get(\"{key}\")"));
1216 }
1217 }
1218 PathSegment::Length => {
1219 out.push_str(".size()");
1220 }
1221 }
1222 }
1223 out
1224}
1225
1226fn kotlin_getter(name: &str) -> String {
1231 let camel = name.to_lower_camel_case();
1232 match camel.as_str() {
1233 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
1234 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
1235 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
1236 _ => camel,
1237 }
1238}
1239
1240fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
1241 let mut out = result_var.to_string();
1242 for seg in segments {
1243 match seg {
1244 PathSegment::Field(f) => {
1245 out.push('.');
1246 out.push_str(&kotlin_getter(f));
1247 out.push_str("()");
1248 }
1249 PathSegment::ArrayField { name, index } => {
1250 out.push('.');
1251 out.push_str(&kotlin_getter(name));
1252 if *index == 0 {
1253 out.push_str("().first()");
1254 } else {
1255 out.push_str(&format!("().get({index})"));
1256 }
1257 }
1258 PathSegment::MapAccess { field, key } => {
1259 out.push('.');
1260 out.push_str(&kotlin_getter(field));
1261 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1262 if is_numeric {
1263 out.push_str(&format!("().get({key})"));
1264 } else {
1265 out.push_str(&format!("().get(\"{key}\")"));
1266 }
1267 }
1268 PathSegment::Length => {
1269 out.push_str(".size");
1270 }
1271 }
1272 }
1273 out
1274}
1275
1276fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1277 let mut out = result_var.to_string();
1278 let mut path_so_far = String::new();
1279 for (i, seg) in segments.iter().enumerate() {
1280 let is_leaf = i == segments.len() - 1;
1281 match seg {
1282 PathSegment::Field(f) => {
1283 if !path_so_far.is_empty() {
1284 path_so_far.push('.');
1285 }
1286 path_so_far.push_str(f);
1287 out.push('.');
1288 out.push_str(&f.to_lower_camel_case());
1289 out.push_str("()");
1290 let _ = is_leaf;
1291 let _ = optional_fields;
1292 }
1293 PathSegment::ArrayField { name, index } => {
1294 if !path_so_far.is_empty() {
1295 path_so_far.push('.');
1296 }
1297 path_so_far.push_str(name);
1298 out.push('.');
1299 out.push_str(&name.to_lower_camel_case());
1300 out.push_str(&format!("().get({index})"));
1301 }
1302 PathSegment::MapAccess { field, key } => {
1303 if !path_so_far.is_empty() {
1304 path_so_far.push('.');
1305 }
1306 path_so_far.push_str(field);
1307 out.push('.');
1308 out.push_str(&field.to_lower_camel_case());
1309 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1311 if is_numeric {
1312 out.push_str(&format!("().get({key})"));
1313 } else {
1314 out.push_str(&format!("().get(\"{key}\")"));
1315 }
1316 }
1317 PathSegment::Length => {
1318 out.push_str(".size()");
1319 }
1320 }
1321 }
1322 out
1323}
1324
1325fn render_kotlin_with_optionals(
1340 segments: &[PathSegment],
1341 result_var: &str,
1342 optional_fields: &HashSet<String>,
1343) -> String {
1344 let mut out = result_var.to_string();
1345 let mut path_so_far = String::new();
1346 let mut prev_was_nullable = false;
1354 for seg in segments {
1355 let nav = if prev_was_nullable { "?." } else { "." };
1356 match seg {
1357 PathSegment::Field(f) => {
1358 if !path_so_far.is_empty() {
1359 path_so_far.push('.');
1360 }
1361 path_so_far.push_str(f);
1362 let is_optional = optional_fields.contains(&path_so_far);
1367 out.push_str(nav);
1368 out.push_str(&kotlin_getter(f));
1369 out.push_str("()");
1370 prev_was_nullable = prev_was_nullable || is_optional;
1371 }
1372 PathSegment::ArrayField { name, index } => {
1373 if !path_so_far.is_empty() {
1374 path_so_far.push('.');
1375 }
1376 path_so_far.push_str(name);
1377 let is_optional = optional_fields.contains(&path_so_far);
1378 out.push_str(nav);
1379 out.push_str(&kotlin_getter(name));
1380 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1381 if *index == 0 {
1382 out.push_str(&format!("(){safe}.first()"));
1383 } else {
1384 out.push_str(&format!("(){safe}.get({index})"));
1385 }
1386 path_so_far.push_str("[0]");
1390 prev_was_nullable = prev_was_nullable || is_optional;
1391 }
1392 PathSegment::MapAccess { field, key } => {
1393 if !path_so_far.is_empty() {
1394 path_so_far.push('.');
1395 }
1396 path_so_far.push_str(field);
1397 let is_optional = optional_fields.contains(&path_so_far);
1398 out.push_str(nav);
1399 out.push_str(&kotlin_getter(field));
1400 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1401 if is_numeric {
1402 if prev_was_nullable || is_optional {
1403 out.push_str(&format!("()?.get({key})"));
1404 } else {
1405 out.push_str(&format!("().get({key})"));
1406 }
1407 } else if prev_was_nullable || is_optional {
1408 out.push_str(&format!("()?.get(\"{key}\")"));
1409 } else {
1410 out.push_str(&format!("().get(\"{key}\")"));
1411 }
1412 prev_was_nullable = prev_was_nullable || is_optional;
1413 }
1414 PathSegment::Length => {
1415 let size_nav = if prev_was_nullable { "?" } else { "" };
1418 out.push_str(&format!("{size_nav}.size"));
1419 prev_was_nullable = false;
1420 }
1421 }
1422 }
1423 out
1424}
1425
1426fn render_kotlin_android_with_optionals(
1437 segments: &[PathSegment],
1438 result_var: &str,
1439 optional_fields: &HashSet<String>,
1440) -> String {
1441 let mut out = result_var.to_string();
1442 let mut path_so_far = String::new();
1443 let mut prev_was_nullable = false;
1444 for seg in segments {
1445 let nav = if prev_was_nullable { "?." } else { "." };
1446 match seg {
1447 PathSegment::Field(f) => {
1448 if !path_so_far.is_empty() {
1449 path_so_far.push('.');
1450 }
1451 path_so_far.push_str(f);
1452 let is_optional = optional_fields.contains(&path_so_far);
1453 out.push_str(nav);
1454 out.push_str(&kotlin_getter(f));
1456 prev_was_nullable = prev_was_nullable || is_optional;
1457 }
1458 PathSegment::ArrayField { name, index } => {
1459 if !path_so_far.is_empty() {
1460 path_so_far.push('.');
1461 }
1462 path_so_far.push_str(name);
1463 let is_optional = optional_fields.contains(&path_so_far);
1464 out.push_str(nav);
1465 out.push_str(&kotlin_getter(name));
1467 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1468 if *index == 0 {
1469 out.push_str(&format!("{safe}.first()"));
1470 } else {
1471 out.push_str(&format!("{safe}.get({index})"));
1472 }
1473 path_so_far.push_str("[0]");
1474 prev_was_nullable = prev_was_nullable || is_optional;
1475 }
1476 PathSegment::MapAccess { field, key } => {
1477 if !path_so_far.is_empty() {
1478 path_so_far.push('.');
1479 }
1480 path_so_far.push_str(field);
1481 let is_optional = optional_fields.contains(&path_so_far);
1482 out.push_str(nav);
1483 out.push_str(&kotlin_getter(field));
1485 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1486 if is_numeric {
1487 if prev_was_nullable || is_optional {
1488 out.push_str(&format!("?.get({key})"));
1489 } else {
1490 out.push_str(&format!(".get({key})"));
1491 }
1492 } else if prev_was_nullable || is_optional {
1493 out.push_str(&format!("?.get(\"{key}\")"));
1494 } else {
1495 out.push_str(&format!(".get(\"{key}\")"));
1496 }
1497 prev_was_nullable = prev_was_nullable || is_optional;
1498 }
1499 PathSegment::Length => {
1500 let size_nav = if prev_was_nullable { "?" } else { "" };
1501 out.push_str(&format!("{size_nav}.size"));
1502 prev_was_nullable = false;
1503 }
1504 }
1505 }
1506 out
1507}
1508
1509fn render_kotlin_android(segments: &[PathSegment], result_var: &str) -> String {
1513 let mut out = result_var.to_string();
1514 for seg in segments {
1515 match seg {
1516 PathSegment::Field(f) => {
1517 out.push('.');
1518 out.push_str(&kotlin_getter(f));
1519 }
1521 PathSegment::ArrayField { name, index } => {
1522 out.push('.');
1523 out.push_str(&kotlin_getter(name));
1524 if *index == 0 {
1525 out.push_str(".first()");
1526 } else {
1527 out.push_str(&format!(".get({index})"));
1528 }
1529 }
1530 PathSegment::MapAccess { field, key } => {
1531 out.push('.');
1532 out.push_str(&kotlin_getter(field));
1533 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1534 if is_numeric {
1535 out.push_str(&format!(".get({key})"));
1536 } else {
1537 out.push_str(&format!(".get(\"{key}\")"));
1538 }
1539 }
1540 PathSegment::Length => {
1541 out.push_str(".size");
1542 }
1543 }
1544 }
1545 out
1546}
1547
1548fn render_rust_with_optionals(
1554 segments: &[PathSegment],
1555 result_var: &str,
1556 optional_fields: &HashSet<String>,
1557 method_calls: &HashSet<String>,
1558) -> String {
1559 let mut out = result_var.to_string();
1560 let mut path_so_far = String::new();
1561 for (i, seg) in segments.iter().enumerate() {
1562 let is_leaf = i == segments.len() - 1;
1563 match seg {
1564 PathSegment::Field(f) => {
1565 if !path_so_far.is_empty() {
1566 path_so_far.push('.');
1567 }
1568 path_so_far.push_str(f);
1569 out.push('.');
1570 out.push_str(&f.to_snake_case());
1571 let is_method = method_calls.contains(&path_so_far);
1572 if is_method {
1573 out.push_str("()");
1574 if !is_leaf && optional_fields.contains(&path_so_far) {
1575 out.push_str(".as_ref().unwrap()");
1576 }
1577 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1578 out.push_str(".as_ref().unwrap()");
1579 }
1580 }
1581 PathSegment::ArrayField { name, index } => {
1582 if !path_so_far.is_empty() {
1583 path_so_far.push('.');
1584 }
1585 path_so_far.push_str(name);
1586 out.push('.');
1587 out.push_str(&name.to_snake_case());
1588 let path_with_idx = format!("{path_so_far}[0]");
1592 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1593 if is_opt {
1594 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1595 } else {
1596 out.push_str(&format!("[{index}]"));
1597 }
1598 path_so_far.push_str("[0]");
1603 }
1604 PathSegment::MapAccess { field, key } => {
1605 if !path_so_far.is_empty() {
1606 path_so_far.push('.');
1607 }
1608 path_so_far.push_str(field);
1609 out.push('.');
1610 out.push_str(&field.to_snake_case());
1611 if key.chars().all(|c| c.is_ascii_digit()) {
1612 let path_with_idx = format!("{path_so_far}[0]");
1614 let is_opt =
1615 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1616 if is_opt {
1617 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1618 } else {
1619 out.push_str(&format!("[{key}]"));
1620 }
1621 path_so_far.push_str("[0]");
1622 } else {
1623 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1624 }
1625 }
1626 PathSegment::Length => {
1627 out.push_str(".len()");
1628 }
1629 }
1630 }
1631 out
1632}
1633
1634fn render_zig_with_optionals(
1647 segments: &[PathSegment],
1648 result_var: &str,
1649 optional_fields: &HashSet<String>,
1650 method_calls: &HashSet<String>,
1651) -> String {
1652 let mut out = result_var.to_string();
1653 let mut path_so_far = String::new();
1654 for seg in segments {
1655 match seg {
1656 PathSegment::Field(f) => {
1657 if !path_so_far.is_empty() {
1658 path_so_far.push('.');
1659 }
1660 path_so_far.push_str(f);
1661 out.push('.');
1662 out.push_str(f);
1663 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1664 out.push_str(".?");
1665 }
1666 }
1667 PathSegment::ArrayField { name, index } => {
1668 if !path_so_far.is_empty() {
1669 path_so_far.push('.');
1670 }
1671 path_so_far.push_str(name);
1672 out.push('.');
1673 out.push_str(name);
1674 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1675 out.push_str(".?");
1676 }
1677 out.push_str(&format!("[{index}]"));
1678 }
1679 PathSegment::MapAccess { field, key } => {
1680 if !path_so_far.is_empty() {
1681 path_so_far.push('.');
1682 }
1683 path_so_far.push_str(field);
1684 out.push('.');
1685 out.push_str(field);
1686 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1687 out.push_str(".?");
1688 }
1689 if key.chars().all(|c| c.is_ascii_digit()) {
1690 out.push_str(&format!("[{key}]"));
1691 } else {
1692 out.push_str(&format!(".get(\"{key}\")"));
1693 }
1694 }
1695 PathSegment::Length => {
1696 out.push_str(".len");
1697 }
1698 }
1699 }
1700 out
1701}
1702
1703fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1704 let mut out = result_var.to_string();
1705 for seg in segments {
1706 match seg {
1707 PathSegment::Field(f) => {
1708 out.push('.');
1709 out.push_str(&f.to_pascal_case());
1710 }
1711 PathSegment::ArrayField { name, index } => {
1712 out.push('.');
1713 out.push_str(&name.to_pascal_case());
1714 out.push_str(&format!("[{index}]"));
1715 }
1716 PathSegment::MapAccess { field, key } => {
1717 out.push('.');
1718 out.push_str(&field.to_pascal_case());
1719 if key.chars().all(|c| c.is_ascii_digit()) {
1720 out.push_str(&format!("[{key}]"));
1721 } else {
1722 out.push_str(&format!("[\"{key}\"]"));
1723 }
1724 }
1725 PathSegment::Length => {
1726 out.push_str(".Count");
1727 }
1728 }
1729 }
1730 out
1731}
1732
1733fn render_csharp_with_optionals(
1734 segments: &[PathSegment],
1735 result_var: &str,
1736 optional_fields: &HashSet<String>,
1737) -> String {
1738 let mut out = result_var.to_string();
1739 let mut path_so_far = String::new();
1740 for (i, seg) in segments.iter().enumerate() {
1741 let is_leaf = i == segments.len() - 1;
1742 match seg {
1743 PathSegment::Field(f) => {
1744 if !path_so_far.is_empty() {
1745 path_so_far.push('.');
1746 }
1747 path_so_far.push_str(f);
1748 out.push('.');
1749 out.push_str(&f.to_pascal_case());
1750 if !is_leaf && optional_fields.contains(&path_so_far) {
1751 out.push('!');
1752 }
1753 }
1754 PathSegment::ArrayField { name, index } => {
1755 if !path_so_far.is_empty() {
1756 path_so_far.push('.');
1757 }
1758 path_so_far.push_str(name);
1759 out.push('.');
1760 out.push_str(&name.to_pascal_case());
1761 out.push_str(&format!("[{index}]"));
1762 }
1763 PathSegment::MapAccess { field, key } => {
1764 if !path_so_far.is_empty() {
1765 path_so_far.push('.');
1766 }
1767 path_so_far.push_str(field);
1768 out.push('.');
1769 out.push_str(&field.to_pascal_case());
1770 if key.chars().all(|c| c.is_ascii_digit()) {
1771 out.push_str(&format!("[{key}]"));
1772 } else {
1773 out.push_str(&format!("[\"{key}\"]"));
1774 }
1775 }
1776 PathSegment::Length => {
1777 out.push_str(".Count");
1778 }
1779 }
1780 }
1781 out
1782}
1783
1784fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1785 let mut out = result_var.to_string();
1786 for seg in segments {
1787 match seg {
1788 PathSegment::Field(f) => {
1789 out.push_str("->");
1790 out.push_str(&f.to_lower_camel_case());
1793 }
1794 PathSegment::ArrayField { name, index } => {
1795 out.push_str("->");
1796 out.push_str(&name.to_lower_camel_case());
1797 out.push_str(&format!("[{index}]"));
1798 }
1799 PathSegment::MapAccess { field, key } => {
1800 out.push_str("->");
1801 out.push_str(&field.to_lower_camel_case());
1802 out.push_str(&format!("[\"{key}\"]"));
1803 }
1804 PathSegment::Length => {
1805 let current = std::mem::take(&mut out);
1806 out = format!("count({current})");
1807 }
1808 }
1809 }
1810 out
1811}
1812
1813fn render_php_with_getters(segments: &[PathSegment], result_var: &str, getter_map: &PhpGetterMap) -> String {
1831 let mut out = result_var.to_string();
1832 let mut current_type: Option<String> = getter_map.root_type.clone();
1833 for seg in segments {
1834 match seg {
1835 PathSegment::Field(f) => {
1836 let camel = f.to_lower_camel_case();
1837 if getter_map.needs_getter(current_type.as_deref(), f.as_str()) {
1838 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1843 out.push_str("->");
1844 out.push_str(&getter);
1845 out.push_str("()");
1846 } else {
1847 out.push_str("->");
1848 out.push_str(&camel);
1849 }
1850 current_type = getter_map.advance(current_type.as_deref(), f.as_str());
1851 }
1852 PathSegment::ArrayField { name, index } => {
1853 let camel = name.to_lower_camel_case();
1854 if getter_map.needs_getter(current_type.as_deref(), name.as_str()) {
1855 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1856 out.push_str("->");
1857 out.push_str(&getter);
1858 out.push_str("()");
1859 } else {
1860 out.push_str("->");
1861 out.push_str(&camel);
1862 }
1863 out.push_str(&format!("[{index}]"));
1864 current_type = getter_map.advance(current_type.as_deref(), name.as_str());
1865 }
1866 PathSegment::MapAccess { field, key } => {
1867 let camel = field.to_lower_camel_case();
1868 if getter_map.needs_getter(current_type.as_deref(), field.as_str()) {
1869 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1870 out.push_str("->");
1871 out.push_str(&getter);
1872 out.push_str("()");
1873 } else {
1874 out.push_str("->");
1875 out.push_str(&camel);
1876 }
1877 out.push_str(&format!("[\"{key}\"]"));
1878 current_type = getter_map.advance(current_type.as_deref(), field.as_str());
1879 }
1880 PathSegment::Length => {
1881 let current = std::mem::take(&mut out);
1882 out = format!("count({current})");
1883 }
1884 }
1885 }
1886 out
1887}
1888
1889fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1890 let mut out = result_var.to_string();
1891 for seg in segments {
1892 match seg {
1893 PathSegment::Field(f) => {
1894 out.push('$');
1895 out.push_str(f);
1896 }
1897 PathSegment::ArrayField { name, index } => {
1898 out.push('$');
1899 out.push_str(name);
1900 out.push_str(&format!("[[{}]]", index + 1));
1902 }
1903 PathSegment::MapAccess { field, key } => {
1904 out.push('$');
1905 out.push_str(field);
1906 out.push_str(&format!("[[\"{key}\"]]"));
1907 }
1908 PathSegment::Length => {
1909 let current = std::mem::take(&mut out);
1910 out = format!("length({current})");
1911 }
1912 }
1913 }
1914 out
1915}
1916
1917fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1918 let mut parts = Vec::new();
1919 let mut trailing_length = false;
1920 for seg in segments {
1921 match seg {
1922 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1923 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1924 PathSegment::MapAccess { field, key } => {
1925 parts.push(field.to_snake_case());
1926 parts.push(key.clone());
1927 }
1928 PathSegment::Length => {
1929 trailing_length = true;
1930 }
1931 }
1932 }
1933 let suffix = parts.join("_");
1934 if trailing_length {
1935 format!("result_{suffix}_count({result_var})")
1936 } else {
1937 format!("result_{suffix}({result_var})")
1938 }
1939}
1940
1941fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
1948 let mut out = result_var.to_string();
1949 for seg in segments {
1950 match seg {
1951 PathSegment::Field(f) => {
1952 out.push('.');
1953 out.push_str(&f.to_lower_camel_case());
1954 }
1955 PathSegment::ArrayField { name, index } => {
1956 out.push('.');
1957 out.push_str(&name.to_lower_camel_case());
1958 out.push_str(&format!("[{index}]"));
1959 }
1960 PathSegment::MapAccess { field, key } => {
1961 out.push('.');
1962 out.push_str(&field.to_lower_camel_case());
1963 if key.chars().all(|c| c.is_ascii_digit()) {
1964 out.push_str(&format!("[{key}]"));
1965 } else {
1966 out.push_str(&format!("[\"{key}\"]"));
1967 }
1968 }
1969 PathSegment::Length => {
1970 out.push_str(".length");
1971 }
1972 }
1973 }
1974 out
1975}
1976
1977fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1983 let mut out = result_var.to_string();
1984 let mut path_so_far = String::new();
1985 let mut prev_was_nullable = false;
1986 for seg in segments {
1987 let nav = if prev_was_nullable { "?." } else { "." };
1988 match seg {
1989 PathSegment::Field(f) => {
1990 if !path_so_far.is_empty() {
1991 path_so_far.push('.');
1992 }
1993 path_so_far.push_str(f);
1994 let is_optional = optional_fields.contains(&path_so_far);
1995 out.push_str(nav);
1996 out.push_str(&f.to_lower_camel_case());
1997 prev_was_nullable = is_optional;
1998 }
1999 PathSegment::ArrayField { name, index } => {
2000 if !path_so_far.is_empty() {
2001 path_so_far.push('.');
2002 }
2003 path_so_far.push_str(name);
2004 let is_optional = optional_fields.contains(&path_so_far);
2005 out.push_str(nav);
2006 out.push_str(&name.to_lower_camel_case());
2007 if is_optional {
2011 out.push('!');
2012 }
2013 out.push_str(&format!("[{index}]"));
2014 prev_was_nullable = false;
2015 }
2016 PathSegment::MapAccess { field, key } => {
2017 if !path_so_far.is_empty() {
2018 path_so_far.push('.');
2019 }
2020 path_so_far.push_str(field);
2021 let is_optional = optional_fields.contains(&path_so_far);
2022 out.push_str(nav);
2023 out.push_str(&field.to_lower_camel_case());
2024 if key.chars().all(|c| c.is_ascii_digit()) {
2025 out.push_str(&format!("[{key}]"));
2026 } else {
2027 out.push_str(&format!("[\"{key}\"]"));
2028 }
2029 prev_was_nullable = is_optional;
2030 }
2031 PathSegment::Length => {
2032 out.push_str(nav);
2035 out.push_str("length");
2036 prev_was_nullable = false;
2037 }
2038 }
2039 }
2040 out
2041}
2042
2043#[cfg(test)]
2044mod tests {
2045 use super::*;
2046
2047 fn make_resolver() -> FieldResolver {
2048 let mut fields = HashMap::new();
2049 fields.insert("title".to_string(), "metadata.document.title".to_string());
2050 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
2051 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
2052 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
2053 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
2054 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
2055 let mut optional = HashSet::new();
2056 optional.insert("metadata.document.title".to_string());
2057 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
2058 }
2059
2060 fn make_resolver_with_doc_optional() -> FieldResolver {
2061 let mut fields = HashMap::new();
2062 fields.insert("title".to_string(), "metadata.document.title".to_string());
2063 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
2064 let mut optional = HashSet::new();
2065 optional.insert("document".to_string());
2066 optional.insert("metadata.document.title".to_string());
2067 optional.insert("metadata.document".to_string());
2068 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
2069 }
2070
2071 #[test]
2072 fn test_resolve_alias() {
2073 let r = make_resolver();
2074 assert_eq!(r.resolve("title"), "metadata.document.title");
2075 }
2076
2077 #[test]
2078 fn test_resolve_passthrough() {
2079 let r = make_resolver();
2080 assert_eq!(r.resolve("content"), "content");
2081 }
2082
2083 #[test]
2084 fn test_is_optional() {
2085 let r = make_resolver();
2086 assert!(r.is_optional("metadata.document.title"));
2087 assert!(!r.is_optional("content"));
2088 }
2089
2090 #[test]
2091 fn test_accessor_rust_struct() {
2092 let r = make_resolver();
2093 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
2094 }
2095
2096 #[test]
2097 fn test_accessor_rust_map() {
2098 let r = make_resolver();
2099 assert_eq!(
2100 r.accessor("tags", "rust", "result"),
2101 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
2102 );
2103 }
2104
2105 #[test]
2106 fn test_accessor_python() {
2107 let r = make_resolver();
2108 assert_eq!(
2109 r.accessor("title", "python", "result"),
2110 "result.metadata.document.title"
2111 );
2112 }
2113
2114 #[test]
2115 fn test_accessor_go() {
2116 let r = make_resolver();
2117 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
2118 }
2119
2120 #[test]
2121 fn test_accessor_go_initialism_fields() {
2122 let mut fields = std::collections::HashMap::new();
2123 fields.insert("content".to_string(), "html".to_string());
2124 fields.insert("link_url".to_string(), "links.url".to_string());
2125 let r = FieldResolver::new(
2126 &fields,
2127 &HashSet::new(),
2128 &HashSet::new(),
2129 &HashSet::new(),
2130 &HashSet::new(),
2131 );
2132 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
2133 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
2134 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
2135 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
2136 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
2137 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
2138 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
2139 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
2140 }
2141
2142 #[test]
2143 fn test_accessor_typescript() {
2144 let r = make_resolver();
2145 assert_eq!(
2146 r.accessor("title", "typescript", "result"),
2147 "result.metadata.document.title"
2148 );
2149 }
2150
2151 #[test]
2152 fn test_accessor_typescript_snake_to_camel() {
2153 let r = make_resolver();
2154 assert_eq!(
2155 r.accessor("og", "typescript", "result"),
2156 "result.metadata.document.openGraph"
2157 );
2158 assert_eq!(
2159 r.accessor("twitter", "typescript", "result"),
2160 "result.metadata.document.twitterCard"
2161 );
2162 assert_eq!(
2163 r.accessor("canonical", "typescript", "result"),
2164 "result.metadata.document.canonicalUrl"
2165 );
2166 }
2167
2168 #[test]
2169 fn test_accessor_typescript_map_snake_to_camel() {
2170 let r = make_resolver();
2171 assert_eq!(
2172 r.accessor("og_tag", "typescript", "result"),
2173 "result.metadata.openGraphTags[\"og_title\"]"
2174 );
2175 }
2176
2177 #[test]
2178 fn test_accessor_typescript_numeric_index_is_unquoted() {
2179 let mut fields = HashMap::new();
2183 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
2184 let r = FieldResolver::new(
2185 &fields,
2186 &HashSet::new(),
2187 &HashSet::new(),
2188 &HashSet::new(),
2189 &HashSet::new(),
2190 );
2191 assert_eq!(
2192 r.accessor("first_score", "typescript", "result"),
2193 "result.results[0].relevanceScore"
2194 );
2195 }
2196
2197 #[test]
2198 fn test_accessor_node_alias() {
2199 let r = make_resolver();
2200 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
2201 }
2202
2203 #[test]
2204 fn test_accessor_wasm_camel_case() {
2205 let r = make_resolver();
2206 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
2207 assert_eq!(
2208 r.accessor("twitter", "wasm", "result"),
2209 "result.metadata.document.twitterCard"
2210 );
2211 assert_eq!(
2212 r.accessor("canonical", "wasm", "result"),
2213 "result.metadata.document.canonicalUrl"
2214 );
2215 }
2216
2217 #[test]
2218 fn test_accessor_wasm_map_access() {
2219 let r = make_resolver();
2220 assert_eq!(
2221 r.accessor("og_tag", "wasm", "result"),
2222 "result.metadata.openGraphTags.get(\"og_title\")"
2223 );
2224 }
2225
2226 #[test]
2227 fn test_accessor_java() {
2228 let r = make_resolver();
2229 assert_eq!(
2230 r.accessor("title", "java", "result"),
2231 "result.metadata().document().title()"
2232 );
2233 }
2234
2235 #[test]
2236 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
2237 let mut fields = HashMap::new();
2238 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2239 fields.insert("node_count".to_string(), "nodes.length".to_string());
2240 let mut arrays = HashSet::new();
2241 arrays.insert("nodes".to_string());
2242 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
2243 assert_eq!(
2244 r.accessor("first_node_name", "kotlin", "result"),
2245 "result.nodes().first().name()"
2246 );
2247 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
2248 }
2249
2250 #[test]
2251 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
2252 let r = make_resolver_with_doc_optional();
2253 assert_eq!(
2254 r.accessor("title", "kotlin", "result"),
2255 "result.metadata().document()?.title()"
2256 );
2257 }
2258
2259 #[test]
2260 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
2261 let mut fields = HashMap::new();
2262 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2263 fields.insert("tag".to_string(), "tags[name]".to_string());
2264 let mut optional = HashSet::new();
2265 optional.insert("nodes".to_string());
2266 optional.insert("tags".to_string());
2267 let mut arrays = HashSet::new();
2268 arrays.insert("nodes".to_string());
2269 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2270 assert_eq!(
2271 r.accessor("first_node_name", "kotlin", "result"),
2272 "result.nodes()?.first()?.name()"
2273 );
2274 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
2275 }
2276
2277 #[test]
2283 fn test_accessor_kotlin_optional_field_after_indexed_array() {
2284 let mut fields = HashMap::new();
2287 fields.insert(
2288 "tool_call_name".to_string(),
2289 "choices[0].message.tool_calls[0].function.name".to_string(),
2290 );
2291 let mut optional = HashSet::new();
2292 optional.insert("choices[0].message.tool_calls".to_string());
2293 let mut arrays = HashSet::new();
2294 arrays.insert("choices".to_string());
2295 arrays.insert("choices[0].message.tool_calls".to_string());
2296 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2297 let expr = r.accessor("tool_call_name", "kotlin", "result");
2298 assert!(
2300 expr.contains("toolCalls()?.first()"),
2301 "expected toolCalls()?.first() for optional list, got: {expr}"
2302 );
2303 }
2304
2305 #[test]
2306 fn test_accessor_csharp() {
2307 let r = make_resolver();
2308 assert_eq!(
2309 r.accessor("title", "csharp", "result"),
2310 "result.Metadata.Document.Title"
2311 );
2312 }
2313
2314 #[test]
2315 fn test_accessor_php() {
2316 let r = make_resolver();
2317 assert_eq!(
2318 r.accessor("title", "php", "$result"),
2319 "$result->metadata->document->title"
2320 );
2321 }
2322
2323 #[test]
2324 fn test_accessor_r() {
2325 let r = make_resolver();
2326 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
2327 }
2328
2329 #[test]
2330 fn test_accessor_c() {
2331 let r = make_resolver();
2332 assert_eq!(
2333 r.accessor("title", "c", "result"),
2334 "result_metadata_document_title(result)"
2335 );
2336 }
2337
2338 #[test]
2339 fn test_rust_unwrap_binding() {
2340 let r = make_resolver();
2341 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
2342 assert_eq!(var, "metadata_document_title");
2343 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
2344 }
2345
2346 #[test]
2347 fn test_rust_unwrap_binding_non_optional() {
2348 let r = make_resolver();
2349 assert!(r.rust_unwrap_binding("content", "result").is_none());
2350 }
2351
2352 #[test]
2353 fn test_rust_unwrap_binding_collapses_double_underscore() {
2354 let mut aliases = HashMap::new();
2359 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
2360 let mut optional = HashSet::new();
2361 optional.insert("json_ld[].name".to_string());
2362 let mut array = HashSet::new();
2363 array.insert("json_ld".to_string());
2364 let result_fields = HashSet::new();
2365 let method_calls = HashSet::new();
2366 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
2367 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
2368 assert_eq!(var, "json_ld_name");
2369 }
2370
2371 #[test]
2372 fn test_direct_field_no_alias() {
2373 let r = make_resolver();
2374 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2375 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
2376 }
2377
2378 #[test]
2379 fn test_accessor_rust_with_optionals() {
2380 let r = make_resolver_with_doc_optional();
2381 assert_eq!(
2382 r.accessor("title", "rust", "result"),
2383 "result.metadata.document.as_ref().unwrap().title"
2384 );
2385 }
2386
2387 #[test]
2388 fn test_accessor_csharp_with_optionals() {
2389 let r = make_resolver_with_doc_optional();
2390 assert_eq!(
2391 r.accessor("title", "csharp", "result"),
2392 "result.Metadata.Document!.Title"
2393 );
2394 }
2395
2396 #[test]
2397 fn test_accessor_rust_non_optional_field() {
2398 let r = make_resolver();
2399 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2400 }
2401
2402 #[test]
2403 fn test_accessor_csharp_non_optional_field() {
2404 let r = make_resolver();
2405 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
2406 }
2407
2408 #[test]
2409 fn test_accessor_rust_method_call() {
2410 let mut fields = HashMap::new();
2412 fields.insert(
2413 "excel_sheet_count".to_string(),
2414 "metadata.format.excel.sheet_count".to_string(),
2415 );
2416 let mut optional = HashSet::new();
2417 optional.insert("metadata.format".to_string());
2418 optional.insert("metadata.format.excel".to_string());
2419 let mut method_calls = HashSet::new();
2420 method_calls.insert("metadata.format.excel".to_string());
2421 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
2422 assert_eq!(
2423 r.accessor("excel_sheet_count", "rust", "result"),
2424 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
2425 );
2426 }
2427
2428 fn make_php_getter_resolver() -> FieldResolver {
2433 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2434 getters.insert(
2435 "Root".to_string(),
2436 ["metadata".to_string(), "links".to_string()].into_iter().collect(),
2437 );
2438 let map = PhpGetterMap {
2439 getters,
2440 field_types: HashMap::new(),
2441 root_type: Some("Root".to_string()),
2442 all_fields: HashMap::new(),
2443 };
2444 FieldResolver::new_with_php_getters(
2445 &HashMap::new(),
2446 &HashSet::new(),
2447 &HashSet::new(),
2448 &HashSet::new(),
2449 &HashSet::new(),
2450 &HashMap::new(),
2451 map,
2452 )
2453 }
2454
2455 #[test]
2456 fn render_php_uses_getter_method_for_non_scalar_field() {
2457 let r = make_php_getter_resolver();
2458 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
2459 }
2460
2461 #[test]
2462 fn render_php_uses_property_for_scalar_field() {
2463 let r = make_php_getter_resolver();
2464 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2465 }
2466
2467 #[test]
2468 fn render_php_nested_non_scalar_uses_getter_then_property() {
2469 let mut fields = HashMap::new();
2470 fields.insert("title".to_string(), "metadata.title".to_string());
2471 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2472 getters.insert("Root".to_string(), ["metadata".to_string()].into_iter().collect());
2473 getters.insert("Metadata".to_string(), HashSet::new());
2475 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2476 field_types.insert(
2477 "Root".to_string(),
2478 [("metadata".to_string(), "Metadata".to_string())].into_iter().collect(),
2479 );
2480 let map = PhpGetterMap {
2481 getters,
2482 field_types,
2483 root_type: Some("Root".to_string()),
2484 all_fields: HashMap::new(),
2485 };
2486 let r = FieldResolver::new_with_php_getters(
2487 &fields,
2488 &HashSet::new(),
2489 &HashSet::new(),
2490 &HashSet::new(),
2491 &HashSet::new(),
2492 &HashMap::new(),
2493 map,
2494 );
2495 assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
2497 }
2498
2499 #[test]
2500 fn render_php_array_field_uses_getter_when_non_scalar() {
2501 let mut fields = HashMap::new();
2502 fields.insert("first_link".to_string(), "links[0]".to_string());
2503 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2504 getters.insert("Root".to_string(), ["links".to_string()].into_iter().collect());
2505 let map = PhpGetterMap {
2506 getters,
2507 field_types: HashMap::new(),
2508 root_type: Some("Root".to_string()),
2509 all_fields: HashMap::new(),
2510 };
2511 let r = FieldResolver::new_with_php_getters(
2512 &fields,
2513 &HashSet::new(),
2514 &HashSet::new(),
2515 &HashSet::new(),
2516 &HashSet::new(),
2517 &HashMap::new(),
2518 map,
2519 );
2520 assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
2521 }
2522
2523 #[test]
2524 fn render_php_falls_back_to_property_when_getter_fields_empty() {
2525 let r = FieldResolver::new(
2528 &HashMap::new(),
2529 &HashSet::new(),
2530 &HashSet::new(),
2531 &HashSet::new(),
2532 &HashSet::new(),
2533 );
2534 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2535 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
2536 }
2537
2538 #[test]
2542 fn render_php_with_getters_distinguishes_same_field_name_on_different_types() {
2543 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2544 getters.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2546 getters.insert("B".to_string(), HashSet::new());
2548 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2551 all_fields.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2552 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2553 let map_a = PhpGetterMap {
2554 getters: getters.clone(),
2555 field_types: HashMap::new(),
2556 root_type: Some("A".to_string()),
2557 all_fields: all_fields.clone(),
2558 };
2559 let map_b = PhpGetterMap {
2560 getters,
2561 field_types: HashMap::new(),
2562 root_type: Some("B".to_string()),
2563 all_fields,
2564 };
2565 let r_a = FieldResolver::new_with_php_getters(
2566 &HashMap::new(),
2567 &HashSet::new(),
2568 &HashSet::new(),
2569 &HashSet::new(),
2570 &HashSet::new(),
2571 &HashMap::new(),
2572 map_a,
2573 );
2574 let r_b = FieldResolver::new_with_php_getters(
2575 &HashMap::new(),
2576 &HashSet::new(),
2577 &HashSet::new(),
2578 &HashSet::new(),
2579 &HashSet::new(),
2580 &HashMap::new(),
2581 map_b,
2582 );
2583 assert_eq!(r_a.accessor("content", "php", "$a"), "$a->getContent()");
2584 assert_eq!(r_b.accessor("content", "php", "$b"), "$b->content");
2585 }
2586
2587 #[test]
2591 fn render_php_with_getters_chains_through_correct_type() {
2592 let mut fields = HashMap::new();
2593 fields.insert("nested_content".to_string(), "inner.content".to_string());
2594 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2595 getters.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2597 getters.insert("B".to_string(), HashSet::new());
2599 getters.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2602 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2603 field_types.insert(
2604 "Outer".to_string(),
2605 [("inner".to_string(), "B".to_string())].into_iter().collect(),
2606 );
2607 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2608 all_fields.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2609 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2610 all_fields.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2611 let map = PhpGetterMap {
2612 getters,
2613 field_types,
2614 root_type: Some("Outer".to_string()),
2615 all_fields,
2616 };
2617 let r = FieldResolver::new_with_php_getters(
2618 &fields,
2619 &HashSet::new(),
2620 &HashSet::new(),
2621 &HashSet::new(),
2622 &HashSet::new(),
2623 &HashMap::new(),
2624 map,
2625 );
2626 assert_eq!(
2627 r.accessor("nested_content", "php", "$result"),
2628 "$result->getInner()->content"
2629 );
2630 }
2631
2632 fn make_resolver_with_result_fields(result_fields: &[&str]) -> FieldResolver {
2637 let rf: HashSet<String> = result_fields.iter().map(|s| s.to_string()).collect();
2638 FieldResolver::new(&HashMap::new(), &HashSet::new(), &rf, &HashSet::new(), &HashSet::new())
2639 }
2640
2641 #[test]
2644 fn is_valid_for_result_accepts_virtual_namespace_prefix() {
2645 let r = make_resolver_with_result_fields(&["browser_used", "js_render_hint", "status_code"]);
2646 assert!(
2647 r.is_valid_for_result("browser.browser_used"),
2648 "browser.browser_used should be valid via namespace-prefix stripping"
2649 );
2650 assert!(
2651 r.is_valid_for_result("browser.js_render_hint"),
2652 "browser.js_render_hint should be valid via namespace-prefix stripping"
2653 );
2654 }
2655
2656 #[test]
2659 fn is_valid_for_result_accepts_namespace_prefix_before_array_field() {
2660 let r = make_resolver_with_result_fields(&["action_results", "final_html", "final_url"]);
2661 assert!(
2662 r.is_valid_for_result("interaction.action_results[0].action_type"),
2663 "interaction. prefix should be stripped so action_results is recognised"
2664 );
2665 }
2666
2667 #[test]
2669 fn is_valid_for_result_rejects_unknown_field_even_after_namespace_strip() {
2670 let r = make_resolver_with_result_fields(&["pages", "final_url"]);
2671 assert!(
2672 !r.is_valid_for_result("browser.browser_used"),
2673 "browser_used is not in result_fields so should be rejected"
2674 );
2675 assert!(
2676 !r.is_valid_for_result("ns.unknown_field"),
2677 "unknown_field is not in result_fields so should be rejected"
2678 );
2679 }
2680
2681 #[test]
2684 fn accessor_strips_namespace_prefix_for_python() {
2685 let r = make_resolver_with_result_fields(&["browser_used", "js_render_hint"]);
2686 assert_eq!(
2687 r.accessor("browser.browser_used", "python", "result"),
2688 "result.browser_used"
2689 );
2690 assert_eq!(
2691 r.accessor("browser.js_render_hint", "python", "result"),
2692 "result.js_render_hint"
2693 );
2694 }
2695
2696 #[test]
2698 fn accessor_strips_namespace_prefix_for_csharp() {
2699 let r = make_resolver_with_result_fields(&["browser_used"]);
2700 assert_eq!(
2701 r.accessor("browser.browser_used", "csharp", "result"),
2702 "result.BrowserUsed"
2703 );
2704 }
2705
2706 #[test]
2709 fn accessor_strips_namespace_prefix_for_indexed_array_field() {
2710 let r = make_resolver_with_result_fields(&["action_results", "final_html", "final_url"]);
2711 assert_eq!(
2713 r.accessor("interaction.action_results[0].action_type", "python", "result"),
2714 "result.action_results[0].action_type"
2715 );
2716 assert_eq!(
2718 r.accessor("interaction.action_results[0].action_type", "typescript", "result"),
2719 "result.actionResults[0].actionType"
2720 );
2721 }
2722
2723 #[test]
2726 fn is_valid_for_result_is_permissive_when_result_fields_empty() {
2727 let r = make_resolver_with_result_fields(&[]);
2728 assert!(r.is_valid_for_result("browser.browser_used"));
2729 assert!(r.is_valid_for_result("anything.at.all"));
2730 }
2731
2732 #[test]
2735 fn accessor_does_not_strip_real_first_segment() {
2736 let r = make_resolver_with_result_fields(&["metadata", "status_code"]);
2737 assert_eq!(
2739 r.accessor("metadata.title", "python", "result"),
2740 "result.metadata.title"
2741 );
2742 }
2743}