1use alef_codegen::naming::to_go_name;
8use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
9use std::collections::{HashMap, HashSet};
10
11pub struct FieldResolver {
13 aliases: HashMap<String, String>,
14 optional_fields: HashSet<String>,
15 result_fields: HashSet<String>,
16 array_fields: HashSet<String>,
17 method_calls: HashSet<String>,
18 error_field_aliases: HashMap<String, String>,
22 php_getter_map: PhpGetterMap,
33}
34
35#[derive(Debug, Clone, Default)]
50pub struct PhpGetterMap {
51 pub getters: HashMap<String, HashSet<String>>,
52 pub field_types: HashMap<String, HashMap<String, String>>,
53 pub root_type: Option<String>,
54}
55
56impl PhpGetterMap {
57 pub fn needs_getter(&self, owner_type: Option<&str>, field_name: &str) -> bool {
64 if let Some(t) = owner_type {
65 if let Some(fields) = self.getters.get(t) {
66 return fields.contains(field_name);
67 }
68 }
69 self.getters.values().any(|set| set.contains(field_name))
70 }
71
72 pub fn advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
75 let owner = owner_type?;
76 self.field_types.get(owner).and_then(|m| m.get(field_name).cloned())
77 }
78
79 pub fn is_empty(&self) -> bool {
82 self.getters.is_empty()
83 }
84}
85
86#[derive(Debug, Clone)]
88enum PathSegment {
89 Field(String),
91 ArrayField { name: String, index: usize },
96 MapAccess { field: String, key: String },
98 Length,
100}
101
102impl FieldResolver {
103 pub fn new(
107 fields: &HashMap<String, String>,
108 optional: &HashSet<String>,
109 result_fields: &HashSet<String>,
110 array_fields: &HashSet<String>,
111 method_calls: &HashSet<String>,
112 ) -> Self {
113 Self {
114 aliases: fields.clone(),
115 optional_fields: optional.clone(),
116 result_fields: result_fields.clone(),
117 array_fields: array_fields.clone(),
118 method_calls: method_calls.clone(),
119 error_field_aliases: HashMap::new(),
120 php_getter_map: PhpGetterMap::default(),
121 }
122 }
123
124 pub fn new_with_error_aliases(
130 fields: &HashMap<String, String>,
131 optional: &HashSet<String>,
132 result_fields: &HashSet<String>,
133 array_fields: &HashSet<String>,
134 method_calls: &HashSet<String>,
135 error_field_aliases: &HashMap<String, String>,
136 ) -> Self {
137 Self {
138 aliases: fields.clone(),
139 optional_fields: optional.clone(),
140 result_fields: result_fields.clone(),
141 array_fields: array_fields.clone(),
142 method_calls: method_calls.clone(),
143 error_field_aliases: error_field_aliases.clone(),
144 php_getter_map: PhpGetterMap::default(),
145 }
146 }
147
148 pub fn new_with_php_getters(
163 fields: &HashMap<String, String>,
164 optional: &HashSet<String>,
165 result_fields: &HashSet<String>,
166 array_fields: &HashSet<String>,
167 method_calls: &HashSet<String>,
168 error_field_aliases: &HashMap<String, String>,
169 php_getter_map: PhpGetterMap,
170 ) -> Self {
171 Self {
172 aliases: fields.clone(),
173 optional_fields: optional.clone(),
174 result_fields: result_fields.clone(),
175 array_fields: array_fields.clone(),
176 method_calls: method_calls.clone(),
177 error_field_aliases: error_field_aliases.clone(),
178 php_getter_map,
179 }
180 }
181
182 pub fn resolve<'a>(&'a self, fixture_field: &'a str) -> &'a str {
185 self.aliases
186 .get(fixture_field)
187 .map(String::as_str)
188 .unwrap_or(fixture_field)
189 }
190
191 pub fn is_optional(&self, field: &str) -> bool {
193 if self.optional_fields.contains(field) {
194 return true;
195 }
196 let index_normalized = normalize_numeric_indices(field);
197 if index_normalized != field && self.optional_fields.contains(index_normalized.as_str()) {
198 return true;
199 }
200 let de_indexed = strip_numeric_indices(field);
203 if de_indexed != field && self.optional_fields.contains(de_indexed.as_str()) {
204 return true;
205 }
206 let normalized = field.replace("[].", ".");
207 if normalized != field && self.optional_fields.contains(normalized.as_str()) {
208 return true;
209 }
210 for af in &self.array_fields {
211 if let Some(rest) = field.strip_prefix(af.as_str()) {
212 if let Some(rest) = rest.strip_prefix('.') {
213 let with_bracket = format!("{af}[].{rest}");
214 if self.optional_fields.contains(with_bracket.as_str()) {
215 return true;
216 }
217 }
218 }
219 }
220 false
221 }
222
223 pub fn has_alias(&self, fixture_field: &str) -> bool {
225 self.aliases.contains_key(fixture_field)
226 }
227
228 pub fn has_explicit_field(&self, field_name: &str) -> bool {
234 if self.result_fields.is_empty() {
235 return false;
236 }
237 self.result_fields.contains(field_name)
238 }
239
240 pub fn is_valid_for_result(&self, fixture_field: &str) -> bool {
242 if self.result_fields.is_empty() {
243 return true;
244 }
245 let resolved = self.resolve(fixture_field);
246 let first_segment = resolved.split('.').next().unwrap_or(resolved);
247 let first_segment = first_segment.split('[').next().unwrap_or(first_segment);
248 self.result_fields.contains(first_segment)
249 }
250
251 pub fn is_array(&self, field: &str) -> bool {
253 self.array_fields.contains(field)
254 }
255
256 pub fn is_collection_root(&self, field: &str) -> bool {
269 let prefix = format!("{field}[");
270 self.array_fields.iter().any(|af| af.starts_with(&prefix))
271 || self.optional_fields.iter().any(|of| of.starts_with(&prefix))
272 }
273
274 pub fn tagged_union_split(&self, fixture_field: &str) -> Option<(String, String, String)> {
286 let resolved = self.resolve(fixture_field);
287 let segments: Vec<&str> = resolved.split('.').collect();
288 let mut path_so_far = String::new();
289 for (i, seg) in segments.iter().enumerate() {
290 if !path_so_far.is_empty() {
291 path_so_far.push('.');
292 }
293 path_so_far.push_str(seg);
294 if self.method_calls.contains(&path_so_far) {
295 let prefix = segments[..i].join(".");
297 let variant = (*seg).to_string();
298 let suffix = segments[i + 1..].join(".");
299 return Some((prefix, variant, suffix));
300 }
301 }
302 None
303 }
304
305 pub fn has_map_access(&self, fixture_field: &str) -> bool {
307 let resolved = self.resolve(fixture_field);
308 let segments = parse_path(resolved);
309 segments.iter().any(|s| {
310 if let PathSegment::MapAccess { key, .. } = s {
311 !key.chars().all(|c| c.is_ascii_digit())
312 } else {
313 false
314 }
315 })
316 }
317
318 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
320 let resolved = self.resolve(fixture_field);
321 let segments = parse_path(resolved);
322 let segments = self.inject_array_indexing(segments);
323 match language {
324 "java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
325 "kotlin" => render_kotlin_with_optionals(&segments, result_var, &self.optional_fields),
326 "kotlin_android" => render_kotlin_android_with_optionals(&segments, result_var, &self.optional_fields),
329 "rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
330 "csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
331 "zig" => render_zig_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
332 "swift" => render_swift_with_optionals(&segments, result_var, &self.optional_fields),
333 "dart" => render_dart_with_optionals(&segments, result_var, &self.optional_fields),
334 "php" if !self.php_getter_map.is_empty() => {
335 render_php_with_getters(&segments, result_var, &self.php_getter_map)
336 }
337 _ => render_accessor(&segments, language, result_var),
338 }
339 }
340
341 pub fn accessor_for_error(&self, sub_field: &str, language: &str, err_var: &str) -> String {
355 let resolved = self
356 .error_field_aliases
357 .get(sub_field)
358 .map(String::as_str)
359 .unwrap_or(sub_field);
360 let segments = parse_path(resolved);
361 match language {
364 "rust" => render_rust_with_optionals(&segments, err_var, &self.optional_fields, &self.method_calls),
365 _ => render_accessor(&segments, language, err_var),
366 }
367 }
368
369 pub fn has_error_aliases(&self) -> bool {
376 !self.error_field_aliases.is_empty()
377 }
378
379 fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
380 if self.array_fields.is_empty() {
381 return segments;
382 }
383 let len = segments.len();
384 let mut result = Vec::with_capacity(len);
385 let mut path_so_far = String::new();
386 for i in 0..len {
387 let seg = &segments[i];
388 match seg {
389 PathSegment::Field(f) => {
390 if !path_so_far.is_empty() {
391 path_so_far.push('.');
392 }
393 path_so_far.push_str(f);
394 let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
395 if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
396 result.push(PathSegment::ArrayField {
398 name: f.clone(),
399 index: 0,
400 });
401 } else {
402 result.push(seg.clone());
403 }
404 }
405 PathSegment::ArrayField { .. } => {
408 result.push(seg.clone());
409 }
410 PathSegment::MapAccess { field, key } => {
411 if !path_so_far.is_empty() {
412 path_so_far.push('.');
413 }
414 path_so_far.push_str(field);
415 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
416 if is_numeric && self.array_fields.contains(&path_so_far) {
417 let index: usize = key.parse().unwrap_or(0);
419 result.push(PathSegment::ArrayField {
420 name: field.clone(),
421 index,
422 });
423 } else {
424 result.push(seg.clone());
425 }
426 }
427 _ => {
428 result.push(seg.clone());
429 }
430 }
431 }
432 result
433 }
434
435 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
437 let resolved = self.resolve(fixture_field);
438 if !self.is_optional(resolved) {
439 return None;
440 }
441 let segments = parse_path(resolved);
442 let segments = self.inject_array_indexing(segments);
443 let local_var = {
448 let raw = resolved.replace(['.', '['], "_").replace(']', "");
449 let mut collapsed = String::with_capacity(raw.len());
450 let mut prev_underscore = false;
451 for ch in raw.chars() {
452 if ch == '_' {
453 if !prev_underscore {
454 collapsed.push('_');
455 }
456 prev_underscore = true;
457 } else {
458 collapsed.push(ch);
459 prev_underscore = false;
460 }
461 }
462 collapsed.trim_matches('_').to_string()
463 };
464 let accessor = render_accessor(&segments, "rust", result_var);
465 let has_map_access = segments.iter().any(|s| {
466 if let PathSegment::MapAccess { key, .. } = s {
467 !key.chars().all(|c| c.is_ascii_digit())
468 } else {
469 false
470 }
471 });
472 let is_array = self.is_array(resolved);
473 let binding = if has_map_access {
474 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
475 } else if is_array {
476 format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
477 } else {
478 format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
479 };
480 Some((binding, local_var))
481 }
482}
483
484fn strip_numeric_indices(path: &str) -> String {
489 let mut result = String::with_capacity(path.len());
490 let mut chars = path.chars().peekable();
491 while let Some(c) = chars.next() {
492 if c == '[' {
493 let mut key = String::new();
494 let mut closed = false;
495 for inner in chars.by_ref() {
496 if inner == ']' {
497 closed = true;
498 break;
499 }
500 key.push(inner);
501 }
502 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
503 } else {
505 result.push('[');
506 result.push_str(&key);
507 if closed {
508 result.push(']');
509 }
510 }
511 } else {
512 result.push(c);
513 }
514 }
515 while result.contains("..") {
517 result = result.replace("..", ".");
518 }
519 if result.starts_with('.') {
520 result.remove(0);
521 }
522 result
523}
524
525fn normalize_numeric_indices(path: &str) -> String {
526 let mut result = String::with_capacity(path.len());
527 let mut chars = path.chars().peekable();
528 while let Some(c) = chars.next() {
529 if c == '[' {
530 let mut key = String::new();
531 let mut closed = false;
532 for inner in chars.by_ref() {
533 if inner == ']' {
534 closed = true;
535 break;
536 }
537 key.push(inner);
538 }
539 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
540 result.push_str("[0]");
541 } else {
542 result.push('[');
543 result.push_str(&key);
544 if closed {
545 result.push(']');
546 }
547 }
548 } else {
549 result.push(c);
550 }
551 }
552 result
553}
554
555fn parse_path(path: &str) -> Vec<PathSegment> {
556 let mut segments = Vec::new();
557 for part in path.split('.') {
558 if part == "length" || part == "count" || part == "size" {
559 segments.push(PathSegment::Length);
560 } else if let Some(bracket_pos) = part.find('[') {
561 let name = part[..bracket_pos].to_string();
562 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
563 if key.is_empty() {
564 segments.push(PathSegment::ArrayField { name, index: 0 });
566 } else if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
567 let index: usize = key.parse().unwrap_or(0);
569 segments.push(PathSegment::ArrayField { name, index });
570 } else {
571 segments.push(PathSegment::MapAccess { field: name, key });
573 }
574 } else {
575 segments.push(PathSegment::Field(part.to_string()));
576 }
577 }
578 segments
579}
580
581fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
582 match language {
583 "rust" => render_rust(segments, result_var),
584 "python" => render_dot_access(segments, result_var, "python"),
585 "typescript" | "node" => render_typescript(segments, result_var),
586 "wasm" => render_wasm(segments, result_var),
587 "go" => render_go(segments, result_var),
588 "java" => render_java(segments, result_var),
589 "kotlin" => render_kotlin(segments, result_var),
590 "kotlin_android" => render_kotlin_android(segments, result_var),
591 "csharp" => render_pascal_dot(segments, result_var),
592 "ruby" => render_dot_access(segments, result_var, "ruby"),
593 "php" => render_php(segments, result_var),
594 "elixir" => render_dot_access(segments, result_var, "elixir"),
595 "r" => render_r(segments, result_var),
596 "c" => render_c(segments, result_var),
597 "swift" => render_swift(segments, result_var),
598 "dart" => render_dart(segments, result_var),
599 _ => render_dot_access(segments, result_var, language),
600 }
601}
602
603fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
609 let mut out = result_var.to_string();
610 for seg in segments {
611 match seg {
612 PathSegment::Field(f) => {
613 out.push('.');
614 out.push_str(f);
615 out.push_str("()");
616 }
617 PathSegment::ArrayField { name, index } => {
618 out.push('.');
619 out.push_str(name);
620 out.push_str(&format!("()[{index}]"));
621 }
622 PathSegment::MapAccess { field, key } => {
623 out.push('.');
624 out.push_str(field);
625 if key.chars().all(|c| c.is_ascii_digit()) {
626 out.push_str(&format!("()[{key}]"));
627 } else {
628 out.push_str(&format!("()[\"{key}\"]"));
629 }
630 }
631 PathSegment::Length => {
632 out.push_str(".count");
633 }
634 }
635 }
636 out
637}
638
639fn render_swift_with_optionals(
649 segments: &[PathSegment],
650 result_var: &str,
651 optional_fields: &HashSet<String>,
652) -> String {
653 let mut out = result_var.to_string();
654 let mut path_so_far = String::new();
655 let total = segments.len();
656 for (i, seg) in segments.iter().enumerate() {
657 let is_leaf = i == total - 1;
658 match seg {
659 PathSegment::Field(f) => {
660 if !path_so_far.is_empty() {
661 path_so_far.push('.');
662 }
663 path_so_far.push_str(f);
664 out.push('.');
665 out.push_str(f);
666 out.push_str("()");
667 if !is_leaf && optional_fields.contains(&path_so_far) {
670 out.push('?');
671 }
672 }
673 PathSegment::ArrayField { name, index } => {
674 if !path_so_far.is_empty() {
675 path_so_far.push('.');
676 }
677 path_so_far.push_str(name);
678 let is_optional = optional_fields.contains(&path_so_far);
682 out.push('.');
683 out.push_str(name);
684 if is_optional {
685 out.push_str(&format!("()?[{index}]"));
688 } else {
689 out.push_str(&format!("()[{index}]"));
690 }
691 path_so_far.push_str("[0]");
695 let _ = is_leaf;
702 }
703 PathSegment::MapAccess { field, key } => {
704 if !path_so_far.is_empty() {
705 path_so_far.push('.');
706 }
707 path_so_far.push_str(field);
708 out.push('.');
709 out.push_str(field);
710 if key.chars().all(|c| c.is_ascii_digit()) {
711 out.push_str(&format!("()[{key}]"));
712 } else {
713 out.push_str(&format!("()[\"{key}\"]"));
714 }
715 }
716 PathSegment::Length => {
717 out.push_str(".count");
718 }
719 }
720 }
721 out
722}
723
724fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
725 let mut out = result_var.to_string();
726 for seg in segments {
727 match seg {
728 PathSegment::Field(f) => {
729 out.push('.');
730 out.push_str(&f.to_snake_case());
731 }
732 PathSegment::ArrayField { name, index } => {
733 out.push('.');
734 out.push_str(&name.to_snake_case());
735 out.push_str(&format!("[{index}]"));
736 }
737 PathSegment::MapAccess { field, key } => {
738 out.push('.');
739 out.push_str(&field.to_snake_case());
740 if key.chars().all(|c| c.is_ascii_digit()) {
741 out.push_str(&format!("[{key}]"));
742 } else {
743 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
744 }
745 }
746 PathSegment::Length => {
747 out.push_str(".len()");
748 }
749 }
750 }
751 out
752}
753
754fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
755 let mut out = result_var.to_string();
756 for seg in segments {
757 match seg {
758 PathSegment::Field(f) => {
759 out.push('.');
760 out.push_str(f);
761 }
762 PathSegment::ArrayField { name, index } => {
763 if language == "elixir" {
764 let current = std::mem::take(&mut out);
765 out = format!("Enum.at({current}.{name}, {index})");
766 } else {
767 out.push('.');
768 out.push_str(name);
769 out.push_str(&format!("[{index}]"));
770 }
771 }
772 PathSegment::MapAccess { field, key } => {
773 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
774 if is_numeric && language == "elixir" {
775 let current = std::mem::take(&mut out);
776 out = format!("Enum.at({current}.{field}, {key})");
777 } else {
778 out.push('.');
779 out.push_str(field);
780 if is_numeric {
781 let idx: usize = key.parse().unwrap_or(0);
782 out.push_str(&format!("[{idx}]"));
783 } else if language == "elixir" || language == "ruby" {
784 out.push_str(&format!("[\"{key}\"]"));
787 } else {
788 out.push_str(&format!(".get(\"{key}\")"));
789 }
790 }
791 }
792 PathSegment::Length => match language {
793 "ruby" => out.push_str(".length"),
794 "elixir" => {
795 let current = std::mem::take(&mut out);
796 out = format!("length({current})");
797 }
798 "gleam" => {
799 let current = std::mem::take(&mut out);
800 out = format!("list.length({current})");
801 }
802 _ => {
803 let current = std::mem::take(&mut out);
804 out = format!("len({current})");
805 }
806 },
807 }
808 }
809 out
810}
811
812fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
813 let mut out = result_var.to_string();
814 for seg in segments {
815 match seg {
816 PathSegment::Field(f) => {
817 out.push('.');
818 out.push_str(&f.to_lower_camel_case());
819 }
820 PathSegment::ArrayField { name, index } => {
821 out.push('.');
822 out.push_str(&name.to_lower_camel_case());
823 out.push_str(&format!("[{index}]"));
824 }
825 PathSegment::MapAccess { field, key } => {
826 out.push('.');
827 out.push_str(&field.to_lower_camel_case());
828 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
831 out.push_str(&format!("[{key}]"));
832 } else {
833 out.push_str(&format!("[\"{key}\"]"));
834 }
835 }
836 PathSegment::Length => {
837 out.push_str(".length");
838 }
839 }
840 }
841 out
842}
843
844fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
845 let mut out = result_var.to_string();
846 for seg in segments {
847 match seg {
848 PathSegment::Field(f) => {
849 out.push('.');
850 out.push_str(&f.to_lower_camel_case());
851 }
852 PathSegment::ArrayField { name, index } => {
853 out.push('.');
854 out.push_str(&name.to_lower_camel_case());
855 out.push_str(&format!("[{index}]"));
856 }
857 PathSegment::MapAccess { field, key } => {
858 out.push('.');
859 out.push_str(&field.to_lower_camel_case());
860 out.push_str(&format!(".get(\"{key}\")"));
861 }
862 PathSegment::Length => {
863 out.push_str(".length");
864 }
865 }
866 }
867 out
868}
869
870fn render_go(segments: &[PathSegment], result_var: &str) -> String {
871 let mut out = result_var.to_string();
872 for seg in segments {
873 match seg {
874 PathSegment::Field(f) => {
875 out.push('.');
876 out.push_str(&to_go_name(f));
877 }
878 PathSegment::ArrayField { name, index } => {
879 out.push('.');
880 out.push_str(&to_go_name(name));
881 out.push_str(&format!("[{index}]"));
882 }
883 PathSegment::MapAccess { field, key } => {
884 out.push('.');
885 out.push_str(&to_go_name(field));
886 if key.chars().all(|c| c.is_ascii_digit()) {
887 out.push_str(&format!("[{key}]"));
888 } else {
889 out.push_str(&format!("[\"{key}\"]"));
890 }
891 }
892 PathSegment::Length => {
893 let current = std::mem::take(&mut out);
894 out = format!("len({current})");
895 }
896 }
897 }
898 out
899}
900
901fn render_java(segments: &[PathSegment], result_var: &str) -> String {
902 let mut out = result_var.to_string();
903 for seg in segments {
904 match seg {
905 PathSegment::Field(f) => {
906 out.push('.');
907 out.push_str(&f.to_lower_camel_case());
908 out.push_str("()");
909 }
910 PathSegment::ArrayField { name, index } => {
911 out.push('.');
912 out.push_str(&name.to_lower_camel_case());
913 out.push_str(&format!("().get({index})"));
914 }
915 PathSegment::MapAccess { field, key } => {
916 out.push('.');
917 out.push_str(&field.to_lower_camel_case());
918 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
920 if is_numeric {
921 out.push_str(&format!("().get({key})"));
922 } else {
923 out.push_str(&format!("().get(\"{key}\")"));
924 }
925 }
926 PathSegment::Length => {
927 out.push_str(".size()");
928 }
929 }
930 }
931 out
932}
933
934fn kotlin_getter(name: &str) -> String {
939 let camel = name.to_lower_camel_case();
940 match camel.as_str() {
941 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
942 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
943 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
944 _ => camel,
945 }
946}
947
948fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
949 let mut out = result_var.to_string();
950 for seg in segments {
951 match seg {
952 PathSegment::Field(f) => {
953 out.push('.');
954 out.push_str(&kotlin_getter(f));
955 out.push_str("()");
956 }
957 PathSegment::ArrayField { name, index } => {
958 out.push('.');
959 out.push_str(&kotlin_getter(name));
960 if *index == 0 {
961 out.push_str("().first()");
962 } else {
963 out.push_str(&format!("().get({index})"));
964 }
965 }
966 PathSegment::MapAccess { field, key } => {
967 out.push('.');
968 out.push_str(&kotlin_getter(field));
969 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
970 if is_numeric {
971 out.push_str(&format!("().get({key})"));
972 } else {
973 out.push_str(&format!("().get(\"{key}\")"));
974 }
975 }
976 PathSegment::Length => {
977 out.push_str(".size");
978 }
979 }
980 }
981 out
982}
983
984fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
985 let mut out = result_var.to_string();
986 let mut path_so_far = String::new();
987 for (i, seg) in segments.iter().enumerate() {
988 let is_leaf = i == segments.len() - 1;
989 match seg {
990 PathSegment::Field(f) => {
991 if !path_so_far.is_empty() {
992 path_so_far.push('.');
993 }
994 path_so_far.push_str(f);
995 out.push('.');
996 out.push_str(&f.to_lower_camel_case());
997 out.push_str("()");
998 let _ = is_leaf;
999 let _ = optional_fields;
1000 }
1001 PathSegment::ArrayField { name, index } => {
1002 if !path_so_far.is_empty() {
1003 path_so_far.push('.');
1004 }
1005 path_so_far.push_str(name);
1006 out.push('.');
1007 out.push_str(&name.to_lower_camel_case());
1008 out.push_str(&format!("().get({index})"));
1009 }
1010 PathSegment::MapAccess { field, key } => {
1011 if !path_so_far.is_empty() {
1012 path_so_far.push('.');
1013 }
1014 path_so_far.push_str(field);
1015 out.push('.');
1016 out.push_str(&field.to_lower_camel_case());
1017 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1019 if is_numeric {
1020 out.push_str(&format!("().get({key})"));
1021 } else {
1022 out.push_str(&format!("().get(\"{key}\")"));
1023 }
1024 }
1025 PathSegment::Length => {
1026 out.push_str(".size()");
1027 }
1028 }
1029 }
1030 out
1031}
1032
1033fn render_kotlin_with_optionals(
1048 segments: &[PathSegment],
1049 result_var: &str,
1050 optional_fields: &HashSet<String>,
1051) -> String {
1052 let mut out = result_var.to_string();
1053 let mut path_so_far = String::new();
1054 let mut prev_was_nullable = false;
1062 for seg in segments {
1063 let nav = if prev_was_nullable { "?." } else { "." };
1064 match seg {
1065 PathSegment::Field(f) => {
1066 if !path_so_far.is_empty() {
1067 path_so_far.push('.');
1068 }
1069 path_so_far.push_str(f);
1070 let is_optional = optional_fields.contains(&path_so_far);
1075 out.push_str(nav);
1076 out.push_str(&kotlin_getter(f));
1077 out.push_str("()");
1078 prev_was_nullable = prev_was_nullable || is_optional;
1079 }
1080 PathSegment::ArrayField { name, index } => {
1081 if !path_so_far.is_empty() {
1082 path_so_far.push('.');
1083 }
1084 path_so_far.push_str(name);
1085 let is_optional = optional_fields.contains(&path_so_far);
1086 out.push_str(nav);
1087 out.push_str(&kotlin_getter(name));
1088 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1089 if *index == 0 {
1090 out.push_str(&format!("(){safe}.first()"));
1091 } else {
1092 out.push_str(&format!("(){safe}.get({index})"));
1093 }
1094 path_so_far.push_str("[0]");
1098 prev_was_nullable = prev_was_nullable || is_optional;
1099 }
1100 PathSegment::MapAccess { field, key } => {
1101 if !path_so_far.is_empty() {
1102 path_so_far.push('.');
1103 }
1104 path_so_far.push_str(field);
1105 let is_optional = optional_fields.contains(&path_so_far);
1106 out.push_str(nav);
1107 out.push_str(&kotlin_getter(field));
1108 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1109 if is_numeric {
1110 if prev_was_nullable || is_optional {
1111 out.push_str(&format!("()?.get({key})"));
1112 } else {
1113 out.push_str(&format!("().get({key})"));
1114 }
1115 } else if prev_was_nullable || is_optional {
1116 out.push_str(&format!("()?.get(\"{key}\")"));
1117 } else {
1118 out.push_str(&format!("().get(\"{key}\")"));
1119 }
1120 prev_was_nullable = prev_was_nullable || is_optional;
1121 }
1122 PathSegment::Length => {
1123 let size_nav = if prev_was_nullable { "?" } else { "" };
1126 out.push_str(&format!("{size_nav}.size"));
1127 prev_was_nullable = false;
1128 }
1129 }
1130 }
1131 out
1132}
1133
1134fn render_kotlin_android_with_optionals(
1145 segments: &[PathSegment],
1146 result_var: &str,
1147 optional_fields: &HashSet<String>,
1148) -> String {
1149 let mut out = result_var.to_string();
1150 let mut path_so_far = String::new();
1151 let mut prev_was_nullable = false;
1152 for seg in segments {
1153 let nav = if prev_was_nullable { "?." } else { "." };
1154 match seg {
1155 PathSegment::Field(f) => {
1156 if !path_so_far.is_empty() {
1157 path_so_far.push('.');
1158 }
1159 path_so_far.push_str(f);
1160 let is_optional = optional_fields.contains(&path_so_far);
1161 out.push_str(nav);
1162 out.push_str(&kotlin_getter(f));
1164 prev_was_nullable = prev_was_nullable || is_optional;
1165 }
1166 PathSegment::ArrayField { name, index } => {
1167 if !path_so_far.is_empty() {
1168 path_so_far.push('.');
1169 }
1170 path_so_far.push_str(name);
1171 let is_optional = optional_fields.contains(&path_so_far);
1172 out.push_str(nav);
1173 out.push_str(&kotlin_getter(name));
1175 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1176 if *index == 0 {
1177 out.push_str(&format!("{safe}.first()"));
1178 } else {
1179 out.push_str(&format!("{safe}.get({index})"));
1180 }
1181 path_so_far.push_str("[0]");
1182 prev_was_nullable = prev_was_nullable || is_optional;
1183 }
1184 PathSegment::MapAccess { field, key } => {
1185 if !path_so_far.is_empty() {
1186 path_so_far.push('.');
1187 }
1188 path_so_far.push_str(field);
1189 let is_optional = optional_fields.contains(&path_so_far);
1190 out.push_str(nav);
1191 out.push_str(&kotlin_getter(field));
1193 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1194 if is_numeric {
1195 if prev_was_nullable || is_optional {
1196 out.push_str(&format!("?.get({key})"));
1197 } else {
1198 out.push_str(&format!(".get({key})"));
1199 }
1200 } else if prev_was_nullable || is_optional {
1201 out.push_str(&format!("?.get(\"{key}\")"));
1202 } else {
1203 out.push_str(&format!(".get(\"{key}\")"));
1204 }
1205 prev_was_nullable = prev_was_nullable || is_optional;
1206 }
1207 PathSegment::Length => {
1208 let size_nav = if prev_was_nullable { "?" } else { "" };
1209 out.push_str(&format!("{size_nav}.size"));
1210 prev_was_nullable = false;
1211 }
1212 }
1213 }
1214 out
1215}
1216
1217fn render_kotlin_android(segments: &[PathSegment], result_var: &str) -> String {
1221 let mut out = result_var.to_string();
1222 for seg in segments {
1223 match seg {
1224 PathSegment::Field(f) => {
1225 out.push('.');
1226 out.push_str(&kotlin_getter(f));
1227 }
1229 PathSegment::ArrayField { name, index } => {
1230 out.push('.');
1231 out.push_str(&kotlin_getter(name));
1232 if *index == 0 {
1233 out.push_str(".first()");
1234 } else {
1235 out.push_str(&format!(".get({index})"));
1236 }
1237 }
1238 PathSegment::MapAccess { field, key } => {
1239 out.push('.');
1240 out.push_str(&kotlin_getter(field));
1241 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1242 if is_numeric {
1243 out.push_str(&format!(".get({key})"));
1244 } else {
1245 out.push_str(&format!(".get(\"{key}\")"));
1246 }
1247 }
1248 PathSegment::Length => {
1249 out.push_str(".size");
1250 }
1251 }
1252 }
1253 out
1254}
1255
1256fn render_rust_with_optionals(
1262 segments: &[PathSegment],
1263 result_var: &str,
1264 optional_fields: &HashSet<String>,
1265 method_calls: &HashSet<String>,
1266) -> String {
1267 let mut out = result_var.to_string();
1268 let mut path_so_far = String::new();
1269 for (i, seg) in segments.iter().enumerate() {
1270 let is_leaf = i == segments.len() - 1;
1271 match seg {
1272 PathSegment::Field(f) => {
1273 if !path_so_far.is_empty() {
1274 path_so_far.push('.');
1275 }
1276 path_so_far.push_str(f);
1277 out.push('.');
1278 out.push_str(&f.to_snake_case());
1279 let is_method = method_calls.contains(&path_so_far);
1280 if is_method {
1281 out.push_str("()");
1282 if !is_leaf && optional_fields.contains(&path_so_far) {
1283 out.push_str(".as_ref().unwrap()");
1284 }
1285 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1286 out.push_str(".as_ref().unwrap()");
1287 }
1288 }
1289 PathSegment::ArrayField { name, index } => {
1290 if !path_so_far.is_empty() {
1291 path_so_far.push('.');
1292 }
1293 path_so_far.push_str(name);
1294 out.push('.');
1295 out.push_str(&name.to_snake_case());
1296 let path_with_idx = format!("{path_so_far}[0]");
1300 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1301 if is_opt {
1302 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1303 } else {
1304 out.push_str(&format!("[{index}]"));
1305 }
1306 path_so_far.push_str("[0]");
1311 }
1312 PathSegment::MapAccess { field, key } => {
1313 if !path_so_far.is_empty() {
1314 path_so_far.push('.');
1315 }
1316 path_so_far.push_str(field);
1317 out.push('.');
1318 out.push_str(&field.to_snake_case());
1319 if key.chars().all(|c| c.is_ascii_digit()) {
1320 let path_with_idx = format!("{path_so_far}[0]");
1322 let is_opt =
1323 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1324 if is_opt {
1325 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1326 } else {
1327 out.push_str(&format!("[{key}]"));
1328 }
1329 path_so_far.push_str("[0]");
1330 } else {
1331 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1332 }
1333 }
1334 PathSegment::Length => {
1335 out.push_str(".len()");
1336 }
1337 }
1338 }
1339 out
1340}
1341
1342fn render_zig_with_optionals(
1355 segments: &[PathSegment],
1356 result_var: &str,
1357 optional_fields: &HashSet<String>,
1358 method_calls: &HashSet<String>,
1359) -> String {
1360 let mut out = result_var.to_string();
1361 let mut path_so_far = String::new();
1362 for seg in segments {
1363 match seg {
1364 PathSegment::Field(f) => {
1365 if !path_so_far.is_empty() {
1366 path_so_far.push('.');
1367 }
1368 path_so_far.push_str(f);
1369 out.push('.');
1370 out.push_str(f);
1371 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1372 out.push_str(".?");
1373 }
1374 }
1375 PathSegment::ArrayField { name, index } => {
1376 if !path_so_far.is_empty() {
1377 path_so_far.push('.');
1378 }
1379 path_so_far.push_str(name);
1380 out.push('.');
1381 out.push_str(name);
1382 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1383 out.push_str(".?");
1384 }
1385 out.push_str(&format!("[{index}]"));
1386 }
1387 PathSegment::MapAccess { field, key } => {
1388 if !path_so_far.is_empty() {
1389 path_so_far.push('.');
1390 }
1391 path_so_far.push_str(field);
1392 out.push('.');
1393 out.push_str(field);
1394 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1395 out.push_str(".?");
1396 }
1397 if key.chars().all(|c| c.is_ascii_digit()) {
1398 out.push_str(&format!("[{key}]"));
1399 } else {
1400 out.push_str(&format!(".get(\"{key}\")"));
1401 }
1402 }
1403 PathSegment::Length => {
1404 out.push_str(".len");
1405 }
1406 }
1407 }
1408 out
1409}
1410
1411fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1412 let mut out = result_var.to_string();
1413 for seg in segments {
1414 match seg {
1415 PathSegment::Field(f) => {
1416 out.push('.');
1417 out.push_str(&f.to_pascal_case());
1418 }
1419 PathSegment::ArrayField { name, index } => {
1420 out.push('.');
1421 out.push_str(&name.to_pascal_case());
1422 out.push_str(&format!("[{index}]"));
1423 }
1424 PathSegment::MapAccess { field, key } => {
1425 out.push('.');
1426 out.push_str(&field.to_pascal_case());
1427 if key.chars().all(|c| c.is_ascii_digit()) {
1428 out.push_str(&format!("[{key}]"));
1429 } else {
1430 out.push_str(&format!("[\"{key}\"]"));
1431 }
1432 }
1433 PathSegment::Length => {
1434 out.push_str(".Count");
1435 }
1436 }
1437 }
1438 out
1439}
1440
1441fn render_csharp_with_optionals(
1442 segments: &[PathSegment],
1443 result_var: &str,
1444 optional_fields: &HashSet<String>,
1445) -> String {
1446 let mut out = result_var.to_string();
1447 let mut path_so_far = String::new();
1448 for (i, seg) in segments.iter().enumerate() {
1449 let is_leaf = i == segments.len() - 1;
1450 match seg {
1451 PathSegment::Field(f) => {
1452 if !path_so_far.is_empty() {
1453 path_so_far.push('.');
1454 }
1455 path_so_far.push_str(f);
1456 out.push('.');
1457 out.push_str(&f.to_pascal_case());
1458 if !is_leaf && optional_fields.contains(&path_so_far) {
1459 out.push('!');
1460 }
1461 }
1462 PathSegment::ArrayField { name, index } => {
1463 if !path_so_far.is_empty() {
1464 path_so_far.push('.');
1465 }
1466 path_so_far.push_str(name);
1467 out.push('.');
1468 out.push_str(&name.to_pascal_case());
1469 out.push_str(&format!("[{index}]"));
1470 }
1471 PathSegment::MapAccess { field, key } => {
1472 if !path_so_far.is_empty() {
1473 path_so_far.push('.');
1474 }
1475 path_so_far.push_str(field);
1476 out.push('.');
1477 out.push_str(&field.to_pascal_case());
1478 if key.chars().all(|c| c.is_ascii_digit()) {
1479 out.push_str(&format!("[{key}]"));
1480 } else {
1481 out.push_str(&format!("[\"{key}\"]"));
1482 }
1483 }
1484 PathSegment::Length => {
1485 out.push_str(".Count");
1486 }
1487 }
1488 }
1489 out
1490}
1491
1492fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1493 let mut out = result_var.to_string();
1494 for seg in segments {
1495 match seg {
1496 PathSegment::Field(f) => {
1497 out.push_str("->");
1498 out.push_str(&f.to_lower_camel_case());
1501 }
1502 PathSegment::ArrayField { name, index } => {
1503 out.push_str("->");
1504 out.push_str(&name.to_lower_camel_case());
1505 out.push_str(&format!("[{index}]"));
1506 }
1507 PathSegment::MapAccess { field, key } => {
1508 out.push_str("->");
1509 out.push_str(&field.to_lower_camel_case());
1510 out.push_str(&format!("[\"{key}\"]"));
1511 }
1512 PathSegment::Length => {
1513 let current = std::mem::take(&mut out);
1514 out = format!("count({current})");
1515 }
1516 }
1517 }
1518 out
1519}
1520
1521fn render_php_with_getters(segments: &[PathSegment], result_var: &str, getter_map: &PhpGetterMap) -> String {
1539 let mut out = result_var.to_string();
1540 let mut current_type: Option<String> = getter_map.root_type.clone();
1541 for seg in segments {
1542 match seg {
1543 PathSegment::Field(f) => {
1544 let camel = f.to_lower_camel_case();
1545 if getter_map.needs_getter(current_type.as_deref(), f.as_str()) {
1546 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1551 out.push_str("->");
1552 out.push_str(&getter);
1553 out.push_str("()");
1554 } else {
1555 out.push_str("->");
1556 out.push_str(&camel);
1557 }
1558 current_type = getter_map.advance(current_type.as_deref(), f.as_str());
1559 }
1560 PathSegment::ArrayField { name, index } => {
1561 let camel = name.to_lower_camel_case();
1562 if getter_map.needs_getter(current_type.as_deref(), name.as_str()) {
1563 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1564 out.push_str("->");
1565 out.push_str(&getter);
1566 out.push_str("()");
1567 } else {
1568 out.push_str("->");
1569 out.push_str(&camel);
1570 }
1571 out.push_str(&format!("[{index}]"));
1572 current_type = getter_map.advance(current_type.as_deref(), name.as_str());
1573 }
1574 PathSegment::MapAccess { field, key } => {
1575 let camel = field.to_lower_camel_case();
1576 if getter_map.needs_getter(current_type.as_deref(), field.as_str()) {
1577 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1578 out.push_str("->");
1579 out.push_str(&getter);
1580 out.push_str("()");
1581 } else {
1582 out.push_str("->");
1583 out.push_str(&camel);
1584 }
1585 out.push_str(&format!("[\"{key}\"]"));
1586 current_type = getter_map.advance(current_type.as_deref(), field.as_str());
1587 }
1588 PathSegment::Length => {
1589 let current = std::mem::take(&mut out);
1590 out = format!("count({current})");
1591 }
1592 }
1593 }
1594 out
1595}
1596
1597fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1598 let mut out = result_var.to_string();
1599 for seg in segments {
1600 match seg {
1601 PathSegment::Field(f) => {
1602 out.push('$');
1603 out.push_str(f);
1604 }
1605 PathSegment::ArrayField { name, index } => {
1606 out.push('$');
1607 out.push_str(name);
1608 out.push_str(&format!("[[{}]]", index + 1));
1610 }
1611 PathSegment::MapAccess { field, key } => {
1612 out.push('$');
1613 out.push_str(field);
1614 out.push_str(&format!("[[\"{key}\"]]"));
1615 }
1616 PathSegment::Length => {
1617 let current = std::mem::take(&mut out);
1618 out = format!("length({current})");
1619 }
1620 }
1621 }
1622 out
1623}
1624
1625fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1626 let mut parts = Vec::new();
1627 let mut trailing_length = false;
1628 for seg in segments {
1629 match seg {
1630 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1631 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1632 PathSegment::MapAccess { field, key } => {
1633 parts.push(field.to_snake_case());
1634 parts.push(key.clone());
1635 }
1636 PathSegment::Length => {
1637 trailing_length = true;
1638 }
1639 }
1640 }
1641 let suffix = parts.join("_");
1642 if trailing_length {
1643 format!("result_{suffix}_count({result_var})")
1644 } else {
1645 format!("result_{suffix}({result_var})")
1646 }
1647}
1648
1649fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
1656 let mut out = result_var.to_string();
1657 for seg in segments {
1658 match seg {
1659 PathSegment::Field(f) => {
1660 out.push('.');
1661 out.push_str(&f.to_lower_camel_case());
1662 }
1663 PathSegment::ArrayField { name, index } => {
1664 out.push('.');
1665 out.push_str(&name.to_lower_camel_case());
1666 out.push_str(&format!("[{index}]"));
1667 }
1668 PathSegment::MapAccess { field, key } => {
1669 out.push('.');
1670 out.push_str(&field.to_lower_camel_case());
1671 if key.chars().all(|c| c.is_ascii_digit()) {
1672 out.push_str(&format!("[{key}]"));
1673 } else {
1674 out.push_str(&format!("[\"{key}\"]"));
1675 }
1676 }
1677 PathSegment::Length => {
1678 out.push_str(".length");
1679 }
1680 }
1681 }
1682 out
1683}
1684
1685fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1691 let mut out = result_var.to_string();
1692 let mut path_so_far = String::new();
1693 let mut prev_was_nullable = false;
1694 for seg in segments {
1695 let nav = if prev_was_nullable { "?." } else { "." };
1696 match seg {
1697 PathSegment::Field(f) => {
1698 if !path_so_far.is_empty() {
1699 path_so_far.push('.');
1700 }
1701 path_so_far.push_str(f);
1702 let is_optional = optional_fields.contains(&path_so_far);
1703 out.push_str(nav);
1704 out.push_str(&f.to_lower_camel_case());
1705 prev_was_nullable = is_optional;
1706 }
1707 PathSegment::ArrayField { name, index } => {
1708 if !path_so_far.is_empty() {
1709 path_so_far.push('.');
1710 }
1711 path_so_far.push_str(name);
1712 let is_optional = optional_fields.contains(&path_so_far);
1713 out.push_str(nav);
1714 out.push_str(&name.to_lower_camel_case());
1715 if is_optional {
1719 out.push('!');
1720 }
1721 out.push_str(&format!("[{index}]"));
1722 prev_was_nullable = false;
1723 }
1724 PathSegment::MapAccess { field, key } => {
1725 if !path_so_far.is_empty() {
1726 path_so_far.push('.');
1727 }
1728 path_so_far.push_str(field);
1729 let is_optional = optional_fields.contains(&path_so_far);
1730 out.push_str(nav);
1731 out.push_str(&field.to_lower_camel_case());
1732 if key.chars().all(|c| c.is_ascii_digit()) {
1733 out.push_str(&format!("[{key}]"));
1734 } else {
1735 out.push_str(&format!("[\"{key}\"]"));
1736 }
1737 prev_was_nullable = is_optional;
1738 }
1739 PathSegment::Length => {
1740 out.push_str(nav);
1743 out.push_str("length");
1744 prev_was_nullable = false;
1745 }
1746 }
1747 }
1748 out
1749}
1750
1751#[cfg(test)]
1752mod tests {
1753 use super::*;
1754
1755 fn make_resolver() -> FieldResolver {
1756 let mut fields = HashMap::new();
1757 fields.insert("title".to_string(), "metadata.document.title".to_string());
1758 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1759 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
1760 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
1761 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
1762 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
1763 let mut optional = HashSet::new();
1764 optional.insert("metadata.document.title".to_string());
1765 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1766 }
1767
1768 fn make_resolver_with_doc_optional() -> FieldResolver {
1769 let mut fields = HashMap::new();
1770 fields.insert("title".to_string(), "metadata.document.title".to_string());
1771 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1772 let mut optional = HashSet::new();
1773 optional.insert("document".to_string());
1774 optional.insert("metadata.document.title".to_string());
1775 optional.insert("metadata.document".to_string());
1776 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1777 }
1778
1779 #[test]
1780 fn test_resolve_alias() {
1781 let r = make_resolver();
1782 assert_eq!(r.resolve("title"), "metadata.document.title");
1783 }
1784
1785 #[test]
1786 fn test_resolve_passthrough() {
1787 let r = make_resolver();
1788 assert_eq!(r.resolve("content"), "content");
1789 }
1790
1791 #[test]
1792 fn test_is_optional() {
1793 let r = make_resolver();
1794 assert!(r.is_optional("metadata.document.title"));
1795 assert!(!r.is_optional("content"));
1796 }
1797
1798 #[test]
1799 fn test_accessor_rust_struct() {
1800 let r = make_resolver();
1801 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
1802 }
1803
1804 #[test]
1805 fn test_accessor_rust_map() {
1806 let r = make_resolver();
1807 assert_eq!(
1808 r.accessor("tags", "rust", "result"),
1809 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
1810 );
1811 }
1812
1813 #[test]
1814 fn test_accessor_python() {
1815 let r = make_resolver();
1816 assert_eq!(
1817 r.accessor("title", "python", "result"),
1818 "result.metadata.document.title"
1819 );
1820 }
1821
1822 #[test]
1823 fn test_accessor_go() {
1824 let r = make_resolver();
1825 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
1826 }
1827
1828 #[test]
1829 fn test_accessor_go_initialism_fields() {
1830 let mut fields = std::collections::HashMap::new();
1831 fields.insert("content".to_string(), "html".to_string());
1832 fields.insert("link_url".to_string(), "links.url".to_string());
1833 let r = FieldResolver::new(
1834 &fields,
1835 &HashSet::new(),
1836 &HashSet::new(),
1837 &HashSet::new(),
1838 &HashSet::new(),
1839 );
1840 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
1841 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
1842 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
1843 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
1844 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
1845 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
1846 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
1847 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
1848 }
1849
1850 #[test]
1851 fn test_accessor_typescript() {
1852 let r = make_resolver();
1853 assert_eq!(
1854 r.accessor("title", "typescript", "result"),
1855 "result.metadata.document.title"
1856 );
1857 }
1858
1859 #[test]
1860 fn test_accessor_typescript_snake_to_camel() {
1861 let r = make_resolver();
1862 assert_eq!(
1863 r.accessor("og", "typescript", "result"),
1864 "result.metadata.document.openGraph"
1865 );
1866 assert_eq!(
1867 r.accessor("twitter", "typescript", "result"),
1868 "result.metadata.document.twitterCard"
1869 );
1870 assert_eq!(
1871 r.accessor("canonical", "typescript", "result"),
1872 "result.metadata.document.canonicalUrl"
1873 );
1874 }
1875
1876 #[test]
1877 fn test_accessor_typescript_map_snake_to_camel() {
1878 let r = make_resolver();
1879 assert_eq!(
1880 r.accessor("og_tag", "typescript", "result"),
1881 "result.metadata.openGraphTags[\"og_title\"]"
1882 );
1883 }
1884
1885 #[test]
1886 fn test_accessor_typescript_numeric_index_is_unquoted() {
1887 let mut fields = HashMap::new();
1891 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
1892 let r = FieldResolver::new(
1893 &fields,
1894 &HashSet::new(),
1895 &HashSet::new(),
1896 &HashSet::new(),
1897 &HashSet::new(),
1898 );
1899 assert_eq!(
1900 r.accessor("first_score", "typescript", "result"),
1901 "result.results[0].relevanceScore"
1902 );
1903 }
1904
1905 #[test]
1906 fn test_accessor_node_alias() {
1907 let r = make_resolver();
1908 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
1909 }
1910
1911 #[test]
1912 fn test_accessor_wasm_camel_case() {
1913 let r = make_resolver();
1914 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
1915 assert_eq!(
1916 r.accessor("twitter", "wasm", "result"),
1917 "result.metadata.document.twitterCard"
1918 );
1919 assert_eq!(
1920 r.accessor("canonical", "wasm", "result"),
1921 "result.metadata.document.canonicalUrl"
1922 );
1923 }
1924
1925 #[test]
1926 fn test_accessor_wasm_map_access() {
1927 let r = make_resolver();
1928 assert_eq!(
1929 r.accessor("og_tag", "wasm", "result"),
1930 "result.metadata.openGraphTags.get(\"og_title\")"
1931 );
1932 }
1933
1934 #[test]
1935 fn test_accessor_java() {
1936 let r = make_resolver();
1937 assert_eq!(
1938 r.accessor("title", "java", "result"),
1939 "result.metadata().document().title()"
1940 );
1941 }
1942
1943 #[test]
1944 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
1945 let mut fields = HashMap::new();
1946 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1947 fields.insert("node_count".to_string(), "nodes.length".to_string());
1948 let mut arrays = HashSet::new();
1949 arrays.insert("nodes".to_string());
1950 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
1951 assert_eq!(
1952 r.accessor("first_node_name", "kotlin", "result"),
1953 "result.nodes().first().name()"
1954 );
1955 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
1956 }
1957
1958 #[test]
1959 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
1960 let r = make_resolver_with_doc_optional();
1961 assert_eq!(
1962 r.accessor("title", "kotlin", "result"),
1963 "result.metadata().document()?.title()"
1964 );
1965 }
1966
1967 #[test]
1968 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
1969 let mut fields = HashMap::new();
1970 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1971 fields.insert("tag".to_string(), "tags[name]".to_string());
1972 let mut optional = HashSet::new();
1973 optional.insert("nodes".to_string());
1974 optional.insert("tags".to_string());
1975 let mut arrays = HashSet::new();
1976 arrays.insert("nodes".to_string());
1977 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
1978 assert_eq!(
1979 r.accessor("first_node_name", "kotlin", "result"),
1980 "result.nodes()?.first()?.name()"
1981 );
1982 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
1983 }
1984
1985 #[test]
1991 fn test_accessor_kotlin_optional_field_after_indexed_array() {
1992 let mut fields = HashMap::new();
1995 fields.insert(
1996 "tool_call_name".to_string(),
1997 "choices[0].message.tool_calls[0].function.name".to_string(),
1998 );
1999 let mut optional = HashSet::new();
2000 optional.insert("choices[0].message.tool_calls".to_string());
2001 let mut arrays = HashSet::new();
2002 arrays.insert("choices".to_string());
2003 arrays.insert("choices[0].message.tool_calls".to_string());
2004 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2005 let expr = r.accessor("tool_call_name", "kotlin", "result");
2006 assert!(
2008 expr.contains("toolCalls()?.first()"),
2009 "expected toolCalls()?.first() for optional list, got: {expr}"
2010 );
2011 }
2012
2013 #[test]
2014 fn test_accessor_csharp() {
2015 let r = make_resolver();
2016 assert_eq!(
2017 r.accessor("title", "csharp", "result"),
2018 "result.Metadata.Document.Title"
2019 );
2020 }
2021
2022 #[test]
2023 fn test_accessor_php() {
2024 let r = make_resolver();
2025 assert_eq!(
2026 r.accessor("title", "php", "$result"),
2027 "$result->metadata->document->title"
2028 );
2029 }
2030
2031 #[test]
2032 fn test_accessor_r() {
2033 let r = make_resolver();
2034 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
2035 }
2036
2037 #[test]
2038 fn test_accessor_c() {
2039 let r = make_resolver();
2040 assert_eq!(
2041 r.accessor("title", "c", "result"),
2042 "result_metadata_document_title(result)"
2043 );
2044 }
2045
2046 #[test]
2047 fn test_rust_unwrap_binding() {
2048 let r = make_resolver();
2049 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
2050 assert_eq!(var, "metadata_document_title");
2051 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
2052 }
2053
2054 #[test]
2055 fn test_rust_unwrap_binding_non_optional() {
2056 let r = make_resolver();
2057 assert!(r.rust_unwrap_binding("content", "result").is_none());
2058 }
2059
2060 #[test]
2061 fn test_rust_unwrap_binding_collapses_double_underscore() {
2062 let mut aliases = HashMap::new();
2067 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
2068 let mut optional = HashSet::new();
2069 optional.insert("json_ld[].name".to_string());
2070 let mut array = HashSet::new();
2071 array.insert("json_ld".to_string());
2072 let result_fields = HashSet::new();
2073 let method_calls = HashSet::new();
2074 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
2075 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
2076 assert_eq!(var, "json_ld_name");
2077 }
2078
2079 #[test]
2080 fn test_direct_field_no_alias() {
2081 let r = make_resolver();
2082 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2083 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
2084 }
2085
2086 #[test]
2087 fn test_accessor_rust_with_optionals() {
2088 let r = make_resolver_with_doc_optional();
2089 assert_eq!(
2090 r.accessor("title", "rust", "result"),
2091 "result.metadata.document.as_ref().unwrap().title"
2092 );
2093 }
2094
2095 #[test]
2096 fn test_accessor_csharp_with_optionals() {
2097 let r = make_resolver_with_doc_optional();
2098 assert_eq!(
2099 r.accessor("title", "csharp", "result"),
2100 "result.Metadata.Document!.Title"
2101 );
2102 }
2103
2104 #[test]
2105 fn test_accessor_rust_non_optional_field() {
2106 let r = make_resolver();
2107 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2108 }
2109
2110 #[test]
2111 fn test_accessor_csharp_non_optional_field() {
2112 let r = make_resolver();
2113 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
2114 }
2115
2116 #[test]
2117 fn test_accessor_rust_method_call() {
2118 let mut fields = HashMap::new();
2120 fields.insert(
2121 "excel_sheet_count".to_string(),
2122 "metadata.format.excel.sheet_count".to_string(),
2123 );
2124 let mut optional = HashSet::new();
2125 optional.insert("metadata.format".to_string());
2126 optional.insert("metadata.format.excel".to_string());
2127 let mut method_calls = HashSet::new();
2128 method_calls.insert("metadata.format.excel".to_string());
2129 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
2130 assert_eq!(
2131 r.accessor("excel_sheet_count", "rust", "result"),
2132 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
2133 );
2134 }
2135
2136 fn make_php_getter_resolver() -> FieldResolver {
2141 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2142 getters.insert(
2143 "Root".to_string(),
2144 ["metadata".to_string(), "links".to_string()].into_iter().collect(),
2145 );
2146 let map = PhpGetterMap {
2147 getters,
2148 field_types: HashMap::new(),
2149 root_type: Some("Root".to_string()),
2150 };
2151 FieldResolver::new_with_php_getters(
2152 &HashMap::new(),
2153 &HashSet::new(),
2154 &HashSet::new(),
2155 &HashSet::new(),
2156 &HashSet::new(),
2157 &HashMap::new(),
2158 map,
2159 )
2160 }
2161
2162 #[test]
2163 fn render_php_uses_getter_method_for_non_scalar_field() {
2164 let r = make_php_getter_resolver();
2165 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
2166 }
2167
2168 #[test]
2169 fn render_php_uses_property_for_scalar_field() {
2170 let r = make_php_getter_resolver();
2171 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2172 }
2173
2174 #[test]
2175 fn render_php_nested_non_scalar_uses_getter_then_property() {
2176 let mut fields = HashMap::new();
2177 fields.insert("title".to_string(), "metadata.title".to_string());
2178 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2179 getters.insert("Root".to_string(), ["metadata".to_string()].into_iter().collect());
2180 getters.insert("Metadata".to_string(), HashSet::new());
2182 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2183 field_types.insert(
2184 "Root".to_string(),
2185 [("metadata".to_string(), "Metadata".to_string())].into_iter().collect(),
2186 );
2187 let map = PhpGetterMap {
2188 getters,
2189 field_types,
2190 root_type: Some("Root".to_string()),
2191 };
2192 let r = FieldResolver::new_with_php_getters(
2193 &fields,
2194 &HashSet::new(),
2195 &HashSet::new(),
2196 &HashSet::new(),
2197 &HashSet::new(),
2198 &HashMap::new(),
2199 map,
2200 );
2201 assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
2203 }
2204
2205 #[test]
2206 fn render_php_array_field_uses_getter_when_non_scalar() {
2207 let mut fields = HashMap::new();
2208 fields.insert("first_link".to_string(), "links[0]".to_string());
2209 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2210 getters.insert("Root".to_string(), ["links".to_string()].into_iter().collect());
2211 let map = PhpGetterMap {
2212 getters,
2213 field_types: HashMap::new(),
2214 root_type: Some("Root".to_string()),
2215 };
2216 let r = FieldResolver::new_with_php_getters(
2217 &fields,
2218 &HashSet::new(),
2219 &HashSet::new(),
2220 &HashSet::new(),
2221 &HashSet::new(),
2222 &HashMap::new(),
2223 map,
2224 );
2225 assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
2226 }
2227
2228 #[test]
2229 fn render_php_falls_back_to_property_when_getter_fields_empty() {
2230 let r = FieldResolver::new(
2233 &HashMap::new(),
2234 &HashSet::new(),
2235 &HashSet::new(),
2236 &HashSet::new(),
2237 &HashSet::new(),
2238 );
2239 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2240 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
2241 }
2242
2243 #[test]
2247 fn render_php_with_getters_distinguishes_same_field_name_on_different_types() {
2248 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2249 getters.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2251 getters.insert("B".to_string(), HashSet::new());
2253 let map_a = PhpGetterMap {
2254 getters: getters.clone(),
2255 field_types: HashMap::new(),
2256 root_type: Some("A".to_string()),
2257 };
2258 let map_b = PhpGetterMap {
2259 getters,
2260 field_types: HashMap::new(),
2261 root_type: Some("B".to_string()),
2262 };
2263 let r_a = FieldResolver::new_with_php_getters(
2264 &HashMap::new(),
2265 &HashSet::new(),
2266 &HashSet::new(),
2267 &HashSet::new(),
2268 &HashSet::new(),
2269 &HashMap::new(),
2270 map_a,
2271 );
2272 let r_b = FieldResolver::new_with_php_getters(
2273 &HashMap::new(),
2274 &HashSet::new(),
2275 &HashSet::new(),
2276 &HashSet::new(),
2277 &HashSet::new(),
2278 &HashMap::new(),
2279 map_b,
2280 );
2281 assert_eq!(r_a.accessor("content", "php", "$a"), "$a->getContent()");
2282 assert_eq!(r_b.accessor("content", "php", "$b"), "$b->content");
2283 }
2284
2285 #[test]
2289 fn render_php_with_getters_chains_through_correct_type() {
2290 let mut fields = HashMap::new();
2291 fields.insert("nested_content".to_string(), "inner.content".to_string());
2292 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2293 getters.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2295 getters.insert("B".to_string(), HashSet::new());
2297 getters.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2300 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2301 field_types.insert(
2302 "Outer".to_string(),
2303 [("inner".to_string(), "B".to_string())].into_iter().collect(),
2304 );
2305 let map = PhpGetterMap {
2306 getters,
2307 field_types,
2308 root_type: Some("Outer".to_string()),
2309 };
2310 let r = FieldResolver::new_with_php_getters(
2311 &fields,
2312 &HashSet::new(),
2313 &HashSet::new(),
2314 &HashSet::new(),
2315 &HashSet::new(),
2316 &HashMap::new(),
2317 map,
2318 );
2319 assert_eq!(
2320 r.accessor("nested_content", "php", "$result"),
2321 "$result->getInner()->content"
2322 );
2323 }
2324}