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 pub all_fields: HashMap<String, HashSet<String>>,
60}
61
62impl PhpGetterMap {
63 pub fn needs_getter(&self, owner_type: Option<&str>, field_name: &str) -> bool {
70 if let Some(t) = owner_type {
71 let owner_has_field = self.all_fields.get(t).is_some_and(|s| s.contains(field_name));
76 if owner_has_field {
77 if let Some(fields) = self.getters.get(t) {
78 return fields.contains(field_name);
79 }
80 }
81 }
82 self.getters.values().any(|set| set.contains(field_name))
83 }
84
85 pub fn advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
88 let owner = owner_type?;
89 self.field_types.get(owner).and_then(|m| m.get(field_name).cloned())
90 }
91
92 pub fn is_empty(&self) -> bool {
95 self.getters.is_empty()
96 }
97}
98
99#[derive(Debug, Clone)]
101enum PathSegment {
102 Field(String),
104 ArrayField { name: String, index: usize },
109 MapAccess { field: String, key: String },
111 Length,
113}
114
115impl FieldResolver {
116 pub fn new(
120 fields: &HashMap<String, String>,
121 optional: &HashSet<String>,
122 result_fields: &HashSet<String>,
123 array_fields: &HashSet<String>,
124 method_calls: &HashSet<String>,
125 ) -> Self {
126 Self {
127 aliases: fields.clone(),
128 optional_fields: optional.clone(),
129 result_fields: result_fields.clone(),
130 array_fields: array_fields.clone(),
131 method_calls: method_calls.clone(),
132 error_field_aliases: HashMap::new(),
133 php_getter_map: PhpGetterMap::default(),
134 }
135 }
136
137 pub fn new_with_error_aliases(
143 fields: &HashMap<String, String>,
144 optional: &HashSet<String>,
145 result_fields: &HashSet<String>,
146 array_fields: &HashSet<String>,
147 method_calls: &HashSet<String>,
148 error_field_aliases: &HashMap<String, String>,
149 ) -> Self {
150 Self {
151 aliases: fields.clone(),
152 optional_fields: optional.clone(),
153 result_fields: result_fields.clone(),
154 array_fields: array_fields.clone(),
155 method_calls: method_calls.clone(),
156 error_field_aliases: error_field_aliases.clone(),
157 php_getter_map: PhpGetterMap::default(),
158 }
159 }
160
161 pub fn new_with_php_getters(
176 fields: &HashMap<String, String>,
177 optional: &HashSet<String>,
178 result_fields: &HashSet<String>,
179 array_fields: &HashSet<String>,
180 method_calls: &HashSet<String>,
181 error_field_aliases: &HashMap<String, String>,
182 php_getter_map: PhpGetterMap,
183 ) -> Self {
184 Self {
185 aliases: fields.clone(),
186 optional_fields: optional.clone(),
187 result_fields: result_fields.clone(),
188 array_fields: array_fields.clone(),
189 method_calls: method_calls.clone(),
190 error_field_aliases: error_field_aliases.clone(),
191 php_getter_map,
192 }
193 }
194
195 pub fn resolve<'a>(&'a self, fixture_field: &'a str) -> &'a str {
198 self.aliases
199 .get(fixture_field)
200 .map(String::as_str)
201 .unwrap_or(fixture_field)
202 }
203
204 pub fn is_optional(&self, field: &str) -> bool {
206 if self.optional_fields.contains(field) {
207 return true;
208 }
209 let index_normalized = normalize_numeric_indices(field);
210 if index_normalized != field && self.optional_fields.contains(index_normalized.as_str()) {
211 return true;
212 }
213 let de_indexed = strip_numeric_indices(field);
216 if de_indexed != field && self.optional_fields.contains(de_indexed.as_str()) {
217 return true;
218 }
219 let normalized = field.replace("[].", ".");
220 if normalized != field && self.optional_fields.contains(normalized.as_str()) {
221 return true;
222 }
223 for af in &self.array_fields {
224 if let Some(rest) = field.strip_prefix(af.as_str()) {
225 if let Some(rest) = rest.strip_prefix('.') {
226 let with_bracket = format!("{af}[].{rest}");
227 if self.optional_fields.contains(with_bracket.as_str()) {
228 return true;
229 }
230 }
231 }
232 }
233 false
234 }
235
236 pub fn has_alias(&self, fixture_field: &str) -> bool {
238 self.aliases.contains_key(fixture_field)
239 }
240
241 pub fn has_explicit_field(&self, field_name: &str) -> bool {
247 if self.result_fields.is_empty() {
248 return false;
249 }
250 self.result_fields.contains(field_name)
251 }
252
253 pub fn is_valid_for_result(&self, fixture_field: &str) -> bool {
255 if self.result_fields.is_empty() {
256 return true;
257 }
258 let resolved = self.resolve(fixture_field);
259 let first_segment = resolved.split('.').next().unwrap_or(resolved);
260 let first_segment = first_segment.split('[').next().unwrap_or(first_segment);
261 self.result_fields.contains(first_segment)
262 }
263
264 pub fn is_array(&self, field: &str) -> bool {
266 self.array_fields.contains(field)
267 }
268
269 pub fn is_collection_root(&self, field: &str) -> bool {
282 let prefix = format!("{field}[");
283 self.array_fields.iter().any(|af| af.starts_with(&prefix))
284 || self.optional_fields.iter().any(|of| of.starts_with(&prefix))
285 }
286
287 pub fn tagged_union_split(&self, fixture_field: &str) -> Option<(String, String, String)> {
299 let resolved = self.resolve(fixture_field);
300 let segments: Vec<&str> = resolved.split('.').collect();
301 let mut path_so_far = String::new();
302 for (i, seg) in segments.iter().enumerate() {
303 if !path_so_far.is_empty() {
304 path_so_far.push('.');
305 }
306 path_so_far.push_str(seg);
307 if self.method_calls.contains(&path_so_far) {
308 let prefix = segments[..i].join(".");
310 let variant = (*seg).to_string();
311 let suffix = segments[i + 1..].join(".");
312 return Some((prefix, variant, suffix));
313 }
314 }
315 None
316 }
317
318 pub fn has_map_access(&self, fixture_field: &str) -> bool {
320 let resolved = self.resolve(fixture_field);
321 let segments = parse_path(resolved);
322 segments.iter().any(|s| {
323 if let PathSegment::MapAccess { key, .. } = s {
324 !key.chars().all(|c| c.is_ascii_digit())
325 } else {
326 false
327 }
328 })
329 }
330
331 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
333 let resolved = self.resolve(fixture_field);
334 let segments = parse_path(resolved);
335 let segments = self.inject_array_indexing(segments);
336 match language {
337 "java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
338 "kotlin" => render_kotlin_with_optionals(&segments, result_var, &self.optional_fields),
339 "kotlin_android" => render_kotlin_android_with_optionals(&segments, result_var, &self.optional_fields),
342 "rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
343 "csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
344 "zig" => render_zig_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
345 "swift" => render_swift_with_optionals(&segments, result_var, &self.optional_fields),
346 "dart" => render_dart_with_optionals(&segments, result_var, &self.optional_fields),
347 "php" if !self.php_getter_map.is_empty() => {
348 render_php_with_getters(&segments, result_var, &self.php_getter_map)
349 }
350 _ => render_accessor(&segments, language, result_var),
351 }
352 }
353
354 pub fn accessor_for_error(&self, sub_field: &str, language: &str, err_var: &str) -> String {
368 let resolved = self
369 .error_field_aliases
370 .get(sub_field)
371 .map(String::as_str)
372 .unwrap_or(sub_field);
373 let segments = parse_path(resolved);
374 match language {
377 "rust" => render_rust_with_optionals(&segments, err_var, &self.optional_fields, &self.method_calls),
378 _ => render_accessor(&segments, language, err_var),
379 }
380 }
381
382 pub fn has_error_aliases(&self) -> bool {
389 !self.error_field_aliases.is_empty()
390 }
391
392 fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
393 if self.array_fields.is_empty() {
394 return segments;
395 }
396 let len = segments.len();
397 let mut result = Vec::with_capacity(len);
398 let mut path_so_far = String::new();
399 for i in 0..len {
400 let seg = &segments[i];
401 match seg {
402 PathSegment::Field(f) => {
403 if !path_so_far.is_empty() {
404 path_so_far.push('.');
405 }
406 path_so_far.push_str(f);
407 let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
408 if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
409 result.push(PathSegment::ArrayField {
411 name: f.clone(),
412 index: 0,
413 });
414 } else {
415 result.push(seg.clone());
416 }
417 }
418 PathSegment::ArrayField { .. } => {
421 result.push(seg.clone());
422 }
423 PathSegment::MapAccess { field, key } => {
424 if !path_so_far.is_empty() {
425 path_so_far.push('.');
426 }
427 path_so_far.push_str(field);
428 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
429 if is_numeric && self.array_fields.contains(&path_so_far) {
430 let index: usize = key.parse().unwrap_or(0);
432 result.push(PathSegment::ArrayField {
433 name: field.clone(),
434 index,
435 });
436 } else {
437 result.push(seg.clone());
438 }
439 }
440 _ => {
441 result.push(seg.clone());
442 }
443 }
444 }
445 result
446 }
447
448 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
450 let resolved = self.resolve(fixture_field);
451 if !self.is_optional(resolved) {
452 return None;
453 }
454 let segments = parse_path(resolved);
455 let segments = self.inject_array_indexing(segments);
456 let local_var = {
461 let raw = resolved.replace(['.', '['], "_").replace(']', "");
462 let mut collapsed = String::with_capacity(raw.len());
463 let mut prev_underscore = false;
464 for ch in raw.chars() {
465 if ch == '_' {
466 if !prev_underscore {
467 collapsed.push('_');
468 }
469 prev_underscore = true;
470 } else {
471 collapsed.push(ch);
472 prev_underscore = false;
473 }
474 }
475 collapsed.trim_matches('_').to_string()
476 };
477 let accessor = render_accessor(&segments, "rust", result_var);
478 let has_map_access = segments.iter().any(|s| {
479 if let PathSegment::MapAccess { key, .. } = s {
480 !key.chars().all(|c| c.is_ascii_digit())
481 } else {
482 false
483 }
484 });
485 let is_array = self.is_array(resolved);
486 let binding = if has_map_access {
487 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
488 } else if is_array {
489 format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
490 } else {
491 format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
492 };
493 Some((binding, local_var))
494 }
495}
496
497fn strip_numeric_indices(path: &str) -> String {
502 let mut result = String::with_capacity(path.len());
503 let mut chars = path.chars().peekable();
504 while let Some(c) = chars.next() {
505 if c == '[' {
506 let mut key = String::new();
507 let mut closed = false;
508 for inner in chars.by_ref() {
509 if inner == ']' {
510 closed = true;
511 break;
512 }
513 key.push(inner);
514 }
515 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
516 } else {
518 result.push('[');
519 result.push_str(&key);
520 if closed {
521 result.push(']');
522 }
523 }
524 } else {
525 result.push(c);
526 }
527 }
528 while result.contains("..") {
530 result = result.replace("..", ".");
531 }
532 if result.starts_with('.') {
533 result.remove(0);
534 }
535 result
536}
537
538fn normalize_numeric_indices(path: &str) -> String {
539 let mut result = String::with_capacity(path.len());
540 let mut chars = path.chars().peekable();
541 while let Some(c) = chars.next() {
542 if c == '[' {
543 let mut key = String::new();
544 let mut closed = false;
545 for inner in chars.by_ref() {
546 if inner == ']' {
547 closed = true;
548 break;
549 }
550 key.push(inner);
551 }
552 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
553 result.push_str("[0]");
554 } else {
555 result.push('[');
556 result.push_str(&key);
557 if closed {
558 result.push(']');
559 }
560 }
561 } else {
562 result.push(c);
563 }
564 }
565 result
566}
567
568fn parse_path(path: &str) -> Vec<PathSegment> {
569 let mut segments = Vec::new();
570 for part in path.split('.') {
571 if part == "length" || part == "count" || part == "size" {
572 segments.push(PathSegment::Length);
573 } else if let Some(bracket_pos) = part.find('[') {
574 let name = part[..bracket_pos].to_string();
575 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
576 if key.is_empty() {
577 segments.push(PathSegment::ArrayField { name, index: 0 });
579 } else if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
580 let index: usize = key.parse().unwrap_or(0);
582 segments.push(PathSegment::ArrayField { name, index });
583 } else {
584 segments.push(PathSegment::MapAccess { field: name, key });
586 }
587 } else {
588 segments.push(PathSegment::Field(part.to_string()));
589 }
590 }
591 segments
592}
593
594fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
595 match language {
596 "rust" => render_rust(segments, result_var),
597 "python" => render_dot_access(segments, result_var, "python"),
598 "typescript" | "node" => render_typescript(segments, result_var),
599 "wasm" => render_wasm(segments, result_var),
600 "go" => render_go(segments, result_var),
601 "java" => render_java(segments, result_var),
602 "kotlin" => render_kotlin(segments, result_var),
603 "kotlin_android" => render_kotlin_android(segments, result_var),
604 "csharp" => render_pascal_dot(segments, result_var),
605 "ruby" => render_dot_access(segments, result_var, "ruby"),
606 "php" => render_php(segments, result_var),
607 "elixir" => render_dot_access(segments, result_var, "elixir"),
608 "r" => render_r(segments, result_var),
609 "c" => render_c(segments, result_var),
610 "swift" => render_swift(segments, result_var),
611 "dart" => render_dart(segments, result_var),
612 _ => render_dot_access(segments, result_var, language),
613 }
614}
615
616fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
628 let mut out = result_var.to_string();
629 for seg in segments {
630 match seg {
631 PathSegment::Field(f) => {
632 out.push('.');
633 out.push_str(f);
634 }
635 PathSegment::ArrayField { name, index } => {
636 out.push('.');
637 out.push_str(name);
638 out.push_str(&format!("[{index}]"));
639 }
640 PathSegment::MapAccess { field, key } => {
641 out.push('.');
642 out.push_str(field);
643 if key.chars().all(|c| c.is_ascii_digit()) {
644 out.push_str(&format!("[{key}]"));
645 } else {
646 out.push_str(&format!("[\"{key}\"]"));
647 }
648 }
649 PathSegment::Length => {
650 out.push_str(".count");
651 }
652 }
653 }
654 out
655}
656
657fn render_swift_with_optionals(
667 segments: &[PathSegment],
668 result_var: &str,
669 optional_fields: &HashSet<String>,
670) -> String {
671 let mut out = result_var.to_string();
672 let mut path_so_far = String::new();
673 let total = segments.len();
674 for (i, seg) in segments.iter().enumerate() {
675 let is_leaf = i == total - 1;
676 match seg {
677 PathSegment::Field(f) => {
678 if !path_so_far.is_empty() {
679 path_so_far.push('.');
680 }
681 path_so_far.push_str(f);
682 out.push('.');
683 out.push_str(f);
684 if !is_leaf && optional_fields.contains(&path_so_far) {
688 out.push('?');
689 }
690 }
691 PathSegment::ArrayField { name, index } => {
692 if !path_so_far.is_empty() {
693 path_so_far.push('.');
694 }
695 path_so_far.push_str(name);
696 let is_optional = optional_fields.contains(&path_so_far);
697 out.push('.');
698 out.push_str(name);
699 if is_optional {
700 out.push_str(&format!("?[{index}]"));
702 } else {
703 out.push_str(&format!("[{index}]"));
704 }
705 path_so_far.push_str("[0]");
706 let _ = is_leaf;
707 }
708 PathSegment::MapAccess { field, key } => {
709 if !path_so_far.is_empty() {
710 path_so_far.push('.');
711 }
712 path_so_far.push_str(field);
713 out.push('.');
714 out.push_str(field);
715 if key.chars().all(|c| c.is_ascii_digit()) {
716 out.push_str(&format!("[{key}]"));
717 } else {
718 out.push_str(&format!("[\"{key}\"]"));
719 }
720 }
721 PathSegment::Length => {
722 out.push_str(".count");
723 }
724 }
725 }
726 out
727}
728
729fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
730 let mut out = result_var.to_string();
731 for seg in segments {
732 match seg {
733 PathSegment::Field(f) => {
734 out.push('.');
735 out.push_str(&f.to_snake_case());
736 }
737 PathSegment::ArrayField { name, index } => {
738 out.push('.');
739 out.push_str(&name.to_snake_case());
740 out.push_str(&format!("[{index}]"));
741 }
742 PathSegment::MapAccess { field, key } => {
743 out.push('.');
744 out.push_str(&field.to_snake_case());
745 if key.chars().all(|c| c.is_ascii_digit()) {
746 out.push_str(&format!("[{key}]"));
747 } else {
748 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
749 }
750 }
751 PathSegment::Length => {
752 out.push_str(".len()");
753 }
754 }
755 }
756 out
757}
758
759fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
760 let mut out = result_var.to_string();
761 for seg in segments {
762 match seg {
763 PathSegment::Field(f) => {
764 out.push('.');
765 out.push_str(f);
766 }
767 PathSegment::ArrayField { name, index } => {
768 if language == "elixir" {
769 let current = std::mem::take(&mut out);
770 out = format!("Enum.at({current}.{name}, {index})");
771 } else {
772 out.push('.');
773 out.push_str(name);
774 out.push_str(&format!("[{index}]"));
775 }
776 }
777 PathSegment::MapAccess { field, key } => {
778 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
779 if is_numeric && language == "elixir" {
780 let current = std::mem::take(&mut out);
781 out = format!("Enum.at({current}.{field}, {key})");
782 } else {
783 out.push('.');
784 out.push_str(field);
785 if is_numeric {
786 let idx: usize = key.parse().unwrap_or(0);
787 out.push_str(&format!("[{idx}]"));
788 } else if language == "elixir" || language == "ruby" {
789 out.push_str(&format!("[\"{key}\"]"));
792 } else {
793 out.push_str(&format!(".get(\"{key}\")"));
794 }
795 }
796 }
797 PathSegment::Length => match language {
798 "ruby" => out.push_str(".length"),
799 "elixir" => {
800 let current = std::mem::take(&mut out);
801 out = format!("length({current})");
802 }
803 "gleam" => {
804 let current = std::mem::take(&mut out);
805 out = format!("list.length({current})");
806 }
807 _ => {
808 let current = std::mem::take(&mut out);
809 out = format!("len({current})");
810 }
811 },
812 }
813 }
814 out
815}
816
817fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
818 let mut out = result_var.to_string();
819 for seg in segments {
820 match seg {
821 PathSegment::Field(f) => {
822 out.push('.');
823 out.push_str(&f.to_lower_camel_case());
824 }
825 PathSegment::ArrayField { name, index } => {
826 out.push('.');
827 out.push_str(&name.to_lower_camel_case());
828 out.push_str(&format!("[{index}]"));
829 }
830 PathSegment::MapAccess { field, key } => {
831 out.push('.');
832 out.push_str(&field.to_lower_camel_case());
833 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
836 out.push_str(&format!("[{key}]"));
837 } else {
838 out.push_str(&format!("[\"{key}\"]"));
839 }
840 }
841 PathSegment::Length => {
842 out.push_str(".length");
843 }
844 }
845 }
846 out
847}
848
849fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
850 let mut out = result_var.to_string();
851 for seg in segments {
852 match seg {
853 PathSegment::Field(f) => {
854 out.push('.');
855 out.push_str(&f.to_lower_camel_case());
856 }
857 PathSegment::ArrayField { name, index } => {
858 out.push('.');
859 out.push_str(&name.to_lower_camel_case());
860 out.push_str(&format!("[{index}]"));
861 }
862 PathSegment::MapAccess { field, key } => {
863 out.push('.');
864 out.push_str(&field.to_lower_camel_case());
865 out.push_str(&format!(".get(\"{key}\")"));
866 }
867 PathSegment::Length => {
868 out.push_str(".length");
869 }
870 }
871 }
872 out
873}
874
875fn render_go(segments: &[PathSegment], result_var: &str) -> String {
876 let mut out = result_var.to_string();
877 for seg in segments {
878 match seg {
879 PathSegment::Field(f) => {
880 out.push('.');
881 out.push_str(&to_go_name(f));
882 }
883 PathSegment::ArrayField { name, index } => {
884 out.push('.');
885 out.push_str(&to_go_name(name));
886 out.push_str(&format!("[{index}]"));
887 }
888 PathSegment::MapAccess { field, key } => {
889 out.push('.');
890 out.push_str(&to_go_name(field));
891 if key.chars().all(|c| c.is_ascii_digit()) {
892 out.push_str(&format!("[{key}]"));
893 } else {
894 out.push_str(&format!("[\"{key}\"]"));
895 }
896 }
897 PathSegment::Length => {
898 let current = std::mem::take(&mut out);
899 out = format!("len({current})");
900 }
901 }
902 }
903 out
904}
905
906fn render_java(segments: &[PathSegment], result_var: &str) -> String {
907 let mut out = result_var.to_string();
908 for seg in segments {
909 match seg {
910 PathSegment::Field(f) => {
911 out.push('.');
912 out.push_str(&f.to_lower_camel_case());
913 out.push_str("()");
914 }
915 PathSegment::ArrayField { name, index } => {
916 out.push('.');
917 out.push_str(&name.to_lower_camel_case());
918 out.push_str(&format!("().get({index})"));
919 }
920 PathSegment::MapAccess { field, key } => {
921 out.push('.');
922 out.push_str(&field.to_lower_camel_case());
923 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
925 if is_numeric {
926 out.push_str(&format!("().get({key})"));
927 } else {
928 out.push_str(&format!("().get(\"{key}\")"));
929 }
930 }
931 PathSegment::Length => {
932 out.push_str(".size()");
933 }
934 }
935 }
936 out
937}
938
939fn kotlin_getter(name: &str) -> String {
944 let camel = name.to_lower_camel_case();
945 match camel.as_str() {
946 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
947 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
948 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
949 _ => camel,
950 }
951}
952
953fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
954 let mut out = result_var.to_string();
955 for seg in segments {
956 match seg {
957 PathSegment::Field(f) => {
958 out.push('.');
959 out.push_str(&kotlin_getter(f));
960 out.push_str("()");
961 }
962 PathSegment::ArrayField { name, index } => {
963 out.push('.');
964 out.push_str(&kotlin_getter(name));
965 if *index == 0 {
966 out.push_str("().first()");
967 } else {
968 out.push_str(&format!("().get({index})"));
969 }
970 }
971 PathSegment::MapAccess { field, key } => {
972 out.push('.');
973 out.push_str(&kotlin_getter(field));
974 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
975 if is_numeric {
976 out.push_str(&format!("().get({key})"));
977 } else {
978 out.push_str(&format!("().get(\"{key}\")"));
979 }
980 }
981 PathSegment::Length => {
982 out.push_str(".size");
983 }
984 }
985 }
986 out
987}
988
989fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
990 let mut out = result_var.to_string();
991 let mut path_so_far = String::new();
992 for (i, seg) in segments.iter().enumerate() {
993 let is_leaf = i == segments.len() - 1;
994 match seg {
995 PathSegment::Field(f) => {
996 if !path_so_far.is_empty() {
997 path_so_far.push('.');
998 }
999 path_so_far.push_str(f);
1000 out.push('.');
1001 out.push_str(&f.to_lower_camel_case());
1002 out.push_str("()");
1003 let _ = is_leaf;
1004 let _ = optional_fields;
1005 }
1006 PathSegment::ArrayField { name, index } => {
1007 if !path_so_far.is_empty() {
1008 path_so_far.push('.');
1009 }
1010 path_so_far.push_str(name);
1011 out.push('.');
1012 out.push_str(&name.to_lower_camel_case());
1013 out.push_str(&format!("().get({index})"));
1014 }
1015 PathSegment::MapAccess { field, key } => {
1016 if !path_so_far.is_empty() {
1017 path_so_far.push('.');
1018 }
1019 path_so_far.push_str(field);
1020 out.push('.');
1021 out.push_str(&field.to_lower_camel_case());
1022 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1024 if is_numeric {
1025 out.push_str(&format!("().get({key})"));
1026 } else {
1027 out.push_str(&format!("().get(\"{key}\")"));
1028 }
1029 }
1030 PathSegment::Length => {
1031 out.push_str(".size()");
1032 }
1033 }
1034 }
1035 out
1036}
1037
1038fn render_kotlin_with_optionals(
1053 segments: &[PathSegment],
1054 result_var: &str,
1055 optional_fields: &HashSet<String>,
1056) -> String {
1057 let mut out = result_var.to_string();
1058 let mut path_so_far = String::new();
1059 let mut prev_was_nullable = false;
1067 for seg in segments {
1068 let nav = if prev_was_nullable { "?." } else { "." };
1069 match seg {
1070 PathSegment::Field(f) => {
1071 if !path_so_far.is_empty() {
1072 path_so_far.push('.');
1073 }
1074 path_so_far.push_str(f);
1075 let is_optional = optional_fields.contains(&path_so_far);
1080 out.push_str(nav);
1081 out.push_str(&kotlin_getter(f));
1082 out.push_str("()");
1083 prev_was_nullable = prev_was_nullable || is_optional;
1084 }
1085 PathSegment::ArrayField { name, index } => {
1086 if !path_so_far.is_empty() {
1087 path_so_far.push('.');
1088 }
1089 path_so_far.push_str(name);
1090 let is_optional = optional_fields.contains(&path_so_far);
1091 out.push_str(nav);
1092 out.push_str(&kotlin_getter(name));
1093 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1094 if *index == 0 {
1095 out.push_str(&format!("(){safe}.first()"));
1096 } else {
1097 out.push_str(&format!("(){safe}.get({index})"));
1098 }
1099 path_so_far.push_str("[0]");
1103 prev_was_nullable = prev_was_nullable || is_optional;
1104 }
1105 PathSegment::MapAccess { field, key } => {
1106 if !path_so_far.is_empty() {
1107 path_so_far.push('.');
1108 }
1109 path_so_far.push_str(field);
1110 let is_optional = optional_fields.contains(&path_so_far);
1111 out.push_str(nav);
1112 out.push_str(&kotlin_getter(field));
1113 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1114 if is_numeric {
1115 if prev_was_nullable || is_optional {
1116 out.push_str(&format!("()?.get({key})"));
1117 } else {
1118 out.push_str(&format!("().get({key})"));
1119 }
1120 } else if prev_was_nullable || is_optional {
1121 out.push_str(&format!("()?.get(\"{key}\")"));
1122 } else {
1123 out.push_str(&format!("().get(\"{key}\")"));
1124 }
1125 prev_was_nullable = prev_was_nullable || is_optional;
1126 }
1127 PathSegment::Length => {
1128 let size_nav = if prev_was_nullable { "?" } else { "" };
1131 out.push_str(&format!("{size_nav}.size"));
1132 prev_was_nullable = false;
1133 }
1134 }
1135 }
1136 out
1137}
1138
1139fn render_kotlin_android_with_optionals(
1150 segments: &[PathSegment],
1151 result_var: &str,
1152 optional_fields: &HashSet<String>,
1153) -> String {
1154 let mut out = result_var.to_string();
1155 let mut path_so_far = String::new();
1156 let mut prev_was_nullable = false;
1157 for seg in segments {
1158 let nav = if prev_was_nullable { "?." } else { "." };
1159 match seg {
1160 PathSegment::Field(f) => {
1161 if !path_so_far.is_empty() {
1162 path_so_far.push('.');
1163 }
1164 path_so_far.push_str(f);
1165 let is_optional = optional_fields.contains(&path_so_far);
1166 out.push_str(nav);
1167 out.push_str(&kotlin_getter(f));
1169 prev_was_nullable = prev_was_nullable || is_optional;
1170 }
1171 PathSegment::ArrayField { name, index } => {
1172 if !path_so_far.is_empty() {
1173 path_so_far.push('.');
1174 }
1175 path_so_far.push_str(name);
1176 let is_optional = optional_fields.contains(&path_so_far);
1177 out.push_str(nav);
1178 out.push_str(&kotlin_getter(name));
1180 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1181 if *index == 0 {
1182 out.push_str(&format!("{safe}.first()"));
1183 } else {
1184 out.push_str(&format!("{safe}.get({index})"));
1185 }
1186 path_so_far.push_str("[0]");
1187 prev_was_nullable = prev_was_nullable || is_optional;
1188 }
1189 PathSegment::MapAccess { field, key } => {
1190 if !path_so_far.is_empty() {
1191 path_so_far.push('.');
1192 }
1193 path_so_far.push_str(field);
1194 let is_optional = optional_fields.contains(&path_so_far);
1195 out.push_str(nav);
1196 out.push_str(&kotlin_getter(field));
1198 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1199 if is_numeric {
1200 if prev_was_nullable || is_optional {
1201 out.push_str(&format!("?.get({key})"));
1202 } else {
1203 out.push_str(&format!(".get({key})"));
1204 }
1205 } else if prev_was_nullable || is_optional {
1206 out.push_str(&format!("?.get(\"{key}\")"));
1207 } else {
1208 out.push_str(&format!(".get(\"{key}\")"));
1209 }
1210 prev_was_nullable = prev_was_nullable || is_optional;
1211 }
1212 PathSegment::Length => {
1213 let size_nav = if prev_was_nullable { "?" } else { "" };
1214 out.push_str(&format!("{size_nav}.size"));
1215 prev_was_nullable = false;
1216 }
1217 }
1218 }
1219 out
1220}
1221
1222fn render_kotlin_android(segments: &[PathSegment], result_var: &str) -> String {
1226 let mut out = result_var.to_string();
1227 for seg in segments {
1228 match seg {
1229 PathSegment::Field(f) => {
1230 out.push('.');
1231 out.push_str(&kotlin_getter(f));
1232 }
1234 PathSegment::ArrayField { name, index } => {
1235 out.push('.');
1236 out.push_str(&kotlin_getter(name));
1237 if *index == 0 {
1238 out.push_str(".first()");
1239 } else {
1240 out.push_str(&format!(".get({index})"));
1241 }
1242 }
1243 PathSegment::MapAccess { field, key } => {
1244 out.push('.');
1245 out.push_str(&kotlin_getter(field));
1246 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1247 if is_numeric {
1248 out.push_str(&format!(".get({key})"));
1249 } else {
1250 out.push_str(&format!(".get(\"{key}\")"));
1251 }
1252 }
1253 PathSegment::Length => {
1254 out.push_str(".size");
1255 }
1256 }
1257 }
1258 out
1259}
1260
1261fn render_rust_with_optionals(
1267 segments: &[PathSegment],
1268 result_var: &str,
1269 optional_fields: &HashSet<String>,
1270 method_calls: &HashSet<String>,
1271) -> String {
1272 let mut out = result_var.to_string();
1273 let mut path_so_far = String::new();
1274 for (i, seg) in segments.iter().enumerate() {
1275 let is_leaf = i == segments.len() - 1;
1276 match seg {
1277 PathSegment::Field(f) => {
1278 if !path_so_far.is_empty() {
1279 path_so_far.push('.');
1280 }
1281 path_so_far.push_str(f);
1282 out.push('.');
1283 out.push_str(&f.to_snake_case());
1284 let is_method = method_calls.contains(&path_so_far);
1285 if is_method {
1286 out.push_str("()");
1287 if !is_leaf && optional_fields.contains(&path_so_far) {
1288 out.push_str(".as_ref().unwrap()");
1289 }
1290 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1291 out.push_str(".as_ref().unwrap()");
1292 }
1293 }
1294 PathSegment::ArrayField { name, index } => {
1295 if !path_so_far.is_empty() {
1296 path_so_far.push('.');
1297 }
1298 path_so_far.push_str(name);
1299 out.push('.');
1300 out.push_str(&name.to_snake_case());
1301 let path_with_idx = format!("{path_so_far}[0]");
1305 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1306 if is_opt {
1307 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1308 } else {
1309 out.push_str(&format!("[{index}]"));
1310 }
1311 path_so_far.push_str("[0]");
1316 }
1317 PathSegment::MapAccess { field, key } => {
1318 if !path_so_far.is_empty() {
1319 path_so_far.push('.');
1320 }
1321 path_so_far.push_str(field);
1322 out.push('.');
1323 out.push_str(&field.to_snake_case());
1324 if key.chars().all(|c| c.is_ascii_digit()) {
1325 let path_with_idx = format!("{path_so_far}[0]");
1327 let is_opt =
1328 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1329 if is_opt {
1330 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1331 } else {
1332 out.push_str(&format!("[{key}]"));
1333 }
1334 path_so_far.push_str("[0]");
1335 } else {
1336 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1337 }
1338 }
1339 PathSegment::Length => {
1340 out.push_str(".len()");
1341 }
1342 }
1343 }
1344 out
1345}
1346
1347fn render_zig_with_optionals(
1360 segments: &[PathSegment],
1361 result_var: &str,
1362 optional_fields: &HashSet<String>,
1363 method_calls: &HashSet<String>,
1364) -> String {
1365 let mut out = result_var.to_string();
1366 let mut path_so_far = String::new();
1367 for seg in segments {
1368 match seg {
1369 PathSegment::Field(f) => {
1370 if !path_so_far.is_empty() {
1371 path_so_far.push('.');
1372 }
1373 path_so_far.push_str(f);
1374 out.push('.');
1375 out.push_str(f);
1376 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1377 out.push_str(".?");
1378 }
1379 }
1380 PathSegment::ArrayField { name, index } => {
1381 if !path_so_far.is_empty() {
1382 path_so_far.push('.');
1383 }
1384 path_so_far.push_str(name);
1385 out.push('.');
1386 out.push_str(name);
1387 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1388 out.push_str(".?");
1389 }
1390 out.push_str(&format!("[{index}]"));
1391 }
1392 PathSegment::MapAccess { field, key } => {
1393 if !path_so_far.is_empty() {
1394 path_so_far.push('.');
1395 }
1396 path_so_far.push_str(field);
1397 out.push('.');
1398 out.push_str(field);
1399 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1400 out.push_str(".?");
1401 }
1402 if key.chars().all(|c| c.is_ascii_digit()) {
1403 out.push_str(&format!("[{key}]"));
1404 } else {
1405 out.push_str(&format!(".get(\"{key}\")"));
1406 }
1407 }
1408 PathSegment::Length => {
1409 out.push_str(".len");
1410 }
1411 }
1412 }
1413 out
1414}
1415
1416fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1417 let mut out = result_var.to_string();
1418 for seg in segments {
1419 match seg {
1420 PathSegment::Field(f) => {
1421 out.push('.');
1422 out.push_str(&f.to_pascal_case());
1423 }
1424 PathSegment::ArrayField { name, index } => {
1425 out.push('.');
1426 out.push_str(&name.to_pascal_case());
1427 out.push_str(&format!("[{index}]"));
1428 }
1429 PathSegment::MapAccess { field, key } => {
1430 out.push('.');
1431 out.push_str(&field.to_pascal_case());
1432 if key.chars().all(|c| c.is_ascii_digit()) {
1433 out.push_str(&format!("[{key}]"));
1434 } else {
1435 out.push_str(&format!("[\"{key}\"]"));
1436 }
1437 }
1438 PathSegment::Length => {
1439 out.push_str(".Count");
1440 }
1441 }
1442 }
1443 out
1444}
1445
1446fn render_csharp_with_optionals(
1447 segments: &[PathSegment],
1448 result_var: &str,
1449 optional_fields: &HashSet<String>,
1450) -> String {
1451 let mut out = result_var.to_string();
1452 let mut path_so_far = String::new();
1453 for (i, seg) in segments.iter().enumerate() {
1454 let is_leaf = i == segments.len() - 1;
1455 match seg {
1456 PathSegment::Field(f) => {
1457 if !path_so_far.is_empty() {
1458 path_so_far.push('.');
1459 }
1460 path_so_far.push_str(f);
1461 out.push('.');
1462 out.push_str(&f.to_pascal_case());
1463 if !is_leaf && optional_fields.contains(&path_so_far) {
1464 out.push('!');
1465 }
1466 }
1467 PathSegment::ArrayField { name, index } => {
1468 if !path_so_far.is_empty() {
1469 path_so_far.push('.');
1470 }
1471 path_so_far.push_str(name);
1472 out.push('.');
1473 out.push_str(&name.to_pascal_case());
1474 out.push_str(&format!("[{index}]"));
1475 }
1476 PathSegment::MapAccess { field, key } => {
1477 if !path_so_far.is_empty() {
1478 path_so_far.push('.');
1479 }
1480 path_so_far.push_str(field);
1481 out.push('.');
1482 out.push_str(&field.to_pascal_case());
1483 if key.chars().all(|c| c.is_ascii_digit()) {
1484 out.push_str(&format!("[{key}]"));
1485 } else {
1486 out.push_str(&format!("[\"{key}\"]"));
1487 }
1488 }
1489 PathSegment::Length => {
1490 out.push_str(".Count");
1491 }
1492 }
1493 }
1494 out
1495}
1496
1497fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1498 let mut out = result_var.to_string();
1499 for seg in segments {
1500 match seg {
1501 PathSegment::Field(f) => {
1502 out.push_str("->");
1503 out.push_str(&f.to_lower_camel_case());
1506 }
1507 PathSegment::ArrayField { name, index } => {
1508 out.push_str("->");
1509 out.push_str(&name.to_lower_camel_case());
1510 out.push_str(&format!("[{index}]"));
1511 }
1512 PathSegment::MapAccess { field, key } => {
1513 out.push_str("->");
1514 out.push_str(&field.to_lower_camel_case());
1515 out.push_str(&format!("[\"{key}\"]"));
1516 }
1517 PathSegment::Length => {
1518 let current = std::mem::take(&mut out);
1519 out = format!("count({current})");
1520 }
1521 }
1522 }
1523 out
1524}
1525
1526fn render_php_with_getters(segments: &[PathSegment], result_var: &str, getter_map: &PhpGetterMap) -> String {
1544 let mut out = result_var.to_string();
1545 let mut current_type: Option<String> = getter_map.root_type.clone();
1546 for seg in segments {
1547 match seg {
1548 PathSegment::Field(f) => {
1549 let camel = f.to_lower_camel_case();
1550 if getter_map.needs_getter(current_type.as_deref(), f.as_str()) {
1551 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 current_type = getter_map.advance(current_type.as_deref(), f.as_str());
1564 }
1565 PathSegment::ArrayField { name, index } => {
1566 let camel = name.to_lower_camel_case();
1567 if getter_map.needs_getter(current_type.as_deref(), name.as_str()) {
1568 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1569 out.push_str("->");
1570 out.push_str(&getter);
1571 out.push_str("()");
1572 } else {
1573 out.push_str("->");
1574 out.push_str(&camel);
1575 }
1576 out.push_str(&format!("[{index}]"));
1577 current_type = getter_map.advance(current_type.as_deref(), name.as_str());
1578 }
1579 PathSegment::MapAccess { field, key } => {
1580 let camel = field.to_lower_camel_case();
1581 if getter_map.needs_getter(current_type.as_deref(), field.as_str()) {
1582 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1583 out.push_str("->");
1584 out.push_str(&getter);
1585 out.push_str("()");
1586 } else {
1587 out.push_str("->");
1588 out.push_str(&camel);
1589 }
1590 out.push_str(&format!("[\"{key}\"]"));
1591 current_type = getter_map.advance(current_type.as_deref(), field.as_str());
1592 }
1593 PathSegment::Length => {
1594 let current = std::mem::take(&mut out);
1595 out = format!("count({current})");
1596 }
1597 }
1598 }
1599 out
1600}
1601
1602fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1603 let mut out = result_var.to_string();
1604 for seg in segments {
1605 match seg {
1606 PathSegment::Field(f) => {
1607 out.push('$');
1608 out.push_str(f);
1609 }
1610 PathSegment::ArrayField { name, index } => {
1611 out.push('$');
1612 out.push_str(name);
1613 out.push_str(&format!("[[{}]]", index + 1));
1615 }
1616 PathSegment::MapAccess { field, key } => {
1617 out.push('$');
1618 out.push_str(field);
1619 out.push_str(&format!("[[\"{key}\"]]"));
1620 }
1621 PathSegment::Length => {
1622 let current = std::mem::take(&mut out);
1623 out = format!("length({current})");
1624 }
1625 }
1626 }
1627 out
1628}
1629
1630fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1631 let mut parts = Vec::new();
1632 let mut trailing_length = false;
1633 for seg in segments {
1634 match seg {
1635 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1636 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1637 PathSegment::MapAccess { field, key } => {
1638 parts.push(field.to_snake_case());
1639 parts.push(key.clone());
1640 }
1641 PathSegment::Length => {
1642 trailing_length = true;
1643 }
1644 }
1645 }
1646 let suffix = parts.join("_");
1647 if trailing_length {
1648 format!("result_{suffix}_count({result_var})")
1649 } else {
1650 format!("result_{suffix}({result_var})")
1651 }
1652}
1653
1654fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
1661 let mut out = result_var.to_string();
1662 for seg in segments {
1663 match seg {
1664 PathSegment::Field(f) => {
1665 out.push('.');
1666 out.push_str(&f.to_lower_camel_case());
1667 }
1668 PathSegment::ArrayField { name, index } => {
1669 out.push('.');
1670 out.push_str(&name.to_lower_camel_case());
1671 out.push_str(&format!("[{index}]"));
1672 }
1673 PathSegment::MapAccess { field, key } => {
1674 out.push('.');
1675 out.push_str(&field.to_lower_camel_case());
1676 if key.chars().all(|c| c.is_ascii_digit()) {
1677 out.push_str(&format!("[{key}]"));
1678 } else {
1679 out.push_str(&format!("[\"{key}\"]"));
1680 }
1681 }
1682 PathSegment::Length => {
1683 out.push_str(".length");
1684 }
1685 }
1686 }
1687 out
1688}
1689
1690fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1696 let mut out = result_var.to_string();
1697 let mut path_so_far = String::new();
1698 let mut prev_was_nullable = false;
1699 for seg in segments {
1700 let nav = if prev_was_nullable { "?." } else { "." };
1701 match seg {
1702 PathSegment::Field(f) => {
1703 if !path_so_far.is_empty() {
1704 path_so_far.push('.');
1705 }
1706 path_so_far.push_str(f);
1707 let is_optional = optional_fields.contains(&path_so_far);
1708 out.push_str(nav);
1709 out.push_str(&f.to_lower_camel_case());
1710 prev_was_nullable = is_optional;
1711 }
1712 PathSegment::ArrayField { name, index } => {
1713 if !path_so_far.is_empty() {
1714 path_so_far.push('.');
1715 }
1716 path_so_far.push_str(name);
1717 let is_optional = optional_fields.contains(&path_so_far);
1718 out.push_str(nav);
1719 out.push_str(&name.to_lower_camel_case());
1720 if is_optional {
1724 out.push('!');
1725 }
1726 out.push_str(&format!("[{index}]"));
1727 prev_was_nullable = false;
1728 }
1729 PathSegment::MapAccess { field, key } => {
1730 if !path_so_far.is_empty() {
1731 path_so_far.push('.');
1732 }
1733 path_so_far.push_str(field);
1734 let is_optional = optional_fields.contains(&path_so_far);
1735 out.push_str(nav);
1736 out.push_str(&field.to_lower_camel_case());
1737 if key.chars().all(|c| c.is_ascii_digit()) {
1738 out.push_str(&format!("[{key}]"));
1739 } else {
1740 out.push_str(&format!("[\"{key}\"]"));
1741 }
1742 prev_was_nullable = is_optional;
1743 }
1744 PathSegment::Length => {
1745 out.push_str(nav);
1748 out.push_str("length");
1749 prev_was_nullable = false;
1750 }
1751 }
1752 }
1753 out
1754}
1755
1756#[cfg(test)]
1757mod tests {
1758 use super::*;
1759
1760 fn make_resolver() -> 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 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
1765 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
1766 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
1767 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
1768 let mut optional = HashSet::new();
1769 optional.insert("metadata.document.title".to_string());
1770 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1771 }
1772
1773 fn make_resolver_with_doc_optional() -> FieldResolver {
1774 let mut fields = HashMap::new();
1775 fields.insert("title".to_string(), "metadata.document.title".to_string());
1776 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1777 let mut optional = HashSet::new();
1778 optional.insert("document".to_string());
1779 optional.insert("metadata.document.title".to_string());
1780 optional.insert("metadata.document".to_string());
1781 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1782 }
1783
1784 #[test]
1785 fn test_resolve_alias() {
1786 let r = make_resolver();
1787 assert_eq!(r.resolve("title"), "metadata.document.title");
1788 }
1789
1790 #[test]
1791 fn test_resolve_passthrough() {
1792 let r = make_resolver();
1793 assert_eq!(r.resolve("content"), "content");
1794 }
1795
1796 #[test]
1797 fn test_is_optional() {
1798 let r = make_resolver();
1799 assert!(r.is_optional("metadata.document.title"));
1800 assert!(!r.is_optional("content"));
1801 }
1802
1803 #[test]
1804 fn test_accessor_rust_struct() {
1805 let r = make_resolver();
1806 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
1807 }
1808
1809 #[test]
1810 fn test_accessor_rust_map() {
1811 let r = make_resolver();
1812 assert_eq!(
1813 r.accessor("tags", "rust", "result"),
1814 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
1815 );
1816 }
1817
1818 #[test]
1819 fn test_accessor_python() {
1820 let r = make_resolver();
1821 assert_eq!(
1822 r.accessor("title", "python", "result"),
1823 "result.metadata.document.title"
1824 );
1825 }
1826
1827 #[test]
1828 fn test_accessor_go() {
1829 let r = make_resolver();
1830 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
1831 }
1832
1833 #[test]
1834 fn test_accessor_go_initialism_fields() {
1835 let mut fields = std::collections::HashMap::new();
1836 fields.insert("content".to_string(), "html".to_string());
1837 fields.insert("link_url".to_string(), "links.url".to_string());
1838 let r = FieldResolver::new(
1839 &fields,
1840 &HashSet::new(),
1841 &HashSet::new(),
1842 &HashSet::new(),
1843 &HashSet::new(),
1844 );
1845 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
1846 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
1847 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
1848 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
1849 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
1850 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
1851 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
1852 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
1853 }
1854
1855 #[test]
1856 fn test_accessor_typescript() {
1857 let r = make_resolver();
1858 assert_eq!(
1859 r.accessor("title", "typescript", "result"),
1860 "result.metadata.document.title"
1861 );
1862 }
1863
1864 #[test]
1865 fn test_accessor_typescript_snake_to_camel() {
1866 let r = make_resolver();
1867 assert_eq!(
1868 r.accessor("og", "typescript", "result"),
1869 "result.metadata.document.openGraph"
1870 );
1871 assert_eq!(
1872 r.accessor("twitter", "typescript", "result"),
1873 "result.metadata.document.twitterCard"
1874 );
1875 assert_eq!(
1876 r.accessor("canonical", "typescript", "result"),
1877 "result.metadata.document.canonicalUrl"
1878 );
1879 }
1880
1881 #[test]
1882 fn test_accessor_typescript_map_snake_to_camel() {
1883 let r = make_resolver();
1884 assert_eq!(
1885 r.accessor("og_tag", "typescript", "result"),
1886 "result.metadata.openGraphTags[\"og_title\"]"
1887 );
1888 }
1889
1890 #[test]
1891 fn test_accessor_typescript_numeric_index_is_unquoted() {
1892 let mut fields = HashMap::new();
1896 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
1897 let r = FieldResolver::new(
1898 &fields,
1899 &HashSet::new(),
1900 &HashSet::new(),
1901 &HashSet::new(),
1902 &HashSet::new(),
1903 );
1904 assert_eq!(
1905 r.accessor("first_score", "typescript", "result"),
1906 "result.results[0].relevanceScore"
1907 );
1908 }
1909
1910 #[test]
1911 fn test_accessor_node_alias() {
1912 let r = make_resolver();
1913 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
1914 }
1915
1916 #[test]
1917 fn test_accessor_wasm_camel_case() {
1918 let r = make_resolver();
1919 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
1920 assert_eq!(
1921 r.accessor("twitter", "wasm", "result"),
1922 "result.metadata.document.twitterCard"
1923 );
1924 assert_eq!(
1925 r.accessor("canonical", "wasm", "result"),
1926 "result.metadata.document.canonicalUrl"
1927 );
1928 }
1929
1930 #[test]
1931 fn test_accessor_wasm_map_access() {
1932 let r = make_resolver();
1933 assert_eq!(
1934 r.accessor("og_tag", "wasm", "result"),
1935 "result.metadata.openGraphTags.get(\"og_title\")"
1936 );
1937 }
1938
1939 #[test]
1940 fn test_accessor_java() {
1941 let r = make_resolver();
1942 assert_eq!(
1943 r.accessor("title", "java", "result"),
1944 "result.metadata().document().title()"
1945 );
1946 }
1947
1948 #[test]
1949 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
1950 let mut fields = HashMap::new();
1951 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1952 fields.insert("node_count".to_string(), "nodes.length".to_string());
1953 let mut arrays = HashSet::new();
1954 arrays.insert("nodes".to_string());
1955 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
1956 assert_eq!(
1957 r.accessor("first_node_name", "kotlin", "result"),
1958 "result.nodes().first().name()"
1959 );
1960 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
1961 }
1962
1963 #[test]
1964 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
1965 let r = make_resolver_with_doc_optional();
1966 assert_eq!(
1967 r.accessor("title", "kotlin", "result"),
1968 "result.metadata().document()?.title()"
1969 );
1970 }
1971
1972 #[test]
1973 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
1974 let mut fields = HashMap::new();
1975 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1976 fields.insert("tag".to_string(), "tags[name]".to_string());
1977 let mut optional = HashSet::new();
1978 optional.insert("nodes".to_string());
1979 optional.insert("tags".to_string());
1980 let mut arrays = HashSet::new();
1981 arrays.insert("nodes".to_string());
1982 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
1983 assert_eq!(
1984 r.accessor("first_node_name", "kotlin", "result"),
1985 "result.nodes()?.first()?.name()"
1986 );
1987 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
1988 }
1989
1990 #[test]
1996 fn test_accessor_kotlin_optional_field_after_indexed_array() {
1997 let mut fields = HashMap::new();
2000 fields.insert(
2001 "tool_call_name".to_string(),
2002 "choices[0].message.tool_calls[0].function.name".to_string(),
2003 );
2004 let mut optional = HashSet::new();
2005 optional.insert("choices[0].message.tool_calls".to_string());
2006 let mut arrays = HashSet::new();
2007 arrays.insert("choices".to_string());
2008 arrays.insert("choices[0].message.tool_calls".to_string());
2009 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
2010 let expr = r.accessor("tool_call_name", "kotlin", "result");
2011 assert!(
2013 expr.contains("toolCalls()?.first()"),
2014 "expected toolCalls()?.first() for optional list, got: {expr}"
2015 );
2016 }
2017
2018 #[test]
2019 fn test_accessor_csharp() {
2020 let r = make_resolver();
2021 assert_eq!(
2022 r.accessor("title", "csharp", "result"),
2023 "result.Metadata.Document.Title"
2024 );
2025 }
2026
2027 #[test]
2028 fn test_accessor_php() {
2029 let r = make_resolver();
2030 assert_eq!(
2031 r.accessor("title", "php", "$result"),
2032 "$result->metadata->document->title"
2033 );
2034 }
2035
2036 #[test]
2037 fn test_accessor_r() {
2038 let r = make_resolver();
2039 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
2040 }
2041
2042 #[test]
2043 fn test_accessor_c() {
2044 let r = make_resolver();
2045 assert_eq!(
2046 r.accessor("title", "c", "result"),
2047 "result_metadata_document_title(result)"
2048 );
2049 }
2050
2051 #[test]
2052 fn test_rust_unwrap_binding() {
2053 let r = make_resolver();
2054 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
2055 assert_eq!(var, "metadata_document_title");
2056 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
2057 }
2058
2059 #[test]
2060 fn test_rust_unwrap_binding_non_optional() {
2061 let r = make_resolver();
2062 assert!(r.rust_unwrap_binding("content", "result").is_none());
2063 }
2064
2065 #[test]
2066 fn test_rust_unwrap_binding_collapses_double_underscore() {
2067 let mut aliases = HashMap::new();
2072 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
2073 let mut optional = HashSet::new();
2074 optional.insert("json_ld[].name".to_string());
2075 let mut array = HashSet::new();
2076 array.insert("json_ld".to_string());
2077 let result_fields = HashSet::new();
2078 let method_calls = HashSet::new();
2079 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
2080 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
2081 assert_eq!(var, "json_ld_name");
2082 }
2083
2084 #[test]
2085 fn test_direct_field_no_alias() {
2086 let r = make_resolver();
2087 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2088 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
2089 }
2090
2091 #[test]
2092 fn test_accessor_rust_with_optionals() {
2093 let r = make_resolver_with_doc_optional();
2094 assert_eq!(
2095 r.accessor("title", "rust", "result"),
2096 "result.metadata.document.as_ref().unwrap().title"
2097 );
2098 }
2099
2100 #[test]
2101 fn test_accessor_csharp_with_optionals() {
2102 let r = make_resolver_with_doc_optional();
2103 assert_eq!(
2104 r.accessor("title", "csharp", "result"),
2105 "result.Metadata.Document!.Title"
2106 );
2107 }
2108
2109 #[test]
2110 fn test_accessor_rust_non_optional_field() {
2111 let r = make_resolver();
2112 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2113 }
2114
2115 #[test]
2116 fn test_accessor_csharp_non_optional_field() {
2117 let r = make_resolver();
2118 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
2119 }
2120
2121 #[test]
2122 fn test_accessor_rust_method_call() {
2123 let mut fields = HashMap::new();
2125 fields.insert(
2126 "excel_sheet_count".to_string(),
2127 "metadata.format.excel.sheet_count".to_string(),
2128 );
2129 let mut optional = HashSet::new();
2130 optional.insert("metadata.format".to_string());
2131 optional.insert("metadata.format.excel".to_string());
2132 let mut method_calls = HashSet::new();
2133 method_calls.insert("metadata.format.excel".to_string());
2134 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
2135 assert_eq!(
2136 r.accessor("excel_sheet_count", "rust", "result"),
2137 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
2138 );
2139 }
2140
2141 fn make_php_getter_resolver() -> FieldResolver {
2146 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2147 getters.insert(
2148 "Root".to_string(),
2149 ["metadata".to_string(), "links".to_string()].into_iter().collect(),
2150 );
2151 let map = PhpGetterMap {
2152 getters,
2153 field_types: HashMap::new(),
2154 root_type: Some("Root".to_string()),
2155 all_fields: HashMap::new(),
2156 };
2157 FieldResolver::new_with_php_getters(
2158 &HashMap::new(),
2159 &HashSet::new(),
2160 &HashSet::new(),
2161 &HashSet::new(),
2162 &HashSet::new(),
2163 &HashMap::new(),
2164 map,
2165 )
2166 }
2167
2168 #[test]
2169 fn render_php_uses_getter_method_for_non_scalar_field() {
2170 let r = make_php_getter_resolver();
2171 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
2172 }
2173
2174 #[test]
2175 fn render_php_uses_property_for_scalar_field() {
2176 let r = make_php_getter_resolver();
2177 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2178 }
2179
2180 #[test]
2181 fn render_php_nested_non_scalar_uses_getter_then_property() {
2182 let mut fields = HashMap::new();
2183 fields.insert("title".to_string(), "metadata.title".to_string());
2184 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2185 getters.insert("Root".to_string(), ["metadata".to_string()].into_iter().collect());
2186 getters.insert("Metadata".to_string(), HashSet::new());
2188 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2189 field_types.insert(
2190 "Root".to_string(),
2191 [("metadata".to_string(), "Metadata".to_string())].into_iter().collect(),
2192 );
2193 let map = PhpGetterMap {
2194 getters,
2195 field_types,
2196 root_type: Some("Root".to_string()),
2197 all_fields: HashMap::new(),
2198 };
2199 let r = FieldResolver::new_with_php_getters(
2200 &fields,
2201 &HashSet::new(),
2202 &HashSet::new(),
2203 &HashSet::new(),
2204 &HashSet::new(),
2205 &HashMap::new(),
2206 map,
2207 );
2208 assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
2210 }
2211
2212 #[test]
2213 fn render_php_array_field_uses_getter_when_non_scalar() {
2214 let mut fields = HashMap::new();
2215 fields.insert("first_link".to_string(), "links[0]".to_string());
2216 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2217 getters.insert("Root".to_string(), ["links".to_string()].into_iter().collect());
2218 let map = PhpGetterMap {
2219 getters,
2220 field_types: HashMap::new(),
2221 root_type: Some("Root".to_string()),
2222 all_fields: HashMap::new(),
2223 };
2224 let r = FieldResolver::new_with_php_getters(
2225 &fields,
2226 &HashSet::new(),
2227 &HashSet::new(),
2228 &HashSet::new(),
2229 &HashSet::new(),
2230 &HashMap::new(),
2231 map,
2232 );
2233 assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
2234 }
2235
2236 #[test]
2237 fn render_php_falls_back_to_property_when_getter_fields_empty() {
2238 let r = FieldResolver::new(
2241 &HashMap::new(),
2242 &HashSet::new(),
2243 &HashSet::new(),
2244 &HashSet::new(),
2245 &HashSet::new(),
2246 );
2247 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2248 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
2249 }
2250
2251 #[test]
2255 fn render_php_with_getters_distinguishes_same_field_name_on_different_types() {
2256 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2257 getters.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2259 getters.insert("B".to_string(), HashSet::new());
2261 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2264 all_fields.insert("A".to_string(), ["content".to_string()].into_iter().collect());
2265 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2266 let map_a = PhpGetterMap {
2267 getters: getters.clone(),
2268 field_types: HashMap::new(),
2269 root_type: Some("A".to_string()),
2270 all_fields: all_fields.clone(),
2271 };
2272 let map_b = PhpGetterMap {
2273 getters,
2274 field_types: HashMap::new(),
2275 root_type: Some("B".to_string()),
2276 all_fields,
2277 };
2278 let r_a = FieldResolver::new_with_php_getters(
2279 &HashMap::new(),
2280 &HashSet::new(),
2281 &HashSet::new(),
2282 &HashSet::new(),
2283 &HashSet::new(),
2284 &HashMap::new(),
2285 map_a,
2286 );
2287 let r_b = FieldResolver::new_with_php_getters(
2288 &HashMap::new(),
2289 &HashSet::new(),
2290 &HashSet::new(),
2291 &HashSet::new(),
2292 &HashSet::new(),
2293 &HashMap::new(),
2294 map_b,
2295 );
2296 assert_eq!(r_a.accessor("content", "php", "$a"), "$a->getContent()");
2297 assert_eq!(r_b.accessor("content", "php", "$b"), "$b->content");
2298 }
2299
2300 #[test]
2304 fn render_php_with_getters_chains_through_correct_type() {
2305 let mut fields = HashMap::new();
2306 fields.insert("nested_content".to_string(), "inner.content".to_string());
2307 let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
2308 getters.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2310 getters.insert("B".to_string(), HashSet::new());
2312 getters.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2315 let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
2316 field_types.insert(
2317 "Outer".to_string(),
2318 [("inner".to_string(), "B".to_string())].into_iter().collect(),
2319 );
2320 let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
2321 all_fields.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
2322 all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
2323 all_fields.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
2324 let map = PhpGetterMap {
2325 getters,
2326 field_types,
2327 root_type: Some("Outer".to_string()),
2328 all_fields,
2329 };
2330 let r = FieldResolver::new_with_php_getters(
2331 &fields,
2332 &HashSet::new(),
2333 &HashSet::new(),
2334 &HashSet::new(),
2335 &HashSet::new(),
2336 &HashMap::new(),
2337 map,
2338 );
2339 assert_eq!(
2340 r.accessor("nested_content", "php", "$result"),
2341 "$result->getInner()->content"
2342 );
2343 }
2344}