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