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}
23
24#[derive(Debug, Clone)]
26enum PathSegment {
27 Field(String),
29 ArrayField { name: String, index: usize },
34 MapAccess { field: String, key: String },
36 Length,
38}
39
40impl FieldResolver {
41 pub fn new(
45 fields: &HashMap<String, String>,
46 optional: &HashSet<String>,
47 result_fields: &HashSet<String>,
48 array_fields: &HashSet<String>,
49 method_calls: &HashSet<String>,
50 ) -> Self {
51 Self {
52 aliases: fields.clone(),
53 optional_fields: optional.clone(),
54 result_fields: result_fields.clone(),
55 array_fields: array_fields.clone(),
56 method_calls: method_calls.clone(),
57 error_field_aliases: HashMap::new(),
58 }
59 }
60
61 pub fn new_with_error_aliases(
67 fields: &HashMap<String, String>,
68 optional: &HashSet<String>,
69 result_fields: &HashSet<String>,
70 array_fields: &HashSet<String>,
71 method_calls: &HashSet<String>,
72 error_field_aliases: &HashMap<String, String>,
73 ) -> Self {
74 Self {
75 aliases: fields.clone(),
76 optional_fields: optional.clone(),
77 result_fields: result_fields.clone(),
78 array_fields: array_fields.clone(),
79 method_calls: method_calls.clone(),
80 error_field_aliases: error_field_aliases.clone(),
81 }
82 }
83
84 pub fn resolve<'a>(&'a self, fixture_field: &'a str) -> &'a str {
87 self.aliases
88 .get(fixture_field)
89 .map(String::as_str)
90 .unwrap_or(fixture_field)
91 }
92
93 pub fn is_optional(&self, field: &str) -> bool {
95 if self.optional_fields.contains(field) {
96 return true;
97 }
98 let index_normalized = normalize_numeric_indices(field);
99 if index_normalized != field && self.optional_fields.contains(index_normalized.as_str()) {
100 return true;
101 }
102 let normalized = field.replace("[].", ".");
103 if normalized != field && self.optional_fields.contains(normalized.as_str()) {
104 return true;
105 }
106 for af in &self.array_fields {
107 if let Some(rest) = field.strip_prefix(af.as_str()) {
108 if let Some(rest) = rest.strip_prefix('.') {
109 let with_bracket = format!("{af}[].{rest}");
110 if self.optional_fields.contains(with_bracket.as_str()) {
111 return true;
112 }
113 }
114 }
115 }
116 false
117 }
118
119 pub fn has_alias(&self, fixture_field: &str) -> bool {
121 self.aliases.contains_key(fixture_field)
122 }
123
124 pub fn is_valid_for_result(&self, fixture_field: &str) -> bool {
126 if self.result_fields.is_empty() {
127 return true;
128 }
129 let resolved = self.resolve(fixture_field);
130 let first_segment = resolved.split('.').next().unwrap_or(resolved);
131 let first_segment = first_segment.split('[').next().unwrap_or(first_segment);
132 self.result_fields.contains(first_segment)
133 }
134
135 pub fn is_array(&self, field: &str) -> bool {
137 self.array_fields.contains(field)
138 }
139
140 pub fn tagged_union_split(&self, fixture_field: &str) -> Option<(String, String, String)> {
152 let resolved = self.resolve(fixture_field);
153 let segments: Vec<&str> = resolved.split('.').collect();
154 let mut path_so_far = String::new();
155 for (i, seg) in segments.iter().enumerate() {
156 if !path_so_far.is_empty() {
157 path_so_far.push('.');
158 }
159 path_so_far.push_str(seg);
160 if self.method_calls.contains(&path_so_far) {
161 let prefix = segments[..i].join(".");
163 let variant = (*seg).to_string();
164 let suffix = segments[i + 1..].join(".");
165 return Some((prefix, variant, suffix));
166 }
167 }
168 None
169 }
170
171 pub fn has_map_access(&self, fixture_field: &str) -> bool {
173 let resolved = self.resolve(fixture_field);
174 let segments = parse_path(resolved);
175 segments.iter().any(|s| {
176 if let PathSegment::MapAccess { key, .. } = s {
177 !key.chars().all(|c| c.is_ascii_digit())
178 } else {
179 false
180 }
181 })
182 }
183
184 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
186 let resolved = self.resolve(fixture_field);
187 let segments = parse_path(resolved);
188 let segments = self.inject_array_indexing(segments);
189 match language {
190 "java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
191 "kotlin" => render_kotlin_with_optionals(&segments, result_var, &self.optional_fields),
192 "rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
193 "csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
194 "zig" => render_zig_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
195 "swift" => render_swift_with_optionals(&segments, result_var, &self.optional_fields),
196 "dart" => render_dart_with_optionals(&segments, result_var, &self.optional_fields),
197 _ => render_accessor(&segments, language, result_var),
198 }
199 }
200
201 pub fn accessor_for_error(&self, sub_field: &str, language: &str, err_var: &str) -> String {
215 let resolved = self
216 .error_field_aliases
217 .get(sub_field)
218 .map(String::as_str)
219 .unwrap_or(sub_field);
220 let segments = parse_path(resolved);
221 match language {
224 "rust" => render_rust_with_optionals(&segments, err_var, &self.optional_fields, &self.method_calls),
225 _ => render_accessor(&segments, language, err_var),
226 }
227 }
228
229 pub fn has_error_aliases(&self) -> bool {
236 !self.error_field_aliases.is_empty()
237 }
238
239 fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
240 if self.array_fields.is_empty() {
241 return segments;
242 }
243 let len = segments.len();
244 let mut result = Vec::with_capacity(len);
245 let mut path_so_far = String::new();
246 for i in 0..len {
247 let seg = &segments[i];
248 match seg {
249 PathSegment::Field(f) => {
250 if !path_so_far.is_empty() {
251 path_so_far.push('.');
252 }
253 path_so_far.push_str(f);
254 let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
255 if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
256 result.push(PathSegment::ArrayField {
258 name: f.clone(),
259 index: 0,
260 });
261 } else {
262 result.push(seg.clone());
263 }
264 }
265 PathSegment::ArrayField { .. } => {
268 result.push(seg.clone());
269 }
270 PathSegment::MapAccess { field, key } => {
271 if !path_so_far.is_empty() {
272 path_so_far.push('.');
273 }
274 path_so_far.push_str(field);
275 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
276 if is_numeric && self.array_fields.contains(&path_so_far) {
277 let index: usize = key.parse().unwrap_or(0);
279 result.push(PathSegment::ArrayField {
280 name: field.clone(),
281 index,
282 });
283 } else {
284 result.push(seg.clone());
285 }
286 }
287 _ => {
288 result.push(seg.clone());
289 }
290 }
291 }
292 result
293 }
294
295 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
297 let resolved = self.resolve(fixture_field);
298 if !self.is_optional(resolved) {
299 return None;
300 }
301 let segments = parse_path(resolved);
302 let segments = self.inject_array_indexing(segments);
303 let local_var = resolved.replace(['.', '['], "_").replace(']', "");
304 let accessor = render_accessor(&segments, "rust", result_var);
305 let has_map_access = segments.iter().any(|s| {
306 if let PathSegment::MapAccess { key, .. } = s {
307 !key.chars().all(|c| c.is_ascii_digit())
308 } else {
309 false
310 }
311 });
312 let is_array = self.is_array(resolved);
313 let binding = if has_map_access {
314 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
315 } else if is_array {
316 format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
317 } else {
318 format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
319 };
320 Some((binding, local_var))
321 }
322}
323
324fn normalize_numeric_indices(path: &str) -> String {
325 let mut result = String::with_capacity(path.len());
326 let mut chars = path.chars().peekable();
327 while let Some(c) = chars.next() {
328 if c == '[' {
329 let mut key = String::new();
330 let mut closed = false;
331 for inner in chars.by_ref() {
332 if inner == ']' {
333 closed = true;
334 break;
335 }
336 key.push(inner);
337 }
338 if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
339 result.push_str("[0]");
340 } else {
341 result.push('[');
342 result.push_str(&key);
343 if closed {
344 result.push(']');
345 }
346 }
347 } else {
348 result.push(c);
349 }
350 }
351 result
352}
353
354fn parse_path(path: &str) -> Vec<PathSegment> {
355 let mut segments = Vec::new();
356 for part in path.split('.') {
357 if part == "length" || part == "count" || part == "size" {
358 segments.push(PathSegment::Length);
359 } else if let Some(bracket_pos) = part.find('[') {
360 let name = part[..bracket_pos].to_string();
361 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
362 if key.is_empty() {
363 segments.push(PathSegment::ArrayField { name, index: 0 });
365 } else if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
366 let index: usize = key.parse().unwrap_or(0);
368 segments.push(PathSegment::ArrayField { name, index });
369 } else {
370 segments.push(PathSegment::MapAccess { field: name, key });
372 }
373 } else {
374 segments.push(PathSegment::Field(part.to_string()));
375 }
376 }
377 segments
378}
379
380fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
381 match language {
382 "rust" => render_rust(segments, result_var),
383 "python" => render_dot_access(segments, result_var, "python"),
384 "typescript" | "node" => render_typescript(segments, result_var),
385 "wasm" => render_wasm(segments, result_var),
386 "go" => render_go(segments, result_var),
387 "java" => render_java(segments, result_var),
388 "kotlin" => render_kotlin(segments, result_var),
389 "csharp" => render_pascal_dot(segments, result_var),
390 "ruby" => render_dot_access(segments, result_var, "ruby"),
391 "php" => render_php(segments, result_var),
392 "elixir" => render_dot_access(segments, result_var, "elixir"),
393 "r" => render_r(segments, result_var),
394 "c" => render_c(segments, result_var),
395 "swift" => render_swift(segments, result_var),
396 "dart" => render_dart(segments, result_var),
397 _ => render_dot_access(segments, result_var, language),
398 }
399}
400
401fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
407 let mut out = result_var.to_string();
408 for seg in segments {
409 match seg {
410 PathSegment::Field(f) => {
411 out.push('.');
412 out.push_str(&f.to_lower_camel_case());
413 out.push_str("()");
414 }
415 PathSegment::ArrayField { name, index } => {
416 out.push('.');
417 out.push_str(&name.to_lower_camel_case());
418 out.push_str(&format!("()[{index}]"));
419 }
420 PathSegment::MapAccess { field, key } => {
421 out.push('.');
422 out.push_str(&field.to_lower_camel_case());
423 if key.chars().all(|c| c.is_ascii_digit()) {
424 out.push_str(&format!("()[{key}]"));
425 } else {
426 out.push_str(&format!("()[\"{key}\"]"));
427 }
428 }
429 PathSegment::Length => {
430 out.push_str(".count");
431 }
432 }
433 }
434 out
435}
436
437fn render_swift_with_optionals(
447 segments: &[PathSegment],
448 result_var: &str,
449 optional_fields: &HashSet<String>,
450) -> String {
451 let mut out = result_var.to_string();
452 let mut path_so_far = String::new();
453 let total = segments.len();
454 for (i, seg) in segments.iter().enumerate() {
455 let is_leaf = i == total - 1;
456 match seg {
457 PathSegment::Field(f) => {
458 if !path_so_far.is_empty() {
459 path_so_far.push('.');
460 }
461 path_so_far.push_str(f);
462 out.push('.');
463 out.push_str(f);
464 out.push_str("()");
465 if !is_leaf && optional_fields.contains(&path_so_far) {
468 out.push('?');
469 }
470 }
471 PathSegment::ArrayField { name, index } => {
472 if !path_so_far.is_empty() {
473 path_so_far.push('.');
474 }
475 path_so_far.push_str(name);
476 out.push('.');
477 out.push_str(&name.to_lower_camel_case());
478 out.push_str(&format!("()[{index}]"));
479 }
480 PathSegment::MapAccess { field, key } => {
481 if !path_so_far.is_empty() {
482 path_so_far.push('.');
483 }
484 path_so_far.push_str(field);
485 out.push('.');
486 out.push_str(&field.to_lower_camel_case());
487 if key.chars().all(|c| c.is_ascii_digit()) {
488 out.push_str(&format!("()[{key}]"));
489 } else {
490 out.push_str(&format!("()[\"{key}\"]"));
491 }
492 }
493 PathSegment::Length => {
494 out.push_str(".count");
495 }
496 }
497 }
498 out
499}
500
501fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
502 let mut out = result_var.to_string();
503 for seg in segments {
504 match seg {
505 PathSegment::Field(f) => {
506 out.push('.');
507 out.push_str(&f.to_snake_case());
508 }
509 PathSegment::ArrayField { name, index } => {
510 out.push('.');
511 out.push_str(&name.to_snake_case());
512 out.push_str(&format!("[{index}]"));
513 }
514 PathSegment::MapAccess { field, key } => {
515 out.push('.');
516 out.push_str(&field.to_snake_case());
517 if key.chars().all(|c| c.is_ascii_digit()) {
518 out.push_str(&format!("[{key}]"));
519 } else {
520 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
521 }
522 }
523 PathSegment::Length => {
524 out.push_str(".len()");
525 }
526 }
527 }
528 out
529}
530
531fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
532 let mut out = result_var.to_string();
533 for seg in segments {
534 match seg {
535 PathSegment::Field(f) => {
536 out.push('.');
537 out.push_str(f);
538 }
539 PathSegment::ArrayField { name, index } => {
540 if language == "elixir" {
541 let current = std::mem::take(&mut out);
542 out = format!("Enum.at({current}.{name}, {index})");
543 } else {
544 out.push('.');
545 out.push_str(name);
546 out.push_str(&format!("[{index}]"));
547 }
548 }
549 PathSegment::MapAccess { field, key } => {
550 let is_numeric = key.chars().all(|c| c.is_ascii_digit());
551 if is_numeric && language == "elixir" {
552 let current = std::mem::take(&mut out);
553 out = format!("Enum.at({current}.{field}, {key})");
554 } else {
555 out.push('.');
556 out.push_str(field);
557 if is_numeric {
558 let idx: usize = key.parse().unwrap_or(0);
559 out.push_str(&format!("[{idx}]"));
560 } else if language == "elixir" {
561 out.push_str(&format!("[\"{key}\"]"));
562 } else {
563 out.push_str(&format!(".get(\"{key}\")"));
564 }
565 }
566 }
567 PathSegment::Length => match language {
568 "ruby" => out.push_str(".length"),
569 "elixir" => {
570 let current = std::mem::take(&mut out);
571 out = format!("length({current})");
572 }
573 "gleam" => {
574 let current = std::mem::take(&mut out);
575 out = format!("list.length({current})");
576 }
577 _ => {
578 let current = std::mem::take(&mut out);
579 out = format!("len({current})");
580 }
581 },
582 }
583 }
584 out
585}
586
587fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
588 let mut out = result_var.to_string();
589 for seg in segments {
590 match seg {
591 PathSegment::Field(f) => {
592 out.push('.');
593 out.push_str(&f.to_lower_camel_case());
594 }
595 PathSegment::ArrayField { name, index } => {
596 out.push('.');
597 out.push_str(&name.to_lower_camel_case());
598 out.push_str(&format!("[{index}]"));
599 }
600 PathSegment::MapAccess { field, key } => {
601 out.push('.');
602 out.push_str(&field.to_lower_camel_case());
603 if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
606 out.push_str(&format!("[{key}]"));
607 } else {
608 out.push_str(&format!("[\"{key}\"]"));
609 }
610 }
611 PathSegment::Length => {
612 out.push_str(".length");
613 }
614 }
615 }
616 out
617}
618
619fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
620 let mut out = result_var.to_string();
621 for seg in segments {
622 match seg {
623 PathSegment::Field(f) => {
624 out.push('.');
625 out.push_str(&f.to_lower_camel_case());
626 }
627 PathSegment::ArrayField { name, index } => {
628 out.push('.');
629 out.push_str(&name.to_lower_camel_case());
630 out.push_str(&format!("[{index}]"));
631 }
632 PathSegment::MapAccess { field, key } => {
633 out.push('.');
634 out.push_str(&field.to_lower_camel_case());
635 out.push_str(&format!(".get(\"{key}\")"));
636 }
637 PathSegment::Length => {
638 out.push_str(".length");
639 }
640 }
641 }
642 out
643}
644
645fn render_go(segments: &[PathSegment], result_var: &str) -> String {
646 let mut out = result_var.to_string();
647 for seg in segments {
648 match seg {
649 PathSegment::Field(f) => {
650 out.push('.');
651 out.push_str(&to_go_name(f));
652 }
653 PathSegment::ArrayField { name, index } => {
654 out.push('.');
655 out.push_str(&to_go_name(name));
656 out.push_str(&format!("[{index}]"));
657 }
658 PathSegment::MapAccess { field, key } => {
659 out.push('.');
660 out.push_str(&to_go_name(field));
661 if key.chars().all(|c| c.is_ascii_digit()) {
662 out.push_str(&format!("[{key}]"));
663 } else {
664 out.push_str(&format!("[\"{key}\"]"));
665 }
666 }
667 PathSegment::Length => {
668 let current = std::mem::take(&mut out);
669 out = format!("len({current})");
670 }
671 }
672 }
673 out
674}
675
676fn render_java(segments: &[PathSegment], result_var: &str) -> String {
677 let mut out = result_var.to_string();
678 for seg in segments {
679 match seg {
680 PathSegment::Field(f) => {
681 out.push('.');
682 out.push_str(&f.to_lower_camel_case());
683 out.push_str("()");
684 }
685 PathSegment::ArrayField { name, index } => {
686 out.push('.');
687 out.push_str(&name.to_lower_camel_case());
688 out.push_str(&format!("().get({index})"));
689 }
690 PathSegment::MapAccess { field, key } => {
691 out.push('.');
692 out.push_str(&field.to_lower_camel_case());
693 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
695 if is_numeric {
696 out.push_str(&format!("().get({key})"));
697 } else {
698 out.push_str(&format!("().get(\"{key}\")"));
699 }
700 }
701 PathSegment::Length => {
702 out.push_str(".size()");
703 }
704 }
705 }
706 out
707}
708
709fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
716 let mut out = result_var.to_string();
717 for seg in segments {
718 match seg {
719 PathSegment::Field(f) => {
720 out.push('.');
721 out.push_str(&f.to_lower_camel_case());
722 out.push_str("()");
723 }
724 PathSegment::ArrayField { name, index } => {
725 out.push('.');
726 out.push_str(&name.to_lower_camel_case());
727 if *index == 0 {
728 out.push_str("().first()");
729 } else {
730 out.push_str(&format!("().get({index})"));
731 }
732 }
733 PathSegment::MapAccess { field, key } => {
734 out.push('.');
735 out.push_str(&field.to_lower_camel_case());
736 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
737 if is_numeric {
738 out.push_str(&format!("().get({key})"));
739 } else {
740 out.push_str(&format!("().get(\"{key}\")"));
741 }
742 }
743 PathSegment::Length => {
744 out.push_str(".size");
745 }
746 }
747 }
748 out
749}
750
751fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
752 let mut out = result_var.to_string();
753 let mut path_so_far = String::new();
754 for (i, seg) in segments.iter().enumerate() {
755 let is_leaf = i == segments.len() - 1;
756 match seg {
757 PathSegment::Field(f) => {
758 if !path_so_far.is_empty() {
759 path_so_far.push('.');
760 }
761 path_so_far.push_str(f);
762 out.push('.');
763 out.push_str(&f.to_lower_camel_case());
764 out.push_str("()");
765 let _ = is_leaf;
766 let _ = optional_fields;
767 }
768 PathSegment::ArrayField { name, index } => {
769 if !path_so_far.is_empty() {
770 path_so_far.push('.');
771 }
772 path_so_far.push_str(name);
773 out.push('.');
774 out.push_str(&name.to_lower_camel_case());
775 out.push_str(&format!("().get({index})"));
776 }
777 PathSegment::MapAccess { field, key } => {
778 if !path_so_far.is_empty() {
779 path_so_far.push('.');
780 }
781 path_so_far.push_str(field);
782 out.push('.');
783 out.push_str(&field.to_lower_camel_case());
784 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
786 if is_numeric {
787 out.push_str(&format!("().get({key})"));
788 } else {
789 out.push_str(&format!("().get(\"{key}\")"));
790 }
791 }
792 PathSegment::Length => {
793 out.push_str(".size()");
794 }
795 }
796 }
797 out
798}
799
800fn render_kotlin_with_optionals(
806 segments: &[PathSegment],
807 result_var: &str,
808 optional_fields: &HashSet<String>,
809) -> String {
810 let mut out = result_var.to_string();
811 let mut path_so_far = String::new();
812 let mut prev_was_nullable = false;
815 for seg in segments {
816 let nav = if prev_was_nullable { "?." } else { "." };
817 match seg {
818 PathSegment::Field(f) => {
819 if !path_so_far.is_empty() {
820 path_so_far.push('.');
821 }
822 path_so_far.push_str(f);
823 let is_optional = optional_fields.contains(&path_so_far);
827 out.push_str(nav);
828 out.push_str(&f.to_lower_camel_case());
829 out.push_str("()");
830 prev_was_nullable = is_optional;
831 }
832 PathSegment::ArrayField { name, index } => {
833 if !path_so_far.is_empty() {
834 path_so_far.push('.');
835 }
836 path_so_far.push_str(name);
837 let is_optional = optional_fields.contains(&path_so_far);
838 out.push_str(nav);
839 out.push_str(&name.to_lower_camel_case());
840 let safe = if prev_was_nullable || is_optional { "?" } else { "" };
841 if *index == 0 {
842 out.push_str(&format!("(){safe}.first()"));
843 } else {
844 out.push_str(&format!("(){safe}.get({index})"));
845 }
846 prev_was_nullable = is_optional;
847 }
848 PathSegment::MapAccess { field, key } => {
849 if !path_so_far.is_empty() {
850 path_so_far.push('.');
851 }
852 path_so_far.push_str(field);
853 let is_optional = optional_fields.contains(&path_so_far);
854 out.push_str(nav);
855 out.push_str(&field.to_lower_camel_case());
856 let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
857 if is_numeric {
858 if is_optional {
859 out.push_str(&format!("()?.get({key})"));
860 } else {
861 out.push_str(&format!("().get({key})"));
862 }
863 } else if is_optional {
864 out.push_str(&format!("()?.get(\"{key}\")"));
865 } else {
866 out.push_str(&format!("().get(\"{key}\")"));
867 }
868 prev_was_nullable = is_optional;
869 }
870 PathSegment::Length => {
871 let size_nav = if prev_was_nullable { "?" } else { "" };
874 out.push_str(&format!("{size_nav}.size"));
875 prev_was_nullable = false;
876 }
877 }
878 }
879 out
880}
881
882fn render_rust_with_optionals(
888 segments: &[PathSegment],
889 result_var: &str,
890 optional_fields: &HashSet<String>,
891 method_calls: &HashSet<String>,
892) -> String {
893 let mut out = result_var.to_string();
894 let mut path_so_far = String::new();
895 for (i, seg) in segments.iter().enumerate() {
896 let is_leaf = i == segments.len() - 1;
897 match seg {
898 PathSegment::Field(f) => {
899 if !path_so_far.is_empty() {
900 path_so_far.push('.');
901 }
902 path_so_far.push_str(f);
903 out.push('.');
904 out.push_str(&f.to_snake_case());
905 let is_method = method_calls.contains(&path_so_far);
906 if is_method {
907 out.push_str("()");
908 if !is_leaf && optional_fields.contains(&path_so_far) {
909 out.push_str(".as_ref().unwrap()");
910 }
911 } else if !is_leaf && optional_fields.contains(&path_so_far) {
912 out.push_str(".as_ref().unwrap()");
913 }
914 }
915 PathSegment::ArrayField { name, index } => {
916 if !path_so_far.is_empty() {
917 path_so_far.push('.');
918 }
919 path_so_far.push_str(name);
920 out.push('.');
921 out.push_str(&name.to_snake_case());
922 out.push_str(&format!("[{index}]"));
923 }
924 PathSegment::MapAccess { field, key } => {
925 if !path_so_far.is_empty() {
926 path_so_far.push('.');
927 }
928 path_so_far.push_str(field);
929 out.push('.');
930 out.push_str(&field.to_snake_case());
931 if key.chars().all(|c| c.is_ascii_digit()) {
932 let is_opt = optional_fields.contains(&path_so_far);
933 if is_opt {
934 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
935 } else {
936 out.push_str(&format!("[{key}]"));
937 }
938 path_so_far.push_str("[0]");
939 } else {
940 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
941 }
942 }
943 PathSegment::Length => {
944 out.push_str(".len()");
945 }
946 }
947 }
948 out
949}
950
951fn render_zig_with_optionals(
964 segments: &[PathSegment],
965 result_var: &str,
966 optional_fields: &HashSet<String>,
967 method_calls: &HashSet<String>,
968) -> String {
969 let mut out = result_var.to_string();
970 let mut path_so_far = String::new();
971 for seg in segments {
972 match seg {
973 PathSegment::Field(f) => {
974 if !path_so_far.is_empty() {
975 path_so_far.push('.');
976 }
977 path_so_far.push_str(f);
978 out.push('.');
979 out.push_str(f);
980 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
981 out.push_str(".?");
982 }
983 }
984 PathSegment::ArrayField { name, index } => {
985 if !path_so_far.is_empty() {
986 path_so_far.push('.');
987 }
988 path_so_far.push_str(name);
989 out.push('.');
990 out.push_str(name);
991 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
992 out.push_str(".?");
993 }
994 out.push_str(&format!("[{index}]"));
995 }
996 PathSegment::MapAccess { field, key } => {
997 if !path_so_far.is_empty() {
998 path_so_far.push('.');
999 }
1000 path_so_far.push_str(field);
1001 out.push('.');
1002 out.push_str(field);
1003 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1004 out.push_str(".?");
1005 }
1006 if key.chars().all(|c| c.is_ascii_digit()) {
1007 out.push_str(&format!("[{key}]"));
1008 } else {
1009 out.push_str(&format!(".get(\"{key}\")"));
1010 }
1011 }
1012 PathSegment::Length => {
1013 out.push_str(".len");
1014 }
1015 }
1016 }
1017 out
1018}
1019
1020fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1021 let mut out = result_var.to_string();
1022 for seg in segments {
1023 match seg {
1024 PathSegment::Field(f) => {
1025 out.push('.');
1026 out.push_str(&f.to_pascal_case());
1027 }
1028 PathSegment::ArrayField { name, index } => {
1029 out.push('.');
1030 out.push_str(&name.to_pascal_case());
1031 out.push_str(&format!("[{index}]"));
1032 }
1033 PathSegment::MapAccess { field, key } => {
1034 out.push('.');
1035 out.push_str(&field.to_pascal_case());
1036 if key.chars().all(|c| c.is_ascii_digit()) {
1037 out.push_str(&format!("[{key}]"));
1038 } else {
1039 out.push_str(&format!("[\"{key}\"]"));
1040 }
1041 }
1042 PathSegment::Length => {
1043 out.push_str(".Count");
1044 }
1045 }
1046 }
1047 out
1048}
1049
1050fn render_csharp_with_optionals(
1051 segments: &[PathSegment],
1052 result_var: &str,
1053 optional_fields: &HashSet<String>,
1054) -> String {
1055 let mut out = result_var.to_string();
1056 let mut path_so_far = String::new();
1057 for (i, seg) in segments.iter().enumerate() {
1058 let is_leaf = i == segments.len() - 1;
1059 match seg {
1060 PathSegment::Field(f) => {
1061 if !path_so_far.is_empty() {
1062 path_so_far.push('.');
1063 }
1064 path_so_far.push_str(f);
1065 out.push('.');
1066 out.push_str(&f.to_pascal_case());
1067 if !is_leaf && optional_fields.contains(&path_so_far) {
1068 out.push('!');
1069 }
1070 }
1071 PathSegment::ArrayField { name, index } => {
1072 if !path_so_far.is_empty() {
1073 path_so_far.push('.');
1074 }
1075 path_so_far.push_str(name);
1076 out.push('.');
1077 out.push_str(&name.to_pascal_case());
1078 out.push_str(&format!("[{index}]"));
1079 }
1080 PathSegment::MapAccess { field, key } => {
1081 if !path_so_far.is_empty() {
1082 path_so_far.push('.');
1083 }
1084 path_so_far.push_str(field);
1085 out.push('.');
1086 out.push_str(&field.to_pascal_case());
1087 if key.chars().all(|c| c.is_ascii_digit()) {
1088 out.push_str(&format!("[{key}]"));
1089 } else {
1090 out.push_str(&format!("[\"{key}\"]"));
1091 }
1092 }
1093 PathSegment::Length => {
1094 out.push_str(".Count");
1095 }
1096 }
1097 }
1098 out
1099}
1100
1101fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1102 let mut out = result_var.to_string();
1103 for seg in segments {
1104 match seg {
1105 PathSegment::Field(f) => {
1106 out.push_str("->");
1107 out.push_str(&f.to_lower_camel_case());
1110 }
1111 PathSegment::ArrayField { name, index } => {
1112 out.push_str("->");
1113 out.push_str(&name.to_lower_camel_case());
1114 out.push_str(&format!("[{index}]"));
1115 }
1116 PathSegment::MapAccess { field, key } => {
1117 out.push_str("->");
1118 out.push_str(&field.to_lower_camel_case());
1119 out.push_str(&format!("[\"{key}\"]"));
1120 }
1121 PathSegment::Length => {
1122 let current = std::mem::take(&mut out);
1123 out = format!("count({current})");
1124 }
1125 }
1126 }
1127 out
1128}
1129
1130fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1131 let mut out = result_var.to_string();
1132 for seg in segments {
1133 match seg {
1134 PathSegment::Field(f) => {
1135 out.push('$');
1136 out.push_str(f);
1137 }
1138 PathSegment::ArrayField { name, index } => {
1139 out.push('$');
1140 out.push_str(name);
1141 out.push_str(&format!("[[{}]]", index + 1));
1143 }
1144 PathSegment::MapAccess { field, key } => {
1145 out.push('$');
1146 out.push_str(field);
1147 out.push_str(&format!("[[\"{key}\"]]"));
1148 }
1149 PathSegment::Length => {
1150 let current = std::mem::take(&mut out);
1151 out = format!("length({current})");
1152 }
1153 }
1154 }
1155 out
1156}
1157
1158fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1159 let mut parts = Vec::new();
1160 let mut trailing_length = false;
1161 for seg in segments {
1162 match seg {
1163 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1164 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1165 PathSegment::MapAccess { field, key } => {
1166 parts.push(field.to_snake_case());
1167 parts.push(key.clone());
1168 }
1169 PathSegment::Length => {
1170 trailing_length = true;
1171 }
1172 }
1173 }
1174 let suffix = parts.join("_");
1175 if trailing_length {
1176 format!("result_{suffix}_count({result_var})")
1177 } else {
1178 format!("result_{suffix}({result_var})")
1179 }
1180}
1181
1182fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
1189 let mut out = result_var.to_string();
1190 for seg in segments {
1191 match seg {
1192 PathSegment::Field(f) => {
1193 out.push('.');
1194 out.push_str(&f.to_lower_camel_case());
1195 }
1196 PathSegment::ArrayField { name, index } => {
1197 out.push('.');
1198 out.push_str(&name.to_lower_camel_case());
1199 out.push_str(&format!("[{index}]"));
1200 }
1201 PathSegment::MapAccess { field, key } => {
1202 out.push('.');
1203 out.push_str(&field.to_lower_camel_case());
1204 if key.chars().all(|c| c.is_ascii_digit()) {
1205 out.push_str(&format!("[{key}]"));
1206 } else {
1207 out.push_str(&format!("[\"{key}\"]"));
1208 }
1209 }
1210 PathSegment::Length => {
1211 out.push_str(".length");
1212 }
1213 }
1214 }
1215 out
1216}
1217
1218fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1224 let mut out = result_var.to_string();
1225 let mut path_so_far = String::new();
1226 let mut prev_was_nullable = false;
1227 for seg in segments {
1228 let nav = if prev_was_nullable { "?." } else { "." };
1229 match seg {
1230 PathSegment::Field(f) => {
1231 if !path_so_far.is_empty() {
1232 path_so_far.push('.');
1233 }
1234 path_so_far.push_str(f);
1235 let is_optional = optional_fields.contains(&path_so_far);
1236 out.push_str(nav);
1237 out.push_str(&f.to_lower_camel_case());
1238 prev_was_nullable = is_optional;
1239 }
1240 PathSegment::ArrayField { name, index } => {
1241 if !path_so_far.is_empty() {
1242 path_so_far.push('.');
1243 }
1244 path_so_far.push_str(name);
1245 out.push_str(nav);
1246 out.push_str(&name.to_lower_camel_case());
1247 out.push_str(&format!("[{index}]"));
1248 prev_was_nullable = false;
1249 }
1250 PathSegment::MapAccess { field, key } => {
1251 if !path_so_far.is_empty() {
1252 path_so_far.push('.');
1253 }
1254 path_so_far.push_str(field);
1255 let is_optional = optional_fields.contains(&path_so_far);
1256 out.push_str(nav);
1257 out.push_str(&field.to_lower_camel_case());
1258 if key.chars().all(|c| c.is_ascii_digit()) {
1259 out.push_str(&format!("[{key}]"));
1260 } else {
1261 out.push_str(&format!("[\"{key}\"]"));
1262 }
1263 prev_was_nullable = is_optional;
1264 }
1265 PathSegment::Length => {
1266 out.push_str(".length");
1267 prev_was_nullable = false;
1268 }
1269 }
1270 }
1271 out
1272}
1273
1274#[cfg(test)]
1275mod tests {
1276 use super::*;
1277
1278 fn make_resolver() -> FieldResolver {
1279 let mut fields = HashMap::new();
1280 fields.insert("title".to_string(), "metadata.document.title".to_string());
1281 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1282 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
1283 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
1284 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
1285 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
1286 let mut optional = HashSet::new();
1287 optional.insert("metadata.document.title".to_string());
1288 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1289 }
1290
1291 fn make_resolver_with_doc_optional() -> FieldResolver {
1292 let mut fields = HashMap::new();
1293 fields.insert("title".to_string(), "metadata.document.title".to_string());
1294 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1295 let mut optional = HashSet::new();
1296 optional.insert("document".to_string());
1297 optional.insert("metadata.document.title".to_string());
1298 optional.insert("metadata.document".to_string());
1299 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1300 }
1301
1302 #[test]
1303 fn test_resolve_alias() {
1304 let r = make_resolver();
1305 assert_eq!(r.resolve("title"), "metadata.document.title");
1306 }
1307
1308 #[test]
1309 fn test_resolve_passthrough() {
1310 let r = make_resolver();
1311 assert_eq!(r.resolve("content"), "content");
1312 }
1313
1314 #[test]
1315 fn test_is_optional() {
1316 let r = make_resolver();
1317 assert!(r.is_optional("metadata.document.title"));
1318 assert!(!r.is_optional("content"));
1319 }
1320
1321 #[test]
1322 fn test_accessor_rust_struct() {
1323 let r = make_resolver();
1324 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
1325 }
1326
1327 #[test]
1328 fn test_accessor_rust_map() {
1329 let r = make_resolver();
1330 assert_eq!(
1331 r.accessor("tags", "rust", "result"),
1332 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
1333 );
1334 }
1335
1336 #[test]
1337 fn test_accessor_python() {
1338 let r = make_resolver();
1339 assert_eq!(
1340 r.accessor("title", "python", "result"),
1341 "result.metadata.document.title"
1342 );
1343 }
1344
1345 #[test]
1346 fn test_accessor_go() {
1347 let r = make_resolver();
1348 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
1349 }
1350
1351 #[test]
1352 fn test_accessor_go_initialism_fields() {
1353 let mut fields = std::collections::HashMap::new();
1354 fields.insert("content".to_string(), "html".to_string());
1355 fields.insert("link_url".to_string(), "links.url".to_string());
1356 let r = FieldResolver::new(
1357 &fields,
1358 &HashSet::new(),
1359 &HashSet::new(),
1360 &HashSet::new(),
1361 &HashSet::new(),
1362 );
1363 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
1364 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
1365 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
1366 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
1367 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
1368 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
1369 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
1370 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
1371 }
1372
1373 #[test]
1374 fn test_accessor_typescript() {
1375 let r = make_resolver();
1376 assert_eq!(
1377 r.accessor("title", "typescript", "result"),
1378 "result.metadata.document.title"
1379 );
1380 }
1381
1382 #[test]
1383 fn test_accessor_typescript_snake_to_camel() {
1384 let r = make_resolver();
1385 assert_eq!(
1386 r.accessor("og", "typescript", "result"),
1387 "result.metadata.document.openGraph"
1388 );
1389 assert_eq!(
1390 r.accessor("twitter", "typescript", "result"),
1391 "result.metadata.document.twitterCard"
1392 );
1393 assert_eq!(
1394 r.accessor("canonical", "typescript", "result"),
1395 "result.metadata.document.canonicalUrl"
1396 );
1397 }
1398
1399 #[test]
1400 fn test_accessor_typescript_map_snake_to_camel() {
1401 let r = make_resolver();
1402 assert_eq!(
1403 r.accessor("og_tag", "typescript", "result"),
1404 "result.metadata.openGraphTags[\"og_title\"]"
1405 );
1406 }
1407
1408 #[test]
1409 fn test_accessor_typescript_numeric_index_is_unquoted() {
1410 let mut fields = HashMap::new();
1414 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
1415 let r = FieldResolver::new(
1416 &fields,
1417 &HashSet::new(),
1418 &HashSet::new(),
1419 &HashSet::new(),
1420 &HashSet::new(),
1421 );
1422 assert_eq!(
1423 r.accessor("first_score", "typescript", "result"),
1424 "result.results[0].relevanceScore"
1425 );
1426 }
1427
1428 #[test]
1429 fn test_accessor_node_alias() {
1430 let r = make_resolver();
1431 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
1432 }
1433
1434 #[test]
1435 fn test_accessor_wasm_camel_case() {
1436 let r = make_resolver();
1437 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
1438 assert_eq!(
1439 r.accessor("twitter", "wasm", "result"),
1440 "result.metadata.document.twitterCard"
1441 );
1442 assert_eq!(
1443 r.accessor("canonical", "wasm", "result"),
1444 "result.metadata.document.canonicalUrl"
1445 );
1446 }
1447
1448 #[test]
1449 fn test_accessor_wasm_map_access() {
1450 let r = make_resolver();
1451 assert_eq!(
1452 r.accessor("og_tag", "wasm", "result"),
1453 "result.metadata.openGraphTags.get(\"og_title\")"
1454 );
1455 }
1456
1457 #[test]
1458 fn test_accessor_java() {
1459 let r = make_resolver();
1460 assert_eq!(
1461 r.accessor("title", "java", "result"),
1462 "result.metadata().document().title()"
1463 );
1464 }
1465
1466 #[test]
1467 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
1468 let mut fields = HashMap::new();
1469 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1470 fields.insert("node_count".to_string(), "nodes.length".to_string());
1471 let mut arrays = HashSet::new();
1472 arrays.insert("nodes".to_string());
1473 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
1474 assert_eq!(
1475 r.accessor("first_node_name", "kotlin", "result"),
1476 "result.nodes().first().name()"
1477 );
1478 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
1479 }
1480
1481 #[test]
1482 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
1483 let r = make_resolver_with_doc_optional();
1484 assert_eq!(
1485 r.accessor("title", "kotlin", "result"),
1486 "result.metadata().document()?.title()"
1487 );
1488 }
1489
1490 #[test]
1491 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
1492 let mut fields = HashMap::new();
1493 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1494 fields.insert("tag".to_string(), "tags[name]".to_string());
1495 let mut optional = HashSet::new();
1496 optional.insert("nodes".to_string());
1497 optional.insert("tags".to_string());
1498 let mut arrays = HashSet::new();
1499 arrays.insert("nodes".to_string());
1500 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
1501 assert_eq!(
1502 r.accessor("first_node_name", "kotlin", "result"),
1503 "result.nodes()?.first()?.name()"
1504 );
1505 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
1506 }
1507
1508 #[test]
1509 fn test_accessor_csharp() {
1510 let r = make_resolver();
1511 assert_eq!(
1512 r.accessor("title", "csharp", "result"),
1513 "result.Metadata.Document.Title"
1514 );
1515 }
1516
1517 #[test]
1518 fn test_accessor_php() {
1519 let r = make_resolver();
1520 assert_eq!(
1521 r.accessor("title", "php", "$result"),
1522 "$result->metadata->document->title"
1523 );
1524 }
1525
1526 #[test]
1527 fn test_accessor_r() {
1528 let r = make_resolver();
1529 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
1530 }
1531
1532 #[test]
1533 fn test_accessor_c() {
1534 let r = make_resolver();
1535 assert_eq!(
1536 r.accessor("title", "c", "result"),
1537 "result_metadata_document_title(result)"
1538 );
1539 }
1540
1541 #[test]
1542 fn test_rust_unwrap_binding() {
1543 let r = make_resolver();
1544 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
1545 assert_eq!(var, "metadata_document_title");
1546 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
1547 }
1548
1549 #[test]
1550 fn test_rust_unwrap_binding_non_optional() {
1551 let r = make_resolver();
1552 assert!(r.rust_unwrap_binding("content", "result").is_none());
1553 }
1554
1555 #[test]
1556 fn test_direct_field_no_alias() {
1557 let r = make_resolver();
1558 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1559 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
1560 }
1561
1562 #[test]
1563 fn test_accessor_rust_with_optionals() {
1564 let r = make_resolver_with_doc_optional();
1565 assert_eq!(
1566 r.accessor("title", "rust", "result"),
1567 "result.metadata.document.as_ref().unwrap().title"
1568 );
1569 }
1570
1571 #[test]
1572 fn test_accessor_csharp_with_optionals() {
1573 let r = make_resolver_with_doc_optional();
1574 assert_eq!(
1575 r.accessor("title", "csharp", "result"),
1576 "result.Metadata.Document!.Title"
1577 );
1578 }
1579
1580 #[test]
1581 fn test_accessor_rust_non_optional_field() {
1582 let r = make_resolver();
1583 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1584 }
1585
1586 #[test]
1587 fn test_accessor_csharp_non_optional_field() {
1588 let r = make_resolver();
1589 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
1590 }
1591
1592 #[test]
1593 fn test_accessor_rust_method_call() {
1594 let mut fields = HashMap::new();
1596 fields.insert(
1597 "excel_sheet_count".to_string(),
1598 "metadata.format.excel.sheet_count".to_string(),
1599 );
1600 let mut optional = HashSet::new();
1601 optional.insert("metadata.format".to_string());
1602 optional.insert("metadata.format.excel".to_string());
1603 let mut method_calls = HashSet::new();
1604 method_calls.insert("metadata.format.excel".to_string());
1605 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
1606 assert_eq!(
1607 r.accessor("excel_sheet_count", "rust", "result"),
1608 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
1609 );
1610 }
1611}