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}
19
20#[derive(Debug, Clone)]
22enum PathSegment {
23 Field(String),
25 ArrayField(String),
27 MapAccess { field: String, key: String },
29 Length,
31}
32
33impl FieldResolver {
34 pub fn new(
38 fields: &HashMap<String, String>,
39 optional: &HashSet<String>,
40 result_fields: &HashSet<String>,
41 array_fields: &HashSet<String>,
42 method_calls: &HashSet<String>,
43 ) -> Self {
44 Self {
45 aliases: fields.clone(),
46 optional_fields: optional.clone(),
47 result_fields: result_fields.clone(),
48 array_fields: array_fields.clone(),
49 method_calls: method_calls.clone(),
50 }
51 }
52
53 pub fn resolve<'a>(&'a self, fixture_field: &'a str) -> &'a str {
56 self.aliases
57 .get(fixture_field)
58 .map(String::as_str)
59 .unwrap_or(fixture_field)
60 }
61
62 pub fn is_optional(&self, field: &str) -> bool {
64 if self.optional_fields.contains(field) {
65 return true;
66 }
67 let index_normalized = normalize_numeric_indices(field);
68 if index_normalized != field && self.optional_fields.contains(index_normalized.as_str()) {
69 return true;
70 }
71 let normalized = field.replace("[].", ".");
72 if normalized != field && self.optional_fields.contains(normalized.as_str()) {
73 return true;
74 }
75 for af in &self.array_fields {
76 if let Some(rest) = field.strip_prefix(af.as_str()) {
77 if let Some(rest) = rest.strip_prefix('.') {
78 let with_bracket = format!("{af}[].{rest}");
79 if self.optional_fields.contains(with_bracket.as_str()) {
80 return true;
81 }
82 }
83 }
84 }
85 false
86 }
87
88 pub fn has_alias(&self, fixture_field: &str) -> bool {
90 self.aliases.contains_key(fixture_field)
91 }
92
93 pub fn is_valid_for_result(&self, fixture_field: &str) -> bool {
95 if self.result_fields.is_empty() {
96 return true;
97 }
98 let resolved = self.resolve(fixture_field);
99 let first_segment = resolved.split('.').next().unwrap_or(resolved);
100 let first_segment = first_segment.split('[').next().unwrap_or(first_segment);
101 self.result_fields.contains(first_segment)
102 }
103
104 pub fn is_array(&self, field: &str) -> bool {
106 self.array_fields.contains(field)
107 }
108
109 pub fn tagged_union_split(&self, fixture_field: &str) -> Option<(String, String, String)> {
121 let resolved = self.resolve(fixture_field);
122 let segments: Vec<&str> = resolved.split('.').collect();
123 let mut path_so_far = String::new();
124 for (i, seg) in segments.iter().enumerate() {
125 if !path_so_far.is_empty() {
126 path_so_far.push('.');
127 }
128 path_so_far.push_str(seg);
129 if self.method_calls.contains(&path_so_far) {
130 let prefix = segments[..i].join(".");
132 let variant = (*seg).to_string();
133 let suffix = segments[i + 1..].join(".");
134 return Some((prefix, variant, suffix));
135 }
136 }
137 None
138 }
139
140 pub fn has_map_access(&self, fixture_field: &str) -> bool {
142 let resolved = self.resolve(fixture_field);
143 let segments = parse_path(resolved);
144 segments.iter().any(|s| {
145 if let PathSegment::MapAccess { key, .. } = s {
146 !key.chars().all(|c| c.is_ascii_digit())
147 } else {
148 false
149 }
150 })
151 }
152
153 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
155 let resolved = self.resolve(fixture_field);
156 let segments = parse_path(resolved);
157 let segments = self.inject_array_indexing(segments);
158 match language {
159 "java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
160 "rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
161 "csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
162 "zig" => render_zig_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
163 "swift" => render_swift_with_optionals(&segments, result_var, &self.optional_fields),
164 _ => render_accessor(&segments, language, result_var),
165 }
166 }
167
168 fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
169 if self.array_fields.is_empty() {
170 return segments;
171 }
172 let len = segments.len();
173 let mut result = Vec::with_capacity(len);
174 let mut path_so_far = String::new();
175 for i in 0..len {
176 let seg = &segments[i];
177 match seg {
178 PathSegment::Field(f) => {
179 if !path_so_far.is_empty() {
180 path_so_far.push('.');
181 }
182 path_so_far.push_str(f);
183 let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
184 if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
185 result.push(PathSegment::ArrayField(f.clone()));
186 } else {
187 result.push(seg.clone());
188 }
189 }
190 _ => {
191 result.push(seg.clone());
192 }
193 }
194 }
195 result
196 }
197
198 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
200 let resolved = self.resolve(fixture_field);
201 if !self.is_optional(resolved) {
202 return None;
203 }
204 let segments = parse_path(resolved);
205 let segments = self.inject_array_indexing(segments);
206 let local_var = resolved.replace(['.', '['], "_").replace(']', "");
207 let accessor = render_accessor(&segments, "rust", result_var);
208 let has_map_access = segments.iter().any(|s| {
209 if let PathSegment::MapAccess { key, .. } = s {
210 !key.chars().all(|c| c.is_ascii_digit())
211 } else {
212 false
213 }
214 });
215 let is_array = self.is_array(resolved);
216 let binding = if has_map_access {
217 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
218 } else if is_array {
219 format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
220 } else {
221 format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
222 };
223 Some((binding, local_var))
224 }
225}
226
227fn normalize_numeric_indices(path: &str) -> String {
228 let mut result = String::with_capacity(path.len());
229 let mut chars = path.chars().peekable();
230 while let Some(c) = chars.next() {
231 if c == '[' {
232 let mut key = String::new();
233 let mut closed = false;
234 for inner in chars.by_ref() {
235 if inner == ']' {
236 closed = true;
237 break;
238 }
239 key.push(inner);
240 }
241 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
242 result.push_str("[0]");
243 } else {
244 result.push('[');
245 result.push_str(&key);
246 if closed {
247 result.push(']');
248 }
249 }
250 } else {
251 result.push(c);
252 }
253 }
254 result
255}
256
257fn parse_path(path: &str) -> Vec<PathSegment> {
258 let mut segments = Vec::new();
259 for part in path.split('.') {
260 if part == "length" || part == "count" || part == "size" {
261 segments.push(PathSegment::Length);
262 } else if let Some(bracket_pos) = part.find('[') {
263 let field = part[..bracket_pos].to_string();
264 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
265 if key.is_empty() {
266 segments.push(PathSegment::ArrayField(field));
267 } else {
268 segments.push(PathSegment::MapAccess { field, key });
269 }
270 } else {
271 segments.push(PathSegment::Field(part.to_string()));
272 }
273 }
274 segments
275}
276
277fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
278 match language {
279 "rust" => render_rust(segments, result_var),
280 "python" => render_dot_access(segments, result_var, "python"),
281 "typescript" | "node" => render_typescript(segments, result_var),
282 "wasm" => render_wasm(segments, result_var),
283 "go" => render_go(segments, result_var),
284 "java" => render_java(segments, result_var),
285 "csharp" => render_pascal_dot(segments, result_var),
286 "ruby" => render_dot_access(segments, result_var, "ruby"),
287 "php" => render_php(segments, result_var),
288 "elixir" => render_dot_access(segments, result_var, "elixir"),
289 "r" => render_r(segments, result_var),
290 "c" => render_c(segments, result_var),
291 "swift" => render_swift(segments, result_var),
292 _ => render_dot_access(segments, result_var, language),
293 }
294}
295
296fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
302 let mut out = result_var.to_string();
303 for seg in segments {
304 match seg {
305 PathSegment::Field(f) => {
306 out.push('.');
307 out.push_str(f);
308 out.push_str("()");
309 }
310 PathSegment::ArrayField(f) => {
311 out.push('.');
312 out.push_str(f);
313 out.push_str("()[0]");
314 }
315 PathSegment::MapAccess { field, key } => {
316 out.push('.');
317 out.push_str(field);
318 if key.chars().all(|c| c.is_ascii_digit()) {
319 out.push_str(&format!("()[{key}]"));
320 } else {
321 out.push_str(&format!("()[\"{key}\"]"));
322 }
323 }
324 PathSegment::Length => {
325 out.push_str(".count");
326 }
327 }
328 }
329 out
330}
331
332fn render_swift_with_optionals(
342 segments: &[PathSegment],
343 result_var: &str,
344 optional_fields: &HashSet<String>,
345) -> String {
346 let mut out = result_var.to_string();
347 let mut path_so_far = String::new();
348 let total = segments.len();
349 for (i, seg) in segments.iter().enumerate() {
350 let is_leaf = i == total - 1;
351 match seg {
352 PathSegment::Field(f) => {
353 if !path_so_far.is_empty() {
354 path_so_far.push('.');
355 }
356 path_so_far.push_str(f);
357 out.push('.');
358 out.push_str(f);
359 out.push_str("()");
360 if !is_leaf && optional_fields.contains(&path_so_far) {
363 out.push('?');
364 }
365 }
366 PathSegment::ArrayField(f) => {
367 if !path_so_far.is_empty() {
368 path_so_far.push('.');
369 }
370 path_so_far.push_str(f);
371 out.push('.');
372 out.push_str(f);
373 out.push_str("()[0]");
374 }
375 PathSegment::MapAccess { field, key } => {
376 if !path_so_far.is_empty() {
377 path_so_far.push('.');
378 }
379 path_so_far.push_str(field);
380 out.push('.');
381 out.push_str(field);
382 if key.chars().all(|c| c.is_ascii_digit()) {
383 out.push_str(&format!("()[{key}]"));
384 } else {
385 out.push_str(&format!("()[\"{key}\"]"));
386 }
387 }
388 PathSegment::Length => {
389 out.push_str(".count");
390 }
391 }
392 }
393 out
394}
395
396fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
397 let mut out = result_var.to_string();
398 for seg in segments {
399 match seg {
400 PathSegment::Field(f) => {
401 out.push('.');
402 out.push_str(&f.to_snake_case());
403 }
404 PathSegment::ArrayField(f) => {
405 out.push('.');
406 out.push_str(&f.to_snake_case());
407 out.push_str("[0]");
408 }
409 PathSegment::MapAccess { field, key } => {
410 out.push('.');
411 out.push_str(&field.to_snake_case());
412 if key.chars().all(|c| c.is_ascii_digit()) {
413 out.push_str(&format!("[{key}]"));
414 } else {
415 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
416 }
417 }
418 PathSegment::Length => {
419 out.push_str(".len()");
420 }
421 }
422 }
423 out
424}
425
426fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
427 let mut out = result_var.to_string();
428 for seg in segments {
429 match seg {
430 PathSegment::Field(f) => {
431 out.push('.');
432 out.push_str(f);
433 }
434 PathSegment::ArrayField(f) => {
435 if language == "elixir" {
436 let current = std::mem::take(&mut out);
437 out = format!("Enum.at({current}.{f}, 0)");
438 } else {
439 out.push('.');
440 out.push_str(f);
441 out.push_str("[0]");
442 }
443 }
444 PathSegment::MapAccess { field, key } => {
445 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
446 if is_numeric && language == "elixir" {
447 let current = std::mem::take(&mut out);
448 out = format!("Enum.at({current}.{field}, {key})");
449 } else {
450 out.push('.');
451 out.push_str(field);
452 if is_numeric {
453 let idx: usize = key.parse().unwrap_or(0);
454 out.push_str(&format!("[{idx}]"));
455 } else if language == "elixir" {
456 out.push_str(&format!("[\"{key}\"]"));
457 } else {
458 out.push_str(&format!(".get(\"{key}\")"));
459 }
460 }
461 }
462 PathSegment::Length => match language {
463 "ruby" => out.push_str(".length"),
464 "elixir" => {
465 let current = std::mem::take(&mut out);
466 out = format!("length({current})");
467 }
468 _ => {
469 let current = std::mem::take(&mut out);
470 out = format!("len({current})");
471 }
472 },
473 }
474 }
475 out
476}
477
478fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
479 let mut out = result_var.to_string();
480 for seg in segments {
481 match seg {
482 PathSegment::Field(f) => {
483 out.push('.');
484 out.push_str(&f.to_lower_camel_case());
485 }
486 PathSegment::ArrayField(f) => {
487 out.push('.');
488 out.push_str(&f.to_lower_camel_case());
489 out.push_str("[0]");
490 }
491 PathSegment::MapAccess { field, key } => {
492 out.push('.');
493 out.push_str(&field.to_lower_camel_case());
494 out.push_str(&format!("[\"{key}\"]"));
495 }
496 PathSegment::Length => {
497 out.push_str(".length");
498 }
499 }
500 }
501 out
502}
503
504fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
505 let mut out = result_var.to_string();
506 for seg in segments {
507 match seg {
508 PathSegment::Field(f) => {
509 out.push('.');
510 out.push_str(&f.to_lower_camel_case());
511 }
512 PathSegment::ArrayField(f) => {
513 out.push('.');
514 out.push_str(&f.to_lower_camel_case());
515 out.push_str("[0]");
516 }
517 PathSegment::MapAccess { field, key } => {
518 out.push('.');
519 out.push_str(&field.to_lower_camel_case());
520 out.push_str(&format!(".get(\"{key}\")"));
521 }
522 PathSegment::Length => {
523 out.push_str(".length");
524 }
525 }
526 }
527 out
528}
529
530fn render_go(segments: &[PathSegment], result_var: &str) -> String {
531 let mut out = result_var.to_string();
532 for seg in segments {
533 match seg {
534 PathSegment::Field(f) => {
535 out.push('.');
536 out.push_str(&to_go_name(f));
537 }
538 PathSegment::ArrayField(f) => {
539 out.push('.');
540 out.push_str(&to_go_name(f));
541 out.push_str("[0]");
542 }
543 PathSegment::MapAccess { field, key } => {
544 out.push('.');
545 out.push_str(&to_go_name(field));
546 if key.chars().all(|c| c.is_ascii_digit()) {
547 out.push_str(&format!("[{key}]"));
548 } else {
549 out.push_str(&format!("[\"{key}\"]"));
550 }
551 }
552 PathSegment::Length => {
553 let current = std::mem::take(&mut out);
554 out = format!("len({current})");
555 }
556 }
557 }
558 out
559}
560
561fn render_java(segments: &[PathSegment], result_var: &str) -> String {
562 let mut out = result_var.to_string();
563 for seg in segments {
564 match seg {
565 PathSegment::Field(f) => {
566 out.push('.');
567 out.push_str(&f.to_lower_camel_case());
568 out.push_str("()");
569 }
570 PathSegment::ArrayField(f) => {
571 out.push('.');
572 out.push_str(&f.to_lower_camel_case());
573 out.push_str("().getFirst()");
574 }
575 PathSegment::MapAccess { field, key } => {
576 out.push('.');
577 out.push_str(&field.to_lower_camel_case());
578 out.push_str(&format!("().get(\"{key}\")"));
579 }
580 PathSegment::Length => {
581 out.push_str(".size()");
582 }
583 }
584 }
585 out
586}
587
588fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
589 let mut out = result_var.to_string();
590 let mut path_so_far = String::new();
591 for (i, seg) in segments.iter().enumerate() {
592 let is_leaf = i == segments.len() - 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.to_lower_camel_case());
601 out.push_str("()");
602 let _ = is_leaf;
603 let _ = optional_fields;
604 }
605 PathSegment::ArrayField(f) => {
606 if !path_so_far.is_empty() {
607 path_so_far.push('.');
608 }
609 path_so_far.push_str(f);
610 out.push('.');
611 out.push_str(&f.to_lower_camel_case());
612 out.push_str("().getFirst()");
613 }
614 PathSegment::MapAccess { field, key } => {
615 if !path_so_far.is_empty() {
616 path_so_far.push('.');
617 }
618 path_so_far.push_str(field);
619 out.push('.');
620 out.push_str(&field.to_lower_camel_case());
621 out.push_str(&format!("().get(\"{key}\")"));
622 }
623 PathSegment::Length => {
624 out.push_str(".size()");
625 }
626 }
627 }
628 out
629}
630
631fn render_rust_with_optionals(
637 segments: &[PathSegment],
638 result_var: &str,
639 optional_fields: &HashSet<String>,
640 method_calls: &HashSet<String>,
641) -> String {
642 let mut out = result_var.to_string();
643 let mut path_so_far = String::new();
644 for (i, seg) in segments.iter().enumerate() {
645 let is_leaf = i == segments.len() - 1;
646 match seg {
647 PathSegment::Field(f) => {
648 if !path_so_far.is_empty() {
649 path_so_far.push('.');
650 }
651 path_so_far.push_str(f);
652 out.push('.');
653 out.push_str(&f.to_snake_case());
654 let is_method = method_calls.contains(&path_so_far);
655 if is_method {
656 out.push_str("()");
657 if !is_leaf && optional_fields.contains(&path_so_far) {
658 out.push_str(".as_ref().unwrap()");
659 }
660 } else if !is_leaf && optional_fields.contains(&path_so_far) {
661 out.push_str(".as_ref().unwrap()");
662 }
663 }
664 PathSegment::ArrayField(f) => {
665 if !path_so_far.is_empty() {
666 path_so_far.push('.');
667 }
668 path_so_far.push_str(f);
669 out.push('.');
670 out.push_str(&f.to_snake_case());
671 out.push_str("[0]");
672 }
673 PathSegment::MapAccess { field, key } => {
674 if !path_so_far.is_empty() {
675 path_so_far.push('.');
676 }
677 path_so_far.push_str(field);
678 out.push('.');
679 out.push_str(&field.to_snake_case());
680 if key.chars().all(|c| c.is_ascii_digit()) {
681 let is_opt = optional_fields.contains(&path_so_far);
682 if is_opt {
683 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
684 } else {
685 out.push_str(&format!("[{key}]"));
686 }
687 path_so_far.push_str("[0]");
688 } else {
689 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
690 }
691 }
692 PathSegment::Length => {
693 out.push_str(".len()");
694 }
695 }
696 }
697 out
698}
699
700fn render_zig_with_optionals(
713 segments: &[PathSegment],
714 result_var: &str,
715 optional_fields: &HashSet<String>,
716 method_calls: &HashSet<String>,
717) -> String {
718 let mut out = result_var.to_string();
719 let mut path_so_far = String::new();
720 for seg in segments {
721 match seg {
722 PathSegment::Field(f) => {
723 if !path_so_far.is_empty() {
724 path_so_far.push('.');
725 }
726 path_so_far.push_str(f);
727 out.push('.');
728 out.push_str(f);
729 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
730 out.push_str(".?");
731 }
732 }
733 PathSegment::ArrayField(f) => {
734 if !path_so_far.is_empty() {
735 path_so_far.push('.');
736 }
737 path_so_far.push_str(f);
738 out.push('.');
739 out.push_str(f);
740 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
741 out.push_str(".?");
742 }
743 out.push_str("[0]");
744 }
745 PathSegment::MapAccess { field, key } => {
746 if !path_so_far.is_empty() {
747 path_so_far.push('.');
748 }
749 path_so_far.push_str(field);
750 out.push('.');
751 out.push_str(field);
752 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
753 out.push_str(".?");
754 }
755 if key.chars().all(|c| c.is_ascii_digit()) {
756 out.push_str(&format!("[{key}]"));
757 } else {
758 out.push_str(&format!(".get(\"{key}\")"));
759 }
760 }
761 PathSegment::Length => {
762 out.push_str(".len");
763 }
764 }
765 }
766 out
767}
768
769fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
770 let mut out = result_var.to_string();
771 for seg in segments {
772 match seg {
773 PathSegment::Field(f) => {
774 out.push('.');
775 out.push_str(&f.to_pascal_case());
776 }
777 PathSegment::ArrayField(f) => {
778 out.push('.');
779 out.push_str(&f.to_pascal_case());
780 out.push_str("[0]");
781 }
782 PathSegment::MapAccess { field, key } => {
783 out.push('.');
784 out.push_str(&field.to_pascal_case());
785 if key.chars().all(|c| c.is_ascii_digit()) {
786 out.push_str(&format!("[{key}]"));
787 } else {
788 out.push_str(&format!("[\"{key}\"]"));
789 }
790 }
791 PathSegment::Length => {
792 out.push_str(".Count");
793 }
794 }
795 }
796 out
797}
798
799fn render_csharp_with_optionals(
800 segments: &[PathSegment],
801 result_var: &str,
802 optional_fields: &HashSet<String>,
803) -> String {
804 let mut out = result_var.to_string();
805 let mut path_so_far = String::new();
806 for (i, seg) in segments.iter().enumerate() {
807 let is_leaf = i == segments.len() - 1;
808 match seg {
809 PathSegment::Field(f) => {
810 if !path_so_far.is_empty() {
811 path_so_far.push('.');
812 }
813 path_so_far.push_str(f);
814 out.push('.');
815 out.push_str(&f.to_pascal_case());
816 if !is_leaf && optional_fields.contains(&path_so_far) {
817 out.push('!');
818 }
819 }
820 PathSegment::ArrayField(f) => {
821 if !path_so_far.is_empty() {
822 path_so_far.push('.');
823 }
824 path_so_far.push_str(f);
825 out.push('.');
826 out.push_str(&f.to_pascal_case());
827 out.push_str("[0]");
828 }
829 PathSegment::MapAccess { field, key } => {
830 if !path_so_far.is_empty() {
831 path_so_far.push('.');
832 }
833 path_so_far.push_str(field);
834 out.push('.');
835 out.push_str(&field.to_pascal_case());
836 if key.chars().all(|c| c.is_ascii_digit()) {
837 out.push_str(&format!("[{key}]"));
838 } else {
839 out.push_str(&format!("[\"{key}\"]"));
840 }
841 }
842 PathSegment::Length => {
843 out.push_str(".Count");
844 }
845 }
846 }
847 out
848}
849
850fn render_php(segments: &[PathSegment], result_var: &str) -> String {
851 let mut out = result_var.to_string();
852 for seg in segments {
853 match seg {
854 PathSegment::Field(f) => {
855 out.push_str("->");
856 out.push_str(f);
857 }
858 PathSegment::ArrayField(f) => {
859 out.push_str("->");
860 out.push_str(f);
861 out.push_str("[0]");
862 }
863 PathSegment::MapAccess { field, key } => {
864 out.push_str("->");
865 out.push_str(field);
866 out.push_str(&format!("[\"{key}\"]"));
867 }
868 PathSegment::Length => {
869 let current = std::mem::take(&mut out);
870 out = format!("count({current})");
871 }
872 }
873 }
874 out
875}
876
877fn render_r(segments: &[PathSegment], result_var: &str) -> String {
878 let mut out = result_var.to_string();
879 for seg in segments {
880 match seg {
881 PathSegment::Field(f) => {
882 out.push('$');
883 out.push_str(f);
884 }
885 PathSegment::ArrayField(f) => {
886 out.push('$');
887 out.push_str(f);
888 out.push_str("[[1]]");
889 }
890 PathSegment::MapAccess { field, key } => {
891 out.push('$');
892 out.push_str(field);
893 out.push_str(&format!("[[\"{key}\"]]"));
894 }
895 PathSegment::Length => {
896 let current = std::mem::take(&mut out);
897 out = format!("length({current})");
898 }
899 }
900 }
901 out
902}
903
904fn render_c(segments: &[PathSegment], result_var: &str) -> String {
905 let mut parts = Vec::new();
906 let mut trailing_length = false;
907 for seg in segments {
908 match seg {
909 PathSegment::Field(f) | PathSegment::ArrayField(f) => parts.push(f.to_snake_case()),
910 PathSegment::MapAccess { field, key } => {
911 parts.push(field.to_snake_case());
912 parts.push(key.clone());
913 }
914 PathSegment::Length => {
915 trailing_length = true;
916 }
917 }
918 }
919 let suffix = parts.join("_");
920 if trailing_length {
921 format!("result_{suffix}_count({result_var})")
922 } else {
923 format!("result_{suffix}({result_var})")
924 }
925}
926
927#[cfg(test)]
928mod tests {
929 use super::*;
930
931 fn make_resolver() -> FieldResolver {
932 let mut fields = HashMap::new();
933 fields.insert("title".to_string(), "metadata.document.title".to_string());
934 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
935 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
936 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
937 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
938 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
939 let mut optional = HashSet::new();
940 optional.insert("metadata.document.title".to_string());
941 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
942 }
943
944 fn make_resolver_with_doc_optional() -> FieldResolver {
945 let mut fields = HashMap::new();
946 fields.insert("title".to_string(), "metadata.document.title".to_string());
947 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
948 let mut optional = HashSet::new();
949 optional.insert("document".to_string());
950 optional.insert("metadata.document.title".to_string());
951 optional.insert("metadata.document".to_string());
952 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
953 }
954
955 #[test]
956 fn test_resolve_alias() {
957 let r = make_resolver();
958 assert_eq!(r.resolve("title"), "metadata.document.title");
959 }
960
961 #[test]
962 fn test_resolve_passthrough() {
963 let r = make_resolver();
964 assert_eq!(r.resolve("content"), "content");
965 }
966
967 #[test]
968 fn test_is_optional() {
969 let r = make_resolver();
970 assert!(r.is_optional("metadata.document.title"));
971 assert!(!r.is_optional("content"));
972 }
973
974 #[test]
975 fn test_accessor_rust_struct() {
976 let r = make_resolver();
977 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
978 }
979
980 #[test]
981 fn test_accessor_rust_map() {
982 let r = make_resolver();
983 assert_eq!(
984 r.accessor("tags", "rust", "result"),
985 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
986 );
987 }
988
989 #[test]
990 fn test_accessor_python() {
991 let r = make_resolver();
992 assert_eq!(
993 r.accessor("title", "python", "result"),
994 "result.metadata.document.title"
995 );
996 }
997
998 #[test]
999 fn test_accessor_go() {
1000 let r = make_resolver();
1001 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
1002 }
1003
1004 #[test]
1005 fn test_accessor_go_initialism_fields() {
1006 let mut fields = std::collections::HashMap::new();
1007 fields.insert("content".to_string(), "html".to_string());
1008 fields.insert("link_url".to_string(), "links.url".to_string());
1009 let r = FieldResolver::new(
1010 &fields,
1011 &HashSet::new(),
1012 &HashSet::new(),
1013 &HashSet::new(),
1014 &HashSet::new(),
1015 );
1016 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
1017 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
1018 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
1019 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
1020 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
1021 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
1022 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
1023 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
1024 }
1025
1026 #[test]
1027 fn test_accessor_typescript() {
1028 let r = make_resolver();
1029 assert_eq!(
1030 r.accessor("title", "typescript", "result"),
1031 "result.metadata.document.title"
1032 );
1033 }
1034
1035 #[test]
1036 fn test_accessor_typescript_snake_to_camel() {
1037 let r = make_resolver();
1038 assert_eq!(
1039 r.accessor("og", "typescript", "result"),
1040 "result.metadata.document.openGraph"
1041 );
1042 assert_eq!(
1043 r.accessor("twitter", "typescript", "result"),
1044 "result.metadata.document.twitterCard"
1045 );
1046 assert_eq!(
1047 r.accessor("canonical", "typescript", "result"),
1048 "result.metadata.document.canonicalUrl"
1049 );
1050 }
1051
1052 #[test]
1053 fn test_accessor_typescript_map_snake_to_camel() {
1054 let r = make_resolver();
1055 assert_eq!(
1056 r.accessor("og_tag", "typescript", "result"),
1057 "result.metadata.openGraphTags[\"og_title\"]"
1058 );
1059 }
1060
1061 #[test]
1062 fn test_accessor_node_alias() {
1063 let r = make_resolver();
1064 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
1065 }
1066
1067 #[test]
1068 fn test_accessor_wasm_camel_case() {
1069 let r = make_resolver();
1070 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
1071 assert_eq!(
1072 r.accessor("twitter", "wasm", "result"),
1073 "result.metadata.document.twitterCard"
1074 );
1075 assert_eq!(
1076 r.accessor("canonical", "wasm", "result"),
1077 "result.metadata.document.canonicalUrl"
1078 );
1079 }
1080
1081 #[test]
1082 fn test_accessor_wasm_map_access() {
1083 let r = make_resolver();
1084 assert_eq!(
1085 r.accessor("og_tag", "wasm", "result"),
1086 "result.metadata.openGraphTags.get(\"og_title\")"
1087 );
1088 }
1089
1090 #[test]
1091 fn test_accessor_java() {
1092 let r = make_resolver();
1093 assert_eq!(
1094 r.accessor("title", "java", "result"),
1095 "result.metadata().document().title()"
1096 );
1097 }
1098
1099 #[test]
1100 fn test_accessor_csharp() {
1101 let r = make_resolver();
1102 assert_eq!(
1103 r.accessor("title", "csharp", "result"),
1104 "result.Metadata.Document.Title"
1105 );
1106 }
1107
1108 #[test]
1109 fn test_accessor_php() {
1110 let r = make_resolver();
1111 assert_eq!(
1112 r.accessor("title", "php", "$result"),
1113 "$result->metadata->document->title"
1114 );
1115 }
1116
1117 #[test]
1118 fn test_accessor_r() {
1119 let r = make_resolver();
1120 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
1121 }
1122
1123 #[test]
1124 fn test_accessor_c() {
1125 let r = make_resolver();
1126 assert_eq!(
1127 r.accessor("title", "c", "result"),
1128 "result_metadata_document_title(result)"
1129 );
1130 }
1131
1132 #[test]
1133 fn test_rust_unwrap_binding() {
1134 let r = make_resolver();
1135 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
1136 assert_eq!(var, "metadata_document_title");
1137 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
1138 }
1139
1140 #[test]
1141 fn test_rust_unwrap_binding_non_optional() {
1142 let r = make_resolver();
1143 assert!(r.rust_unwrap_binding("content", "result").is_none());
1144 }
1145
1146 #[test]
1147 fn test_direct_field_no_alias() {
1148 let r = make_resolver();
1149 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1150 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
1151 }
1152
1153 #[test]
1154 fn test_accessor_rust_with_optionals() {
1155 let r = make_resolver_with_doc_optional();
1156 assert_eq!(
1157 r.accessor("title", "rust", "result"),
1158 "result.metadata.document.as_ref().unwrap().title"
1159 );
1160 }
1161
1162 #[test]
1163 fn test_accessor_csharp_with_optionals() {
1164 let r = make_resolver_with_doc_optional();
1165 assert_eq!(
1166 r.accessor("title", "csharp", "result"),
1167 "result.Metadata.Document!.Title"
1168 );
1169 }
1170
1171 #[test]
1172 fn test_accessor_rust_non_optional_field() {
1173 let r = make_resolver();
1174 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1175 }
1176
1177 #[test]
1178 fn test_accessor_csharp_non_optional_field() {
1179 let r = make_resolver();
1180 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
1181 }
1182
1183 #[test]
1184 fn test_accessor_rust_method_call() {
1185 let mut fields = HashMap::new();
1187 fields.insert(
1188 "excel_sheet_count".to_string(),
1189 "metadata.format.excel.sheet_count".to_string(),
1190 );
1191 let mut optional = HashSet::new();
1192 optional.insert("metadata.format".to_string());
1193 optional.insert("metadata.format.excel".to_string());
1194 let mut method_calls = HashSet::new();
1195 method_calls.insert("metadata.format.excel".to_string());
1196 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
1197 assert_eq!(
1198 r.accessor("excel_sheet_count", "rust", "result"),
1199 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
1200 );
1201 }
1202}