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 {
615 let mut out = result_var.to_string();
616 for seg in segments {
617 match seg {
618 PathSegment::Field(f) => {
619 out.push('.');
620 out.push_str(f);
621 }
622 PathSegment::ArrayField { name, index } => {
623 out.push('.');
624 out.push_str(name);
625 out.push_str(&format!("[{index}]"));
626 }
627 PathSegment::MapAccess { field, key } => {
628 out.push('.');
629 out.push_str(field);
630 if key.chars().all(|c| c.is_ascii_digit()) {
631 out.push_str(&format!("[{key}]"));
632 } else {
633 out.push_str(&format!("[\"{key}\"]"));
634 }
635 }
636 PathSegment::Length => {
637 out.push_str(".count");
638 }
639 }
640 }
641 out
642}
643
644fn render_swift_with_optionals(
654 segments: &[PathSegment],
655 result_var: &str,
656 optional_fields: &HashSet<String>,
657) -> String {
658 let mut out = result_var.to_string();
659 let mut path_so_far = String::new();
660 let total = segments.len();
661 for (i, seg) in segments.iter().enumerate() {
662 let is_leaf = i == total - 1;
663 match seg {
664 PathSegment::Field(f) => {
665 if !path_so_far.is_empty() {
666 path_so_far.push('.');
667 }
668 path_so_far.push_str(f);
669 out.push('.');
670 out.push_str(f);
671 if !is_leaf && optional_fields.contains(&path_so_far) {
675 out.push('?');
676 }
677 }
678 PathSegment::ArrayField { name, index } => {
679 if !path_so_far.is_empty() {
680 path_so_far.push('.');
681 }
682 path_so_far.push_str(name);
683 let is_optional = optional_fields.contains(&path_so_far);
684 out.push('.');
685 out.push_str(name);
686 if is_optional {
687 out.push_str(&format!("?[{index}]"));
689 } else {
690 out.push_str(&format!("[{index}]"));
691 }
692 path_so_far.push_str("[0]");
693 let _ = is_leaf;
694 }
695 PathSegment::MapAccess { field, key } => {
696 if !path_so_far.is_empty() {
697 path_so_far.push('.');
698 }
699 path_so_far.push_str(field);
700 out.push('.');
701 out.push_str(field);
702 if key.chars().all(|c| c.is_ascii_digit()) {
703 out.push_str(&format!("[{key}]"));
704 } else {
705 out.push_str(&format!("[\"{key}\"]"));
706 }
707 }
708 PathSegment::Length => {
709 out.push_str(".count");
710 }
711 }
712 }
713 out
714}
715
716fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
717 let mut out = result_var.to_string();
718 for seg in segments {
719 match seg {
720 PathSegment::Field(f) => {
721 out.push('.');
722 out.push_str(&f.to_snake_case());
723 }
724 PathSegment::ArrayField { name, index } => {
725 out.push('.');
726 out.push_str(&name.to_snake_case());
727 out.push_str(&format!("[{index}]"));
728 }
729 PathSegment::MapAccess { field, key } => {
730 out.push('.');
731 out.push_str(&field.to_snake_case());
732 if key.chars().all(|c| c.is_ascii_digit()) {
733 out.push_str(&format!("[{key}]"));
734 } else {
735 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
736 }
737 }
738 PathSegment::Length => {
739 out.push_str(".len()");
740 }
741 }
742 }
743 out
744}
745
746fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
747 let mut out = result_var.to_string();
748 for seg in segments {
749 match seg {
750 PathSegment::Field(f) => {
751 out.push('.');
752 out.push_str(f);
753 }
754 PathSegment::ArrayField { name, index } => {
755 if language == "elixir" {
756 let current = std::mem::take(&mut out);
757 out = format!("Enum.at({current}.{name}, {index})");
758 } else {
759 out.push('.');
760 out.push_str(name);
761 out.push_str(&format!("[{index}]"));
762 }
763 }
764 PathSegment::MapAccess { field, key } => {
765 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
766 if is_numeric && language == "elixir" {
767 let current = std::mem::take(&mut out);
768 out = format!("Enum.at({current}.{field}, {key})");
769 } else {
770 out.push('.');
771 out.push_str(field);
772 if is_numeric {
773 let idx: usize = key.parse().unwrap_or(0);
774 out.push_str(&format!("[{idx}]"));
775 } else if language == "elixir" || language == "ruby" {
776 out.push_str(&format!("[\"{key}\"]"));
779 } else {
780 out.push_str(&format!(".get(\"{key}\")"));
781 }
782 }
783 }
784 PathSegment::Length => match language {
785 "ruby" => out.push_str(".length"),
786 "elixir" => {
787 let current = std::mem::take(&mut out);
788 out = format!("length({current})");
789 }
790 "gleam" => {
791 let current = std::mem::take(&mut out);
792 out = format!("list.length({current})");
793 }
794 _ => {
795 let current = std::mem::take(&mut out);
796 out = format!("len({current})");
797 }
798 },
799 }
800 }
801 out
802}
803
804fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
805 let mut out = result_var.to_string();
806 for seg in segments {
807 match seg {
808 PathSegment::Field(f) => {
809 out.push('.');
810 out.push_str(&f.to_lower_camel_case());
811 }
812 PathSegment::ArrayField { name, index } => {
813 out.push('.');
814 out.push_str(&name.to_lower_camel_case());
815 out.push_str(&format!("[{index}]"));
816 }
817 PathSegment::MapAccess { field, key } => {
818 out.push('.');
819 out.push_str(&field.to_lower_camel_case());
820 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
823 out.push_str(&format!("[{key}]"));
824 } else {
825 out.push_str(&format!("[\"{key}\"]"));
826 }
827 }
828 PathSegment::Length => {
829 out.push_str(".length");
830 }
831 }
832 }
833 out
834}
835
836fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
837 let mut out = result_var.to_string();
838 for seg in segments {
839 match seg {
840 PathSegment::Field(f) => {
841 out.push('.');
842 out.push_str(&f.to_lower_camel_case());
843 }
844 PathSegment::ArrayField { name, index } => {
845 out.push('.');
846 out.push_str(&name.to_lower_camel_case());
847 out.push_str(&format!("[{index}]"));
848 }
849 PathSegment::MapAccess { field, key } => {
850 out.push('.');
851 out.push_str(&field.to_lower_camel_case());
852 out.push_str(&format!(".get(\"{key}\")"));
853 }
854 PathSegment::Length => {
855 out.push_str(".length");
856 }
857 }
858 }
859 out
860}
861
862fn render_go(segments: &[PathSegment], result_var: &str) -> String {
863 let mut out = result_var.to_string();
864 for seg in segments {
865 match seg {
866 PathSegment::Field(f) => {
867 out.push('.');
868 out.push_str(&to_go_name(f));
869 }
870 PathSegment::ArrayField { name, index } => {
871 out.push('.');
872 out.push_str(&to_go_name(name));
873 out.push_str(&format!("[{index}]"));
874 }
875 PathSegment::MapAccess { field, key } => {
876 out.push('.');
877 out.push_str(&to_go_name(field));
878 if key.chars().all(|c| c.is_ascii_digit()) {
879 out.push_str(&format!("[{key}]"));
880 } else {
881 out.push_str(&format!("[\"{key}\"]"));
882 }
883 }
884 PathSegment::Length => {
885 let current = std::mem::take(&mut out);
886 out = format!("len({current})");
887 }
888 }
889 }
890 out
891}
892
893fn render_java(segments: &[PathSegment], result_var: &str) -> String {
894 let mut out = result_var.to_string();
895 for seg in segments {
896 match seg {
897 PathSegment::Field(f) => {
898 out.push('.');
899 out.push_str(&f.to_lower_camel_case());
900 out.push_str("()");
901 }
902 PathSegment::ArrayField { name, index } => {
903 out.push('.');
904 out.push_str(&name.to_lower_camel_case());
905 out.push_str(&format!("().get({index})"));
906 }
907 PathSegment::MapAccess { field, key } => {
908 out.push('.');
909 out.push_str(&field.to_lower_camel_case());
910 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
912 if is_numeric {
913 out.push_str(&format!("().get({key})"));
914 } else {
915 out.push_str(&format!("().get(\"{key}\")"));
916 }
917 }
918 PathSegment::Length => {
919 out.push_str(".size()");
920 }
921 }
922 }
923 out
924}
925
926fn kotlin_getter(name: &str) -> String {
931 let camel = name.to_lower_camel_case();
932 match camel.as_str() {
933 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
934 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
935 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
936 _ => camel,
937 }
938}
939
940fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
941 let mut out = result_var.to_string();
942 for seg in segments {
943 match seg {
944 PathSegment::Field(f) => {
945 out.push('.');
946 out.push_str(&kotlin_getter(f));
947 out.push_str("()");
948 }
949 PathSegment::ArrayField { name, index } => {
950 out.push('.');
951 out.push_str(&kotlin_getter(name));
952 if *index == 0 {
953 out.push_str("().first()");
954 } else {
955 out.push_str(&format!("().get({index})"));
956 }
957 }
958 PathSegment::MapAccess { field, key } => {
959 out.push('.');
960 out.push_str(&kotlin_getter(field));
961 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
962 if is_numeric {
963 out.push_str(&format!("().get({key})"));
964 } else {
965 out.push_str(&format!("().get(\"{key}\")"));
966 }
967 }
968 PathSegment::Length => {
969 out.push_str(".size");
970 }
971 }
972 }
973 out
974}
975
976fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
977 let mut out = result_var.to_string();
978 let mut path_so_far = String::new();
979 for (i, seg) in segments.iter().enumerate() {
980 let is_leaf = i == segments.len() - 1;
981 match seg {
982 PathSegment::Field(f) => {
983 if !path_so_far.is_empty() {
984 path_so_far.push('.');
985 }
986 path_so_far.push_str(f);
987 out.push('.');
988 out.push_str(&f.to_lower_camel_case());
989 out.push_str("()");
990 let _ = is_leaf;
991 let _ = optional_fields;
992 }
993 PathSegment::ArrayField { name, index } => {
994 if !path_so_far.is_empty() {
995 path_so_far.push('.');
996 }
997 path_so_far.push_str(name);
998 out.push('.');
999 out.push_str(&name.to_lower_camel_case());
1000 out.push_str(&format!("().get({index})"));
1001 }
1002 PathSegment::MapAccess { field, key } => {
1003 if !path_so_far.is_empty() {
1004 path_so_far.push('.');
1005 }
1006 path_so_far.push_str(field);
1007 out.push('.');
1008 out.push_str(&field.to_lower_camel_case());
1009 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1011 if is_numeric {
1012 out.push_str(&format!("().get({key})"));
1013 } else {
1014 out.push_str(&format!("().get(\"{key}\")"));
1015 }
1016 }
1017 PathSegment::Length => {
1018 out.push_str(".size()");
1019 }
1020 }
1021 }
1022 out
1023}
1024
1025fn render_kotlin_with_optionals(
1040 segments: &[PathSegment],
1041 result_var: &str,
1042 optional_fields: &HashSet<String>,
1043) -> String {
1044 let mut out = result_var.to_string();
1045 let mut path_so_far = String::new();
1046 let mut prev_was_nullable = false;
1054 for seg in segments {
1055 let nav = if prev_was_nullable { "?." } else { "." };
1056 match seg {
1057 PathSegment::Field(f) => {
1058 if !path_so_far.is_empty() {
1059 path_so_far.push('.');
1060 }
1061 path_so_far.push_str(f);
1062 let is_optional = optional_fields.contains(&path_so_far);
1067 out.push_str(nav);
1068 out.push_str(&kotlin_getter(f));
1069 out.push_str("()");
1070 prev_was_nullable = prev_was_nullable || is_optional;
1071 }
1072 PathSegment::ArrayField { name, index } => {
1073 if !path_so_far.is_empty() {
1074 path_so_far.push('.');
1075 }
1076 path_so_far.push_str(name);
1077 let is_optional = optional_fields.contains(&path_so_far);
1078 out.push_str(nav);
1079 out.push_str(&kotlin_getter(name));
1080 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1081 if *index == 0 {
1082 out.push_str(&format!("(){safe}.first()"));
1083 } else {
1084 out.push_str(&format!("(){safe}.get({index})"));
1085 }
1086 path_so_far.push_str("[0]");
1090 prev_was_nullable = prev_was_nullable || is_optional;
1091 }
1092 PathSegment::MapAccess { field, key } => {
1093 if !path_so_far.is_empty() {
1094 path_so_far.push('.');
1095 }
1096 path_so_far.push_str(field);
1097 let is_optional = optional_fields.contains(&path_so_far);
1098 out.push_str(nav);
1099 out.push_str(&kotlin_getter(field));
1100 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1101 if is_numeric {
1102 if prev_was_nullable || is_optional {
1103 out.push_str(&format!("()?.get({key})"));
1104 } else {
1105 out.push_str(&format!("().get({key})"));
1106 }
1107 } else if prev_was_nullable || is_optional {
1108 out.push_str(&format!("()?.get(\"{key}\")"));
1109 } else {
1110 out.push_str(&format!("().get(\"{key}\")"));
1111 }
1112 prev_was_nullable = prev_was_nullable || is_optional;
1113 }
1114 PathSegment::Length => {
1115 let size_nav = if prev_was_nullable { "?" } else { "" };
1118 out.push_str(&format!("{size_nav}.size"));
1119 prev_was_nullable = false;
1120 }
1121 }
1122 }
1123 out
1124}
1125
1126fn render_kotlin_android_with_optionals(
1137 segments: &[PathSegment],
1138 result_var: &str,
1139 optional_fields: &HashSet<String>,
1140) -> String {
1141 let mut out = result_var.to_string();
1142 let mut path_so_far = String::new();
1143 let mut prev_was_nullable = false;
1144 for seg in segments {
1145 let nav = if prev_was_nullable { "?." } else { "." };
1146 match seg {
1147 PathSegment::Field(f) => {
1148 if !path_so_far.is_empty() {
1149 path_so_far.push('.');
1150 }
1151 path_so_far.push_str(f);
1152 let is_optional = optional_fields.contains(&path_so_far);
1153 out.push_str(nav);
1154 out.push_str(&kotlin_getter(f));
1156 prev_was_nullable = prev_was_nullable || is_optional;
1157 }
1158 PathSegment::ArrayField { name, index } => {
1159 if !path_so_far.is_empty() {
1160 path_so_far.push('.');
1161 }
1162 path_so_far.push_str(name);
1163 let is_optional = optional_fields.contains(&path_so_far);
1164 out.push_str(nav);
1165 out.push_str(&kotlin_getter(name));
1167 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1168 if *index == 0 {
1169 out.push_str(&format!("{safe}.first()"));
1170 } else {
1171 out.push_str(&format!("{safe}.get({index})"));
1172 }
1173 path_so_far.push_str("[0]");
1174 prev_was_nullable = prev_was_nullable || is_optional;
1175 }
1176 PathSegment::MapAccess { field, key } => {
1177 if !path_so_far.is_empty() {
1178 path_so_far.push('.');
1179 }
1180 path_so_far.push_str(field);
1181 let is_optional = optional_fields.contains(&path_so_far);
1182 out.push_str(nav);
1183 out.push_str(&kotlin_getter(field));
1185 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1186 if is_numeric {
1187 if prev_was_nullable || is_optional {
1188 out.push_str(&format!("?.get({key})"));
1189 } else {
1190 out.push_str(&format!(".get({key})"));
1191 }
1192 } else if prev_was_nullable || is_optional {
1193 out.push_str(&format!("?.get(\"{key}\")"));
1194 } else {
1195 out.push_str(&format!(".get(\"{key}\")"));
1196 }
1197 prev_was_nullable = prev_was_nullable || is_optional;
1198 }
1199 PathSegment::Length => {
1200 let size_nav = if prev_was_nullable { "?" } else { "" };
1201 out.push_str(&format!("{size_nav}.size"));
1202 prev_was_nullable = false;
1203 }
1204 }
1205 }
1206 out
1207}
1208
1209fn render_kotlin_android(segments: &[PathSegment], result_var: &str) -> String {
1213 let mut out = result_var.to_string();
1214 for seg in segments {
1215 match seg {
1216 PathSegment::Field(f) => {
1217 out.push('.');
1218 out.push_str(&kotlin_getter(f));
1219 }
1221 PathSegment::ArrayField { name, index } => {
1222 out.push('.');
1223 out.push_str(&kotlin_getter(name));
1224 if *index == 0 {
1225 out.push_str(".first()");
1226 } else {
1227 out.push_str(&format!(".get({index})"));
1228 }
1229 }
1230 PathSegment::MapAccess { field, key } => {
1231 out.push('.');
1232 out.push_str(&kotlin_getter(field));
1233 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1234 if is_numeric {
1235 out.push_str(&format!(".get({key})"));
1236 } else {
1237 out.push_str(&format!(".get(\"{key}\")"));
1238 }
1239 }
1240 PathSegment::Length => {
1241 out.push_str(".size");
1242 }
1243 }
1244 }
1245 out
1246}
1247
1248fn render_rust_with_optionals(
1254 segments: &[PathSegment],
1255 result_var: &str,
1256 optional_fields: &HashSet<String>,
1257 method_calls: &HashSet<String>,
1258) -> String {
1259 let mut out = result_var.to_string();
1260 let mut path_so_far = String::new();
1261 for (i, seg) in segments.iter().enumerate() {
1262 let is_leaf = i == segments.len() - 1;
1263 match seg {
1264 PathSegment::Field(f) => {
1265 if !path_so_far.is_empty() {
1266 path_so_far.push('.');
1267 }
1268 path_so_far.push_str(f);
1269 out.push('.');
1270 out.push_str(&f.to_snake_case());
1271 let is_method = method_calls.contains(&path_so_far);
1272 if is_method {
1273 out.push_str("()");
1274 if !is_leaf && optional_fields.contains(&path_so_far) {
1275 out.push_str(".as_ref().unwrap()");
1276 }
1277 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1278 out.push_str(".as_ref().unwrap()");
1279 }
1280 }
1281 PathSegment::ArrayField { name, index } => {
1282 if !path_so_far.is_empty() {
1283 path_so_far.push('.');
1284 }
1285 path_so_far.push_str(name);
1286 out.push('.');
1287 out.push_str(&name.to_snake_case());
1288 let path_with_idx = format!("{path_so_far}[0]");
1292 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1293 if is_opt {
1294 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1295 } else {
1296 out.push_str(&format!("[{index}]"));
1297 }
1298 path_so_far.push_str("[0]");
1303 }
1304 PathSegment::MapAccess { field, key } => {
1305 if !path_so_far.is_empty() {
1306 path_so_far.push('.');
1307 }
1308 path_so_far.push_str(field);
1309 out.push('.');
1310 out.push_str(&field.to_snake_case());
1311 if key.chars().all(|c| c.is_ascii_digit()) {
1312 let path_with_idx = format!("{path_so_far}[0]");
1314 let is_opt =
1315 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1316 if is_opt {
1317 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1318 } else {
1319 out.push_str(&format!("[{key}]"));
1320 }
1321 path_so_far.push_str("[0]");
1322 } else {
1323 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1324 }
1325 }
1326 PathSegment::Length => {
1327 out.push_str(".len()");
1328 }
1329 }
1330 }
1331 out
1332}
1333
1334fn render_zig_with_optionals(
1347 segments: &[PathSegment],
1348 result_var: &str,
1349 optional_fields: &HashSet<String>,
1350 method_calls: &HashSet<String>,
1351) -> String {
1352 let mut out = result_var.to_string();
1353 let mut path_so_far = String::new();
1354 for seg in segments {
1355 match seg {
1356 PathSegment::Field(f) => {
1357 if !path_so_far.is_empty() {
1358 path_so_far.push('.');
1359 }
1360 path_so_far.push_str(f);
1361 out.push('.');
1362 out.push_str(f);
1363 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1364 out.push_str(".?");
1365 }
1366 }
1367 PathSegment::ArrayField { name, index } => {
1368 if !path_so_far.is_empty() {
1369 path_so_far.push('.');
1370 }
1371 path_so_far.push_str(name);
1372 out.push('.');
1373 out.push_str(name);
1374 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1375 out.push_str(".?");
1376 }
1377 out.push_str(&format!("[{index}]"));
1378 }
1379 PathSegment::MapAccess { field, key } => {
1380 if !path_so_far.is_empty() {
1381 path_so_far.push('.');
1382 }
1383 path_so_far.push_str(field);
1384 out.push('.');
1385 out.push_str(field);
1386 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1387 out.push_str(".?");
1388 }
1389 if key.chars().all(|c| c.is_ascii_digit()) {
1390 out.push_str(&format!("[{key}]"));
1391 } else {
1392 out.push_str(&format!(".get(\"{key}\")"));
1393 }
1394 }
1395 PathSegment::Length => {
1396 out.push_str(".len");
1397 }
1398 }
1399 }
1400 out
1401}
1402
1403fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1404 let mut out = result_var.to_string();
1405 for seg in segments {
1406 match seg {
1407 PathSegment::Field(f) => {
1408 out.push('.');
1409 out.push_str(&f.to_pascal_case());
1410 }
1411 PathSegment::ArrayField { name, index } => {
1412 out.push('.');
1413 out.push_str(&name.to_pascal_case());
1414 out.push_str(&format!("[{index}]"));
1415 }
1416 PathSegment::MapAccess { field, key } => {
1417 out.push('.');
1418 out.push_str(&field.to_pascal_case());
1419 if key.chars().all(|c| c.is_ascii_digit()) {
1420 out.push_str(&format!("[{key}]"));
1421 } else {
1422 out.push_str(&format!("[\"{key}\"]"));
1423 }
1424 }
1425 PathSegment::Length => {
1426 out.push_str(".Count");
1427 }
1428 }
1429 }
1430 out
1431}
1432
1433fn render_csharp_with_optionals(
1434 segments: &[PathSegment],
1435 result_var: &str,
1436 optional_fields: &HashSet<String>,
1437) -> String {
1438 let mut out = result_var.to_string();
1439 let mut path_so_far = String::new();
1440 for (i, seg) in segments.iter().enumerate() {
1441 let is_leaf = i == segments.len() - 1;
1442 match seg {
1443 PathSegment::Field(f) => {
1444 if !path_so_far.is_empty() {
1445 path_so_far.push('.');
1446 }
1447 path_so_far.push_str(f);
1448 out.push('.');
1449 out.push_str(&f.to_pascal_case());
1450 if !is_leaf && optional_fields.contains(&path_so_far) {
1451 out.push('!');
1452 }
1453 }
1454 PathSegment::ArrayField { name, index } => {
1455 if !path_so_far.is_empty() {
1456 path_so_far.push('.');
1457 }
1458 path_so_far.push_str(name);
1459 out.push('.');
1460 out.push_str(&name.to_pascal_case());
1461 out.push_str(&format!("[{index}]"));
1462 }
1463 PathSegment::MapAccess { field, key } => {
1464 if !path_so_far.is_empty() {
1465 path_so_far.push('.');
1466 }
1467 path_so_far.push_str(field);
1468 out.push('.');
1469 out.push_str(&field.to_pascal_case());
1470 if key.chars().all(|c| c.is_ascii_digit()) {
1471 out.push_str(&format!("[{key}]"));
1472 } else {
1473 out.push_str(&format!("[\"{key}\"]"));
1474 }
1475 }
1476 PathSegment::Length => {
1477 out.push_str(".Count");
1478 }
1479 }
1480 }
1481 out
1482}
1483
1484fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1485 let mut out = result_var.to_string();
1486 for seg in segments {
1487 match seg {
1488 PathSegment::Field(f) => {
1489 out.push_str("->");
1490 out.push_str(&f.to_lower_camel_case());
1493 }
1494 PathSegment::ArrayField { name, index } => {
1495 out.push_str("->");
1496 out.push_str(&name.to_lower_camel_case());
1497 out.push_str(&format!("[{index}]"));
1498 }
1499 PathSegment::MapAccess { field, key } => {
1500 out.push_str("->");
1501 out.push_str(&field.to_lower_camel_case());
1502 out.push_str(&format!("[\"{key}\"]"));
1503 }
1504 PathSegment::Length => {
1505 let current = std::mem::take(&mut out);
1506 out = format!("count({current})");
1507 }
1508 }
1509 }
1510 out
1511}
1512
1513fn render_php_with_getters(segments: &[PathSegment], result_var: &str, getter_map: &PhpGetterMap) -> String {
1531 let mut out = result_var.to_string();
1532 let mut current_type: Option<String> = getter_map.root_type.clone();
1533 for seg in segments {
1534 match seg {
1535 PathSegment::Field(f) => {
1536 let camel = f.to_lower_camel_case();
1537 if getter_map.needs_getter(current_type.as_deref(), f.as_str()) {
1538 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1543 out.push_str("->");
1544 out.push_str(&getter);
1545 out.push_str("()");
1546 } else {
1547 out.push_str("->");
1548 out.push_str(&camel);
1549 }
1550 current_type = getter_map.advance(current_type.as_deref(), f.as_str());
1551 }
1552 PathSegment::ArrayField { name, index } => {
1553 let camel = name.to_lower_camel_case();
1554 if getter_map.needs_getter(current_type.as_deref(), name.as_str()) {
1555 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1556 out.push_str("->");
1557 out.push_str(&getter);
1558 out.push_str("()");
1559 } else {
1560 out.push_str("->");
1561 out.push_str(&camel);
1562 }
1563 out.push_str(&format!("[{index}]"));
1564 current_type = getter_map.advance(current_type.as_deref(), name.as_str());
1565 }
1566 PathSegment::MapAccess { field, key } => {
1567 let camel = field.to_lower_camel_case();
1568 if getter_map.needs_getter(current_type.as_deref(), field.as_str()) {
1569 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1570 out.push_str("->");
1571 out.push_str(&getter);
1572 out.push_str("()");
1573 } else {
1574 out.push_str("->");
1575 out.push_str(&camel);
1576 }
1577 out.push_str(&format!("[\"{key}\"]"));
1578 current_type = getter_map.advance(current_type.as_deref(), field.as_str());
1579 }
1580 PathSegment::Length => {
1581 let current = std::mem::take(&mut out);
1582 out = format!("count({current})");
1583 }
1584 }
1585 }
1586 out
1587}
1588
1589fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1590 let mut out = result_var.to_string();
1591 for seg in segments {
1592 match seg {
1593 PathSegment::Field(f) => {
1594 out.push('$');
1595 out.push_str(f);
1596 }
1597 PathSegment::ArrayField { name, index } => {
1598 out.push('$');
1599 out.push_str(name);
1600 out.push_str(&format!("[[{}]]", index + 1));
1602 }
1603 PathSegment::MapAccess { field, key } => {
1604 out.push('$');
1605 out.push_str(field);
1606 out.push_str(&format!("[[\"{key}\"]]"));
1607 }
1608 PathSegment::Length => {
1609 let current = std::mem::take(&mut out);
1610 out = format!("length({current})");
1611 }
1612 }
1613 }
1614 out
1615}
1616
1617fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1618 let mut parts = Vec::new();
1619 let mut trailing_length = false;
1620 for seg in segments {
1621 match seg {
1622 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1623 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1624 PathSegment::MapAccess { field, key } => {
1625 parts.push(field.to_snake_case());
1626 parts.push(key.clone());
1627 }
1628 PathSegment::Length => {
1629 trailing_length = true;
1630 }
1631 }
1632 }
1633 let suffix = parts.join("_");
1634 if trailing_length {
1635 format!("result_{suffix}_count({result_var})")
1636 } else {
1637 format!("result_{suffix}({result_var})")
1638 }
1639}
1640
1641fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
1648 let mut out = result_var.to_string();
1649 for seg in segments {
1650 match seg {
1651 PathSegment::Field(f) => {
1652 out.push('.');
1653 out.push_str(&f.to_lower_camel_case());
1654 }
1655 PathSegment::ArrayField { name, index } => {
1656 out.push('.');
1657 out.push_str(&name.to_lower_camel_case());
1658 out.push_str(&format!("[{index}]"));
1659 }
1660 PathSegment::MapAccess { field, key } => {
1661 out.push('.');
1662 out.push_str(&field.to_lower_camel_case());
1663 if key.chars().all(|c| c.is_ascii_digit()) {
1664 out.push_str(&format!("[{key}]"));
1665 } else {
1666 out.push_str(&format!("[\"{key}\"]"));
1667 }
1668 }
1669 PathSegment::Length => {
1670 out.push_str(".length");
1671 }
1672 }
1673 }
1674 out
1675}
1676
1677fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1683 let mut out = result_var.to_string();
1684 let mut path_so_far = String::new();
1685 let mut prev_was_nullable = false;
1686 for seg in segments {
1687 let nav = if prev_was_nullable { "?." } else { "." };
1688 match seg {
1689 PathSegment::Field(f) => {
1690 if !path_so_far.is_empty() {
1691 path_so_far.push('.');
1692 }
1693 path_so_far.push_str(f);
1694 let is_optional = optional_fields.contains(&path_so_far);
1695 out.push_str(nav);
1696 out.push_str(&f.to_lower_camel_case());
1697 prev_was_nullable = is_optional;
1698 }
1699 PathSegment::ArrayField { name, index } => {
1700 if !path_so_far.is_empty() {
1701 path_so_far.push('.');
1702 }
1703 path_so_far.push_str(name);
1704 let is_optional = optional_fields.contains(&path_so_far);
1705 out.push_str(nav);
1706 out.push_str(&name.to_lower_camel_case());
1707 if is_optional {
1711 out.push('!');
1712 }
1713 out.push_str(&format!("[{index}]"));
1714 prev_was_nullable = false;
1715 }
1716 PathSegment::MapAccess { field, key } => {
1717 if !path_so_far.is_empty() {
1718 path_so_far.push('.');
1719 }
1720 path_so_far.push_str(field);
1721 let is_optional = optional_fields.contains(&path_so_far);
1722 out.push_str(nav);
1723 out.push_str(&field.to_lower_camel_case());
1724 if key.chars().all(|c| c.is_ascii_digit()) {
1725 out.push_str(&format!("[{key}]"));
1726 } else {
1727 out.push_str(&format!("[\"{key}\"]"));
1728 }
1729 prev_was_nullable = is_optional;
1730 }
1731 PathSegment::Length => {
1732 out.push_str(nav);
1735 out.push_str("length");
1736 prev_was_nullable = false;
1737 }
1738 }
1739 }
1740 out
1741}
1742
1743#[cfg(test)]
1744mod tests {
1745 use super::*;
1746
1747 fn make_resolver() -> FieldResolver {
1748 let mut fields = HashMap::new();
1749 fields.insert("title".to_string(), "metadata.document.title".to_string());
1750 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1751 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
1752 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
1753 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
1754 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
1755 let mut optional = HashSet::new();
1756 optional.insert("metadata.document.title".to_string());
1757 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1758 }
1759
1760 fn make_resolver_with_doc_optional() -> FieldResolver {
1761 let mut fields = HashMap::new();
1762 fields.insert("title".to_string(), "metadata.document.title".to_string());
1763 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1764 let mut optional = HashSet::new();
1765 optional.insert("document".to_string());
1766 optional.insert("metadata.document.title".to_string());
1767 optional.insert("metadata.document".to_string());
1768 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1769 }
1770
1771 #[test]
1772 fn test_resolve_alias() {
1773 let r = make_resolver();
1774 assert_eq!(r.resolve("title"), "metadata.document.title");
1775 }
1776
1777 #[test]
1778 fn test_resolve_passthrough() {
1779 let r = make_resolver();
1780 assert_eq!(r.resolve("content"), "content");
1781 }
1782
1783 #[test]
1784 fn test_is_optional() {
1785 let r = make_resolver();
1786 assert!(r.is_optional("metadata.document.title"));
1787 assert!(!r.is_optional("content"));
1788 }
1789
1790 #[test]
1791 fn test_accessor_rust_struct() {
1792 let r = make_resolver();
1793 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
1794 }
1795
1796 #[test]
1797 fn test_accessor_rust_map() {
1798 let r = make_resolver();
1799 assert_eq!(
1800 r.accessor("tags", "rust", "result"),
1801 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
1802 );
1803 }
1804
1805 #[test]
1806 fn test_accessor_python() {
1807 let r = make_resolver();
1808 assert_eq!(
1809 r.accessor("title", "python", "result"),
1810 "result.metadata.document.title"
1811 );
1812 }
1813
1814 #[test]
1815 fn test_accessor_go() {
1816 let r = make_resolver();
1817 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
1818 }
1819
1820 #[test]
1821 fn test_accessor_go_initialism_fields() {
1822 let mut fields = std::collections::HashMap::new();
1823 fields.insert("content".to_string(), "html".to_string());
1824 fields.insert("link_url".to_string(), "links.url".to_string());
1825 let r = FieldResolver::new(
1826 &fields,
1827 &HashSet::new(),
1828 &HashSet::new(),
1829 &HashSet::new(),
1830 &HashSet::new(),
1831 );
1832 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
1833 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
1834 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
1835 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
1836 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
1837 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
1838 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
1839 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
1840 }
1841
1842 #[test]
1843 fn test_accessor_typescript() {
1844 let r = make_resolver();
1845 assert_eq!(
1846 r.accessor("title", "typescript", "result"),
1847 "result.metadata.document.title"
1848 );
1849 }
1850
1851 #[test]
1852 fn test_accessor_typescript_snake_to_camel() {
1853 let r = make_resolver();
1854 assert_eq!(
1855 r.accessor("og", "typescript", "result"),
1856 "result.metadata.document.openGraph"
1857 );
1858 assert_eq!(
1859 r.accessor("twitter", "typescript", "result"),
1860 "result.metadata.document.twitterCard"
1861 );
1862 assert_eq!(
1863 r.accessor("canonical", "typescript", "result"),
1864 "result.metadata.document.canonicalUrl"
1865 );
1866 }
1867
1868 #[test]
1869 fn test_accessor_typescript_map_snake_to_camel() {
1870 let r = make_resolver();
1871 assert_eq!(
1872 r.accessor("og_tag", "typescript", "result"),
1873 "result.metadata.openGraphTags[\"og_title\"]"
1874 );
1875 }
1876
1877 #[test]
1878 fn test_accessor_typescript_numeric_index_is_unquoted() {
1879 let mut fields = HashMap::new();
1883 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
1884 let r = FieldResolver::new(
1885 &fields,
1886 &HashSet::new(),
1887 &HashSet::new(),
1888 &HashSet::new(),
1889 &HashSet::new(),
1890 );
1891 assert_eq!(
1892 r.accessor("first_score", "typescript", "result"),
1893 "result.results[0].relevanceScore"
1894 );
1895 }
1896
1897 #[test]
1898 fn test_accessor_node_alias() {
1899 let r = make_resolver();
1900 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
1901 }
1902
1903 #[test]
1904 fn test_accessor_wasm_camel_case() {
1905 let r = make_resolver();
1906 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
1907 assert_eq!(
1908 r.accessor("twitter", "wasm", "result"),
1909 "result.metadata.document.twitterCard"
1910 );
1911 assert_eq!(
1912 r.accessor("canonical", "wasm", "result"),
1913 "result.metadata.document.canonicalUrl"
1914 );
1915 }
1916
1917 #[test]
1918 fn test_accessor_wasm_map_access() {
1919 let r = make_resolver();
1920 assert_eq!(
1921 r.accessor("og_tag", "wasm", "result"),
1922 "result.metadata.openGraphTags.get(\"og_title\")"
1923 );
1924 }
1925
1926 #[test]
1927 fn test_accessor_java() {
1928 let r = make_resolver();
1929 assert_eq!(
1930 r.accessor("title", "java", "result"),
1931 "result.metadata().document().title()"
1932 );
1933 }
1934
1935 #[test]
1936 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
1937 let mut fields = HashMap::new();
1938 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1939 fields.insert("node_count".to_string(), "nodes.length".to_string());
1940 let mut arrays = HashSet::new();
1941 arrays.insert("nodes".to_string());
1942 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
1943 assert_eq!(
1944 r.accessor("first_node_name", "kotlin", "result"),
1945 "result.nodes().first().name()"
1946 );
1947 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
1948 }
1949
1950 #[test]
1951 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
1952 let r = make_resolver_with_doc_optional();
1953 assert_eq!(
1954 r.accessor("title", "kotlin", "result"),
1955 "result.metadata().document()?.title()"
1956 );
1957 }
1958
1959 #[test]
1960 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
1961 let mut fields = HashMap::new();
1962 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1963 fields.insert("tag".to_string(), "tags[name]".to_string());
1964 let mut optional = HashSet::new();
1965 optional.insert("nodes".to_string());
1966 optional.insert("tags".to_string());
1967 let mut arrays = HashSet::new();
1968 arrays.insert("nodes".to_string());
1969 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
1970 assert_eq!(
1971 r.accessor("first_node_name", "kotlin", "result"),
1972 "result.nodes()?.first()?.name()"
1973 );
1974 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
1975 }
1976
1977 #[test]
1983 fn test_accessor_kotlin_optional_field_after_indexed_array() {
1984 let mut fields = HashMap::new();
1987 fields.insert(
1988 "tool_call_name".to_string(),
1989 "choices[0].message.tool_calls[0].function.name".to_string(),
1990 );
1991 let mut optional = HashSet::new();
1992 optional.insert("choices[0].message.tool_calls".to_string());
1993 let mut arrays = HashSet::new();
1994 arrays.insert("choices".to_string());
1995 arrays.insert("choices[0].message.tool_calls".to_string());
1996 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
1997 let expr = r.accessor("tool_call_name", "kotlin", "result");
1998 assert!(
2000 expr.contains("toolCalls()?.first()"),
2001 "expected toolCalls()?.first() for optional list, got: {expr}"
2002 );
2003 }
2004
2005 #[test]
2006 fn test_accessor_csharp() {
2007 let r = make_resolver();
2008 assert_eq!(
2009 r.accessor("title", "csharp", "result"),
2010 "result.Metadata.Document.Title"
2011 );
2012 }
2013
2014 #[test]
2015 fn test_accessor_php() {
2016 let r = make_resolver();
2017 assert_eq!(
2018 r.accessor("title", "php", "$result"),
2019 "$result->metadata->document->title"
2020 );
2021 }
2022
2023 #[test]
2024 fn test_accessor_r() {
2025 let r = make_resolver();
2026 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
2027 }
2028
2029 #[test]
2030 fn test_accessor_c() {
2031 let r = make_resolver();
2032 assert_eq!(
2033 r.accessor("title", "c", "result"),
2034 "result_metadata_document_title(result)"
2035 );
2036 }
2037
2038 #[test]
2039 fn test_rust_unwrap_binding() {
2040 let r = make_resolver();
2041 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
2042 assert_eq!(var, "metadata_document_title");
2043 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
2044 }
2045
2046 #[test]
2047 fn test_rust_unwrap_binding_non_optional() {
2048 let r = make_resolver();
2049 assert!(r.rust_unwrap_binding("content", "result").is_none());
2050 }
2051
2052 #[test]
2053 fn test_rust_unwrap_binding_collapses_double_underscore() {
2054 let mut aliases = HashMap::new();
2059 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
2060 let mut optional = HashSet::new();
2061 optional.insert("json_ld[].name".to_string());
2062 let mut array = HashSet::new();
2063 array.insert("json_ld".to_string());
2064 let result_fields = HashSet::new();
2065 let method_calls = HashSet::new();
2066 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
2067 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
2068 assert_eq!(var, "json_ld_name");
2069 }
2070
2071 #[test]
2072 fn test_direct_field_no_alias() {
2073 let r = make_resolver();
2074 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2075 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
2076 }
2077
2078 #[test]
2079 fn test_accessor_rust_with_optionals() {
2080 let r = make_resolver_with_doc_optional();
2081 assert_eq!(
2082 r.accessor("title", "rust", "result"),
2083 "result.metadata.document.as_ref().unwrap().title"
2084 );
2085 }
2086
2087 #[test]
2088 fn test_accessor_csharp_with_optionals() {
2089 let r = make_resolver_with_doc_optional();
2090 assert_eq!(
2091 r.accessor("title", "csharp", "result"),
2092 "result.Metadata.Document!.Title"
2093 );
2094 }
2095
2096 #[test]
2097 fn test_accessor_rust_non_optional_field() {
2098 let r = make_resolver();
2099 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2100 }
2101
2102 #[test]
2103 fn test_accessor_csharp_non_optional_field() {
2104 let r = make_resolver();
2105 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
2106 }
2107
2108 #[test]
2109 fn test_accessor_rust_method_call() {
2110 let mut fields = HashMap::new();
2112 fields.insert(
2113 "excel_sheet_count".to_string(),
2114 "metadata.format.excel.sheet_count".to_string(),
2115 );
2116 let mut optional = HashSet::new();
2117 optional.insert("metadata.format".to_string());
2118 optional.insert("metadata.format.excel".to_string());
2119 let mut method_calls = HashSet::new();
2120 method_calls.insert("metadata.format.excel".to_string());
2121 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
2122 assert_eq!(
2123 r.accessor("excel_sheet_count", "rust", "result"),
2124 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
2125 );
2126 }
2127
2128 fn make_php_getter_resolver() -> FieldResolver {
2133 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2134 getters.insert(
2135 "Root".to_string(),
2136 ["metadata".to_string(), "links".to_string()].into_iter().collect(),
2137 );
2138 let map = PhpGetterMap {
2139 getters,
2140 field_types: HashMap::new(),
2141 root_type: Some("Root".to_string()),
2142 };
2143 FieldResolver::new_with_php_getters(
2144 &HashMap::new(),
2145 &HashSet::new(),
2146 &HashSet::new(),
2147 &HashSet::new(),
2148 &HashSet::new(),
2149 &HashMap::new(),
2150 map,
2151 )
2152 }
2153
2154 #[test]
2155 fn render_php_uses_getter_method_for_non_scalar_field() {
2156 let r = make_php_getter_resolver();
2157 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
2158 }
2159
2160 #[test]
2161 fn render_php_uses_property_for_scalar_field() {
2162 let r = make_php_getter_resolver();
2163 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2164 }
2165
2166 #[test]
2167 fn render_php_nested_non_scalar_uses_getter_then_property() {
2168 let mut fields = HashMap::new();
2169 fields.insert("title".to_string(), "metadata.title".to_string());
2170 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2171 getters.insert("Root".to_string(), ["metadata".to_string()].into_iter().collect());
2172 getters.insert("Metadata".to_string(), HashSet::new());
2174 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2175 field_types.insert(
2176 "Root".to_string(),
2177 [("metadata".to_string(), "Metadata".to_string())].into_iter().collect(),
2178 );
2179 let map = PhpGetterMap {
2180 getters,
2181 field_types,
2182 root_type: Some("Root".to_string()),
2183 };
2184 let r = FieldResolver::new_with_php_getters(
2185 &fields,
2186 &HashSet::new(),
2187 &HashSet::new(),
2188 &HashSet::new(),
2189 &HashSet::new(),
2190 &HashMap::new(),
2191 map,
2192 );
2193 assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
2195 }
2196
2197 #[test]
2198 fn render_php_array_field_uses_getter_when_non_scalar() {
2199 let mut fields = HashMap::new();
2200 fields.insert("first_link".to_string(), "links[0]".to_string());
2201 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2202 getters.insert("Root".to_string(), ["links".to_string()].into_iter().collect());
2203 let map = PhpGetterMap {
2204 getters,
2205 field_types: HashMap::new(),
2206 root_type: Some("Root".to_string()),
2207 };
2208 let r = FieldResolver::new_with_php_getters(
2209 &fields,
2210 &HashSet::new(),
2211 &HashSet::new(),
2212 &HashSet::new(),
2213 &HashSet::new(),
2214 &HashMap::new(),
2215 map,
2216 );
2217 assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
2218 }
2219
2220 #[test]
2221 fn render_php_falls_back_to_property_when_getter_fields_empty() {
2222 let r = FieldResolver::new(
2225 &HashMap::new(),
2226 &HashSet::new(),
2227 &HashSet::new(),
2228 &HashSet::new(),
2229 &HashSet::new(),
2230 );
2231 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2232 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
2233 }
2234
2235 #[test]
2239 fn render_php_with_getters_distinguishes_same_field_name_on_different_types() {
2240 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2241 getters.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2243 getters.insert("B".to_string(), HashSet::new());
2245 let map_a = PhpGetterMap {
2246 getters: getters.clone(),
2247 field_types: HashMap::new(),
2248 root_type: Some("A".to_string()),
2249 };
2250 let map_b = PhpGetterMap {
2251 getters,
2252 field_types: HashMap::new(),
2253 root_type: Some("B".to_string()),
2254 };
2255 let r_a = FieldResolver::new_with_php_getters(
2256 &HashMap::new(),
2257 &HashSet::new(),
2258 &HashSet::new(),
2259 &HashSet::new(),
2260 &HashSet::new(),
2261 &HashMap::new(),
2262 map_a,
2263 );
2264 let r_b = FieldResolver::new_with_php_getters(
2265 &HashMap::new(),
2266 &HashSet::new(),
2267 &HashSet::new(),
2268 &HashSet::new(),
2269 &HashSet::new(),
2270 &HashMap::new(),
2271 map_b,
2272 );
2273 assert_eq!(r_a.accessor("content", "php", "$a"), "$a->getContent()");
2274 assert_eq!(r_b.accessor("content", "php", "$b"), "$b->content");
2275 }
2276
2277 #[test]
2281 fn render_php_with_getters_chains_through_correct_type() {
2282 let mut fields = HashMap::new();
2283 fields.insert("nested_content".to_string(), "inner.content".to_string());
2284 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2285 getters.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2287 getters.insert("B".to_string(), HashSet::new());
2289 getters.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2292 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2293 field_types.insert(
2294 "Outer".to_string(),
2295 [("inner".to_string(), "B".to_string())].into_iter().collect(),
2296 );
2297 let map = PhpGetterMap {
2298 getters,
2299 field_types,
2300 root_type: Some("Outer".to_string()),
2301 };
2302 let r = FieldResolver::new_with_php_getters(
2303 &fields,
2304 &HashSet::new(),
2305 &HashSet::new(),
2306 &HashSet::new(),
2307 &HashSet::new(),
2308 &HashMap::new(),
2309 map,
2310 );
2311 assert_eq!(
2312 r.accessor("nested_content", "php", "$result"),
2313 "$result->getInner()->content"
2314 );
2315 }
2316}