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_fields: HashSet<String>,
26}
27
28#[derive(Debug, Clone)]
30enum PathSegment {
31 Field(String),
33 ArrayField { name: String, index: usize },
38 MapAccess { field: String, key: String },
40 Length,
42}
43
44impl FieldResolver {
45 pub fn new(
49 fields: &HashMap<String, String>,
50 optional: &HashSet<String>,
51 result_fields: &HashSet<String>,
52 array_fields: &HashSet<String>,
53 method_calls: &HashSet<String>,
54 ) -> Self {
55 Self {
56 aliases: fields.clone(),
57 optional_fields: optional.clone(),
58 result_fields: result_fields.clone(),
59 array_fields: array_fields.clone(),
60 method_calls: method_calls.clone(),
61 error_field_aliases: HashMap::new(),
62 php_getter_fields: HashSet::new(),
63 }
64 }
65
66 pub fn new_with_error_aliases(
72 fields: &HashMap<String, String>,
73 optional: &HashSet<String>,
74 result_fields: &HashSet<String>,
75 array_fields: &HashSet<String>,
76 method_calls: &HashSet<String>,
77 error_field_aliases: &HashMap<String, String>,
78 ) -> Self {
79 Self {
80 aliases: fields.clone(),
81 optional_fields: optional.clone(),
82 result_fields: result_fields.clone(),
83 array_fields: array_fields.clone(),
84 method_calls: method_calls.clone(),
85 error_field_aliases: error_field_aliases.clone(),
86 php_getter_fields: HashSet::new(),
87 }
88 }
89
90 pub fn new_with_php_getters(
98 fields: &HashMap<String, String>,
99 optional: &HashSet<String>,
100 result_fields: &HashSet<String>,
101 array_fields: &HashSet<String>,
102 method_calls: &HashSet<String>,
103 error_field_aliases: &HashMap<String, String>,
104 php_getter_fields: &HashSet<String>,
105 ) -> Self {
106 Self {
107 aliases: fields.clone(),
108 optional_fields: optional.clone(),
109 result_fields: result_fields.clone(),
110 array_fields: array_fields.clone(),
111 method_calls: method_calls.clone(),
112 error_field_aliases: error_field_aliases.clone(),
113 php_getter_fields: php_getter_fields.clone(),
114 }
115 }
116
117 pub fn resolve<'a>(&'a self, fixture_field: &'a str) -> &'a str {
120 self.aliases
121 .get(fixture_field)
122 .map(String::as_str)
123 .unwrap_or(fixture_field)
124 }
125
126 pub fn is_optional(&self, field: &str) -> bool {
128 if self.optional_fields.contains(field) {
129 return true;
130 }
131 let index_normalized = normalize_numeric_indices(field);
132 if index_normalized != field && self.optional_fields.contains(index_normalized.as_str()) {
133 return true;
134 }
135 let de_indexed = strip_numeric_indices(field);
138 if de_indexed != field && self.optional_fields.contains(de_indexed.as_str()) {
139 return true;
140 }
141 let normalized = field.replace("[].", ".");
142 if normalized != field && self.optional_fields.contains(normalized.as_str()) {
143 return true;
144 }
145 for af in &self.array_fields {
146 if let Some(rest) = field.strip_prefix(af.as_str()) {
147 if let Some(rest) = rest.strip_prefix('.') {
148 let with_bracket = format!("{af}[].{rest}");
149 if self.optional_fields.contains(with_bracket.as_str()) {
150 return true;
151 }
152 }
153 }
154 }
155 false
156 }
157
158 pub fn has_alias(&self, fixture_field: &str) -> bool {
160 self.aliases.contains_key(fixture_field)
161 }
162
163 pub fn has_explicit_field(&self, field_name: &str) -> bool {
169 if self.result_fields.is_empty() {
170 return false;
171 }
172 self.result_fields.contains(field_name)
173 }
174
175 pub fn is_valid_for_result(&self, fixture_field: &str) -> bool {
177 if self.result_fields.is_empty() {
178 return true;
179 }
180 let resolved = self.resolve(fixture_field);
181 let first_segment = resolved.split('.').next().unwrap_or(resolved);
182 let first_segment = first_segment.split('[').next().unwrap_or(first_segment);
183 self.result_fields.contains(first_segment)
184 }
185
186 pub fn is_array(&self, field: &str) -> bool {
188 self.array_fields.contains(field)
189 }
190
191 pub fn is_collection_root(&self, field: &str) -> bool {
204 let prefix = format!("{field}[");
205 self.array_fields.iter().any(|af| af.starts_with(&prefix))
206 || self.optional_fields.iter().any(|of| of.starts_with(&prefix))
207 }
208
209 pub fn tagged_union_split(&self, fixture_field: &str) -> Option<(String, String, String)> {
221 let resolved = self.resolve(fixture_field);
222 let segments: Vec<&str> = resolved.split('.').collect();
223 let mut path_so_far = String::new();
224 for (i, seg) in segments.iter().enumerate() {
225 if !path_so_far.is_empty() {
226 path_so_far.push('.');
227 }
228 path_so_far.push_str(seg);
229 if self.method_calls.contains(&path_so_far) {
230 let prefix = segments[..i].join(".");
232 let variant = (*seg).to_string();
233 let suffix = segments[i + 1..].join(".");
234 return Some((prefix, variant, suffix));
235 }
236 }
237 None
238 }
239
240 pub fn has_map_access(&self, fixture_field: &str) -> bool {
242 let resolved = self.resolve(fixture_field);
243 let segments = parse_path(resolved);
244 segments.iter().any(|s| {
245 if let PathSegment::MapAccess { key, .. } = s {
246 !key.chars().all(|c| c.is_ascii_digit())
247 } else {
248 false
249 }
250 })
251 }
252
253 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
255 let resolved = self.resolve(fixture_field);
256 let segments = parse_path(resolved);
257 let segments = self.inject_array_indexing(segments);
258 match language {
259 "java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
260 "kotlin" => render_kotlin_with_optionals(&segments, result_var, &self.optional_fields),
261 "kotlin_android" => render_kotlin_android_with_optionals(&segments, result_var, &self.optional_fields),
264 "rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
265 "csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
266 "zig" => render_zig_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
267 "swift" => render_swift_with_optionals(&segments, result_var, &self.optional_fields),
268 "dart" => render_dart_with_optionals(&segments, result_var, &self.optional_fields),
269 "php" if !self.php_getter_fields.is_empty() => {
270 render_php_with_getters(&segments, result_var, &self.php_getter_fields)
271 }
272 _ => render_accessor(&segments, language, result_var),
273 }
274 }
275
276 pub fn accessor_for_error(&self, sub_field: &str, language: &str, err_var: &str) -> String {
290 let resolved = self
291 .error_field_aliases
292 .get(sub_field)
293 .map(String::as_str)
294 .unwrap_or(sub_field);
295 let segments = parse_path(resolved);
296 match language {
299 "rust" => render_rust_with_optionals(&segments, err_var, &self.optional_fields, &self.method_calls),
300 _ => render_accessor(&segments, language, err_var),
301 }
302 }
303
304 pub fn has_error_aliases(&self) -> bool {
311 !self.error_field_aliases.is_empty()
312 }
313
314 fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
315 if self.array_fields.is_empty() {
316 return segments;
317 }
318 let len = segments.len();
319 let mut result = Vec::with_capacity(len);
320 let mut path_so_far = String::new();
321 for i in 0..len {
322 let seg = &segments[i];
323 match seg {
324 PathSegment::Field(f) => {
325 if !path_so_far.is_empty() {
326 path_so_far.push('.');
327 }
328 path_so_far.push_str(f);
329 let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
330 if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
331 result.push(PathSegment::ArrayField {
333 name: f.clone(),
334 index: 0,
335 });
336 } else {
337 result.push(seg.clone());
338 }
339 }
340 PathSegment::ArrayField { .. } => {
343 result.push(seg.clone());
344 }
345 PathSegment::MapAccess { field, key } => {
346 if !path_so_far.is_empty() {
347 path_so_far.push('.');
348 }
349 path_so_far.push_str(field);
350 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
351 if is_numeric && self.array_fields.contains(&path_so_far) {
352 let index: usize = key.parse().unwrap_or(0);
354 result.push(PathSegment::ArrayField {
355 name: field.clone(),
356 index,
357 });
358 } else {
359 result.push(seg.clone());
360 }
361 }
362 _ => {
363 result.push(seg.clone());
364 }
365 }
366 }
367 result
368 }
369
370 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
372 let resolved = self.resolve(fixture_field);
373 if !self.is_optional(resolved) {
374 return None;
375 }
376 let segments = parse_path(resolved);
377 let segments = self.inject_array_indexing(segments);
378 let local_var = {
383 let raw = resolved.replace(['.', '['], "_").replace(']', "");
384 let mut collapsed = String::with_capacity(raw.len());
385 let mut prev_underscore = false;
386 for ch in raw.chars() {
387 if ch == '_' {
388 if !prev_underscore {
389 collapsed.push('_');
390 }
391 prev_underscore = true;
392 } else {
393 collapsed.push(ch);
394 prev_underscore = false;
395 }
396 }
397 collapsed.trim_matches('_').to_string()
398 };
399 let accessor = render_accessor(&segments, "rust", result_var);
400 let has_map_access = segments.iter().any(|s| {
401 if let PathSegment::MapAccess { key, .. } = s {
402 !key.chars().all(|c| c.is_ascii_digit())
403 } else {
404 false
405 }
406 });
407 let is_array = self.is_array(resolved);
408 let binding = if has_map_access {
409 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
410 } else if is_array {
411 format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
412 } else {
413 format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
414 };
415 Some((binding, local_var))
416 }
417}
418
419fn strip_numeric_indices(path: &str) -> String {
424 let mut result = String::with_capacity(path.len());
425 let mut chars = path.chars().peekable();
426 while let Some(c) = chars.next() {
427 if c == '[' {
428 let mut key = String::new();
429 let mut closed = false;
430 for inner in chars.by_ref() {
431 if inner == ']' {
432 closed = true;
433 break;
434 }
435 key.push(inner);
436 }
437 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
438 } else {
440 result.push('[');
441 result.push_str(&key);
442 if closed {
443 result.push(']');
444 }
445 }
446 } else {
447 result.push(c);
448 }
449 }
450 while result.contains("..") {
452 result = result.replace("..", ".");
453 }
454 if result.starts_with('.') {
455 result.remove(0);
456 }
457 result
458}
459
460fn normalize_numeric_indices(path: &str) -> String {
461 let mut result = String::with_capacity(path.len());
462 let mut chars = path.chars().peekable();
463 while let Some(c) = chars.next() {
464 if c == '[' {
465 let mut key = String::new();
466 let mut closed = false;
467 for inner in chars.by_ref() {
468 if inner == ']' {
469 closed = true;
470 break;
471 }
472 key.push(inner);
473 }
474 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
475 result.push_str("[0]");
476 } else {
477 result.push('[');
478 result.push_str(&key);
479 if closed {
480 result.push(']');
481 }
482 }
483 } else {
484 result.push(c);
485 }
486 }
487 result
488}
489
490fn parse_path(path: &str) -> Vec<PathSegment> {
491 let mut segments = Vec::new();
492 for part in path.split('.') {
493 if part == "length" || part == "count" || part == "size" {
494 segments.push(PathSegment::Length);
495 } else if let Some(bracket_pos) = part.find('[') {
496 let name = part[..bracket_pos].to_string();
497 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
498 if key.is_empty() {
499 segments.push(PathSegment::ArrayField { name, index: 0 });
501 } else if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
502 let index: usize = key.parse().unwrap_or(0);
504 segments.push(PathSegment::ArrayField { name, index });
505 } else {
506 segments.push(PathSegment::MapAccess { field: name, key });
508 }
509 } else {
510 segments.push(PathSegment::Field(part.to_string()));
511 }
512 }
513 segments
514}
515
516fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
517 match language {
518 "rust" => render_rust(segments, result_var),
519 "python" => render_dot_access(segments, result_var, "python"),
520 "typescript" | "node" => render_typescript(segments, result_var),
521 "wasm" => render_wasm(segments, result_var),
522 "go" => render_go(segments, result_var),
523 "java" => render_java(segments, result_var),
524 "kotlin" => render_kotlin(segments, result_var),
525 "kotlin_android" => render_kotlin_android(segments, result_var),
526 "csharp" => render_pascal_dot(segments, result_var),
527 "ruby" => render_dot_access(segments, result_var, "ruby"),
528 "php" => render_php(segments, result_var),
529 "elixir" => render_dot_access(segments, result_var, "elixir"),
530 "r" => render_r(segments, result_var),
531 "c" => render_c(segments, result_var),
532 "swift" => render_swift(segments, result_var),
533 "dart" => render_dart(segments, result_var),
534 _ => render_dot_access(segments, result_var, language),
535 }
536}
537
538fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
544 let mut out = result_var.to_string();
545 for seg in segments {
546 match seg {
547 PathSegment::Field(f) => {
548 out.push('.');
549 out.push_str(f);
550 out.push_str("()");
551 }
552 PathSegment::ArrayField { name, index } => {
553 out.push('.');
554 out.push_str(name);
555 out.push_str(&format!("()[{index}]"));
556 }
557 PathSegment::MapAccess { field, key } => {
558 out.push('.');
559 out.push_str(field);
560 if key.chars().all(|c| c.is_ascii_digit()) {
561 out.push_str(&format!("()[{key}]"));
562 } else {
563 out.push_str(&format!("()[\"{key}\"]"));
564 }
565 }
566 PathSegment::Length => {
567 out.push_str(".count");
568 }
569 }
570 }
571 out
572}
573
574fn render_swift_with_optionals(
584 segments: &[PathSegment],
585 result_var: &str,
586 optional_fields: &HashSet<String>,
587) -> String {
588 let mut out = result_var.to_string();
589 let mut path_so_far = String::new();
590 let total = segments.len();
591 for (i, seg) in segments.iter().enumerate() {
592 let is_leaf = i == total - 1;
593 match seg {
594 PathSegment::Field(f) => {
595 if !path_so_far.is_empty() {
596 path_so_far.push('.');
597 }
598 path_so_far.push_str(f);
599 out.push('.');
600 out.push_str(f);
601 out.push_str("()");
602 if !is_leaf && optional_fields.contains(&path_so_far) {
605 out.push('?');
606 }
607 }
608 PathSegment::ArrayField { name, index } => {
609 if !path_so_far.is_empty() {
610 path_so_far.push('.');
611 }
612 path_so_far.push_str(name);
613 let is_optional = optional_fields.contains(&path_so_far);
617 out.push('.');
618 out.push_str(name);
619 if is_optional {
620 out.push_str(&format!("()?[{index}]"));
623 } else {
624 out.push_str(&format!("()[{index}]"));
625 }
626 path_so_far.push_str("[0]");
630 let _ = is_leaf;
637 }
638 PathSegment::MapAccess { field, key } => {
639 if !path_so_far.is_empty() {
640 path_so_far.push('.');
641 }
642 path_so_far.push_str(field);
643 out.push('.');
644 out.push_str(field);
645 if key.chars().all(|c| c.is_ascii_digit()) {
646 out.push_str(&format!("()[{key}]"));
647 } else {
648 out.push_str(&format!("()[\"{key}\"]"));
649 }
650 }
651 PathSegment::Length => {
652 out.push_str(".count");
653 }
654 }
655 }
656 out
657}
658
659fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
660 let mut out = result_var.to_string();
661 for seg in segments {
662 match seg {
663 PathSegment::Field(f) => {
664 out.push('.');
665 out.push_str(&f.to_snake_case());
666 }
667 PathSegment::ArrayField { name, index } => {
668 out.push('.');
669 out.push_str(&name.to_snake_case());
670 out.push_str(&format!("[{index}]"));
671 }
672 PathSegment::MapAccess { field, key } => {
673 out.push('.');
674 out.push_str(&field.to_snake_case());
675 if key.chars().all(|c| c.is_ascii_digit()) {
676 out.push_str(&format!("[{key}]"));
677 } else {
678 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
679 }
680 }
681 PathSegment::Length => {
682 out.push_str(".len()");
683 }
684 }
685 }
686 out
687}
688
689fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
690 let mut out = result_var.to_string();
691 for seg in segments {
692 match seg {
693 PathSegment::Field(f) => {
694 out.push('.');
695 out.push_str(f);
696 }
697 PathSegment::ArrayField { name, index } => {
698 if language == "elixir" {
699 let current = std::mem::take(&mut out);
700 out = format!("Enum.at({current}.{name}, {index})");
701 } else {
702 out.push('.');
703 out.push_str(name);
704 out.push_str(&format!("[{index}]"));
705 }
706 }
707 PathSegment::MapAccess { field, key } => {
708 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
709 if is_numeric && language == "elixir" {
710 let current = std::mem::take(&mut out);
711 out = format!("Enum.at({current}.{field}, {key})");
712 } else {
713 out.push('.');
714 out.push_str(field);
715 if is_numeric {
716 let idx: usize = key.parse().unwrap_or(0);
717 out.push_str(&format!("[{idx}]"));
718 } else if language == "elixir" || language == "ruby" {
719 out.push_str(&format!("[\"{key}\"]"));
722 } else {
723 out.push_str(&format!(".get(\"{key}\")"));
724 }
725 }
726 }
727 PathSegment::Length => match language {
728 "ruby" => out.push_str(".length"),
729 "elixir" => {
730 let current = std::mem::take(&mut out);
731 out = format!("length({current})");
732 }
733 "gleam" => {
734 let current = std::mem::take(&mut out);
735 out = format!("list.length({current})");
736 }
737 _ => {
738 let current = std::mem::take(&mut out);
739 out = format!("len({current})");
740 }
741 },
742 }
743 }
744 out
745}
746
747fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
748 let mut out = result_var.to_string();
749 for seg in segments {
750 match seg {
751 PathSegment::Field(f) => {
752 out.push('.');
753 out.push_str(&f.to_lower_camel_case());
754 }
755 PathSegment::ArrayField { name, index } => {
756 out.push('.');
757 out.push_str(&name.to_lower_camel_case());
758 out.push_str(&format!("[{index}]"));
759 }
760 PathSegment::MapAccess { field, key } => {
761 out.push('.');
762 out.push_str(&field.to_lower_camel_case());
763 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
766 out.push_str(&format!("[{key}]"));
767 } else {
768 out.push_str(&format!("[\"{key}\"]"));
769 }
770 }
771 PathSegment::Length => {
772 out.push_str(".length");
773 }
774 }
775 }
776 out
777}
778
779fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
780 let mut out = result_var.to_string();
781 for seg in segments {
782 match seg {
783 PathSegment::Field(f) => {
784 out.push('.');
785 out.push_str(&f.to_lower_camel_case());
786 }
787 PathSegment::ArrayField { name, index } => {
788 out.push('.');
789 out.push_str(&name.to_lower_camel_case());
790 out.push_str(&format!("[{index}]"));
791 }
792 PathSegment::MapAccess { field, key } => {
793 out.push('.');
794 out.push_str(&field.to_lower_camel_case());
795 out.push_str(&format!(".get(\"{key}\")"));
796 }
797 PathSegment::Length => {
798 out.push_str(".length");
799 }
800 }
801 }
802 out
803}
804
805fn render_go(segments: &[PathSegment], result_var: &str) -> String {
806 let mut out = result_var.to_string();
807 for seg in segments {
808 match seg {
809 PathSegment::Field(f) => {
810 out.push('.');
811 out.push_str(&to_go_name(f));
812 }
813 PathSegment::ArrayField { name, index } => {
814 out.push('.');
815 out.push_str(&to_go_name(name));
816 out.push_str(&format!("[{index}]"));
817 }
818 PathSegment::MapAccess { field, key } => {
819 out.push('.');
820 out.push_str(&to_go_name(field));
821 if key.chars().all(|c| c.is_ascii_digit()) {
822 out.push_str(&format!("[{key}]"));
823 } else {
824 out.push_str(&format!("[\"{key}\"]"));
825 }
826 }
827 PathSegment::Length => {
828 let current = std::mem::take(&mut out);
829 out = format!("len({current})");
830 }
831 }
832 }
833 out
834}
835
836fn render_java(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 out.push_str("()");
844 }
845 PathSegment::ArrayField { name, index } => {
846 out.push('.');
847 out.push_str(&name.to_lower_camel_case());
848 out.push_str(&format!("().get({index})"));
849 }
850 PathSegment::MapAccess { field, key } => {
851 out.push('.');
852 out.push_str(&field.to_lower_camel_case());
853 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
855 if is_numeric {
856 out.push_str(&format!("().get({key})"));
857 } else {
858 out.push_str(&format!("().get(\"{key}\")"));
859 }
860 }
861 PathSegment::Length => {
862 out.push_str(".size()");
863 }
864 }
865 }
866 out
867}
868
869fn kotlin_getter(name: &str) -> String {
874 let camel = name.to_lower_camel_case();
875 match camel.as_str() {
876 "as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
877 | "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
878 | "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
879 _ => camel,
880 }
881}
882
883fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
884 let mut out = result_var.to_string();
885 for seg in segments {
886 match seg {
887 PathSegment::Field(f) => {
888 out.push('.');
889 out.push_str(&kotlin_getter(f));
890 out.push_str("()");
891 }
892 PathSegment::ArrayField { name, index } => {
893 out.push('.');
894 out.push_str(&kotlin_getter(name));
895 if *index == 0 {
896 out.push_str("().first()");
897 } else {
898 out.push_str(&format!("().get({index})"));
899 }
900 }
901 PathSegment::MapAccess { field, key } => {
902 out.push('.');
903 out.push_str(&kotlin_getter(field));
904 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
905 if is_numeric {
906 out.push_str(&format!("().get({key})"));
907 } else {
908 out.push_str(&format!("().get(\"{key}\")"));
909 }
910 }
911 PathSegment::Length => {
912 out.push_str(".size");
913 }
914 }
915 }
916 out
917}
918
919fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
920 let mut out = result_var.to_string();
921 let mut path_so_far = String::new();
922 for (i, seg) in segments.iter().enumerate() {
923 let is_leaf = i == segments.len() - 1;
924 match seg {
925 PathSegment::Field(f) => {
926 if !path_so_far.is_empty() {
927 path_so_far.push('.');
928 }
929 path_so_far.push_str(f);
930 out.push('.');
931 out.push_str(&f.to_lower_camel_case());
932 out.push_str("()");
933 let _ = is_leaf;
934 let _ = optional_fields;
935 }
936 PathSegment::ArrayField { name, index } => {
937 if !path_so_far.is_empty() {
938 path_so_far.push('.');
939 }
940 path_so_far.push_str(name);
941 out.push('.');
942 out.push_str(&name.to_lower_camel_case());
943 out.push_str(&format!("().get({index})"));
944 }
945 PathSegment::MapAccess { field, key } => {
946 if !path_so_far.is_empty() {
947 path_so_far.push('.');
948 }
949 path_so_far.push_str(field);
950 out.push('.');
951 out.push_str(&field.to_lower_camel_case());
952 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
954 if is_numeric {
955 out.push_str(&format!("().get({key})"));
956 } else {
957 out.push_str(&format!("().get(\"{key}\")"));
958 }
959 }
960 PathSegment::Length => {
961 out.push_str(".size()");
962 }
963 }
964 }
965 out
966}
967
968fn render_kotlin_with_optionals(
983 segments: &[PathSegment],
984 result_var: &str,
985 optional_fields: &HashSet<String>,
986) -> String {
987 let mut out = result_var.to_string();
988 let mut path_so_far = String::new();
989 let mut prev_was_nullable = false;
997 for seg in segments {
998 let nav = if prev_was_nullable { "?." } else { "." };
999 match seg {
1000 PathSegment::Field(f) => {
1001 if !path_so_far.is_empty() {
1002 path_so_far.push('.');
1003 }
1004 path_so_far.push_str(f);
1005 let is_optional = optional_fields.contains(&path_so_far);
1010 out.push_str(nav);
1011 out.push_str(&kotlin_getter(f));
1012 out.push_str("()");
1013 prev_was_nullable = prev_was_nullable || is_optional;
1014 }
1015 PathSegment::ArrayField { name, index } => {
1016 if !path_so_far.is_empty() {
1017 path_so_far.push('.');
1018 }
1019 path_so_far.push_str(name);
1020 let is_optional = optional_fields.contains(&path_so_far);
1021 out.push_str(nav);
1022 out.push_str(&kotlin_getter(name));
1023 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1024 if *index == 0 {
1025 out.push_str(&format!("(){safe}.first()"));
1026 } else {
1027 out.push_str(&format!("(){safe}.get({index})"));
1028 }
1029 path_so_far.push_str("[0]");
1033 prev_was_nullable = prev_was_nullable || is_optional;
1034 }
1035 PathSegment::MapAccess { field, key } => {
1036 if !path_so_far.is_empty() {
1037 path_so_far.push('.');
1038 }
1039 path_so_far.push_str(field);
1040 let is_optional = optional_fields.contains(&path_so_far);
1041 out.push_str(nav);
1042 out.push_str(&kotlin_getter(field));
1043 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1044 if is_numeric {
1045 if prev_was_nullable || is_optional {
1046 out.push_str(&format!("()?.get({key})"));
1047 } else {
1048 out.push_str(&format!("().get({key})"));
1049 }
1050 } else if prev_was_nullable || is_optional {
1051 out.push_str(&format!("()?.get(\"{key}\")"));
1052 } else {
1053 out.push_str(&format!("().get(\"{key}\")"));
1054 }
1055 prev_was_nullable = prev_was_nullable || is_optional;
1056 }
1057 PathSegment::Length => {
1058 let size_nav = if prev_was_nullable { "?" } else { "" };
1061 out.push_str(&format!("{size_nav}.size"));
1062 prev_was_nullable = false;
1063 }
1064 }
1065 }
1066 out
1067}
1068
1069fn render_kotlin_android_with_optionals(
1080 segments: &[PathSegment],
1081 result_var: &str,
1082 optional_fields: &HashSet<String>,
1083) -> String {
1084 let mut out = result_var.to_string();
1085 let mut path_so_far = String::new();
1086 let mut prev_was_nullable = false;
1087 for seg in segments {
1088 let nav = if prev_was_nullable { "?." } else { "." };
1089 match seg {
1090 PathSegment::Field(f) => {
1091 if !path_so_far.is_empty() {
1092 path_so_far.push('.');
1093 }
1094 path_so_far.push_str(f);
1095 let is_optional = optional_fields.contains(&path_so_far);
1096 out.push_str(nav);
1097 out.push_str(&kotlin_getter(f));
1099 prev_was_nullable = prev_was_nullable || is_optional;
1100 }
1101 PathSegment::ArrayField { name, index } => {
1102 if !path_so_far.is_empty() {
1103 path_so_far.push('.');
1104 }
1105 path_so_far.push_str(name);
1106 let is_optional = optional_fields.contains(&path_so_far);
1107 out.push_str(nav);
1108 out.push_str(&kotlin_getter(name));
1110 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
1111 if *index == 0 {
1112 out.push_str(&format!("{safe}.first()"));
1113 } else {
1114 out.push_str(&format!("{safe}.get({index})"));
1115 }
1116 path_so_far.push_str("[0]");
1117 prev_was_nullable = prev_was_nullable || is_optional;
1118 }
1119 PathSegment::MapAccess { field, key } => {
1120 if !path_so_far.is_empty() {
1121 path_so_far.push('.');
1122 }
1123 path_so_far.push_str(field);
1124 let is_optional = optional_fields.contains(&path_so_far);
1125 out.push_str(nav);
1126 out.push_str(&kotlin_getter(field));
1128 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1129 if is_numeric {
1130 if prev_was_nullable || is_optional {
1131 out.push_str(&format!("?.get({key})"));
1132 } else {
1133 out.push_str(&format!(".get({key})"));
1134 }
1135 } else if prev_was_nullable || is_optional {
1136 out.push_str(&format!("?.get(\"{key}\")"));
1137 } else {
1138 out.push_str(&format!(".get(\"{key}\")"));
1139 }
1140 prev_was_nullable = prev_was_nullable || is_optional;
1141 }
1142 PathSegment::Length => {
1143 let size_nav = if prev_was_nullable { "?" } else { "" };
1144 out.push_str(&format!("{size_nav}.size"));
1145 prev_was_nullable = false;
1146 }
1147 }
1148 }
1149 out
1150}
1151
1152fn render_kotlin_android(segments: &[PathSegment], result_var: &str) -> String {
1156 let mut out = result_var.to_string();
1157 for seg in segments {
1158 match seg {
1159 PathSegment::Field(f) => {
1160 out.push('.');
1161 out.push_str(&kotlin_getter(f));
1162 }
1164 PathSegment::ArrayField { name, index } => {
1165 out.push('.');
1166 out.push_str(&kotlin_getter(name));
1167 if *index == 0 {
1168 out.push_str(".first()");
1169 } else {
1170 out.push_str(&format!(".get({index})"));
1171 }
1172 }
1173 PathSegment::MapAccess { field, key } => {
1174 out.push('.');
1175 out.push_str(&kotlin_getter(field));
1176 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
1177 if is_numeric {
1178 out.push_str(&format!(".get({key})"));
1179 } else {
1180 out.push_str(&format!(".get(\"{key}\")"));
1181 }
1182 }
1183 PathSegment::Length => {
1184 out.push_str(".size");
1185 }
1186 }
1187 }
1188 out
1189}
1190
1191fn render_rust_with_optionals(
1197 segments: &[PathSegment],
1198 result_var: &str,
1199 optional_fields: &HashSet<String>,
1200 method_calls: &HashSet<String>,
1201) -> String {
1202 let mut out = result_var.to_string();
1203 let mut path_so_far = String::new();
1204 for (i, seg) in segments.iter().enumerate() {
1205 let is_leaf = i == segments.len() - 1;
1206 match seg {
1207 PathSegment::Field(f) => {
1208 if !path_so_far.is_empty() {
1209 path_so_far.push('.');
1210 }
1211 path_so_far.push_str(f);
1212 out.push('.');
1213 out.push_str(&f.to_snake_case());
1214 let is_method = method_calls.contains(&path_so_far);
1215 if is_method {
1216 out.push_str("()");
1217 if !is_leaf && optional_fields.contains(&path_so_far) {
1218 out.push_str(".as_ref().unwrap()");
1219 }
1220 } else if !is_leaf && optional_fields.contains(&path_so_far) {
1221 out.push_str(".as_ref().unwrap()");
1222 }
1223 }
1224 PathSegment::ArrayField { name, index } => {
1225 if !path_so_far.is_empty() {
1226 path_so_far.push('.');
1227 }
1228 path_so_far.push_str(name);
1229 out.push('.');
1230 out.push_str(&name.to_snake_case());
1231 let path_with_idx = format!("{path_so_far}[0]");
1235 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1236 if is_opt {
1237 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
1238 } else {
1239 out.push_str(&format!("[{index}]"));
1240 }
1241 path_so_far.push_str("[0]");
1246 }
1247 PathSegment::MapAccess { field, key } => {
1248 if !path_so_far.is_empty() {
1249 path_so_far.push('.');
1250 }
1251 path_so_far.push_str(field);
1252 out.push('.');
1253 out.push_str(&field.to_snake_case());
1254 if key.chars().all(|c| c.is_ascii_digit()) {
1255 let path_with_idx = format!("{path_so_far}[0]");
1257 let is_opt =
1258 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
1259 if is_opt {
1260 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
1261 } else {
1262 out.push_str(&format!("[{key}]"));
1263 }
1264 path_so_far.push_str("[0]");
1265 } else {
1266 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
1267 }
1268 }
1269 PathSegment::Length => {
1270 out.push_str(".len()");
1271 }
1272 }
1273 }
1274 out
1275}
1276
1277fn render_zig_with_optionals(
1290 segments: &[PathSegment],
1291 result_var: &str,
1292 optional_fields: &HashSet<String>,
1293 method_calls: &HashSet<String>,
1294) -> String {
1295 let mut out = result_var.to_string();
1296 let mut path_so_far = String::new();
1297 for seg in segments {
1298 match seg {
1299 PathSegment::Field(f) => {
1300 if !path_so_far.is_empty() {
1301 path_so_far.push('.');
1302 }
1303 path_so_far.push_str(f);
1304 out.push('.');
1305 out.push_str(f);
1306 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1307 out.push_str(".?");
1308 }
1309 }
1310 PathSegment::ArrayField { name, index } => {
1311 if !path_so_far.is_empty() {
1312 path_so_far.push('.');
1313 }
1314 path_so_far.push_str(name);
1315 out.push('.');
1316 out.push_str(name);
1317 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1318 out.push_str(".?");
1319 }
1320 out.push_str(&format!("[{index}]"));
1321 }
1322 PathSegment::MapAccess { field, key } => {
1323 if !path_so_far.is_empty() {
1324 path_so_far.push('.');
1325 }
1326 path_so_far.push_str(field);
1327 out.push('.');
1328 out.push_str(field);
1329 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1330 out.push_str(".?");
1331 }
1332 if key.chars().all(|c| c.is_ascii_digit()) {
1333 out.push_str(&format!("[{key}]"));
1334 } else {
1335 out.push_str(&format!(".get(\"{key}\")"));
1336 }
1337 }
1338 PathSegment::Length => {
1339 out.push_str(".len");
1340 }
1341 }
1342 }
1343 out
1344}
1345
1346fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1347 let mut out = result_var.to_string();
1348 for seg in segments {
1349 match seg {
1350 PathSegment::Field(f) => {
1351 out.push('.');
1352 out.push_str(&f.to_pascal_case());
1353 }
1354 PathSegment::ArrayField { name, index } => {
1355 out.push('.');
1356 out.push_str(&name.to_pascal_case());
1357 out.push_str(&format!("[{index}]"));
1358 }
1359 PathSegment::MapAccess { field, key } => {
1360 out.push('.');
1361 out.push_str(&field.to_pascal_case());
1362 if key.chars().all(|c| c.is_ascii_digit()) {
1363 out.push_str(&format!("[{key}]"));
1364 } else {
1365 out.push_str(&format!("[\"{key}\"]"));
1366 }
1367 }
1368 PathSegment::Length => {
1369 out.push_str(".Count");
1370 }
1371 }
1372 }
1373 out
1374}
1375
1376fn render_csharp_with_optionals(
1377 segments: &[PathSegment],
1378 result_var: &str,
1379 optional_fields: &HashSet<String>,
1380) -> String {
1381 let mut out = result_var.to_string();
1382 let mut path_so_far = String::new();
1383 for (i, seg) in segments.iter().enumerate() {
1384 let is_leaf = i == segments.len() - 1;
1385 match seg {
1386 PathSegment::Field(f) => {
1387 if !path_so_far.is_empty() {
1388 path_so_far.push('.');
1389 }
1390 path_so_far.push_str(f);
1391 out.push('.');
1392 out.push_str(&f.to_pascal_case());
1393 if !is_leaf && optional_fields.contains(&path_so_far) {
1394 out.push('!');
1395 }
1396 }
1397 PathSegment::ArrayField { name, index } => {
1398 if !path_so_far.is_empty() {
1399 path_so_far.push('.');
1400 }
1401 path_so_far.push_str(name);
1402 out.push('.');
1403 out.push_str(&name.to_pascal_case());
1404 out.push_str(&format!("[{index}]"));
1405 }
1406 PathSegment::MapAccess { field, key } => {
1407 if !path_so_far.is_empty() {
1408 path_so_far.push('.');
1409 }
1410 path_so_far.push_str(field);
1411 out.push('.');
1412 out.push_str(&field.to_pascal_case());
1413 if key.chars().all(|c| c.is_ascii_digit()) {
1414 out.push_str(&format!("[{key}]"));
1415 } else {
1416 out.push_str(&format!("[\"{key}\"]"));
1417 }
1418 }
1419 PathSegment::Length => {
1420 out.push_str(".Count");
1421 }
1422 }
1423 }
1424 out
1425}
1426
1427fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1428 let mut out = result_var.to_string();
1429 for seg in segments {
1430 match seg {
1431 PathSegment::Field(f) => {
1432 out.push_str("->");
1433 out.push_str(&f.to_lower_camel_case());
1436 }
1437 PathSegment::ArrayField { name, index } => {
1438 out.push_str("->");
1439 out.push_str(&name.to_lower_camel_case());
1440 out.push_str(&format!("[{index}]"));
1441 }
1442 PathSegment::MapAccess { field, key } => {
1443 out.push_str("->");
1444 out.push_str(&field.to_lower_camel_case());
1445 out.push_str(&format!("[\"{key}\"]"));
1446 }
1447 PathSegment::Length => {
1448 let current = std::mem::take(&mut out);
1449 out = format!("count({current})");
1450 }
1451 }
1452 }
1453 out
1454}
1455
1456fn render_php_with_getters(segments: &[PathSegment], result_var: &str, getter_fields: &HashSet<String>) -> String {
1470 let mut out = result_var.to_string();
1471 for seg in segments {
1472 match seg {
1473 PathSegment::Field(f) => {
1474 let camel = f.to_lower_camel_case();
1475 if getter_fields.contains(f.as_str()) {
1476 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1481 out.push_str("->");
1482 out.push_str(&getter);
1483 out.push_str("()");
1484 } else {
1485 out.push_str("->");
1486 out.push_str(&camel);
1487 }
1488 }
1489 PathSegment::ArrayField { name, index } => {
1490 let camel = name.to_lower_camel_case();
1491 if getter_fields.contains(name.as_str()) {
1492 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1493 out.push_str("->");
1494 out.push_str(&getter);
1495 out.push_str("()");
1496 } else {
1497 out.push_str("->");
1498 out.push_str(&camel);
1499 }
1500 out.push_str(&format!("[{index}]"));
1501 }
1502 PathSegment::MapAccess { field, key } => {
1503 let camel = field.to_lower_camel_case();
1504 if getter_fields.contains(field.as_str()) {
1505 let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
1506 out.push_str("->");
1507 out.push_str(&getter);
1508 out.push_str("()");
1509 } else {
1510 out.push_str("->");
1511 out.push_str(&camel);
1512 }
1513 out.push_str(&format!("[\"{key}\"]"));
1514 }
1515 PathSegment::Length => {
1516 let current = std::mem::take(&mut out);
1517 out = format!("count({current})");
1518 }
1519 }
1520 }
1521 out
1522}
1523
1524fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1525 let mut out = result_var.to_string();
1526 for seg in segments {
1527 match seg {
1528 PathSegment::Field(f) => {
1529 out.push('$');
1530 out.push_str(f);
1531 }
1532 PathSegment::ArrayField { name, index } => {
1533 out.push('$');
1534 out.push_str(name);
1535 out.push_str(&format!("[[{}]]", index + 1));
1537 }
1538 PathSegment::MapAccess { field, key } => {
1539 out.push('$');
1540 out.push_str(field);
1541 out.push_str(&format!("[[\"{key}\"]]"));
1542 }
1543 PathSegment::Length => {
1544 let current = std::mem::take(&mut out);
1545 out = format!("length({current})");
1546 }
1547 }
1548 }
1549 out
1550}
1551
1552fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1553 let mut parts = Vec::new();
1554 let mut trailing_length = false;
1555 for seg in segments {
1556 match seg {
1557 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1558 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1559 PathSegment::MapAccess { field, key } => {
1560 parts.push(field.to_snake_case());
1561 parts.push(key.clone());
1562 }
1563 PathSegment::Length => {
1564 trailing_length = true;
1565 }
1566 }
1567 }
1568 let suffix = parts.join("_");
1569 if trailing_length {
1570 format!("result_{suffix}_count({result_var})")
1571 } else {
1572 format!("result_{suffix}({result_var})")
1573 }
1574}
1575
1576fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
1583 let mut out = result_var.to_string();
1584 for seg in segments {
1585 match seg {
1586 PathSegment::Field(f) => {
1587 out.push('.');
1588 out.push_str(&f.to_lower_camel_case());
1589 }
1590 PathSegment::ArrayField { name, index } => {
1591 out.push('.');
1592 out.push_str(&name.to_lower_camel_case());
1593 out.push_str(&format!("[{index}]"));
1594 }
1595 PathSegment::MapAccess { field, key } => {
1596 out.push('.');
1597 out.push_str(&field.to_lower_camel_case());
1598 if key.chars().all(|c| c.is_ascii_digit()) {
1599 out.push_str(&format!("[{key}]"));
1600 } else {
1601 out.push_str(&format!("[\"{key}\"]"));
1602 }
1603 }
1604 PathSegment::Length => {
1605 out.push_str(".length");
1606 }
1607 }
1608 }
1609 out
1610}
1611
1612fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1618 let mut out = result_var.to_string();
1619 let mut path_so_far = String::new();
1620 let mut prev_was_nullable = false;
1621 for seg in segments {
1622 let nav = if prev_was_nullable { "?." } else { "." };
1623 match seg {
1624 PathSegment::Field(f) => {
1625 if !path_so_far.is_empty() {
1626 path_so_far.push('.');
1627 }
1628 path_so_far.push_str(f);
1629 let is_optional = optional_fields.contains(&path_so_far);
1630 out.push_str(nav);
1631 out.push_str(&f.to_lower_camel_case());
1632 prev_was_nullable = is_optional;
1633 }
1634 PathSegment::ArrayField { name, index } => {
1635 if !path_so_far.is_empty() {
1636 path_so_far.push('.');
1637 }
1638 path_so_far.push_str(name);
1639 let is_optional = optional_fields.contains(&path_so_far);
1640 out.push_str(nav);
1641 out.push_str(&name.to_lower_camel_case());
1642 if is_optional {
1646 out.push('!');
1647 }
1648 out.push_str(&format!("[{index}]"));
1649 prev_was_nullable = false;
1650 }
1651 PathSegment::MapAccess { field, key } => {
1652 if !path_so_far.is_empty() {
1653 path_so_far.push('.');
1654 }
1655 path_so_far.push_str(field);
1656 let is_optional = optional_fields.contains(&path_so_far);
1657 out.push_str(nav);
1658 out.push_str(&field.to_lower_camel_case());
1659 if key.chars().all(|c| c.is_ascii_digit()) {
1660 out.push_str(&format!("[{key}]"));
1661 } else {
1662 out.push_str(&format!("[\"{key}\"]"));
1663 }
1664 prev_was_nullable = is_optional;
1665 }
1666 PathSegment::Length => {
1667 out.push_str(nav);
1670 out.push_str("length");
1671 prev_was_nullable = false;
1672 }
1673 }
1674 }
1675 out
1676}
1677
1678#[cfg(test)]
1679mod tests {
1680 use super::*;
1681
1682 fn make_resolver() -> FieldResolver {
1683 let mut fields = HashMap::new();
1684 fields.insert("title".to_string(), "metadata.document.title".to_string());
1685 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1686 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
1687 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
1688 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
1689 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
1690 let mut optional = HashSet::new();
1691 optional.insert("metadata.document.title".to_string());
1692 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1693 }
1694
1695 fn make_resolver_with_doc_optional() -> FieldResolver {
1696 let mut fields = HashMap::new();
1697 fields.insert("title".to_string(), "metadata.document.title".to_string());
1698 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1699 let mut optional = HashSet::new();
1700 optional.insert("document".to_string());
1701 optional.insert("metadata.document.title".to_string());
1702 optional.insert("metadata.document".to_string());
1703 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1704 }
1705
1706 #[test]
1707 fn test_resolve_alias() {
1708 let r = make_resolver();
1709 assert_eq!(r.resolve("title"), "metadata.document.title");
1710 }
1711
1712 #[test]
1713 fn test_resolve_passthrough() {
1714 let r = make_resolver();
1715 assert_eq!(r.resolve("content"), "content");
1716 }
1717
1718 #[test]
1719 fn test_is_optional() {
1720 let r = make_resolver();
1721 assert!(r.is_optional("metadata.document.title"));
1722 assert!(!r.is_optional("content"));
1723 }
1724
1725 #[test]
1726 fn test_accessor_rust_struct() {
1727 let r = make_resolver();
1728 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
1729 }
1730
1731 #[test]
1732 fn test_accessor_rust_map() {
1733 let r = make_resolver();
1734 assert_eq!(
1735 r.accessor("tags", "rust", "result"),
1736 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
1737 );
1738 }
1739
1740 #[test]
1741 fn test_accessor_python() {
1742 let r = make_resolver();
1743 assert_eq!(
1744 r.accessor("title", "python", "result"),
1745 "result.metadata.document.title"
1746 );
1747 }
1748
1749 #[test]
1750 fn test_accessor_go() {
1751 let r = make_resolver();
1752 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
1753 }
1754
1755 #[test]
1756 fn test_accessor_go_initialism_fields() {
1757 let mut fields = std::collections::HashMap::new();
1758 fields.insert("content".to_string(), "html".to_string());
1759 fields.insert("link_url".to_string(), "links.url".to_string());
1760 let r = FieldResolver::new(
1761 &fields,
1762 &HashSet::new(),
1763 &HashSet::new(),
1764 &HashSet::new(),
1765 &HashSet::new(),
1766 );
1767 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
1768 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
1769 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
1770 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
1771 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
1772 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
1773 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
1774 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
1775 }
1776
1777 #[test]
1778 fn test_accessor_typescript() {
1779 let r = make_resolver();
1780 assert_eq!(
1781 r.accessor("title", "typescript", "result"),
1782 "result.metadata.document.title"
1783 );
1784 }
1785
1786 #[test]
1787 fn test_accessor_typescript_snake_to_camel() {
1788 let r = make_resolver();
1789 assert_eq!(
1790 r.accessor("og", "typescript", "result"),
1791 "result.metadata.document.openGraph"
1792 );
1793 assert_eq!(
1794 r.accessor("twitter", "typescript", "result"),
1795 "result.metadata.document.twitterCard"
1796 );
1797 assert_eq!(
1798 r.accessor("canonical", "typescript", "result"),
1799 "result.metadata.document.canonicalUrl"
1800 );
1801 }
1802
1803 #[test]
1804 fn test_accessor_typescript_map_snake_to_camel() {
1805 let r = make_resolver();
1806 assert_eq!(
1807 r.accessor("og_tag", "typescript", "result"),
1808 "result.metadata.openGraphTags[\"og_title\"]"
1809 );
1810 }
1811
1812 #[test]
1813 fn test_accessor_typescript_numeric_index_is_unquoted() {
1814 let mut fields = HashMap::new();
1818 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
1819 let r = FieldResolver::new(
1820 &fields,
1821 &HashSet::new(),
1822 &HashSet::new(),
1823 &HashSet::new(),
1824 &HashSet::new(),
1825 );
1826 assert_eq!(
1827 r.accessor("first_score", "typescript", "result"),
1828 "result.results[0].relevanceScore"
1829 );
1830 }
1831
1832 #[test]
1833 fn test_accessor_node_alias() {
1834 let r = make_resolver();
1835 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
1836 }
1837
1838 #[test]
1839 fn test_accessor_wasm_camel_case() {
1840 let r = make_resolver();
1841 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
1842 assert_eq!(
1843 r.accessor("twitter", "wasm", "result"),
1844 "result.metadata.document.twitterCard"
1845 );
1846 assert_eq!(
1847 r.accessor("canonical", "wasm", "result"),
1848 "result.metadata.document.canonicalUrl"
1849 );
1850 }
1851
1852 #[test]
1853 fn test_accessor_wasm_map_access() {
1854 let r = make_resolver();
1855 assert_eq!(
1856 r.accessor("og_tag", "wasm", "result"),
1857 "result.metadata.openGraphTags.get(\"og_title\")"
1858 );
1859 }
1860
1861 #[test]
1862 fn test_accessor_java() {
1863 let r = make_resolver();
1864 assert_eq!(
1865 r.accessor("title", "java", "result"),
1866 "result.metadata().document().title()"
1867 );
1868 }
1869
1870 #[test]
1871 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
1872 let mut fields = HashMap::new();
1873 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1874 fields.insert("node_count".to_string(), "nodes.length".to_string());
1875 let mut arrays = HashSet::new();
1876 arrays.insert("nodes".to_string());
1877 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
1878 assert_eq!(
1879 r.accessor("first_node_name", "kotlin", "result"),
1880 "result.nodes().first().name()"
1881 );
1882 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
1883 }
1884
1885 #[test]
1886 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
1887 let r = make_resolver_with_doc_optional();
1888 assert_eq!(
1889 r.accessor("title", "kotlin", "result"),
1890 "result.metadata().document()?.title()"
1891 );
1892 }
1893
1894 #[test]
1895 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
1896 let mut fields = HashMap::new();
1897 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1898 fields.insert("tag".to_string(), "tags[name]".to_string());
1899 let mut optional = HashSet::new();
1900 optional.insert("nodes".to_string());
1901 optional.insert("tags".to_string());
1902 let mut arrays = HashSet::new();
1903 arrays.insert("nodes".to_string());
1904 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
1905 assert_eq!(
1906 r.accessor("first_node_name", "kotlin", "result"),
1907 "result.nodes()?.first()?.name()"
1908 );
1909 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
1910 }
1911
1912 #[test]
1918 fn test_accessor_kotlin_optional_field_after_indexed_array() {
1919 let mut fields = HashMap::new();
1922 fields.insert(
1923 "tool_call_name".to_string(),
1924 "choices[0].message.tool_calls[0].function.name".to_string(),
1925 );
1926 let mut optional = HashSet::new();
1927 optional.insert("choices[0].message.tool_calls".to_string());
1928 let mut arrays = HashSet::new();
1929 arrays.insert("choices".to_string());
1930 arrays.insert("choices[0].message.tool_calls".to_string());
1931 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
1932 let expr = r.accessor("tool_call_name", "kotlin", "result");
1933 assert!(
1935 expr.contains("toolCalls()?.first()"),
1936 "expected toolCalls()?.first() for optional list, got: {expr}"
1937 );
1938 }
1939
1940 #[test]
1941 fn test_accessor_csharp() {
1942 let r = make_resolver();
1943 assert_eq!(
1944 r.accessor("title", "csharp", "result"),
1945 "result.Metadata.Document.Title"
1946 );
1947 }
1948
1949 #[test]
1950 fn test_accessor_php() {
1951 let r = make_resolver();
1952 assert_eq!(
1953 r.accessor("title", "php", "$result"),
1954 "$result->metadata->document->title"
1955 );
1956 }
1957
1958 #[test]
1959 fn test_accessor_r() {
1960 let r = make_resolver();
1961 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
1962 }
1963
1964 #[test]
1965 fn test_accessor_c() {
1966 let r = make_resolver();
1967 assert_eq!(
1968 r.accessor("title", "c", "result"),
1969 "result_metadata_document_title(result)"
1970 );
1971 }
1972
1973 #[test]
1974 fn test_rust_unwrap_binding() {
1975 let r = make_resolver();
1976 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
1977 assert_eq!(var, "metadata_document_title");
1978 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
1979 }
1980
1981 #[test]
1982 fn test_rust_unwrap_binding_non_optional() {
1983 let r = make_resolver();
1984 assert!(r.rust_unwrap_binding("content", "result").is_none());
1985 }
1986
1987 #[test]
1988 fn test_rust_unwrap_binding_collapses_double_underscore() {
1989 let mut aliases = HashMap::new();
1994 aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
1995 let mut optional = HashSet::new();
1996 optional.insert("json_ld[].name".to_string());
1997 let mut array = HashSet::new();
1998 array.insert("json_ld".to_string());
1999 let result_fields = HashSet::new();
2000 let method_calls = HashSet::new();
2001 let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
2002 let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
2003 assert_eq!(var, "json_ld_name");
2004 }
2005
2006 #[test]
2007 fn test_direct_field_no_alias() {
2008 let r = make_resolver();
2009 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2010 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
2011 }
2012
2013 #[test]
2014 fn test_accessor_rust_with_optionals() {
2015 let r = make_resolver_with_doc_optional();
2016 assert_eq!(
2017 r.accessor("title", "rust", "result"),
2018 "result.metadata.document.as_ref().unwrap().title"
2019 );
2020 }
2021
2022 #[test]
2023 fn test_accessor_csharp_with_optionals() {
2024 let r = make_resolver_with_doc_optional();
2025 assert_eq!(
2026 r.accessor("title", "csharp", "result"),
2027 "result.Metadata.Document!.Title"
2028 );
2029 }
2030
2031 #[test]
2032 fn test_accessor_rust_non_optional_field() {
2033 let r = make_resolver();
2034 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
2035 }
2036
2037 #[test]
2038 fn test_accessor_csharp_non_optional_field() {
2039 let r = make_resolver();
2040 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
2041 }
2042
2043 #[test]
2044 fn test_accessor_rust_method_call() {
2045 let mut fields = HashMap::new();
2047 fields.insert(
2048 "excel_sheet_count".to_string(),
2049 "metadata.format.excel.sheet_count".to_string(),
2050 );
2051 let mut optional = HashSet::new();
2052 optional.insert("metadata.format".to_string());
2053 optional.insert("metadata.format.excel".to_string());
2054 let mut method_calls = HashSet::new();
2055 method_calls.insert("metadata.format.excel".to_string());
2056 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
2057 assert_eq!(
2058 r.accessor("excel_sheet_count", "rust", "result"),
2059 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
2060 );
2061 }
2062
2063 fn make_php_getter_resolver() -> FieldResolver {
2068 let mut php_getter_fields = HashSet::new();
2069 php_getter_fields.insert("metadata".to_string());
2070 php_getter_fields.insert("links".to_string());
2071 FieldResolver::new_with_php_getters(
2072 &HashMap::new(),
2073 &HashSet::new(),
2074 &HashSet::new(),
2075 &HashSet::new(),
2076 &HashSet::new(),
2077 &HashMap::new(),
2078 &php_getter_fields,
2079 )
2080 }
2081
2082 #[test]
2083 fn render_php_uses_getter_method_for_non_scalar_field() {
2084 let r = make_php_getter_resolver();
2085 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
2086 }
2087
2088 #[test]
2089 fn render_php_uses_property_for_scalar_field() {
2090 let r = make_php_getter_resolver();
2091 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2092 }
2093
2094 #[test]
2095 fn render_php_nested_non_scalar_uses_getter_then_property() {
2096 let mut fields = HashMap::new();
2097 fields.insert("title".to_string(), "metadata.title".to_string());
2098 let mut php_getter_fields = HashSet::new();
2099 php_getter_fields.insert("metadata".to_string());
2100 let r = FieldResolver::new_with_php_getters(
2101 &fields,
2102 &HashSet::new(),
2103 &HashSet::new(),
2104 &HashSet::new(),
2105 &HashSet::new(),
2106 &HashMap::new(),
2107 &php_getter_fields,
2108 );
2109 assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
2111 }
2112
2113 #[test]
2114 fn render_php_array_field_uses_getter_when_non_scalar() {
2115 let mut fields = HashMap::new();
2116 fields.insert("first_link".to_string(), "links[0]".to_string());
2117 let php_getter_fields: HashSet<String> = ["links".to_string()].into_iter().collect();
2118 let r = FieldResolver::new_with_php_getters(
2119 &fields,
2120 &HashSet::new(),
2121 &HashSet::new(),
2122 &HashSet::new(),
2123 &HashSet::new(),
2124 &HashMap::new(),
2125 &php_getter_fields,
2126 );
2127 assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
2128 }
2129
2130 #[test]
2131 fn render_php_falls_back_to_property_when_getter_fields_empty() {
2132 let r = FieldResolver::new(
2135 &HashMap::new(),
2136 &HashSet::new(),
2137 &HashSet::new(),
2138 &HashSet::new(),
2139 &HashSet::new(),
2140 );
2141 assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
2142 assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
2143 }
2144}