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 dart_first_class_map: DartFirstClassMap,
45}
46
47#[derive(Debug, Clone, Default)]
62pub struct PhpGetterMap {
63 pub getters: HashMap<String, HashSet<String>>,
64 pub field_types: HashMap<String, HashMap<String, String>>,
65 pub root_type: Option<String>,
66 pub all_fields: HashMap<String, HashSet<String>>,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum StringyFieldKind {
105 Plain,
107 Optional,
109 Vec,
112}
113
114#[derive(Debug, Clone)]
118pub struct StringyField {
119 pub name: String,
120 pub kind: StringyFieldKind,
121}
122
123#[derive(Debug, Clone, Default)]
124pub struct SwiftFirstClassMap {
125 pub first_class_types: HashSet<String>,
126 pub field_types: HashMap<String, HashMap<String, String>>,
127 pub vec_field_names: HashSet<String>,
128 pub root_type: Option<String>,
129 pub stringy_fields_by_type: HashMap<String, Vec<StringyField>>,
135}
136
137impl SwiftFirstClassMap {
138 pub fn is_first_class(&self, type_name: Option<&str>) -> bool {
144 match type_name {
145 Some(t) => self.first_class_types.contains(t),
146 None => true,
147 }
148 }
149
150 pub fn advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
153 let owner = owner_type?;
154 self.field_types.get(owner).and_then(|m| m.get(field_name).cloned())
155 }
156
157 pub fn is_vec_field_name(&self, field_name: &str) -> bool {
162 self.vec_field_names.contains(field_name)
163 }
164
165 pub fn is_empty(&self) -> bool {
167 self.first_class_types.is_empty() && self.field_types.is_empty()
168 }
169
170 pub fn stringy_fields(&self, type_name: &str) -> Option<&[StringyField]> {
173 self.stringy_fields_by_type.get(type_name).map(Vec::as_slice)
174 }
175}
176
177#[derive(Debug, Clone, Default)]
182pub struct DartFirstClassMap {
183 pub field_types: HashMap<String, HashMap<String, String>>,
184 pub root_type: Option<String>,
185 pub stringy_fields_by_type: HashMap<String, Vec<StringyField>>,
188}
189
190impl DartFirstClassMap {
191 pub fn advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
194 let owner = owner_type?;
195 self.field_types.get(owner).and_then(|m| m.get(field_name).cloned())
196 }
197
198 pub fn stringy_fields(&self, type_name: &str) -> Option<&[StringyField]> {
201 self.stringy_fields_by_type.get(type_name).map(Vec::as_slice)
202 }
203
204 pub fn is_empty(&self) -> bool {
206 self.field_types.is_empty() && self.stringy_fields_by_type.is_empty()
207 }
208}
209
210impl PhpGetterMap {
211 pub fn needs_getter(&self, owner_type: Option<&str>, field_name: &str) -> bool {
218 if let Some(t) = owner_type {
219 let owner_has_field = self.all_fields.get(t).is_some_and(|s| s.contains(field_name));
224 if owner_has_field {
225 if let Some(fields) = self.getters.get(t) {
226 return fields.contains(field_name);
227 }
228 }
229 }
230 self.getters.values().any(|set| set.contains(field_name))
231 }
232
233 pub fn advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
236 let owner = owner_type?;
237 self.field_types.get(owner).and_then(|m| m.get(field_name).cloned())
238 }
239
240 pub fn is_empty(&self) -> bool {
243 self.getters.is_empty()
244 }
245}
246
247#[derive(Debug, Clone)]
249enum PathSegment {
250 Field(String),
252 ArrayField { name: String, index: usize },
257 MapAccess { field: String, key: String },
259 Length,
261}
262
263impl FieldResolver {
264 pub fn new(
268 fields: &HashMap<String, String>,
269 optional: &HashSet<String>,
270 result_fields: &HashSet<String>,
271 array_fields: &HashSet<String>,
272 method_calls: &HashSet<String>,
273 ) -> Self {
274 Self {
275 aliases: fields.clone(),
276 optional_fields: optional.clone(),
277 result_fields: result_fields.clone(),
278 array_fields: array_fields.clone(),
279 method_calls: method_calls.clone(),
280 error_field_aliases: HashMap::new(),
281 php_getter_map: PhpGetterMap::default(),
282 swift_first_class_map: SwiftFirstClassMap::default(),
283 dart_first_class_map: DartFirstClassMap::default(),
284 }
285 }
286
287 pub fn new_with_error_aliases(
293 fields: &HashMap<String, String>,
294 optional: &HashSet<String>,
295 result_fields: &HashSet<String>,
296 array_fields: &HashSet<String>,
297 method_calls: &HashSet<String>,
298 error_field_aliases: &HashMap<String, String>,
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: SwiftFirstClassMap::default(),
309 dart_first_class_map: DartFirstClassMap::default(),
310 }
311 }
312
313 pub fn new_with_php_getters(
328 fields: &HashMap<String, String>,
329 optional: &HashSet<String>,
330 result_fields: &HashSet<String>,
331 array_fields: &HashSet<String>,
332 method_calls: &HashSet<String>,
333 error_field_aliases: &HashMap<String, String>,
334 php_getter_map: PhpGetterMap,
335 ) -> Self {
336 Self {
337 aliases: fields.clone(),
338 optional_fields: optional.clone(),
339 result_fields: result_fields.clone(),
340 array_fields: array_fields.clone(),
341 method_calls: method_calls.clone(),
342 error_field_aliases: error_field_aliases.clone(),
343 php_getter_map,
344 swift_first_class_map: SwiftFirstClassMap::default(),
345 dart_first_class_map: DartFirstClassMap::default(),
346 }
347 }
348
349 pub fn with_swift_root_type(&self, root_type: Option<String>) -> Self {
360 let mut clone = self.clone();
361 clone.swift_first_class_map.root_type = root_type;
362 clone
363 }
364
365 #[allow(clippy::too_many_arguments)]
369 pub fn new_with_swift_first_class(
370 fields: &HashMap<String, String>,
371 optional: &HashSet<String>,
372 result_fields: &HashSet<String>,
373 array_fields: &HashSet<String>,
374 method_calls: &HashSet<String>,
375 error_field_aliases: &HashMap<String, String>,
376 swift_first_class_map: SwiftFirstClassMap,
377 ) -> Self {
378 Self {
379 aliases: fields.clone(),
380 optional_fields: optional.clone(),
381 result_fields: result_fields.clone(),
382 array_fields: array_fields.clone(),
383 method_calls: method_calls.clone(),
384 error_field_aliases: error_field_aliases.clone(),
385 php_getter_map: PhpGetterMap::default(),
386 swift_first_class_map,
387 dart_first_class_map: DartFirstClassMap::default(),
388 }
389 }
390
391 #[allow(clippy::too_many_arguments)]
395 pub fn new_with_dart_first_class(
396 fields: &HashMap<String, String>,
397 optional: &HashSet<String>,
398 result_fields: &HashSet<String>,
399 array_fields: &HashSet<String>,
400 method_calls: &HashSet<String>,
401 error_field_aliases: &HashMap<String, String>,
402 dart_first_class_map: DartFirstClassMap,
403 ) -> Self {
404 Self {
405 aliases: fields.clone(),
406 optional_fields: optional.clone(),
407 result_fields: result_fields.clone(),
408 array_fields: array_fields.clone(),
409 method_calls: method_calls.clone(),
410 error_field_aliases: error_field_aliases.clone(),
411 php_getter_map: PhpGetterMap::default(),
412 swift_first_class_map: SwiftFirstClassMap::default(),
413 dart_first_class_map,
414 }
415 }
416
417 pub fn with_dart_root_type(&self, root_type: Option<String>) -> Self {
420 let mut clone = self.clone();
421 clone.dart_first_class_map.root_type = root_type;
422 clone
423 }
424
425 pub fn resolve<'a>(&'a self, fixture_field: &'a str) -> &'a str {
428 self.aliases
429 .get(fixture_field)
430 .map(String::as_str)
431 .unwrap_or(fixture_field)
432 }
433
434 pub fn leaf_is_vec_via_swift_map(&self, field: &str) -> bool {
441 let leaf = field.split('.').next_back().unwrap_or(field);
442 let leaf = leaf.split('[').next().unwrap_or(leaf);
443 self.swift_first_class_map.is_vec_field_name(leaf)
444 }
445
446 pub fn swift_root_type(&self) -> Option<&String> {
449 self.swift_first_class_map.root_type.as_ref()
450 }
451
452 pub fn swift_is_first_class(&self, type_name: Option<&str>) -> bool {
456 self.swift_first_class_map.is_first_class(type_name)
457 }
458
459 pub fn swift_advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
462 self.swift_first_class_map.advance(owner_type, field_name)
463 }
464
465 pub fn swift_stringy_fields(&self, type_name: &str) -> Option<&[StringyField]> {
469 self.swift_first_class_map.stringy_fields(type_name)
470 }
471
472 pub fn dart_root_type(&self) -> Option<&String> {
474 self.dart_first_class_map.root_type.as_ref()
475 }
476
477 pub fn dart_advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
479 self.dart_first_class_map.advance(owner_type, field_name)
480 }
481
482 pub fn dart_stringy_fields(&self, type_name: &str) -> Option<&[StringyField]> {
486 self.dart_first_class_map.stringy_fields(type_name)
487 }
488
489 pub fn is_optional(&self, field: &str) -> bool {
491 if self.is_optional_direct(field) {
492 return true;
493 }
494 if let Some(suffix) = self.namespace_stripped_path(field) {
498 if self.is_optional_direct(suffix) {
499 return true;
500 }
501 }
502 false
503 }
504
505 fn is_optional_direct(&self, field: &str) -> bool {
506 if self.optional_fields.contains(field) {
507 return true;
508 }
509 let index_normalized = normalize_numeric_indices(field);
510 if index_normalized != field && self.optional_fields.contains(index_normalized.as_str()) {
511 return true;
512 }
513 let de_indexed = strip_numeric_indices(field);
516 if de_indexed != field && self.optional_fields.contains(de_indexed.as_str()) {
517 return true;
518 }
519 let normalized = field.replace("[].", ".");
520 if normalized != field && self.optional_fields.contains(normalized.as_str()) {
521 return true;
522 }
523 for af in &self.array_fields {
524 if let Some(rest) = field.strip_prefix(af.as_str()) {
525 if let Some(rest) = rest.strip_prefix('.') {
526 let with_bracket = format!("{af}[].{rest}");
527 if self.optional_fields.contains(with_bracket.as_str()) {
528 return true;
529 }
530 }
531 }
532 }
533 false
534 }
535
536 pub fn has_alias(&self, fixture_field: &str) -> bool {
538 self.aliases.contains_key(fixture_field)
539 }
540
541 pub fn has_explicit_field(&self, field_name: &str) -> bool {
547 if self.result_fields.is_empty() {
548 return false;
549 }
550 self.result_fields.contains(field_name)
551 }
552
553 pub fn is_valid_for_result(&self, fixture_field: &str) -> bool {
562 if self.result_fields.is_empty() {
563 return true;
564 }
565 let resolved = self.resolve(fixture_field);
566 let first_segment = resolved.split('.').next().unwrap_or(resolved);
567 let first_segment = first_segment.split('[').next().unwrap_or(first_segment);
568 if self.result_fields.contains(first_segment) {
569 return true;
570 }
571 if let Some(suffix) = self.namespace_stripped_path(resolved) {
577 let suffix_first = suffix.split('.').next().unwrap_or(suffix);
578 let suffix_first = suffix_first.split('[').next().unwrap_or(suffix_first);
579 return self.result_fields.contains(suffix_first);
580 }
581 false
582 }
583
584 pub fn namespace_stripped_path<'a>(&self, path: &'a str) -> Option<&'a str> {
590 let dot_pos = path.find('.')?;
591 let first = &path[..dot_pos];
592 if first.contains('[') {
595 return None;
596 }
597 if self.result_fields.contains(first) {
600 return None;
601 }
602 let suffix = &path[dot_pos + 1..];
603 if suffix.is_empty() { None } else { Some(suffix) }
604 }
605
606 pub fn is_array(&self, field: &str) -> bool {
608 self.array_fields.contains(field)
609 }
610
611 pub fn is_collection_root(&self, field: &str) -> bool {
624 let prefix = format!("{field}[");
625 self.array_fields.iter().any(|af| af.starts_with(&prefix))
626 || self.optional_fields.iter().any(|of| of.starts_with(&prefix))
627 }
628
629 pub fn tagged_union_split(&self, fixture_field: &str) -> Option<(String, String, String)> {
641 let resolved = self.resolve(fixture_field);
642 let segments: Vec<&str> = resolved.split('.').collect();
643 let mut path_so_far = String::new();
644 for (i, seg) in segments.iter().enumerate() {
645 if !path_so_far.is_empty() {
646 path_so_far.push('.');
647 }
648 path_so_far.push_str(seg);
649 if self.method_calls.contains(&path_so_far) {
650 let prefix = segments[..i].join(".");
652 let variant = (*seg).to_string();
653 let suffix = segments[i + 1..].join(".");
654 return Some((prefix, variant, suffix));
655 }
656 }
657 None
658 }
659
660 pub fn has_map_access(&self, fixture_field: &str) -> bool {
662 let resolved = self.resolve(fixture_field);
663 let segments = parse_path(resolved);
664 segments.iter().any(|s| {
665 if let PathSegment::MapAccess { key, .. } = s {
666 !key.chars().all(|c| c.is_ascii_digit())
667 } else {
668 false
669 }
670 })
671 }
672
673 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
682 let resolved = self.resolve(fixture_field);
683 let effective = if !self.result_fields.is_empty() {
687 if let Some(stripped) = self.namespace_stripped_path(resolved) {
688 let stripped_first = stripped.split('.').next().unwrap_or(stripped);
689 let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
690 if self.result_fields.contains(stripped_first) {
691 stripped
692 } else {
693 resolved
694 }
695 } else {
696 resolved
697 }
698 } else {
699 resolved
700 };
701 let segments = parse_path(effective);
702 let segments = self.inject_array_indexing(segments);
703 match language {
704 "java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
705 "kotlin" => render_kotlin_with_optionals(&segments, result_var, &self.optional_fields),
706 "kotlin_android" => render_kotlin_android_with_optionals(&segments, result_var, &self.optional_fields),
709 "rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
710 "csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
711 "zig" => render_zig_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
712 "swift" if !self.swift_first_class_map.is_empty() => render_swift_with_first_class_map(
713 &segments,
714 result_var,
715 &self.optional_fields,
716 &self.swift_first_class_map,
717 ),
718 "swift" => render_swift_with_optionals(&segments, result_var, &self.optional_fields),
719 "dart" => render_dart_with_optionals(&segments, result_var, &self.optional_fields),
720 "php" if !self.php_getter_map.is_empty() => {
721 render_php_with_getters(&segments, result_var, &self.php_getter_map)
722 }
723 _ => render_accessor(&segments, language, result_var),
724 }
725 }
726
727 pub fn accessor_for_error(&self, sub_field: &str, language: &str, err_var: &str) -> String {
741 let resolved = self
742 .error_field_aliases
743 .get(sub_field)
744 .map(String::as_str)
745 .unwrap_or(sub_field);
746 let segments = parse_path(resolved);
747 match language {
750 "rust" => render_rust_with_optionals(&segments, err_var, &self.optional_fields, &self.method_calls),
751 _ => render_accessor(&segments, language, err_var),
752 }
753 }
754
755 pub fn has_error_aliases(&self) -> bool {
762 !self.error_field_aliases.is_empty()
763 }
764
765 fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
766 if self.array_fields.is_empty() {
767 return segments;
768 }
769 let len = segments.len();
770 let mut result = Vec::with_capacity(len);
771 let mut path_so_far = String::new();
772 for i in 0..len {
773 let seg = &segments[i];
774 match seg {
775 PathSegment::Field(f) => {
776 if !path_so_far.is_empty() {
777 path_so_far.push('.');
778 }
779 path_so_far.push_str(f);
780 let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
781 if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
782 result.push(PathSegment::ArrayField {
784 name: f.clone(),
785 index: 0,
786 });
787 } else {
788 result.push(seg.clone());
789 }
790 }
791 PathSegment::ArrayField { .. } => {
794 result.push(seg.clone());
795 }
796 PathSegment::MapAccess { field, key } => {
797 if !path_so_far.is_empty() {
798 path_so_far.push('.');
799 }
800 path_so_far.push_str(field);
801 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
802 if is_numeric && self.array_fields.contains(&path_so_far) {
803 let index: usize = key.parse().unwrap_or(0);
805 result.push(PathSegment::ArrayField {
806 name: field.clone(),
807 index,
808 });
809 } else {
810 result.push(seg.clone());
811 }
812 }
813 _ => {
814 result.push(seg.clone());
815 }
816 }
817 }
818 result
819 }
820
821 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
823 let resolved = self.resolve(fixture_field);
824 if !self.is_optional(resolved) {
825 return None;
826 }
827 let effective = if !self.result_fields.is_empty() {
831 if let Some(stripped) = self.namespace_stripped_path(resolved) {
832 let stripped_first = stripped.split('.').next().unwrap_or(stripped);
833 let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
834 if self.result_fields.contains(stripped_first) {
835 stripped
836 } else {
837 resolved
838 }
839 } else {
840 resolved
841 }
842 } else {
843 resolved
844 };
845 let segments = parse_path(effective);
846 let segments = self.inject_array_indexing(segments);
847 let local_var = {
852 let raw = effective.replace(['.', '['], "_").replace(']', "");
853 let mut collapsed = String::with_capacity(raw.len());
854 let mut prev_underscore = false;
855 for ch in raw.chars() {
856 if ch == '_' {
857 if !prev_underscore {
858 collapsed.push('_');
859 }
860 prev_underscore = true;
861 } else {
862 collapsed.push(ch);
863 prev_underscore = false;
864 }
865 }
866 collapsed.trim_matches('_').to_string()
867 };
868 let accessor = render_accessor(&segments, "rust", result_var);
869 let has_map_access = segments.iter().any(|s| {
870 if let PathSegment::MapAccess { key, .. } = s {
871 !key.chars().all(|c| c.is_ascii_digit())
872 } else {
873 false
874 }
875 });
876 let is_array = self.is_array(resolved);
877 let binding = if has_map_access {
878 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
879 } else if is_array {
880 format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
881 } else {
882 format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
888 };
889 Some((binding, local_var))
890 }
891}
892
893fn strip_numeric_indices(path: &str) -> String {
898 let mut result = String::with_capacity(path.len());
899 let mut chars = path.chars().peekable();
900 while let Some(c) = chars.next() {
901 if c == '[' {
902 let mut key = String::new();
903 let mut closed = false;
904 for inner in chars.by_ref() {
905 if inner == ']' {
906 closed = true;
907 break;
908 }
909 key.push(inner);
910 }
911 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
912 } else {
914 result.push('[');
915 result.push_str(&key);
916 if closed {
917 result.push(']');
918 }
919 }
920 } else {
921 result.push(c);
922 }
923 }
924 while result.contains("..") {
926 result = result.replace("..", ".");
927 }
928 if result.starts_with('.') {
929 result.remove(0);
930 }
931 result
932}
933
934fn normalize_numeric_indices(path: &str) -> String {
935 let mut result = String::with_capacity(path.len());
936 let mut chars = path.chars().peekable();
937 while let Some(c) = chars.next() {
938 if c == '[' {
939 let mut key = String::new();
940 let mut closed = false;
941 for inner in chars.by_ref() {
942 if inner == ']' {
943 closed = true;
944 break;
945 }
946 key.push(inner);
947 }
948 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
949 result.push_str("[0]");
950 } else {
951 result.push('[');
952 result.push_str(&key);
953 if closed {
954 result.push(']');
955 }
956 }
957 } else {
958 result.push(c);
959 }
960 }
961 result
962}
963
964fn parse_path(path: &str) -> Vec<PathSegment> {
965 let mut segments = Vec::new();
966 for part in path.split('.') {
967 if part == "length" || part == "count" || part == "size" {
968 segments.push(PathSegment::Length);
969 } else if let Some(bracket_pos) = part.find('[') {
970 let name = part[..bracket_pos].to_string();
971 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
972 if key.is_empty() {
973 segments.push(PathSegment::ArrayField { name, index: 0 });
975 } else if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
976 let index: usize = key.parse().unwrap_or(0);
978 segments.push(PathSegment::ArrayField { name, index });
979 } else {
980 segments.push(PathSegment::MapAccess { field: name, key });
982 }
983 } else {
984 segments.push(PathSegment::Field(part.to_string()));
985 }
986 }
987 segments
988}
989
990fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
991 match language {
992 "rust" => render_rust(segments, result_var),
993 "python" => render_dot_access(segments, result_var, "python"),
994 "typescript" | "node" => render_typescript(segments, result_var),
995 "wasm" => render_wasm(segments, result_var),
996 "go" => render_go(segments, result_var),
997 "java" => render_java(segments, result_var),
998 "kotlin" => render_kotlin(segments, result_var),
999 "kotlin_android" => render_kotlin_android(segments, result_var),
1000 "csharp" => render_pascal_dot(segments, result_var),
1001 "ruby" => render_dot_access(segments, result_var, "ruby"),
1002 "php" => render_php(segments, result_var),
1003 "elixir" => render_dot_access(segments, result_var, "elixir"),
1004 "r" => render_r(segments, result_var),
1005 "c" => render_c(segments, result_var),
1006 "swift" => render_swift(segments, result_var),
1007 "dart" => render_dart(segments, result_var),
1008 _ => render_dot_access(segments, result_var, language),
1009 }
1010}
1011
1012fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
1024 let mut out = result_var.to_string();
1025 for seg in segments {
1026 match seg {
1027 PathSegment::Field(f) => {
1028 out.push('.');
1029 out.push_str(&f.to_lower_camel_case());
1030 }
1031 PathSegment::ArrayField { name, index } => {
1032 out.push('.');
1033 out.push_str(&name.to_lower_camel_case());
1034 out.push_str(&format!("[{index}]"));
1035 }
1036 PathSegment::MapAccess { field, key } => {
1037 out.push('.');
1038 out.push_str(&field.to_lower_camel_case());
1039 if key.chars().all(|c| c.is_ascii_digit()) {
1040 out.push_str(&format!("[{key}]"));
1041 } else {
1042 out.push_str(&format!("[\"{key}\"]"));
1043 }
1044 }
1045 PathSegment::Length => {
1046 out.push_str(".count");
1047 }
1048 }
1049 }
1050 out
1051}
1052
1053fn render_swift_with_optionals(
1063 segments: &[PathSegment],
1064 result_var: &str,
1065 optional_fields: &HashSet<String>,
1066) -> String {
1067 let mut out = result_var.to_string();
1068 let mut path_so_far = String::new();
1069 let total = segments.len();
1070 for (i, seg) in segments.iter().enumerate() {
1071 let is_leaf = i == total - 1;
1072 match seg {
1073 PathSegment::Field(f) => {
1074 if !path_so_far.is_empty() {
1075 path_so_far.push('.');
1076 }
1077 path_so_far.push_str(f);
1078 out.push('.');
1079 out.push_str(&f.to_lower_camel_case());
1082 if !is_leaf && optional_fields.contains(&path_so_far) {
1086 out.push('?');
1087 }
1088 }
1089 PathSegment::ArrayField { name, index } => {
1090 if !path_so_far.is_empty() {
1091 path_so_far.push('.');
1092 }
1093 path_so_far.push_str(name);
1094 let is_optional = optional_fields.contains(&path_so_far);
1095 out.push('.');
1096 out.push_str(&name.to_lower_camel_case());
1097 if is_optional {
1098 out.push_str(&format!("?[{index}]"));
1100 } else {
1101 out.push_str(&format!("[{index}]"));
1102 }
1103 path_so_far.push_str("[0]");
1104 let _ = is_leaf;
1105 }
1106 PathSegment::MapAccess { field, key } => {
1107 if !path_so_far.is_empty() {
1108 path_so_far.push('.');
1109 }
1110 path_so_far.push_str(field);
1111 out.push('.');
1112 out.push_str(&field.to_lower_camel_case());
1113 if key.chars().all(|c| c.is_ascii_digit()) {
1114 out.push_str(&format!("[{key}]"));
1115 } else {
1116 out.push_str(&format!("[\"{key}\"]"));
1117 }
1118 }
1119 PathSegment::Length => {
1120 out.push_str(".count");
1121 }
1122 }
1123 }
1124 out
1125}
1126
1127fn render_swift_with_first_class_map(
1132 segments: &[PathSegment],
1133 result_var: &str,
1134 optional_fields: &HashSet<String>,
1135 map: &SwiftFirstClassMap,
1136) -> String {
1137 let mut out = result_var.to_string();
1138 let mut path_so_far = String::new();
1139 let mut current_type: Option<String> = map.root_type.clone();
1140 let mut via_rust_vec = false;
1149 let mut via_opaque = false;
1160 let total = segments.len();
1161 for (i, seg) in segments.iter().enumerate() {
1162 let is_leaf = i == total - 1;
1163 let property_syntax = !via_rust_vec && !via_opaque && map.is_first_class(current_type.as_deref());
1164 if !property_syntax {
1165 via_opaque = true;
1166 }
1167 match seg {
1168 PathSegment::Field(f) => {
1169 if !path_so_far.is_empty() {
1170 path_so_far.push('.');
1171 }
1172 path_so_far.push_str(f);
1173 out.push('.');
1174 out.push_str(&f.to_lower_camel_case());
1177 if !property_syntax {
1178 out.push_str("()");
1179 }
1180 if !is_leaf && optional_fields.contains(&path_so_far) {
1181 out.push('?');
1182 }
1183 current_type = map.advance(current_type.as_deref(), f);
1184 }
1185 PathSegment::ArrayField { name, index } => {
1186 if !path_so_far.is_empty() {
1187 path_so_far.push('.');
1188 }
1189 path_so_far.push_str(name);
1190 let is_optional = optional_fields.contains(&path_so_far);
1191 out.push('.');
1192 out.push_str(&name.to_lower_camel_case());
1193 let access = if property_syntax { "" } else { "()" };
1194 if is_optional {
1195 out.push_str(&format!("{access}?[{index}]"));
1196 } else {
1197 out.push_str(&format!("{access}[{index}]"));
1198 }
1199 path_so_far.push_str("[0]");
1200 current_type = map.advance(current_type.as_deref(), name);
1206 if !property_syntax {
1207 via_rust_vec = true;
1208 }
1209 }
1210 PathSegment::MapAccess { field, key } => {
1211 if !path_so_far.is_empty() {
1212 path_so_far.push('.');
1213 }
1214 path_so_far.push_str(field);
1215 out.push('.');
1216 out.push_str(&field.to_lower_camel_case());
1217 let access = if property_syntax { "" } else { "()" };
1218 if key.chars().all(|c| c.is_ascii_digit()) {
1219 out.push_str(&format!("{access}[{key}]"));
1220 } else {
1221 out.push_str(&format!("{access}[\"{key}\"]"));
1222 }
1223 current_type = map.advance(current_type.as_deref(), field);
1224 }
1225 PathSegment::Length => {
1226 out.push_str(".count");
1227 }
1228 }
1229 }
1230 out
1231}
1232
1233fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
1234 let mut out = result_var.to_string();
1235 for seg in segments {
1236 match seg {
1237 PathSegment::Field(f) => {
1238 out.push('.');
1239 out.push_str(&f.to_snake_case());
1240 }
1241 PathSegment::ArrayField { name, index } => {
1242 out.push('.');
1243 out.push_str(&name.to_snake_case());
1244 out.push_str(&format!("[{index}]"));
1245 }
1246 PathSegment::MapAccess { field, key } => {
1247 out.push('.');
1248 out.push_str(&field.to_snake_case());
1249 if key.chars().all(|c| c.is_ascii_digit()) {
1250 out.push_str(&format!("[{key}]"));
1251 } else {
1252 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1253 }
1254 }
1255 PathSegment::Length => {
1256 out.push_str(".len()");
1257 }
1258 }
1259 }
1260 out
1261}
1262
1263fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
1264 let mut out = result_var.to_string();
1265 for seg in segments {
1266 match seg {
1267 PathSegment::Field(f) => {
1268 out.push('.');
1269 out.push_str(f);
1270 }
1271 PathSegment::ArrayField { name, index } => {
1272 if language == "elixir" {
1273 let current = std::mem::take(&mut out);
1274 out = format!("Enum.at({current}.{name}, {index})");
1275 } else {
1276 out.push('.');
1277 out.push_str(name);
1278 out.push_str(&format!("[{index}]"));
1279 }
1280 }
1281 PathSegment::MapAccess { field, key } => {
1282 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
1283 if is_numeric && language == "elixir" {
1284 let current = std::mem::take(&mut out);
1285 out = format!("Enum.at({current}.{field}, {key})");
1286 } else {
1287 out.push('.');
1288 out.push_str(field);
1289 if is_numeric {
1290 let idx: usize = key.parse().unwrap_or(0);
1291 out.push_str(&format!("[{idx}]"));
1292 } else if language == "elixir" || language == "ruby" {
1293 out.push_str(&format!("[\"{key}\"]"));
1296 } else {
1297 out.push_str(&format!(".get(\"{key}\")"));
1298 }
1299 }
1300 }
1301 PathSegment::Length => match language {
1302 "ruby" => out.push_str(".length"),
1303 "elixir" => {
1304 let current = std::mem::take(&mut out);
1305 out = format!("length({current})");
1306 }
1307 "gleam" => {
1308 let current = std::mem::take(&mut out);
1309 out = format!("list.length({current})");
1310 }
1311 _ => {
1312 let current = std::mem::take(&mut out);
1313 out = format!("len({current})");
1314 }
1315 },
1316 }
1317 }
1318 out
1319}
1320
1321fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
1322 let mut out = result_var.to_string();
1323 for seg in segments {
1324 match seg {
1325 PathSegment::Field(f) => {
1326 out.push('.');
1327 out.push_str(&f.to_lower_camel_case());
1328 }
1329 PathSegment::ArrayField { name, index } => {
1330 out.push('.');
1331 out.push_str(&name.to_lower_camel_case());
1332 out.push_str(&format!("[{index}]"));
1333 }
1334 PathSegment::MapAccess { field, key } => {
1335 out.push('.');
1336 out.push_str(&field.to_lower_camel_case());
1337 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
1340 out.push_str(&format!("[{key}]"));
1341 } else {
1342 out.push_str(&format!("[\"{key}\"]"));
1343 }
1344 }
1345 PathSegment::Length => {
1346 out.push_str(".length");
1347 }
1348 }
1349 }
1350 out
1351}
1352
1353fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
1354 let mut out = result_var.to_string();
1355 for seg in segments {
1356 match seg {
1357 PathSegment::Field(f) => {
1358 out.push('.');
1359 out.push_str(&f.to_lower_camel_case());
1360 }
1361 PathSegment::ArrayField { name, index } => {
1362 out.push('.');
1363 out.push_str(&name.to_lower_camel_case());
1364 out.push_str(&format!("[{index}]"));
1365 }
1366 PathSegment::MapAccess { field, key } => {
1367 out.push('.');
1368 out.push_str(&field.to_lower_camel_case());
1369 out.push_str(&format!(".get(\"{key}\")"));
1370 }
1371 PathSegment::Length => {
1372 out.push_str(".length");
1373 }
1374 }
1375 }
1376 out
1377}
1378
1379fn render_go(segments: &[PathSegment], result_var: &str) -> String {
1380 let mut out = result_var.to_string();
1381 for seg in segments {
1382 match seg {
1383 PathSegment::Field(f) => {
1384 out.push('.');
1385 out.push_str(&to_go_name(f));
1386 }
1387 PathSegment::ArrayField { name, index } => {
1388 out.push('.');
1389 out.push_str(&to_go_name(name));
1390 out.push_str(&format!("[{index}]"));
1391 }
1392 PathSegment::MapAccess { field, key } => {
1393 out.push('.');
1394 out.push_str(&to_go_name(field));
1395 if key.chars().all(|c| c.is_ascii_digit()) {
1396 out.push_str(&format!("[{key}]"));
1397 } else {
1398 out.push_str(&format!("[\"{key}\"]"));
1399 }
1400 }
1401 PathSegment::Length => {
1402 let current = std::mem::take(&mut out);
1403 out = format!("len({current})");
1404 }
1405 }
1406 }
1407 out
1408}
1409
1410fn render_java(segments: &[PathSegment], result_var: &str) -> String {
1411 let mut out = result_var.to_string();
1412 for seg in segments {
1413 match seg {
1414 PathSegment::Field(f) => {
1415 out.push('.');
1416 out.push_str(&f.to_lower_camel_case());
1417 out.push_str("()");
1418 }
1419 PathSegment::ArrayField { name, index } => {
1420 out.push('.');
1421 out.push_str(&name.to_lower_camel_case());
1422 out.push_str(&format!("().get({index})"));
1423 }
1424 PathSegment::MapAccess { field, key } => {
1425 out.push('.');
1426 out.push_str(&field.to_lower_camel_case());
1427 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1429 if is_numeric {
1430 out.push_str(&format!("().get({key})"));
1431 } else {
1432 out.push_str(&format!("().get(\"{key}\")"));
1433 }
1434 }
1435 PathSegment::Length => {
1436 out.push_str(".size()");
1437 }
1438 }
1439 }
1440 out
1441}
1442
1443fn kotlin_getter(name: &str) -> String {
1448 let camel = name.to_lower_camel_case();
1449 match camel.as_str() {
1450 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
1451 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
1452 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
1453 _ => camel,
1454 }
1455}
1456
1457fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
1458 let mut out = result_var.to_string();
1459 for seg in segments {
1460 match seg {
1461 PathSegment::Field(f) => {
1462 out.push('.');
1463 out.push_str(&kotlin_getter(f));
1464 out.push_str("()");
1465 }
1466 PathSegment::ArrayField { name, index } => {
1467 out.push('.');
1468 out.push_str(&kotlin_getter(name));
1469 if *index == 0 {
1470 out.push_str("().first()");
1471 } else {
1472 out.push_str(&format!("().get({index})"));
1473 }
1474 }
1475 PathSegment::MapAccess { field, key } => {
1476 out.push('.');
1477 out.push_str(&kotlin_getter(field));
1478 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1479 if is_numeric {
1480 out.push_str(&format!("().get({key})"));
1481 } else {
1482 out.push_str(&format!("().get(\"{key}\")"));
1483 }
1484 }
1485 PathSegment::Length => {
1486 out.push_str(".size");
1487 }
1488 }
1489 }
1490 out
1491}
1492
1493fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1494 let mut out = result_var.to_string();
1495 let mut path_so_far = String::new();
1496 for (i, seg) in segments.iter().enumerate() {
1497 let is_leaf = i == segments.len() - 1;
1498 match seg {
1499 PathSegment::Field(f) => {
1500 if !path_so_far.is_empty() {
1501 path_so_far.push('.');
1502 }
1503 path_so_far.push_str(f);
1504 out.push('.');
1505 out.push_str(&f.to_lower_camel_case());
1506 out.push_str("()");
1507 let _ = is_leaf;
1508 let _ = optional_fields;
1509 }
1510 PathSegment::ArrayField { name, index } => {
1511 if !path_so_far.is_empty() {
1512 path_so_far.push('.');
1513 }
1514 path_so_far.push_str(name);
1515 out.push('.');
1516 out.push_str(&name.to_lower_camel_case());
1517 out.push_str(&format!("().get({index})"));
1518 }
1519 PathSegment::MapAccess { field, key } => {
1520 if !path_so_far.is_empty() {
1521 path_so_far.push('.');
1522 }
1523 path_so_far.push_str(field);
1524 out.push('.');
1525 out.push_str(&field.to_lower_camel_case());
1526 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1528 if is_numeric {
1529 out.push_str(&format!("().get({key})"));
1530 } else {
1531 out.push_str(&format!("().get(\"{key}\")"));
1532 }
1533 }
1534 PathSegment::Length => {
1535 out.push_str(".size()");
1536 }
1537 }
1538 }
1539 out
1540}
1541
1542fn render_kotlin_with_optionals(
1557 segments: &[PathSegment],
1558 result_var: &str,
1559 optional_fields: &HashSet<String>,
1560) -> String {
1561 let mut out = result_var.to_string();
1562 let mut path_so_far = String::new();
1563 let mut prev_was_nullable = false;
1571 for seg in segments {
1572 let nav = if prev_was_nullable { "?." } else { "." };
1573 match seg {
1574 PathSegment::Field(f) => {
1575 if !path_so_far.is_empty() {
1576 path_so_far.push('.');
1577 }
1578 path_so_far.push_str(f);
1579 let is_optional = optional_fields.contains(&path_so_far);
1584 out.push_str(nav);
1585 out.push_str(&kotlin_getter(f));
1586 out.push_str("()");
1587 prev_was_nullable = prev_was_nullable || is_optional;
1588 }
1589 PathSegment::ArrayField { name, index } => {
1590 if !path_so_far.is_empty() {
1591 path_so_far.push('.');
1592 }
1593 path_so_far.push_str(name);
1594 let is_optional = optional_fields.contains(&path_so_far);
1595 out.push_str(nav);
1596 out.push_str(&kotlin_getter(name));
1597 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1598 if *index == 0 {
1599 out.push_str(&format!("(){safe}.first()"));
1600 } else {
1601 out.push_str(&format!("(){safe}.get({index})"));
1602 }
1603 path_so_far.push_str("[0]");
1607 prev_was_nullable = prev_was_nullable || is_optional;
1608 }
1609 PathSegment::MapAccess { field, key } => {
1610 if !path_so_far.is_empty() {
1611 path_so_far.push('.');
1612 }
1613 path_so_far.push_str(field);
1614 let is_optional = optional_fields.contains(&path_so_far);
1615 out.push_str(nav);
1616 out.push_str(&kotlin_getter(field));
1617 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1618 if is_numeric {
1619 if prev_was_nullable || is_optional {
1620 out.push_str(&format!("()?.get({key})"));
1621 } else {
1622 out.push_str(&format!("().get({key})"));
1623 }
1624 } else if prev_was_nullable || is_optional {
1625 out.push_str(&format!("()?.get(\"{key}\")"));
1626 } else {
1627 out.push_str(&format!("().get(\"{key}\")"));
1628 }
1629 prev_was_nullable = prev_was_nullable || is_optional;
1630 }
1631 PathSegment::Length => {
1632 let size_nav = if prev_was_nullable { "?" } else { "" };
1635 out.push_str(&format!("{size_nav}.size"));
1636 prev_was_nullable = false;
1637 }
1638 }
1639 }
1640 out
1641}
1642
1643fn render_kotlin_android_with_optionals(
1654 segments: &[PathSegment],
1655 result_var: &str,
1656 optional_fields: &HashSet<String>,
1657) -> String {
1658 let mut out = result_var.to_string();
1659 let mut path_so_far = String::new();
1660 let mut prev_was_nullable = false;
1661 for seg in segments {
1662 let nav = if prev_was_nullable { "?." } else { "." };
1663 match seg {
1664 PathSegment::Field(f) => {
1665 if !path_so_far.is_empty() {
1666 path_so_far.push('.');
1667 }
1668 path_so_far.push_str(f);
1669 let is_optional = optional_fields.contains(&path_so_far);
1670 out.push_str(nav);
1671 out.push_str(&kotlin_getter(f));
1673 prev_was_nullable = prev_was_nullable || is_optional;
1674 }
1675 PathSegment::ArrayField { name, index } => {
1676 if !path_so_far.is_empty() {
1677 path_so_far.push('.');
1678 }
1679 path_so_far.push_str(name);
1680 let is_optional = optional_fields.contains(&path_so_far);
1681 out.push_str(nav);
1682 out.push_str(&kotlin_getter(name));
1684 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1685 if *index == 0 {
1686 out.push_str(&format!("{safe}.first()"));
1687 } else {
1688 out.push_str(&format!("{safe}.get({index})"));
1689 }
1690 path_so_far.push_str("[0]");
1691 prev_was_nullable = prev_was_nullable || is_optional;
1692 }
1693 PathSegment::MapAccess { field, key } => {
1694 if !path_so_far.is_empty() {
1695 path_so_far.push('.');
1696 }
1697 path_so_far.push_str(field);
1698 let is_optional = optional_fields.contains(&path_so_far);
1699 out.push_str(nav);
1700 out.push_str(&kotlin_getter(field));
1702 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1703 if is_numeric {
1704 if prev_was_nullable || is_optional {
1705 out.push_str(&format!("?.get({key})"));
1706 } else {
1707 out.push_str(&format!(".get({key})"));
1708 }
1709 } else if prev_was_nullable || is_optional {
1710 out.push_str(&format!("?.get(\"{key}\")"));
1711 } else {
1712 out.push_str(&format!(".get(\"{key}\")"));
1713 }
1714 prev_was_nullable = prev_was_nullable || is_optional;
1715 }
1716 PathSegment::Length => {
1717 let size_nav = if prev_was_nullable { "?" } else { "" };
1718 out.push_str(&format!("{size_nav}.size"));
1719 prev_was_nullable = false;
1720 }
1721 }
1722 }
1723 out
1724}
1725
1726fn render_kotlin_android(segments: &[PathSegment], result_var: &str) -> String {
1730 let mut out = result_var.to_string();
1731 for seg in segments {
1732 match seg {
1733 PathSegment::Field(f) => {
1734 out.push('.');
1735 out.push_str(&kotlin_getter(f));
1736 }
1738 PathSegment::ArrayField { name, index } => {
1739 out.push('.');
1740 out.push_str(&kotlin_getter(name));
1741 if *index == 0 {
1742 out.push_str(".first()");
1743 } else {
1744 out.push_str(&format!(".get({index})"));
1745 }
1746 }
1747 PathSegment::MapAccess { field, key } => {
1748 out.push('.');
1749 out.push_str(&kotlin_getter(field));
1750 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1751 if is_numeric {
1752 out.push_str(&format!(".get({key})"));
1753 } else {
1754 out.push_str(&format!(".get(\"{key}\")"));
1755 }
1756 }
1757 PathSegment::Length => {
1758 out.push_str(".size");
1759 }
1760 }
1761 }
1762 out
1763}
1764
1765fn render_rust_with_optionals(
1771 segments: &[PathSegment],
1772 result_var: &str,
1773 optional_fields: &HashSet<String>,
1774 method_calls: &HashSet<String>,
1775) -> String {
1776 let mut out = result_var.to_string();
1777 let mut path_so_far = String::new();
1778 for (i, seg) in segments.iter().enumerate() {
1779 let is_leaf = i == segments.len() - 1;
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.to_snake_case());
1788 let is_method = method_calls.contains(&path_so_far);
1789 if is_method {
1790 out.push_str("()");
1791 if !is_leaf && optional_fields.contains(&path_so_far) {
1792 out.push_str(".as_ref().unwrap()");
1793 }
1794 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1795 out.push_str(".as_ref().unwrap()");
1796 }
1797 }
1798 PathSegment::ArrayField { name, index } => {
1799 if !path_so_far.is_empty() {
1800 path_so_far.push('.');
1801 }
1802 path_so_far.push_str(name);
1803 out.push('.');
1804 out.push_str(&name.to_snake_case());
1805 let path_with_idx = format!("{path_so_far}[0]");
1809 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1810 if is_opt {
1811 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1812 } else {
1813 out.push_str(&format!("[{index}]"));
1814 }
1815 path_so_far.push_str("[0]");
1820 }
1821 PathSegment::MapAccess { field, key } => {
1822 if !path_so_far.is_empty() {
1823 path_so_far.push('.');
1824 }
1825 path_so_far.push_str(field);
1826 out.push('.');
1827 out.push_str(&field.to_snake_case());
1828 if key.chars().all(|c| c.is_ascii_digit()) {
1829 let path_with_idx = format!("{path_so_far}[0]");
1831 let is_opt =
1832 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1833 if is_opt {
1834 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1835 } else {
1836 out.push_str(&format!("[{key}]"));
1837 }
1838 path_so_far.push_str("[0]");
1839 } else {
1840 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1841 }
1842 }
1843 PathSegment::Length => {
1844 out.push_str(".len()");
1845 }
1846 }
1847 }
1848 out
1849}
1850
1851fn render_zig_with_optionals(
1864 segments: &[PathSegment],
1865 result_var: &str,
1866 optional_fields: &HashSet<String>,
1867 method_calls: &HashSet<String>,
1868) -> String {
1869 let mut out = result_var.to_string();
1870 let mut path_so_far = String::new();
1871 for seg in segments {
1872 match seg {
1873 PathSegment::Field(f) => {
1874 if !path_so_far.is_empty() {
1875 path_so_far.push('.');
1876 }
1877 path_so_far.push_str(f);
1878 out.push('.');
1879 out.push_str(f);
1880 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1881 out.push_str(".?");
1882 }
1883 }
1884 PathSegment::ArrayField { name, index } => {
1885 if !path_so_far.is_empty() {
1886 path_so_far.push('.');
1887 }
1888 path_so_far.push_str(name);
1889 out.push('.');
1890 out.push_str(name);
1891 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1892 out.push_str(".?");
1893 }
1894 out.push_str(&format!("[{index}]"));
1895 }
1896 PathSegment::MapAccess { field, key } => {
1897 if !path_so_far.is_empty() {
1898 path_so_far.push('.');
1899 }
1900 path_so_far.push_str(field);
1901 out.push('.');
1902 out.push_str(field);
1903 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1904 out.push_str(".?");
1905 }
1906 if key.chars().all(|c| c.is_ascii_digit()) {
1907 out.push_str(&format!("[{key}]"));
1908 } else {
1909 out.push_str(&format!(".get(\"{key}\")"));
1910 }
1911 }
1912 PathSegment::Length => {
1913 out.push_str(".len");
1914 }
1915 }
1916 }
1917 out
1918}
1919
1920fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1921 let mut out = result_var.to_string();
1922 for seg in segments {
1923 match seg {
1924 PathSegment::Field(f) => {
1925 out.push('.');
1926 out.push_str(&f.to_pascal_case());
1927 }
1928 PathSegment::ArrayField { name, index } => {
1929 out.push('.');
1930 out.push_str(&name.to_pascal_case());
1931 out.push_str(&format!("[{index}]"));
1932 }
1933 PathSegment::MapAccess { field, key } => {
1934 out.push('.');
1935 out.push_str(&field.to_pascal_case());
1936 if key.chars().all(|c| c.is_ascii_digit()) {
1937 out.push_str(&format!("[{key}]"));
1938 } else {
1939 out.push_str(&format!("[\"{key}\"]"));
1940 }
1941 }
1942 PathSegment::Length => {
1943 out.push_str(".Count");
1944 }
1945 }
1946 }
1947 out
1948}
1949
1950fn render_csharp_with_optionals(
1951 segments: &[PathSegment],
1952 result_var: &str,
1953 optional_fields: &HashSet<String>,
1954) -> String {
1955 let mut out = result_var.to_string();
1956 let mut path_so_far = String::new();
1957 for (i, seg) in segments.iter().enumerate() {
1958 let is_leaf = i == segments.len() - 1;
1959 match seg {
1960 PathSegment::Field(f) => {
1961 if !path_so_far.is_empty() {
1962 path_so_far.push('.');
1963 }
1964 path_so_far.push_str(f);
1965 out.push('.');
1966 out.push_str(&f.to_pascal_case());
1967 if !is_leaf && optional_fields.contains(&path_so_far) {
1968 out.push('!');
1969 }
1970 }
1971 PathSegment::ArrayField { name, index } => {
1972 if !path_so_far.is_empty() {
1973 path_so_far.push('.');
1974 }
1975 path_so_far.push_str(name);
1976 out.push('.');
1977 out.push_str(&name.to_pascal_case());
1978 out.push_str(&format!("[{index}]"));
1979 }
1980 PathSegment::MapAccess { field, key } => {
1981 if !path_so_far.is_empty() {
1982 path_so_far.push('.');
1983 }
1984 path_so_far.push_str(field);
1985 out.push('.');
1986 out.push_str(&field.to_pascal_case());
1987 if key.chars().all(|c| c.is_ascii_digit()) {
1988 out.push_str(&format!("[{key}]"));
1989 } else {
1990 out.push_str(&format!("[\"{key}\"]"));
1991 }
1992 }
1993 PathSegment::Length => {
1994 out.push_str(".Count");
1995 }
1996 }
1997 }
1998 out
1999}
2000
2001fn render_php(segments: &[PathSegment], result_var: &str) -> String {
2002 let mut out = result_var.to_string();
2003 for seg in segments {
2004 match seg {
2005 PathSegment::Field(f) => {
2006 out.push_str("->");
2007 out.push_str(&f.to_lower_camel_case());
2010 }
2011 PathSegment::ArrayField { name, index } => {
2012 out.push_str("->");
2013 out.push_str(&name.to_lower_camel_case());
2014 out.push_str(&format!("[{index}]"));
2015 }
2016 PathSegment::MapAccess { field, key } => {
2017 out.push_str("->");
2018 out.push_str(&field.to_lower_camel_case());
2019 out.push_str(&format!("[\"{key}\"]"));
2020 }
2021 PathSegment::Length => {
2022 let current = std::mem::take(&mut out);
2023 out = format!("count({current})");
2024 }
2025 }
2026 }
2027 out
2028}
2029
2030fn render_php_with_getters(segments: &[PathSegment], result_var: &str, getter_map: &PhpGetterMap) -> String {
2048 let mut out = result_var.to_string();
2049 let mut current_type: Option<String> = getter_map.root_type.clone();
2050 for seg in segments {
2051 match seg {
2052 PathSegment::Field(f) => {
2053 let camel = f.to_lower_camel_case();
2054 if getter_map.needs_getter(current_type.as_deref(), f.as_str()) {
2055 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
2060 out.push_str("->");
2061 out.push_str(&getter);
2062 out.push_str("()");
2063 } else {
2064 out.push_str("->");
2065 out.push_str(&camel);
2066 }
2067 current_type = getter_map.advance(current_type.as_deref(), f.as_str());
2068 }
2069 PathSegment::ArrayField { name, index } => {
2070 let camel = name.to_lower_camel_case();
2071 if getter_map.needs_getter(current_type.as_deref(), name.as_str()) {
2072 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
2073 out.push_str("->");
2074 out.push_str(&getter);
2075 out.push_str("()");
2076 } else {
2077 out.push_str("->");
2078 out.push_str(&camel);
2079 }
2080 out.push_str(&format!("[{index}]"));
2081 current_type = getter_map.advance(current_type.as_deref(), name.as_str());
2082 }
2083 PathSegment::MapAccess { field, key } => {
2084 let camel = field.to_lower_camel_case();
2085 if getter_map.needs_getter(current_type.as_deref(), field.as_str()) {
2086 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
2087 out.push_str("->");
2088 out.push_str(&getter);
2089 out.push_str("()");
2090 } else {
2091 out.push_str("->");
2092 out.push_str(&camel);
2093 }
2094 out.push_str(&format!("[\"{key}\"]"));
2095 current_type = getter_map.advance(current_type.as_deref(), field.as_str());
2096 }
2097 PathSegment::Length => {
2098 let current = std::mem::take(&mut out);
2099 out = format!("count({current})");
2100 }
2101 }
2102 }
2103 out
2104}
2105
2106fn render_r(segments: &[PathSegment], result_var: &str) -> String {
2107 let mut out = result_var.to_string();
2108 for seg in segments {
2109 match seg {
2110 PathSegment::Field(f) => {
2111 out.push('$');
2112 out.push_str(f);
2113 }
2114 PathSegment::ArrayField { name, index } => {
2115 out.push('$');
2116 out.push_str(name);
2117 out.push_str(&format!("[[{}]]", index + 1));
2119 }
2120 PathSegment::MapAccess { field, key } => {
2121 out.push('$');
2122 out.push_str(field);
2123 out.push_str(&format!("[[\"{key}\"]]"));
2124 }
2125 PathSegment::Length => {
2126 let current = std::mem::take(&mut out);
2127 out = format!("length({current})");
2128 }
2129 }
2130 }
2131 out
2132}
2133
2134fn render_c(segments: &[PathSegment], result_var: &str) -> String {
2135 let mut out = result_var.to_string();
2136 for seg in segments {
2137 match seg {
2138 PathSegment::Field(f) => {
2139 let snake = f.to_snake_case();
2140 let current = std::mem::take(&mut out);
2141 out = format!("result_{snake}({current})");
2143 }
2144 PathSegment::ArrayField { name, index } => {
2145 let snake = name.to_snake_case();
2146 let current = std::mem::take(&mut out);
2147 out = format!("result_{snake}({current})[{index}]");
2148 }
2149 PathSegment::MapAccess { field, key } => {
2150 let snake = field.to_snake_case();
2151 let current = std::mem::take(&mut out);
2152 out = format!("result_{snake}({current})[\"{key}\"]");
2153 }
2154 PathSegment::Length => {
2155 let current = std::mem::take(&mut out);
2156 out = format!("result_{current}_count()");
2157 }
2158 }
2159 }
2160 out
2161}
2162
2163fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
2170 let mut out = result_var.to_string();
2171 for seg in segments {
2172 match seg {
2173 PathSegment::Field(f) => {
2174 out.push('.');
2175 out.push_str(&f.to_lower_camel_case());
2176 }
2177 PathSegment::ArrayField { name, index } => {
2178 out.push('.');
2179 out.push_str(&name.to_lower_camel_case());
2180 out.push_str(&format!("[{index}]"));
2181 }
2182 PathSegment::MapAccess { field, key } => {
2183 out.push('.');
2184 out.push_str(&field.to_lower_camel_case());
2185 if key.chars().all(|c| c.is_ascii_digit()) {
2186 out.push_str(&format!("[{key}]"));
2187 } else {
2188 out.push_str(&format!("[\"{key}\"]"));
2189 }
2190 }
2191 PathSegment::Length => {
2192 out.push_str(".length");
2193 }
2194 }
2195 }
2196 out
2197}
2198
2199fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
2205 let mut out = result_var.to_string();
2206 let mut path_so_far = String::new();
2214 let mut path_with_indices = String::new();
2215 let mut prev_was_nullable = false;
2216 let is_optional =
2217 |bare: &str, indexed: &str| -> bool { optional_fields.contains(bare) || optional_fields.contains(indexed) };
2218 for seg in segments {
2219 let nav = if prev_was_nullable { "?." } else { "." };
2220 match seg {
2221 PathSegment::Field(f) => {
2222 if !path_so_far.is_empty() {
2223 path_so_far.push('.');
2224 path_with_indices.push('.');
2225 }
2226 path_so_far.push_str(f);
2227 path_with_indices.push_str(f);
2228 let optional = is_optional(&path_so_far, &path_with_indices);
2229 out.push_str(nav);
2230 out.push_str(&f.to_lower_camel_case());
2231 prev_was_nullable = optional;
2232 }
2233 PathSegment::ArrayField { name, index } => {
2234 if !path_so_far.is_empty() {
2235 path_so_far.push('.');
2236 path_with_indices.push('.');
2237 }
2238 path_so_far.push_str(name);
2239 path_with_indices.push_str(name);
2240 let optional = is_optional(&path_so_far, &path_with_indices);
2241 out.push_str(nav);
2242 out.push_str(&name.to_lower_camel_case());
2243 if optional {
2247 out.push('!');
2248 }
2249 out.push_str(&format!("[{index}]"));
2250 path_with_indices.push_str(&format!("[{index}]"));
2251 prev_was_nullable = false;
2252 }
2253 PathSegment::MapAccess { field, key } => {
2254 if !path_so_far.is_empty() {
2255 path_so_far.push('.');
2256 path_with_indices.push('.');
2257 }
2258 path_so_far.push_str(field);
2259 path_with_indices.push_str(field);
2260 let optional = is_optional(&path_so_far, &path_with_indices);
2261 out.push_str(nav);
2262 out.push_str(&field.to_lower_camel_case());
2263 if key.chars().all(|c| c.is_ascii_digit()) {
2264 out.push_str(&format!("[{key}]"));
2265 path_with_indices.push_str(&format!("[{key}]"));
2266 } else {
2267 out.push_str(&format!("[\"{key}\"]"));
2268 path_with_indices.push_str(&format!("[\"{key}\"]"));
2269 }
2270 prev_was_nullable = optional;
2271 }
2272 PathSegment::Length => {
2273 out.push_str(nav);
2276 out.push_str("length");
2277 prev_was_nullable = false;
2278 }
2279 }
2280 }
2281 out
2282}
2283
2284#[cfg(test)]
2285mod tests {
2286 use super::*;
2287
2288 fn make_resolver() -> FieldResolver {
2289 let mut fields = HashMap::new();
2290 fields.insert("title".to_string(), "metadata.document.title".to_string());
2291 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
2292 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
2293 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
2294 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
2295 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
2296 let mut optional = HashSet::new();
2297 optional.insert("metadata.document.title".to_string());
2298 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
2299 }
2300
2301 fn make_resolver_with_doc_optional() -> FieldResolver {
2302 let mut fields = HashMap::new();
2303 fields.insert("title".to_string(), "metadata.document.title".to_string());
2304 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
2305 let mut optional = HashSet::new();
2306 optional.insert("document".to_string());
2307 optional.insert("metadata.document.title".to_string());
2308 optional.insert("metadata.document".to_string());
2309 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
2310 }
2311
2312 #[test]
2313 fn test_resolve_alias() {
2314 let r = make_resolver();
2315 assert_eq!(r.resolve("title"), "metadata.document.title");
2316 }
2317
2318 #[test]
2319 fn test_resolve_passthrough() {
2320 let r = make_resolver();
2321 assert_eq!(r.resolve("content"), "content");
2322 }
2323
2324 #[test]
2325 fn test_is_optional() {
2326 let r = make_resolver();
2327 assert!(r.is_optional("metadata.document.title"));
2328 assert!(!r.is_optional("content"));
2329 }
2330
2331 #[test]
2332 fn is_optional_strips_namespace_prefix() {
2333 let fields = HashMap::new();
2334 let mut optional = HashSet::new();
2335 optional.insert("action_results.data".to_string());
2336 let result_fields: HashSet<String> = ["action_results".to_string()].into_iter().collect();
2337 let r = FieldResolver::new(&fields, &optional, &result_fields, &HashSet::new(), &HashSet::new());
2338 assert!(r.is_optional("interaction.action_results[0].data"));
2340 assert!(r.is_optional("action_results[0].data"));
2342 }
2343
2344 #[test]
2345 fn test_accessor_rust_struct() {
2346 let r = make_resolver();
2347 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
2348 }
2349
2350 #[test]
2351 fn test_accessor_rust_map() {
2352 let r = make_resolver();
2353 assert_eq!(
2354 r.accessor("tags", "rust", "result"),
2355 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
2356 );
2357 }
2358
2359 #[test]
2360 fn test_accessor_python() {
2361 let r = make_resolver();
2362 assert_eq!(
2363 r.accessor("title", "python", "result"),
2364 "result.metadata.document.title"
2365 );
2366 }
2367
2368 #[test]
2369 fn test_accessor_go() {
2370 let r = make_resolver();
2371 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
2372 }
2373
2374 #[test]
2375 fn test_accessor_go_initialism_fields() {
2376 let mut fields = std::collections::HashMap::new();
2377 fields.insert("content".to_string(), "html".to_string());
2378 fields.insert("link_url".to_string(), "links.url".to_string());
2379 let r = FieldResolver::new(
2380 &fields,
2381 &HashSet::new(),
2382 &HashSet::new(),
2383 &HashSet::new(),
2384 &HashSet::new(),
2385 );
2386 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
2387 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
2388 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
2389 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
2390 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
2391 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
2392 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
2393 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
2394 }
2395
2396 #[test]
2397 fn test_accessor_typescript() {
2398 let r = make_resolver();
2399 assert_eq!(
2400 r.accessor("title", "typescript", "result"),
2401 "result.metadata.document.title"
2402 );
2403 }
2404
2405 #[test]
2406 fn test_accessor_typescript_snake_to_camel() {
2407 let r = make_resolver();
2408 assert_eq!(
2409 r.accessor("og", "typescript", "result"),
2410 "result.metadata.document.openGraph"
2411 );
2412 assert_eq!(
2413 r.accessor("twitter", "typescript", "result"),
2414 "result.metadata.document.twitterCard"
2415 );
2416 assert_eq!(
2417 r.accessor("canonical", "typescript", "result"),
2418 "result.metadata.document.canonicalUrl"
2419 );
2420 }
2421
2422 #[test]
2423 fn test_accessor_typescript_map_snake_to_camel() {
2424 let r = make_resolver();
2425 assert_eq!(
2426 r.accessor("og_tag", "typescript", "result"),
2427 "result.metadata.openGraphTags[\"og_title\"]"
2428 );
2429 }
2430
2431 #[test]
2432 fn test_accessor_typescript_numeric_index_is_unquoted() {
2433 let mut fields = HashMap::new();
2437 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
2438 let r = FieldResolver::new(
2439 &fields,
2440 &HashSet::new(),
2441 &HashSet::new(),
2442 &HashSet::new(),
2443 &HashSet::new(),
2444 );
2445 assert_eq!(
2446 r.accessor("first_score", "typescript", "result"),
2447 "result.results[0].relevanceScore"
2448 );
2449 }
2450
2451 #[test]
2452 fn test_accessor_node_alias() {
2453 let r = make_resolver();
2454 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
2455 }
2456
2457 #[test]
2458 fn test_accessor_wasm_camel_case() {
2459 let r = make_resolver();
2460 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
2461 assert_eq!(
2462 r.accessor("twitter", "wasm", "result"),
2463 "result.metadata.document.twitterCard"
2464 );
2465 assert_eq!(
2466 r.accessor("canonical", "wasm", "result"),
2467 "result.metadata.document.canonicalUrl"
2468 );
2469 }
2470
2471 #[test]
2472 fn test_accessor_wasm_map_access() {
2473 let r = make_resolver();
2474 assert_eq!(
2475 r.accessor("og_tag", "wasm", "result"),
2476 "result.metadata.openGraphTags.get(\"og_title\")"
2477 );
2478 }
2479
2480 #[test]
2481 fn test_accessor_java() {
2482 let r = make_resolver();
2483 assert_eq!(
2484 r.accessor("title", "java", "result"),
2485 "result.metadata().document().title()"
2486 );
2487 }
2488
2489 #[test]
2490 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
2491 let mut fields = HashMap::new();
2492 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2493 fields.insert("node_count".to_string(), "nodes.length".to_string());
2494 let mut arrays = HashSet::new();
2495 arrays.insert("nodes".to_string());
2496 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
2497 assert_eq!(
2498 r.accessor("first_node_name", "kotlin", "result"),
2499 "result.nodes().first().name()"
2500 );
2501 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
2502 }
2503
2504 #[test]
2505 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
2506 let r = make_resolver_with_doc_optional();
2507 assert_eq!(
2508 r.accessor("title", "kotlin", "result"),
2509 "result.metadata().document()?.title()"
2510 );
2511 }
2512
2513 #[test]
2514 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
2515 let mut fields = HashMap::new();
2516 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
2517 fields.insert("tag".to_string(), "tags[name]".to_string());
2518 let mut optional = HashSet::new();
2519 optional.insert("nodes".to_string());
2520 optional.insert("tags".to_string());
2521 let mut arrays = HashSet::new();
2522 arrays.insert("nodes".to_string());
2523 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2524 assert_eq!(
2525 r.accessor("first_node_name", "kotlin", "result"),
2526 "result.nodes()?.first()?.name()"
2527 );
2528 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
2529 }
2530
2531 #[test]
2537 fn test_accessor_kotlin_optional_field_after_indexed_array() {
2538 let mut fields = HashMap::new();
2541 fields.insert(
2542 "tool_call_name".to_string(),
2543 "choices[0].message.tool_calls[0].function.name".to_string(),
2544 );
2545 let mut optional = HashSet::new();
2546 optional.insert("choices[0].message.tool_calls".to_string());
2547 let mut arrays = HashSet::new();
2548 arrays.insert("choices".to_string());
2549 arrays.insert("choices[0].message.tool_calls".to_string());
2550 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2551 let expr = r.accessor("tool_call_name", "kotlin", "result");
2552 assert!(
2554 expr.contains("toolCalls()?.first()"),
2555 "expected toolCalls()?.first() for optional list, got: {expr}"
2556 );
2557 }
2558
2559 #[test]
2560 fn test_accessor_csharp() {
2561 let r = make_resolver();
2562 assert_eq!(
2563 r.accessor("title", "csharp", "result"),
2564 "result.Metadata.Document.Title"
2565 );
2566 }
2567
2568 #[test]
2569 fn test_accessor_php() {
2570 let r = make_resolver();
2571 assert_eq!(
2572 r.accessor("title", "php", "$result"),
2573 "$result->metadata->document->title"
2574 );
2575 }
2576
2577 #[test]
2578 fn test_accessor_r() {
2579 let r = make_resolver();
2580 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
2581 }
2582
2583 #[test]
2584 fn test_accessor_c() {
2585 let r = make_resolver();
2586 assert_eq!(
2587 r.accessor("title", "c", "result"),
2588 "result_title(result_document(result_metadata(result)))"
2589 );
2590 }
2591
2592 #[test]
2593 fn test_rust_unwrap_binding() {
2594 let r = make_resolver();
2595 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
2596 assert_eq!(var, "metadata_document_title");
2597 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
2600 }
2601
2602 #[test]
2603 fn test_rust_unwrap_binding_non_optional() {
2604 let r = make_resolver();
2605 assert!(r.rust_unwrap_binding("content", "result").is_none());
2606 }
2607
2608 #[test]
2609 fn test_rust_unwrap_binding_collapses_double_underscore() {
2610 let mut aliases = HashMap::new();
2615 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
2616 let mut optional = HashSet::new();
2617 optional.insert("json_ld[].name".to_string());
2618 let mut array = HashSet::new();
2619 array.insert("json_ld".to_string());
2620 let result_fields = HashSet::new();
2621 let method_calls = HashSet::new();
2622 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
2623 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
2624 assert_eq!(var, "json_ld_name");
2625 }
2626
2627 #[test]
2628 fn test_direct_field_no_alias() {
2629 let r = make_resolver();
2630 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2631 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
2632 }
2633
2634 #[test]
2635 fn test_accessor_rust_with_optionals() {
2636 let r = make_resolver_with_doc_optional();
2637 assert_eq!(
2638 r.accessor("title", "rust", "result"),
2639 "result.metadata.document.as_ref().unwrap().title"
2640 );
2641 }
2642
2643 #[test]
2644 fn test_accessor_csharp_with_optionals() {
2645 let r = make_resolver_with_doc_optional();
2646 assert_eq!(
2647 r.accessor("title", "csharp", "result"),
2648 "result.Metadata.Document!.Title"
2649 );
2650 }
2651
2652 #[test]
2653 fn test_accessor_rust_non_optional_field() {
2654 let r = make_resolver();
2655 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2656 }
2657
2658 #[test]
2659 fn test_accessor_csharp_non_optional_field() {
2660 let r = make_resolver();
2661 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
2662 }
2663
2664 #[test]
2665 fn test_accessor_rust_method_call() {
2666 let mut fields = HashMap::new();
2668 fields.insert(
2669 "excel_sheet_count".to_string(),
2670 "metadata.format.excel.sheet_count".to_string(),
2671 );
2672 let mut optional = HashSet::new();
2673 optional.insert("metadata.format".to_string());
2674 optional.insert("metadata.format.excel".to_string());
2675 let mut method_calls = HashSet::new();
2676 method_calls.insert("metadata.format.excel".to_string());
2677 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
2678 assert_eq!(
2679 r.accessor("excel_sheet_count", "rust", "result"),
2680 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
2681 );
2682 }
2683
2684 fn make_php_getter_resolver() -> FieldResolver {
2689 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2690 getters.insert(
2691 "Root".to_string(),
2692 ["metadata".to_string(), "links".to_string()].into_iter().collect(),
2693 );
2694 let map = PhpGetterMap {
2695 getters,
2696 field_types: HashMap::new(),
2697 root_type: Some("Root".to_string()),
2698 all_fields: HashMap::new(),
2699 };
2700 FieldResolver::new_with_php_getters(
2701 &HashMap::new(),
2702 &HashSet::new(),
2703 &HashSet::new(),
2704 &HashSet::new(),
2705 &HashSet::new(),
2706 &HashMap::new(),
2707 map,
2708 )
2709 }
2710
2711 #[test]
2712 fn render_php_uses_getter_method_for_non_scalar_field() {
2713 let r = make_php_getter_resolver();
2714 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
2715 }
2716
2717 #[test]
2718 fn render_php_uses_property_for_scalar_field() {
2719 let r = make_php_getter_resolver();
2720 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2721 }
2722
2723 #[test]
2724 fn render_php_nested_non_scalar_uses_getter_then_property() {
2725 let mut fields = HashMap::new();
2726 fields.insert("title".to_string(), "metadata.title".to_string());
2727 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2728 getters.insert("Root".to_string(), ["metadata".to_string()].into_iter().collect());
2729 getters.insert("Metadata".to_string(), HashSet::new());
2731 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2732 field_types.insert(
2733 "Root".to_string(),
2734 [("metadata".to_string(), "Metadata".to_string())].into_iter().collect(),
2735 );
2736 let map = PhpGetterMap {
2737 getters,
2738 field_types,
2739 root_type: Some("Root".to_string()),
2740 all_fields: HashMap::new(),
2741 };
2742 let r = FieldResolver::new_with_php_getters(
2743 &fields,
2744 &HashSet::new(),
2745 &HashSet::new(),
2746 &HashSet::new(),
2747 &HashSet::new(),
2748 &HashMap::new(),
2749 map,
2750 );
2751 assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
2753 }
2754
2755 #[test]
2756 fn render_php_array_field_uses_getter_when_non_scalar() {
2757 let mut fields = HashMap::new();
2758 fields.insert("first_link".to_string(), "links[0]".to_string());
2759 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2760 getters.insert("Root".to_string(), ["links".to_string()].into_iter().collect());
2761 let map = PhpGetterMap {
2762 getters,
2763 field_types: HashMap::new(),
2764 root_type: Some("Root".to_string()),
2765 all_fields: HashMap::new(),
2766 };
2767 let r = FieldResolver::new_with_php_getters(
2768 &fields,
2769 &HashSet::new(),
2770 &HashSet::new(),
2771 &HashSet::new(),
2772 &HashSet::new(),
2773 &HashMap::new(),
2774 map,
2775 );
2776 assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
2777 }
2778
2779 #[test]
2780 fn render_php_falls_back_to_property_when_getter_fields_empty() {
2781 let r = FieldResolver::new(
2784 &HashMap::new(),
2785 &HashSet::new(),
2786 &HashSet::new(),
2787 &HashSet::new(),
2788 &HashSet::new(),
2789 );
2790 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2791 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
2792 }
2793
2794 #[test]
2798 fn render_php_with_getters_distinguishes_same_field_name_on_different_types() {
2799 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2800 getters.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2802 getters.insert("B".to_string(), HashSet::new());
2804 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2807 all_fields.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2808 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2809 let map_a = PhpGetterMap {
2810 getters: getters.clone(),
2811 field_types: HashMap::new(),
2812 root_type: Some("A".to_string()),
2813 all_fields: all_fields.clone(),
2814 };
2815 let map_b = PhpGetterMap {
2816 getters,
2817 field_types: HashMap::new(),
2818 root_type: Some("B".to_string()),
2819 all_fields,
2820 };
2821 let r_a = FieldResolver::new_with_php_getters(
2822 &HashMap::new(),
2823 &HashSet::new(),
2824 &HashSet::new(),
2825 &HashSet::new(),
2826 &HashSet::new(),
2827 &HashMap::new(),
2828 map_a,
2829 );
2830 let r_b = FieldResolver::new_with_php_getters(
2831 &HashMap::new(),
2832 &HashSet::new(),
2833 &HashSet::new(),
2834 &HashSet::new(),
2835 &HashSet::new(),
2836 &HashMap::new(),
2837 map_b,
2838 );
2839 assert_eq!(r_a.accessor("content", "php", "$a"), "$a->getContent()");
2840 assert_eq!(r_b.accessor("content", "php", "$b"), "$b->content");
2841 }
2842
2843 #[test]
2847 fn render_php_with_getters_chains_through_correct_type() {
2848 let mut fields = HashMap::new();
2849 fields.insert("nested_content".to_string(), "inner.content".to_string());
2850 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2851 getters.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2853 getters.insert("B".to_string(), HashSet::new());
2855 getters.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2858 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2859 field_types.insert(
2860 "Outer".to_string(),
2861 [("inner".to_string(), "B".to_string())].into_iter().collect(),
2862 );
2863 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2864 all_fields.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2865 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2866 all_fields.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2867 let map = PhpGetterMap {
2868 getters,
2869 field_types,
2870 root_type: Some("Outer".to_string()),
2871 all_fields,
2872 };
2873 let r = FieldResolver::new_with_php_getters(
2874 &fields,
2875 &HashSet::new(),
2876 &HashSet::new(),
2877 &HashSet::new(),
2878 &HashSet::new(),
2879 &HashMap::new(),
2880 map,
2881 );
2882 assert_eq!(
2883 r.accessor("nested_content", "php", "$result"),
2884 "$result->getInner()->content"
2885 );
2886 }
2887
2888 fn make_resolver_with_result_fields(result_fields: &[&str]) -> FieldResolver {
2893 let rf: HashSet<String> = result_fields.iter().map(|s| s.to_string()).collect();
2894 FieldResolver::new(&HashMap::new(), &HashSet::new(), &rf, &HashSet::new(), &HashSet::new())
2895 }
2896
2897 #[test]
2900 fn is_valid_for_result_accepts_virtual_namespace_prefix() {
2901 let r = make_resolver_with_result_fields(&["browser_used", "js_render_hint", "status_code"]);
2902 assert!(
2903 r.is_valid_for_result("browser.browser_used"),
2904 "browser.browser_used should be valid via namespace-prefix stripping"
2905 );
2906 assert!(
2907 r.is_valid_for_result("browser.js_render_hint"),
2908 "browser.js_render_hint should be valid via namespace-prefix stripping"
2909 );
2910 }
2911
2912 #[test]
2915 fn is_valid_for_result_accepts_namespace_prefix_before_array_field() {
2916 let r = make_resolver_with_result_fields(&["action_results", "final_html", "final_url"]);
2917 assert!(
2918 r.is_valid_for_result("interaction.action_results[0].action_type"),
2919 "interaction. prefix should be stripped so action_results is recognised"
2920 );
2921 }
2922
2923 #[test]
2925 fn is_valid_for_result_rejects_unknown_field_even_after_namespace_strip() {
2926 let r = make_resolver_with_result_fields(&["pages", "final_url"]);
2927 assert!(
2928 !r.is_valid_for_result("browser.browser_used"),
2929 "browser_used is not in result_fields so should be rejected"
2930 );
2931 assert!(
2932 !r.is_valid_for_result("ns.unknown_field"),
2933 "unknown_field is not in result_fields so should be rejected"
2934 );
2935 }
2936
2937 #[test]
2940 fn accessor_strips_namespace_prefix_for_python() {
2941 let r = make_resolver_with_result_fields(&["browser_used", "js_render_hint"]);
2942 assert_eq!(
2943 r.accessor("browser.browser_used", "python", "result"),
2944 "result.browser_used"
2945 );
2946 assert_eq!(
2947 r.accessor("browser.js_render_hint", "python", "result"),
2948 "result.js_render_hint"
2949 );
2950 }
2951
2952 #[test]
2954 fn accessor_strips_namespace_prefix_for_csharp() {
2955 let r = make_resolver_with_result_fields(&["browser_used"]);
2956 assert_eq!(
2957 r.accessor("browser.browser_used", "csharp", "result"),
2958 "result.BrowserUsed"
2959 );
2960 }
2961
2962 #[test]
2965 fn accessor_strips_namespace_prefix_for_indexed_array_field() {
2966 let r = make_resolver_with_result_fields(&["action_results", "final_html", "final_url"]);
2967 assert_eq!(
2969 r.accessor("interaction.action_results[0].action_type", "python", "result"),
2970 "result.action_results[0].action_type"
2971 );
2972 assert_eq!(
2974 r.accessor("interaction.action_results[0].action_type", "typescript", "result"),
2975 "result.actionResults[0].actionType"
2976 );
2977 }
2978
2979 #[test]
2982 fn is_valid_for_result_is_permissive_when_result_fields_empty() {
2983 let r = make_resolver_with_result_fields(&[]);
2984 assert!(r.is_valid_for_result("browser.browser_used"));
2985 assert!(r.is_valid_for_result("anything.at.all"));
2986 }
2987
2988 #[test]
2991 fn accessor_does_not_strip_real_first_segment() {
2992 let r = make_resolver_with_result_fields(&["metadata", "status_code"]);
2993 assert_eq!(
2995 r.accessor("metadata.title", "python", "result"),
2996 "result.metadata.title"
2997 );
2998 }
2999}