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.to_lower_camel_case());
859 }
860 PathSegment::ArrayField(f) => {
861 out.push_str("->");
862 out.push_str(&f.to_lower_camel_case());
863 out.push_str("[0]");
864 }
865 PathSegment::MapAccess { field, key } => {
866 out.push_str("->");
867 out.push_str(&field.to_lower_camel_case());
868 out.push_str(&format!("[\"{key}\"]"));
869 }
870 PathSegment::Length => {
871 let current = std::mem::take(&mut out);
872 out = format!("count({current})");
873 }
874 }
875 }
876 out
877}
878
879fn render_r(segments: &[PathSegment], result_var: &str) -> String {
880 let mut out = result_var.to_string();
881 for seg in segments {
882 match seg {
883 PathSegment::Field(f) => {
884 out.push('$');
885 out.push_str(f);
886 }
887 PathSegment::ArrayField(f) => {
888 out.push('$');
889 out.push_str(f);
890 out.push_str("[[1]]");
891 }
892 PathSegment::MapAccess { field, key } => {
893 out.push('$');
894 out.push_str(field);
895 out.push_str(&format!("[[\"{key}\"]]"));
896 }
897 PathSegment::Length => {
898 let current = std::mem::take(&mut out);
899 out = format!("length({current})");
900 }
901 }
902 }
903 out
904}
905
906fn render_c(segments: &[PathSegment], result_var: &str) -> String {
907 let mut parts = Vec::new();
908 let mut trailing_length = false;
909 for seg in segments {
910 match seg {
911 PathSegment::Field(f) | PathSegment::ArrayField(f) => parts.push(f.to_snake_case()),
912 PathSegment::MapAccess { field, key } => {
913 parts.push(field.to_snake_case());
914 parts.push(key.clone());
915 }
916 PathSegment::Length => {
917 trailing_length = true;
918 }
919 }
920 }
921 let suffix = parts.join("_");
922 if trailing_length {
923 format!("result_{suffix}_count({result_var})")
924 } else {
925 format!("result_{suffix}({result_var})")
926 }
927}
928
929#[cfg(test)]
930mod tests {
931 use super::*;
932
933 fn make_resolver() -> FieldResolver {
934 let mut fields = HashMap::new();
935 fields.insert("title".to_string(), "metadata.document.title".to_string());
936 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
937 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
938 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
939 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
940 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
941 let mut optional = HashSet::new();
942 optional.insert("metadata.document.title".to_string());
943 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
944 }
945
946 fn make_resolver_with_doc_optional() -> FieldResolver {
947 let mut fields = HashMap::new();
948 fields.insert("title".to_string(), "metadata.document.title".to_string());
949 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
950 let mut optional = HashSet::new();
951 optional.insert("document".to_string());
952 optional.insert("metadata.document.title".to_string());
953 optional.insert("metadata.document".to_string());
954 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
955 }
956
957 #[test]
958 fn test_resolve_alias() {
959 let r = make_resolver();
960 assert_eq!(r.resolve("title"), "metadata.document.title");
961 }
962
963 #[test]
964 fn test_resolve_passthrough() {
965 let r = make_resolver();
966 assert_eq!(r.resolve("content"), "content");
967 }
968
969 #[test]
970 fn test_is_optional() {
971 let r = make_resolver();
972 assert!(r.is_optional("metadata.document.title"));
973 assert!(!r.is_optional("content"));
974 }
975
976 #[test]
977 fn test_accessor_rust_struct() {
978 let r = make_resolver();
979 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
980 }
981
982 #[test]
983 fn test_accessor_rust_map() {
984 let r = make_resolver();
985 assert_eq!(
986 r.accessor("tags", "rust", "result"),
987 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
988 );
989 }
990
991 #[test]
992 fn test_accessor_python() {
993 let r = make_resolver();
994 assert_eq!(
995 r.accessor("title", "python", "result"),
996 "result.metadata.document.title"
997 );
998 }
999
1000 #[test]
1001 fn test_accessor_go() {
1002 let r = make_resolver();
1003 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
1004 }
1005
1006 #[test]
1007 fn test_accessor_go_initialism_fields() {
1008 let mut fields = std::collections::HashMap::new();
1009 fields.insert("content".to_string(), "html".to_string());
1010 fields.insert("link_url".to_string(), "links.url".to_string());
1011 let r = FieldResolver::new(
1012 &fields,
1013 &HashSet::new(),
1014 &HashSet::new(),
1015 &HashSet::new(),
1016 &HashSet::new(),
1017 );
1018 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
1019 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
1020 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
1021 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
1022 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
1023 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
1024 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
1025 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
1026 }
1027
1028 #[test]
1029 fn test_accessor_typescript() {
1030 let r = make_resolver();
1031 assert_eq!(
1032 r.accessor("title", "typescript", "result"),
1033 "result.metadata.document.title"
1034 );
1035 }
1036
1037 #[test]
1038 fn test_accessor_typescript_snake_to_camel() {
1039 let r = make_resolver();
1040 assert_eq!(
1041 r.accessor("og", "typescript", "result"),
1042 "result.metadata.document.openGraph"
1043 );
1044 assert_eq!(
1045 r.accessor("twitter", "typescript", "result"),
1046 "result.metadata.document.twitterCard"
1047 );
1048 assert_eq!(
1049 r.accessor("canonical", "typescript", "result"),
1050 "result.metadata.document.canonicalUrl"
1051 );
1052 }
1053
1054 #[test]
1055 fn test_accessor_typescript_map_snake_to_camel() {
1056 let r = make_resolver();
1057 assert_eq!(
1058 r.accessor("og_tag", "typescript", "result"),
1059 "result.metadata.openGraphTags[\"og_title\"]"
1060 );
1061 }
1062
1063 #[test]
1064 fn test_accessor_node_alias() {
1065 let r = make_resolver();
1066 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
1067 }
1068
1069 #[test]
1070 fn test_accessor_wasm_camel_case() {
1071 let r = make_resolver();
1072 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
1073 assert_eq!(
1074 r.accessor("twitter", "wasm", "result"),
1075 "result.metadata.document.twitterCard"
1076 );
1077 assert_eq!(
1078 r.accessor("canonical", "wasm", "result"),
1079 "result.metadata.document.canonicalUrl"
1080 );
1081 }
1082
1083 #[test]
1084 fn test_accessor_wasm_map_access() {
1085 let r = make_resolver();
1086 assert_eq!(
1087 r.accessor("og_tag", "wasm", "result"),
1088 "result.metadata.openGraphTags.get(\"og_title\")"
1089 );
1090 }
1091
1092 #[test]
1093 fn test_accessor_java() {
1094 let r = make_resolver();
1095 assert_eq!(
1096 r.accessor("title", "java", "result"),
1097 "result.metadata().document().title()"
1098 );
1099 }
1100
1101 #[test]
1102 fn test_accessor_csharp() {
1103 let r = make_resolver();
1104 assert_eq!(
1105 r.accessor("title", "csharp", "result"),
1106 "result.Metadata.Document.Title"
1107 );
1108 }
1109
1110 #[test]
1111 fn test_accessor_php() {
1112 let r = make_resolver();
1113 assert_eq!(
1114 r.accessor("title", "php", "$result"),
1115 "$result->metadata->document->title"
1116 );
1117 }
1118
1119 #[test]
1120 fn test_accessor_r() {
1121 let r = make_resolver();
1122 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
1123 }
1124
1125 #[test]
1126 fn test_accessor_c() {
1127 let r = make_resolver();
1128 assert_eq!(
1129 r.accessor("title", "c", "result"),
1130 "result_metadata_document_title(result)"
1131 );
1132 }
1133
1134 #[test]
1135 fn test_rust_unwrap_binding() {
1136 let r = make_resolver();
1137 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
1138 assert_eq!(var, "metadata_document_title");
1139 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
1140 }
1141
1142 #[test]
1143 fn test_rust_unwrap_binding_non_optional() {
1144 let r = make_resolver();
1145 assert!(r.rust_unwrap_binding("content", "result").is_none());
1146 }
1147
1148 #[test]
1149 fn test_direct_field_no_alias() {
1150 let r = make_resolver();
1151 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1152 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
1153 }
1154
1155 #[test]
1156 fn test_accessor_rust_with_optionals() {
1157 let r = make_resolver_with_doc_optional();
1158 assert_eq!(
1159 r.accessor("title", "rust", "result"),
1160 "result.metadata.document.as_ref().unwrap().title"
1161 );
1162 }
1163
1164 #[test]
1165 fn test_accessor_csharp_with_optionals() {
1166 let r = make_resolver_with_doc_optional();
1167 assert_eq!(
1168 r.accessor("title", "csharp", "result"),
1169 "result.Metadata.Document!.Title"
1170 );
1171 }
1172
1173 #[test]
1174 fn test_accessor_rust_non_optional_field() {
1175 let r = make_resolver();
1176 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1177 }
1178
1179 #[test]
1180 fn test_accessor_csharp_non_optional_field() {
1181 let r = make_resolver();
1182 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
1183 }
1184
1185 #[test]
1186 fn test_accessor_rust_method_call() {
1187 let mut fields = HashMap::new();
1189 fields.insert(
1190 "excel_sheet_count".to_string(),
1191 "metadata.format.excel.sheet_count".to_string(),
1192 );
1193 let mut optional = HashSet::new();
1194 optional.insert("metadata.format".to_string());
1195 optional.insert("metadata.format.excel".to_string());
1196 let mut method_calls = HashSet::new();
1197 method_calls.insert("metadata.format.excel".to_string());
1198 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
1199 assert_eq!(
1200 r.accessor("excel_sheet_count", "rust", "result"),
1201 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
1202 );
1203 }
1204}