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