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}
18
19#[derive(Debug, Clone)]
21enum PathSegment {
22 Field(String),
24 ArrayField(String),
26 MapAccess { field: String, key: String },
28 Length,
30}
31
32impl FieldResolver {
33 pub fn new(
36 fields: &HashMap<String, String>,
37 optional: &HashSet<String>,
38 result_fields: &HashSet<String>,
39 array_fields: &HashSet<String>,
40 ) -> Self {
41 Self {
42 aliases: fields.clone(),
43 optional_fields: optional.clone(),
44 result_fields: result_fields.clone(),
45 array_fields: array_fields.clone(),
46 }
47 }
48
49 pub fn resolve<'a>(&'a self, fixture_field: &'a str) -> &'a str {
52 self.aliases
53 .get(fixture_field)
54 .map(String::as_str)
55 .unwrap_or(fixture_field)
56 }
57
58 pub fn is_optional(&self, field: &str) -> bool {
60 if self.optional_fields.contains(field) {
61 return true;
62 }
63 let normalized = field.replace("[].", ".");
66 if normalized != field && self.optional_fields.contains(normalized.as_str()) {
67 return true;
68 }
69 for af in &self.array_fields {
71 if let Some(rest) = field.strip_prefix(af.as_str()) {
72 if let Some(rest) = rest.strip_prefix('.') {
73 let with_bracket = format!("{af}[].{rest}");
74 if self.optional_fields.contains(with_bracket.as_str()) {
75 return true;
76 }
77 }
78 }
79 }
80 false
81 }
82
83 pub fn has_alias(&self, fixture_field: &str) -> bool {
85 self.aliases.contains_key(fixture_field)
86 }
87
88 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);
102 self.result_fields.contains(first_segment)
103 }
104
105 pub fn is_array(&self, field: &str) -> bool {
107 self.array_fields.contains(field)
108 }
109
110 pub fn has_map_access(&self, fixture_field: &str) -> bool {
114 let resolved = self.resolve(fixture_field);
115 let segments = parse_path(resolved);
116 segments.iter().any(|s| matches!(s, PathSegment::MapAccess { .. }))
117 }
118
119 pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
122 let resolved = self.resolve(fixture_field);
123 let segments = parse_path(resolved);
124
125 let segments = self.inject_array_indexing(segments);
128
129 match language {
130 "java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
131 "rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields),
132 "csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
133 _ => render_accessor(&segments, language, result_var),
134 }
135 }
136
137 fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
144 if self.array_fields.is_empty() {
145 return segments;
146 }
147 let len = segments.len();
148 let mut result = Vec::with_capacity(len);
149 let mut path_so_far = String::new();
150 for i in 0..len {
151 let seg = &segments[i];
152 match seg {
153 PathSegment::Field(f) => {
154 if !path_so_far.is_empty() {
155 path_so_far.push('.');
156 }
157 path_so_far.push_str(f);
158 let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
163 if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
164 result.push(PathSegment::ArrayField(f.clone()));
165 } else {
166 result.push(seg.clone());
167 }
168 }
169 _ => {
170 result.push(seg.clone());
171 }
172 }
173 }
174 result
175 }
176
177 pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
180 let resolved = self.resolve(fixture_field);
181 if !self.is_optional(resolved) {
182 return None;
183 }
184 let segments = parse_path(resolved);
185 let segments = self.inject_array_indexing(segments);
186 let local_var = resolved.replace(['.', '['], "_").replace(']', "");
187 let accessor = render_accessor(&segments, "rust", result_var);
188 let has_map_access = segments.iter().any(|s| matches!(s, PathSegment::MapAccess { .. }));
191 let binding = if has_map_access {
192 format!("let {local_var} = {accessor}.unwrap_or(\"\");")
193 } else {
194 format!("let {local_var} = {accessor}.as_deref().unwrap_or(\"\");")
195 };
196 Some((binding, local_var))
197 }
198}
199
200fn parse_path(path: &str) -> Vec<PathSegment> {
203 let mut segments = Vec::new();
204 for part in path.split('.') {
205 if part == "length" || part == "count" || part == "size" {
206 segments.push(PathSegment::Length);
207 } else if let Some(bracket_pos) = part.find('[') {
208 let field = part[..bracket_pos].to_string();
209 let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
210 if key.is_empty() {
211 segments.push(PathSegment::ArrayField(field));
213 } else {
214 segments.push(PathSegment::MapAccess { field, key });
215 }
216 } else {
217 segments.push(PathSegment::Field(part.to_string()));
218 }
219 }
220 segments
221}
222
223fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
225 match language {
226 "rust" => render_rust(segments, result_var),
227 "python" => render_dot_access(segments, result_var, "python"),
228 "typescript" | "node" => render_typescript(segments, result_var),
229 "wasm" => render_wasm(segments, result_var),
230 "go" => render_go(segments, result_var),
231 "java" => render_java(segments, result_var),
232 "csharp" => render_pascal_dot(segments, result_var),
233 "ruby" => render_dot_access(segments, result_var, "ruby"),
234 "php" => render_php(segments, result_var),
235 "elixir" => render_dot_access(segments, result_var, "elixir"),
236 "r" => render_r(segments, result_var),
237 "c" => render_c(segments, result_var),
238 _ => render_dot_access(segments, result_var, language),
239 }
240}
241
242fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
248 let mut out = result_var.to_string();
249 for seg in segments {
250 match seg {
251 PathSegment::Field(f) => {
252 out.push('.');
253 out.push_str(&f.to_snake_case());
254 }
255 PathSegment::ArrayField(f) => {
256 out.push('.');
257 out.push_str(&f.to_snake_case());
258 out.push_str("[0]");
259 }
260 PathSegment::MapAccess { field, key } => {
261 out.push('.');
262 out.push_str(&field.to_snake_case());
263 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
264 }
265 PathSegment::Length => {
266 out.push_str(".len()");
267 }
268 }
269 }
270 out
271}
272
273fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
275 let mut out = result_var.to_string();
276 for seg in segments {
277 match seg {
278 PathSegment::Field(f) => {
279 out.push('.');
280 out.push_str(f);
281 }
282 PathSegment::ArrayField(f) => {
283 if language == "elixir" {
284 let current = std::mem::take(&mut out);
285 out = format!("Enum.at({current}.{f}, 0)");
286 } else {
287 out.push('.');
288 out.push_str(f);
289 out.push_str("[0]");
290 }
291 }
292 PathSegment::MapAccess { field, key } => {
293 out.push('.');
294 out.push_str(field);
295 if language == "elixir" {
297 out.push_str(&format!("[\"{key}\"]"));
298 } else {
299 out.push_str(&format!(".get(\"{key}\")"));
300 }
301 }
302 PathSegment::Length => match language {
303 "ruby" => out.push_str(".length"),
304 "elixir" => {
305 let current = std::mem::take(&mut out);
306 out = format!("length({current})");
307 }
308 _ => {
310 let current = std::mem::take(&mut out);
311 out = format!("len({current})");
312 }
313 },
314 }
315 }
316 out
317}
318
319fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
322 let mut out = result_var.to_string();
323 for seg in segments {
324 match seg {
325 PathSegment::Field(f) => {
326 out.push('.');
327 out.push_str(&f.to_lower_camel_case());
328 }
329 PathSegment::ArrayField(f) => {
330 out.push('.');
331 out.push_str(&f.to_lower_camel_case());
332 out.push_str("[0]");
333 }
334 PathSegment::MapAccess { field, key } => {
335 out.push('.');
336 out.push_str(&field.to_lower_camel_case());
337 out.push_str(&format!("[\"{key}\"]"));
338 }
339 PathSegment::Length => {
340 out.push_str(".length");
341 }
342 }
343 }
344 out
345}
346
347fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
352 let mut out = result_var.to_string();
353 for seg in segments {
354 match seg {
355 PathSegment::Field(f) => {
356 out.push('.');
357 out.push_str(&f.to_lower_camel_case());
358 }
359 PathSegment::ArrayField(f) => {
360 out.push('.');
361 out.push_str(&f.to_lower_camel_case());
362 out.push_str("[0]");
363 }
364 PathSegment::MapAccess { field, key } => {
365 out.push('.');
366 out.push_str(&field.to_lower_camel_case());
367 out.push_str(&format!(".get(\"{key}\")"));
368 }
369 PathSegment::Length => {
370 out.push_str(".length");
371 }
372 }
373 }
374 out
375}
376
377fn render_go(segments: &[PathSegment], result_var: &str) -> String {
382 let mut out = result_var.to_string();
383 for seg in segments {
384 match seg {
385 PathSegment::Field(f) => {
386 out.push('.');
387 out.push_str(&to_go_name(f));
388 }
389 PathSegment::ArrayField(f) => {
390 out.push('.');
391 out.push_str(&to_go_name(f));
392 out.push_str("[0]");
393 }
394 PathSegment::MapAccess { field, key } => {
395 out.push('.');
396 out.push_str(&to_go_name(field));
397 out.push_str(&format!("[\"{key}\"]"));
398 }
399 PathSegment::Length => {
400 let current = std::mem::take(&mut out);
401 out = format!("len({current})");
402 }
403 }
404 }
405 out
406}
407
408fn render_java(segments: &[PathSegment], result_var: &str) -> String {
411 let mut out = result_var.to_string();
412 for seg in segments {
413 match seg {
414 PathSegment::Field(f) => {
415 out.push('.');
416 out.push_str(&f.to_lower_camel_case());
417 out.push_str("()");
418 }
419 PathSegment::ArrayField(f) => {
420 out.push('.');
421 out.push_str(&f.to_lower_camel_case());
422 out.push_str("().getFirst()");
423 }
424 PathSegment::MapAccess { field, key } => {
425 out.push('.');
426 out.push_str(&field.to_lower_camel_case());
427 out.push_str(&format!("().get(\"{key}\")"));
428 }
429 PathSegment::Length => {
430 out.push_str(".size()");
431 }
432 }
433 }
434 out
435}
436
437fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
442 let mut out = result_var.to_string();
443 let mut path_so_far = String::new();
444 for (i, seg) in segments.iter().enumerate() {
445 let is_leaf = i == segments.len() - 1;
446 match seg {
447 PathSegment::Field(f) => {
448 if !path_so_far.is_empty() {
449 path_so_far.push('.');
450 }
451 path_so_far.push_str(f);
452 out.push('.');
453 out.push_str(&f.to_lower_camel_case());
454 out.push_str("()");
455 if !is_leaf && optional_fields.contains(&path_so_far) {
457 out.push_str(".orElseThrow()");
458 }
459 }
460 PathSegment::ArrayField(f) => {
461 if !path_so_far.is_empty() {
462 path_so_far.push('.');
463 }
464 path_so_far.push_str(f);
465 out.push('.');
466 out.push_str(&f.to_lower_camel_case());
467 out.push_str("().getFirst()");
468 }
469 PathSegment::MapAccess { field, key } => {
470 if !path_so_far.is_empty() {
471 path_so_far.push('.');
472 }
473 path_so_far.push_str(field);
474 out.push('.');
475 out.push_str(&field.to_lower_camel_case());
476 out.push_str(&format!("().get(\"{key}\")"));
477 }
478 PathSegment::Length => {
479 out.push_str(".size()");
480 }
481 }
482 }
483 out
484}
485
486fn render_rust_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
491 let mut out = result_var.to_string();
492 let mut path_so_far = String::new();
493 for (i, seg) in segments.iter().enumerate() {
494 let is_leaf = i == segments.len() - 1;
495 match seg {
496 PathSegment::Field(f) => {
497 if !path_so_far.is_empty() {
498 path_so_far.push('.');
499 }
500 path_so_far.push_str(f);
501 out.push('.');
502 out.push_str(&f.to_snake_case());
503 if !is_leaf && optional_fields.contains(&path_so_far) {
505 out.push_str(".as_ref().unwrap()");
506 }
507 }
508 PathSegment::ArrayField(f) => {
509 if !path_so_far.is_empty() {
510 path_so_far.push('.');
511 }
512 path_so_far.push_str(f);
513 out.push('.');
514 out.push_str(&f.to_snake_case());
515 out.push_str("[0]");
516 }
517 PathSegment::MapAccess { field, key } => {
518 if !path_so_far.is_empty() {
519 path_so_far.push('.');
520 }
521 path_so_far.push_str(field);
522 out.push('.');
523 out.push_str(&field.to_snake_case());
524 out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
525 }
526 PathSegment::Length => {
527 out.push_str(".len()");
528 }
529 }
530 }
531 out
532}
533
534fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
536 let mut out = result_var.to_string();
537 for seg in segments {
538 match seg {
539 PathSegment::Field(f) => {
540 out.push('.');
541 out.push_str(&f.to_pascal_case());
542 }
543 PathSegment::ArrayField(f) => {
544 out.push('.');
545 out.push_str(&f.to_pascal_case());
546 out.push_str("[0]");
547 }
548 PathSegment::MapAccess { field, key } => {
549 out.push('.');
550 out.push_str(&field.to_pascal_case());
551 out.push_str(&format!("[\"{key}\"]"));
552 }
553 PathSegment::Length => {
554 out.push_str(".Count");
555 }
556 }
557 }
558 out
559}
560
561fn render_csharp_with_optionals(
566 segments: &[PathSegment],
567 result_var: &str,
568 optional_fields: &HashSet<String>,
569) -> String {
570 let mut out = result_var.to_string();
571 let mut path_so_far = String::new();
572 for (i, seg) in segments.iter().enumerate() {
573 let is_leaf = i == segments.len() - 1;
574 match seg {
575 PathSegment::Field(f) => {
576 if !path_so_far.is_empty() {
577 path_so_far.push('.');
578 }
579 path_so_far.push_str(f);
580 out.push('.');
581 out.push_str(&f.to_pascal_case());
582 if !is_leaf && optional_fields.contains(&path_so_far) {
584 out.push('!');
585 }
586 }
587 PathSegment::ArrayField(f) => {
588 if !path_so_far.is_empty() {
589 path_so_far.push('.');
590 }
591 path_so_far.push_str(f);
592 out.push('.');
593 out.push_str(&f.to_pascal_case());
594 out.push_str("[0]");
595 }
596 PathSegment::MapAccess { field, key } => {
597 if !path_so_far.is_empty() {
598 path_so_far.push('.');
599 }
600 path_so_far.push_str(field);
601 out.push('.');
602 out.push_str(&field.to_pascal_case());
603 out.push_str(&format!("[\"{key}\"]"));
604 }
605 PathSegment::Length => {
606 out.push_str(".Count");
607 }
608 }
609 }
610 out
611}
612
613fn render_php(segments: &[PathSegment], result_var: &str) -> String {
615 let mut out = result_var.to_string();
616 for seg in segments {
617 match seg {
618 PathSegment::Field(f) => {
619 out.push_str("->");
620 out.push_str(f);
621 }
622 PathSegment::ArrayField(f) => {
623 out.push_str("->");
624 out.push_str(f);
625 out.push_str("[0]");
626 }
627 PathSegment::MapAccess { field, key } => {
628 out.push_str("->");
629 out.push_str(field);
630 out.push_str(&format!("[\"{key}\"]"));
631 }
632 PathSegment::Length => {
633 let current = std::mem::take(&mut out);
634 out = format!("count({current})");
635 }
636 }
637 }
638 out
639}
640
641fn render_r(segments: &[PathSegment], result_var: &str) -> String {
643 let mut out = result_var.to_string();
644 for seg in segments {
645 match seg {
646 PathSegment::Field(f) => {
647 out.push('$');
648 out.push_str(f);
649 }
650 PathSegment::ArrayField(f) => {
651 out.push('$');
652 out.push_str(f);
653 out.push_str("[[1]]");
654 }
655 PathSegment::MapAccess { field, key } => {
656 out.push('$');
657 out.push_str(field);
658 out.push_str(&format!("[[\"{key}\"]]"));
659 }
660 PathSegment::Length => {
661 let current = std::mem::take(&mut out);
662 out = format!("length({current})");
663 }
664 }
665 }
666 out
667}
668
669fn render_c(segments: &[PathSegment], result_var: &str) -> String {
671 let mut parts = Vec::new();
672 let mut trailing_length = false;
673 for seg in segments {
674 match seg {
675 PathSegment::Field(f) | PathSegment::ArrayField(f) => parts.push(f.to_snake_case()),
676 PathSegment::MapAccess { field, key } => {
677 parts.push(field.to_snake_case());
678 parts.push(key.clone());
679 }
680 PathSegment::Length => {
681 trailing_length = true;
682 }
683 }
684 }
685 let suffix = parts.join("_");
686 if trailing_length {
687 format!("result_{suffix}_count({result_var})")
688 } else {
689 format!("result_{suffix}({result_var})")
690 }
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696
697 fn make_resolver() -> FieldResolver {
698 let mut fields = HashMap::new();
699 fields.insert("title".to_string(), "metadata.document.title".to_string());
700 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
701 fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
702 fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
703 fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
704 fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
705
706 let mut optional = HashSet::new();
707 optional.insert("metadata.document.title".to_string());
708
709 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new())
710 }
711
712 fn make_resolver_with_doc_optional() -> FieldResolver {
713 let mut fields = HashMap::new();
714 fields.insert("title".to_string(), "metadata.document.title".to_string());
715 fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
716
717 let mut optional = HashSet::new();
718 optional.insert("document".to_string());
719 optional.insert("metadata.document.title".to_string());
720 optional.insert("metadata.document".to_string());
721
722 FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new())
723 }
724
725 #[test]
726 fn test_resolve_alias() {
727 let r = make_resolver();
728 assert_eq!(r.resolve("title"), "metadata.document.title");
729 }
730
731 #[test]
732 fn test_resolve_passthrough() {
733 let r = make_resolver();
734 assert_eq!(r.resolve("content"), "content");
735 }
736
737 #[test]
738 fn test_is_optional() {
739 let r = make_resolver();
740 assert!(r.is_optional("metadata.document.title"));
741 assert!(!r.is_optional("content"));
742 }
743
744 #[test]
745 fn test_accessor_rust_struct() {
746 let r = make_resolver();
747 assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
748 }
749
750 #[test]
751 fn test_accessor_rust_map() {
752 let r = make_resolver();
753 assert_eq!(
754 r.accessor("tags", "rust", "result"),
755 "result.metadata.tags.get(\"name\").map(|s| s.as_str())"
756 );
757 }
758
759 #[test]
760 fn test_accessor_python() {
761 let r = make_resolver();
762 assert_eq!(
763 r.accessor("title", "python", "result"),
764 "result.metadata.document.title"
765 );
766 }
767
768 #[test]
769 fn test_accessor_go() {
770 let r = make_resolver();
771 assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
772 }
773
774 #[test]
775 fn test_accessor_go_initialism_fields() {
776 let mut fields = std::collections::HashMap::new();
779 fields.insert("content".to_string(), "html".to_string());
780 fields.insert("link_url".to_string(), "links.url".to_string());
781 let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &HashSet::new());
782
783 assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
784 assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
785 assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
787 assert_eq!(r.accessor("url", "go", "result"), "result.URL");
788 assert_eq!(r.accessor("id", "go", "result"), "result.ID");
789 assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
790 assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
791 assert_eq!(r.accessor("links", "go", "result"), "result.Links");
792 }
793
794 #[test]
795 fn test_accessor_typescript() {
796 let r = make_resolver();
797 assert_eq!(
798 r.accessor("title", "typescript", "result"),
799 "result.metadata.document.title"
800 );
801 }
802
803 #[test]
804 fn test_accessor_typescript_snake_to_camel() {
805 let r = make_resolver();
806 assert_eq!(
807 r.accessor("og", "typescript", "result"),
808 "result.metadata.document.openGraph"
809 );
810 assert_eq!(
811 r.accessor("twitter", "typescript", "result"),
812 "result.metadata.document.twitterCard"
813 );
814 assert_eq!(
815 r.accessor("canonical", "typescript", "result"),
816 "result.metadata.document.canonicalUrl"
817 );
818 }
819
820 #[test]
821 fn test_accessor_typescript_map_snake_to_camel() {
822 let r = make_resolver();
823 assert_eq!(
824 r.accessor("og_tag", "typescript", "result"),
825 "result.metadata.openGraphTags[\"og_title\"]"
826 );
827 }
828
829 #[test]
830 fn test_accessor_node_alias() {
831 let r = make_resolver();
832 assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
833 }
834
835 #[test]
836 fn test_accessor_wasm_camel_case() {
837 let r = make_resolver();
838 assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
839 assert_eq!(
840 r.accessor("twitter", "wasm", "result"),
841 "result.metadata.document.twitterCard"
842 );
843 assert_eq!(
844 r.accessor("canonical", "wasm", "result"),
845 "result.metadata.document.canonicalUrl"
846 );
847 }
848
849 #[test]
850 fn test_accessor_wasm_map_access() {
851 let r = make_resolver();
852 assert_eq!(
854 r.accessor("og_tag", "wasm", "result"),
855 "result.metadata.openGraphTags.get(\"og_title\")"
856 );
857 }
858
859 #[test]
860 fn test_accessor_java() {
861 let r = make_resolver();
862 assert_eq!(
863 r.accessor("title", "java", "result"),
864 "result.metadata().document().title()"
865 );
866 }
867
868 #[test]
869 fn test_accessor_csharp() {
870 let r = make_resolver();
871 assert_eq!(
872 r.accessor("title", "csharp", "result"),
873 "result.Metadata.Document.Title"
874 );
875 }
876
877 #[test]
878 fn test_accessor_php() {
879 let r = make_resolver();
880 assert_eq!(
881 r.accessor("title", "php", "$result"),
882 "$result->metadata->document->title"
883 );
884 }
885
886 #[test]
887 fn test_accessor_r() {
888 let r = make_resolver();
889 assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
890 }
891
892 #[test]
893 fn test_accessor_c() {
894 let r = make_resolver();
895 assert_eq!(
896 r.accessor("title", "c", "result"),
897 "result_metadata_document_title(result)"
898 );
899 }
900
901 #[test]
902 fn test_rust_unwrap_binding() {
903 let r = make_resolver();
904 let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
905 assert_eq!(var, "metadata_document_title");
906 assert!(binding.contains("as_deref().unwrap_or(\"\")"));
907 }
908
909 #[test]
910 fn test_rust_unwrap_binding_non_optional() {
911 let r = make_resolver();
912 assert!(r.rust_unwrap_binding("content", "result").is_none());
913 }
914
915 #[test]
916 fn test_direct_field_no_alias() {
917 let r = make_resolver();
918 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
919 assert_eq!(r.accessor("content", "go", "result"), "result.Content");
920 }
921
922 #[test]
923 fn test_accessor_rust_with_optionals() {
924 let r = make_resolver_with_doc_optional();
925 assert_eq!(
927 r.accessor("title", "rust", "result"),
928 "result.metadata.document.as_ref().unwrap().title"
929 );
930 }
931
932 #[test]
933 fn test_accessor_csharp_with_optionals() {
934 let r = make_resolver_with_doc_optional();
935 assert_eq!(
937 r.accessor("title", "csharp", "result"),
938 "result.Metadata.Document!.Title"
939 );
940 }
941
942 #[test]
943 fn test_accessor_rust_non_optional_field() {
944 let r = make_resolver();
945 assert_eq!(r.accessor("content", "rust", "result"), "result.content");
947 }
948
949 #[test]
950 fn test_accessor_csharp_non_optional_field() {
951 let r = make_resolver();
952 assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
954 }
955}