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);
413 out.push_str("()");
414 }
415 PathSegment::ArrayField { name, index } => {
416 out.push('.');
417 out.push_str(name);
418 out.push_str(&format!("()[{index}]"));
419 }
420 PathSegment::MapAccess { field, key } => {
421 out.push('.');
422 out.push_str(field);
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);
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);
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 let path_with_idx = format!("{path_so_far}[0]");
926 let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
927 if is_opt {
928 out.push_str(&format!(".as_ref().unwrap()[{index}]"));
929 } else {
930 out.push_str(&format!("[{index}]"));
931 }
932 path_so_far.push_str("[0]");
937 }
938 PathSegment::MapAccess { field, key } => {
939 if !path_so_far.is_empty() {
940 path_so_far.push('.');
941 }
942 path_so_far.push_str(field);
943 out.push('.');
944 out.push_str(&field.to_snake_case());
945 if key.chars().all(|c| c.is_ascii_digit()) {
946 let path_with_idx = format!("{path_so_far}[0]");
948 let is_opt =
949 optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
950 if is_opt {
951 out.push_str(&format!(".as_ref().unwrap()[{key}]"));
952 } else {
953 out.push_str(&format!("[{key}]"));
954 }
955 path_so_far.push_str("[0]");
956 } else {
957 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
958 }
959 }
960 PathSegment::Length => {
961 out.push_str(".len()");
962 }
963 }
964 }
965 out
966}
967
968fn render_zig_with_optionals(
981 segments: &[PathSegment],
982 result_var: &str,
983 optional_fields: &HashSet<String>,
984 method_calls: &HashSet<String>,
985) -> String {
986 let mut out = result_var.to_string();
987 let mut path_so_far = String::new();
988 for seg in segments {
989 match seg {
990 PathSegment::Field(f) => {
991 if !path_so_far.is_empty() {
992 path_so_far.push('.');
993 }
994 path_so_far.push_str(f);
995 out.push('.');
996 out.push_str(f);
997 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
998 out.push_str(".?");
999 }
1000 }
1001 PathSegment::ArrayField { name, index } => {
1002 if !path_so_far.is_empty() {
1003 path_so_far.push('.');
1004 }
1005 path_so_far.push_str(name);
1006 out.push('.');
1007 out.push_str(name);
1008 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1009 out.push_str(".?");
1010 }
1011 out.push_str(&format!("[{index}]"));
1012 }
1013 PathSegment::MapAccess { field, key } => {
1014 if !path_so_far.is_empty() {
1015 path_so_far.push('.');
1016 }
1017 path_so_far.push_str(field);
1018 out.push('.');
1019 out.push_str(field);
1020 if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
1021 out.push_str(".?");
1022 }
1023 if key.chars().all(|c| c.is_ascii_digit()) {
1024 out.push_str(&format!("[{key}]"));
1025 } else {
1026 out.push_str(&format!(".get(\"{key}\")"));
1027 }
1028 }
1029 PathSegment::Length => {
1030 out.push_str(".len");
1031 }
1032 }
1033 }
1034 out
1035}
1036
1037fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
1038 let mut out = result_var.to_string();
1039 for seg in segments {
1040 match seg {
1041 PathSegment::Field(f) => {
1042 out.push('.');
1043 out.push_str(&f.to_pascal_case());
1044 }
1045 PathSegment::ArrayField { name, index } => {
1046 out.push('.');
1047 out.push_str(&name.to_pascal_case());
1048 out.push_str(&format!("[{index}]"));
1049 }
1050 PathSegment::MapAccess { field, key } => {
1051 out.push('.');
1052 out.push_str(&field.to_pascal_case());
1053 if key.chars().all(|c| c.is_ascii_digit()) {
1054 out.push_str(&format!("[{key}]"));
1055 } else {
1056 out.push_str(&format!("[\"{key}\"]"));
1057 }
1058 }
1059 PathSegment::Length => {
1060 out.push_str(".Count");
1061 }
1062 }
1063 }
1064 out
1065}
1066
1067fn render_csharp_with_optionals(
1068 segments: &[PathSegment],
1069 result_var: &str,
1070 optional_fields: &HashSet<String>,
1071) -> String {
1072 let mut out = result_var.to_string();
1073 let mut path_so_far = String::new();
1074 for (i, seg) in segments.iter().enumerate() {
1075 let is_leaf = i == segments.len() - 1;
1076 match seg {
1077 PathSegment::Field(f) => {
1078 if !path_so_far.is_empty() {
1079 path_so_far.push('.');
1080 }
1081 path_so_far.push_str(f);
1082 out.push('.');
1083 out.push_str(&f.to_pascal_case());
1084 if !is_leaf && optional_fields.contains(&path_so_far) {
1085 out.push('!');
1086 }
1087 }
1088 PathSegment::ArrayField { name, index } => {
1089 if !path_so_far.is_empty() {
1090 path_so_far.push('.');
1091 }
1092 path_so_far.push_str(name);
1093 out.push('.');
1094 out.push_str(&name.to_pascal_case());
1095 out.push_str(&format!("[{index}]"));
1096 }
1097 PathSegment::MapAccess { field, key } => {
1098 if !path_so_far.is_empty() {
1099 path_so_far.push('.');
1100 }
1101 path_so_far.push_str(field);
1102 out.push('.');
1103 out.push_str(&field.to_pascal_case());
1104 if key.chars().all(|c| c.is_ascii_digit()) {
1105 out.push_str(&format!("[{key}]"));
1106 } else {
1107 out.push_str(&format!("[\"{key}\"]"));
1108 }
1109 }
1110 PathSegment::Length => {
1111 out.push_str(".Count");
1112 }
1113 }
1114 }
1115 out
1116}
1117
1118fn render_php(segments: &[PathSegment], result_var: &str) -> String {
1119 let mut out = result_var.to_string();
1120 for seg in segments {
1121 match seg {
1122 PathSegment::Field(f) => {
1123 out.push_str("->");
1124 out.push_str(&f.to_lower_camel_case());
1127 }
1128 PathSegment::ArrayField { name, index } => {
1129 out.push_str("->");
1130 out.push_str(&name.to_lower_camel_case());
1131 out.push_str(&format!("[{index}]"));
1132 }
1133 PathSegment::MapAccess { field, key } => {
1134 out.push_str("->");
1135 out.push_str(&field.to_lower_camel_case());
1136 out.push_str(&format!("[\"{key}\"]"));
1137 }
1138 PathSegment::Length => {
1139 let current = std::mem::take(&mut out);
1140 out = format!("count({current})");
1141 }
1142 }
1143 }
1144 out
1145}
1146
1147fn render_r(segments: &[PathSegment], result_var: &str) -> String {
1148 let mut out = result_var.to_string();
1149 for seg in segments {
1150 match seg {
1151 PathSegment::Field(f) => {
1152 out.push('$');
1153 out.push_str(f);
1154 }
1155 PathSegment::ArrayField { name, index } => {
1156 out.push('$');
1157 out.push_str(name);
1158 out.push_str(&format!("[[{}]]", index + 1));
1160 }
1161 PathSegment::MapAccess { field, key } => {
1162 out.push('$');
1163 out.push_str(field);
1164 out.push_str(&format!("[[\"{key}\"]]"));
1165 }
1166 PathSegment::Length => {
1167 let current = std::mem::take(&mut out);
1168 out = format!("length({current})");
1169 }
1170 }
1171 }
1172 out
1173}
1174
1175fn render_c(segments: &[PathSegment], result_var: &str) -> String {
1176 let mut parts = Vec::new();
1177 let mut trailing_length = false;
1178 for seg in segments {
1179 match seg {
1180 PathSegment::Field(f) => parts.push(f.to_snake_case()),
1181 PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
1182 PathSegment::MapAccess { field, key } => {
1183 parts.push(field.to_snake_case());
1184 parts.push(key.clone());
1185 }
1186 PathSegment::Length => {
1187 trailing_length = true;
1188 }
1189 }
1190 }
1191 let suffix = parts.join("_");
1192 if trailing_length {
1193 format!("result_{suffix}_count({result_var})")
1194 } else {
1195 format!("result_{suffix}({result_var})")
1196 }
1197}
1198
1199fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
1206 let mut out = result_var.to_string();
1207 for seg in segments {
1208 match seg {
1209 PathSegment::Field(f) => {
1210 out.push('.');
1211 out.push_str(&f.to_lower_camel_case());
1212 }
1213 PathSegment::ArrayField { name, index } => {
1214 out.push('.');
1215 out.push_str(&name.to_lower_camel_case());
1216 out.push_str(&format!("[{index}]"));
1217 }
1218 PathSegment::MapAccess { field, key } => {
1219 out.push('.');
1220 out.push_str(&field.to_lower_camel_case());
1221 if key.chars().all(|c| c.is_ascii_digit()) {
1222 out.push_str(&format!("[{key}]"));
1223 } else {
1224 out.push_str(&format!("[\"{key}\"]"));
1225 }
1226 }
1227 PathSegment::Length => {
1228 out.push_str(".length");
1229 }
1230 }
1231 }
1232 out
1233}
1234
1235fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
1241 let mut out = result_var.to_string();
1242 let mut path_so_far = String::new();
1243 let mut prev_was_nullable = false;
1244 for seg in segments {
1245 let nav = if prev_was_nullable { "?." } else { "." };
1246 match seg {
1247 PathSegment::Field(f) => {
1248 if !path_so_far.is_empty() {
1249 path_so_far.push('.');
1250 }
1251 path_so_far.push_str(f);
1252 let is_optional = optional_fields.contains(&path_so_far);
1253 out.push_str(nav);
1254 out.push_str(&f.to_lower_camel_case());
1255 prev_was_nullable = is_optional;
1256 }
1257 PathSegment::ArrayField { name, index } => {
1258 if !path_so_far.is_empty() {
1259 path_so_far.push('.');
1260 }
1261 path_so_far.push_str(name);
1262 out.push_str(nav);
1263 out.push_str(&name.to_lower_camel_case());
1264 out.push_str(&format!("[{index}]"));
1265 prev_was_nullable = false;
1266 }
1267 PathSegment::MapAccess { field, key } => {
1268 if !path_so_far.is_empty() {
1269 path_so_far.push('.');
1270 }
1271 path_so_far.push_str(field);
1272 let is_optional = optional_fields.contains(&path_so_far);
1273 out.push_str(nav);
1274 out.push_str(&field.to_lower_camel_case());
1275 if key.chars().all(|c| c.is_ascii_digit()) {
1276 out.push_str(&format!("[{key}]"));
1277 } else {
1278 out.push_str(&format!("[\"{key}\"]"));
1279 }
1280 prev_was_nullable = is_optional;
1281 }
1282 PathSegment::Length => {
1283 out.push_str(".length");
1284 prev_was_nullable = false;
1285 }
1286 }
1287 }
1288 out
1289}
1290
1291#[cfg(test)]
1292mod tests {
1293 use super::*;
1294
1295 fn make_resolver() -> FieldResolver {
1296 let mut fields = HashMap::new();
1297 fields.insert("title".to_string(), "metadata.document.title".to_string());
1298 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1299 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
1300 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
1301 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
1302 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
1303 let mut optional = HashSet::new();
1304 optional.insert("metadata.document.title".to_string());
1305 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1306 }
1307
1308 fn make_resolver_with_doc_optional() -> FieldResolver {
1309 let mut fields = HashMap::new();
1310 fields.insert("title".to_string(), "metadata.document.title".to_string());
1311 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
1312 let mut optional = HashSet::new();
1313 optional.insert("document".to_string());
1314 optional.insert("metadata.document.title".to_string());
1315 optional.insert("metadata.document".to_string());
1316 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
1317 }
1318
1319 #[test]
1320 fn test_resolve_alias() {
1321 let r = make_resolver();
1322 assert_eq!(r.resolve("title"), "metadata.document.title");
1323 }
1324
1325 #[test]
1326 fn test_resolve_passthrough() {
1327 let r = make_resolver();
1328 assert_eq!(r.resolve("content"), "content");
1329 }
1330
1331 #[test]
1332 fn test_is_optional() {
1333 let r = make_resolver();
1334 assert!(r.is_optional("metadata.document.title"));
1335 assert!(!r.is_optional("content"));
1336 }
1337
1338 #[test]
1339 fn test_accessor_rust_struct() {
1340 let r = make_resolver();
1341 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
1342 }
1343
1344 #[test]
1345 fn test_accessor_rust_map() {
1346 let r = make_resolver();
1347 assert_eq!(
1348 r.accessor("tags", "rust", "result"),
1349 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
1350 );
1351 }
1352
1353 #[test]
1354 fn test_accessor_python() {
1355 let r = make_resolver();
1356 assert_eq!(
1357 r.accessor("title", "python", "result"),
1358 "result.metadata.document.title"
1359 );
1360 }
1361
1362 #[test]
1363 fn test_accessor_go() {
1364 let r = make_resolver();
1365 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
1366 }
1367
1368 #[test]
1369 fn test_accessor_go_initialism_fields() {
1370 let mut fields = std::collections::HashMap::new();
1371 fields.insert("content".to_string(), "html".to_string());
1372 fields.insert("link_url".to_string(), "links.url".to_string());
1373 let r = FieldResolver::new(
1374 &fields,
1375 &HashSet::new(),
1376 &HashSet::new(),
1377 &HashSet::new(),
1378 &HashSet::new(),
1379 );
1380 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
1381 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
1382 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
1383 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
1384 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
1385 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
1386 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
1387 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
1388 }
1389
1390 #[test]
1391 fn test_accessor_typescript() {
1392 let r = make_resolver();
1393 assert_eq!(
1394 r.accessor("title", "typescript", "result"),
1395 "result.metadata.document.title"
1396 );
1397 }
1398
1399 #[test]
1400 fn test_accessor_typescript_snake_to_camel() {
1401 let r = make_resolver();
1402 assert_eq!(
1403 r.accessor("og", "typescript", "result"),
1404 "result.metadata.document.openGraph"
1405 );
1406 assert_eq!(
1407 r.accessor("twitter", "typescript", "result"),
1408 "result.metadata.document.twitterCard"
1409 );
1410 assert_eq!(
1411 r.accessor("canonical", "typescript", "result"),
1412 "result.metadata.document.canonicalUrl"
1413 );
1414 }
1415
1416 #[test]
1417 fn test_accessor_typescript_map_snake_to_camel() {
1418 let r = make_resolver();
1419 assert_eq!(
1420 r.accessor("og_tag", "typescript", "result"),
1421 "result.metadata.openGraphTags[\"og_title\"]"
1422 );
1423 }
1424
1425 #[test]
1426 fn test_accessor_typescript_numeric_index_is_unquoted() {
1427 let mut fields = HashMap::new();
1431 fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
1432 let r = FieldResolver::new(
1433 &fields,
1434 &HashSet::new(),
1435 &HashSet::new(),
1436 &HashSet::new(),
1437 &HashSet::new(),
1438 );
1439 assert_eq!(
1440 r.accessor("first_score", "typescript", "result"),
1441 "result.results[0].relevanceScore"
1442 );
1443 }
1444
1445 #[test]
1446 fn test_accessor_node_alias() {
1447 let r = make_resolver();
1448 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
1449 }
1450
1451 #[test]
1452 fn test_accessor_wasm_camel_case() {
1453 let r = make_resolver();
1454 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
1455 assert_eq!(
1456 r.accessor("twitter", "wasm", "result"),
1457 "result.metadata.document.twitterCard"
1458 );
1459 assert_eq!(
1460 r.accessor("canonical", "wasm", "result"),
1461 "result.metadata.document.canonicalUrl"
1462 );
1463 }
1464
1465 #[test]
1466 fn test_accessor_wasm_map_access() {
1467 let r = make_resolver();
1468 assert_eq!(
1469 r.accessor("og_tag", "wasm", "result"),
1470 "result.metadata.openGraphTags.get(\"og_title\")"
1471 );
1472 }
1473
1474 #[test]
1475 fn test_accessor_java() {
1476 let r = make_resolver();
1477 assert_eq!(
1478 r.accessor("title", "java", "result"),
1479 "result.metadata().document().title()"
1480 );
1481 }
1482
1483 #[test]
1484 fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
1485 let mut fields = HashMap::new();
1486 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1487 fields.insert("node_count".to_string(), "nodes.length".to_string());
1488 let mut arrays = HashSet::new();
1489 arrays.insert("nodes".to_string());
1490 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
1491 assert_eq!(
1492 r.accessor("first_node_name", "kotlin", "result"),
1493 "result.nodes().first().name()"
1494 );
1495 assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
1496 }
1497
1498 #[test]
1499 fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
1500 let r = make_resolver_with_doc_optional();
1501 assert_eq!(
1502 r.accessor("title", "kotlin", "result"),
1503 "result.metadata().document()?.title()"
1504 );
1505 }
1506
1507 #[test]
1508 fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
1509 let mut fields = HashMap::new();
1510 fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
1511 fields.insert("tag".to_string(), "tags[name]".to_string());
1512 let mut optional = HashSet::new();
1513 optional.insert("nodes".to_string());
1514 optional.insert("tags".to_string());
1515 let mut arrays = HashSet::new();
1516 arrays.insert("nodes".to_string());
1517 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
1518 assert_eq!(
1519 r.accessor("first_node_name", "kotlin", "result"),
1520 "result.nodes()?.first()?.name()"
1521 );
1522 assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
1523 }
1524
1525 #[test]
1526 fn test_accessor_csharp() {
1527 let r = make_resolver();
1528 assert_eq!(
1529 r.accessor("title", "csharp", "result"),
1530 "result.Metadata.Document.Title"
1531 );
1532 }
1533
1534 #[test]
1535 fn test_accessor_php() {
1536 let r = make_resolver();
1537 assert_eq!(
1538 r.accessor("title", "php", "$result"),
1539 "$result->metadata->document->title"
1540 );
1541 }
1542
1543 #[test]
1544 fn test_accessor_r() {
1545 let r = make_resolver();
1546 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
1547 }
1548
1549 #[test]
1550 fn test_accessor_c() {
1551 let r = make_resolver();
1552 assert_eq!(
1553 r.accessor("title", "c", "result"),
1554 "result_metadata_document_title(result)"
1555 );
1556 }
1557
1558 #[test]
1559 fn test_rust_unwrap_binding() {
1560 let r = make_resolver();
1561 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
1562 assert_eq!(var, "metadata_document_title");
1563 assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
1564 }
1565
1566 #[test]
1567 fn test_rust_unwrap_binding_non_optional() {
1568 let r = make_resolver();
1569 assert!(r.rust_unwrap_binding("content", "result").is_none());
1570 }
1571
1572 #[test]
1573 fn test_direct_field_no_alias() {
1574 let r = make_resolver();
1575 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1576 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
1577 }
1578
1579 #[test]
1580 fn test_accessor_rust_with_optionals() {
1581 let r = make_resolver_with_doc_optional();
1582 assert_eq!(
1583 r.accessor("title", "rust", "result"),
1584 "result.metadata.document.as_ref().unwrap().title"
1585 );
1586 }
1587
1588 #[test]
1589 fn test_accessor_csharp_with_optionals() {
1590 let r = make_resolver_with_doc_optional();
1591 assert_eq!(
1592 r.accessor("title", "csharp", "result"),
1593 "result.Metadata.Document!.Title"
1594 );
1595 }
1596
1597 #[test]
1598 fn test_accessor_rust_non_optional_field() {
1599 let r = make_resolver();
1600 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
1601 }
1602
1603 #[test]
1604 fn test_accessor_csharp_non_optional_field() {
1605 let r = make_resolver();
1606 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
1607 }
1608
1609 #[test]
1610 fn test_accessor_rust_method_call() {
1611 let mut fields = HashMap::new();
1613 fields.insert(
1614 "excel_sheet_count".to_string(),
1615 "metadata.format.excel.sheet_count".to_string(),
1616 );
1617 let mut optional = HashSet::new();
1618 optional.insert("metadata.format".to_string());
1619 optional.insert("metadata.format.excel".to_string());
1620 let mut method_calls = HashSet::new();
1621 method_calls.insert("metadata.format.excel".to_string());
1622 let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
1623 assert_eq!(
1624 r.accessor("excel_sheet_count", "rust", "result"),
1625 "result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
1626 );
1627 }
1628}