use super::*;
use std::collections::{HashMap, HashSet};
fn make_resolver() -> FieldResolver {
let mut fields = HashMap::new();
fields.insert("title".to_string(), "metadata.document.title".to_string());
fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
let mut optional = HashSet::new();
optional.insert("metadata.document.title".to_string());
FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
}
fn make_resolver_with_doc_optional() -> FieldResolver {
let mut fields = HashMap::new();
fields.insert("title".to_string(), "metadata.document.title".to_string());
fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
let mut optional = HashSet::new();
optional.insert("document".to_string());
optional.insert("metadata.document.title".to_string());
optional.insert("metadata.document".to_string());
FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
}
#[test]
fn test_resolve_alias() {
let r = make_resolver();
assert_eq!(r.resolve("title"), "metadata.document.title");
}
#[test]
fn test_resolve_passthrough() {
let r = make_resolver();
assert_eq!(r.resolve("content"), "content");
}
#[test]
fn test_is_optional() {
let r = make_resolver();
assert!(r.is_optional("metadata.document.title"));
assert!(!r.is_optional("content"));
}
#[test]
fn is_optional_strips_namespace_prefix() {
let fields = HashMap::new();
let mut optional = HashSet::new();
optional.insert("action_results.data".to_string());
let result_fields: HashSet<String> = ["action_results".to_string()].into_iter().collect();
let r = FieldResolver::new(&fields, &optional, &result_fields, &HashSet::new(), &HashSet::new());
assert!(r.is_optional("interaction.action_results[0].data"));
assert!(r.is_optional("action_results[0].data"));
}
#[test]
fn test_accessor_rust_struct() {
let r = make_resolver();
assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
}
#[test]
fn test_accessor_rust_map() {
let r = make_resolver();
assert_eq!(
r.accessor("tags", "rust", "result"),
"result.metadata.tags.get(\"name\").map(|s| s.as_str())"
);
}
#[test]
fn test_accessor_python() {
let r = make_resolver();
assert_eq!(
r.accessor("title", "python", "result"),
"result.metadata.document.title"
);
}
#[test]
fn test_accessor_go() {
let r = make_resolver();
assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
}
#[test]
fn test_accessor_go_initialism_fields() {
let mut fields = std::collections::HashMap::new();
fields.insert("content".to_string(), "html".to_string());
fields.insert("link_url".to_string(), "links.url".to_string());
let r = FieldResolver::new(
&fields,
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
);
assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
assert_eq!(r.accessor("url", "go", "result"), "result.URL");
assert_eq!(r.accessor("id", "go", "result"), "result.ID");
assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
assert_eq!(r.accessor("links", "go", "result"), "result.Links");
}
#[test]
fn test_accessor_typescript() {
let r = make_resolver();
assert_eq!(
r.accessor("title", "typescript", "result"),
"result.metadata.document.title"
);
}
#[test]
fn test_accessor_typescript_snake_to_camel() {
let r = make_resolver();
assert_eq!(
r.accessor("og", "typescript", "result"),
"result.metadata.document.openGraph"
);
assert_eq!(
r.accessor("twitter", "typescript", "result"),
"result.metadata.document.twitterCard"
);
assert_eq!(
r.accessor("canonical", "typescript", "result"),
"result.metadata.document.canonicalUrl"
);
}
#[test]
fn test_accessor_typescript_map_snake_to_camel() {
let r = make_resolver();
assert_eq!(
r.accessor("og_tag", "typescript", "result"),
"result.metadata.openGraphTags[\"og_title\"]"
);
}
#[test]
fn test_accessor_typescript_numeric_index_is_unquoted() {
let mut fields = HashMap::new();
fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
let r = FieldResolver::new(
&fields,
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
);
assert_eq!(
r.accessor("first_score", "typescript", "result"),
"result.results[0].relevanceScore"
);
}
#[test]
fn test_accessor_node_alias() {
let r = make_resolver();
assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
}
#[test]
fn test_accessor_wasm_camel_case() {
let r = make_resolver();
assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
assert_eq!(
r.accessor("twitter", "wasm", "result"),
"result.metadata.document.twitterCard"
);
assert_eq!(
r.accessor("canonical", "wasm", "result"),
"result.metadata.document.canonicalUrl"
);
}
#[test]
fn test_accessor_wasm_map_access() {
let r = make_resolver();
assert_eq!(
r.accessor("og_tag", "wasm", "result"),
"result.metadata.openGraphTags.get(\"og_title\")"
);
}
#[test]
fn test_accessor_java() {
let r = make_resolver();
assert_eq!(
r.accessor("title", "java", "result"),
"result.metadata().document().title()"
);
}
#[test]
fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
let mut fields = HashMap::new();
fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
fields.insert("node_count".to_string(), "nodes.length".to_string());
let mut arrays = HashSet::new();
arrays.insert("nodes".to_string());
let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
assert_eq!(
r.accessor("first_node_name", "kotlin", "result"),
"result.nodes().first().name()"
);
assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
}
#[test]
fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
let r = make_resolver_with_doc_optional();
assert_eq!(
r.accessor("title", "kotlin", "result"),
"result.metadata().document()?.title()"
);
}
#[test]
fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
let mut fields = HashMap::new();
fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
fields.insert("tag".to_string(), "tags[name]".to_string());
let mut optional = HashSet::new();
optional.insert("nodes".to_string());
optional.insert("tags".to_string());
let mut arrays = HashSet::new();
arrays.insert("nodes".to_string());
let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
assert_eq!(
r.accessor("first_node_name", "kotlin", "result"),
"result.nodes()?.first()?.name()"
);
assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
}
#[test]
fn test_accessor_kotlin_optional_field_after_indexed_array() {
let mut fields = HashMap::new();
fields.insert(
"tool_call_name".to_string(),
"choices[0].message.tool_calls[0].function.name".to_string(),
);
let mut optional = HashSet::new();
optional.insert("choices[0].message.tool_calls".to_string());
let mut arrays = HashSet::new();
arrays.insert("choices".to_string());
arrays.insert("choices[0].message.tool_calls".to_string());
let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
let expr = r.accessor("tool_call_name", "kotlin", "result");
assert!(
expr.contains("toolCalls()?.first()"),
"expected toolCalls()?.first() for optional list, got: {expr}"
);
}
#[test]
fn test_accessor_csharp() {
let r = make_resolver();
assert_eq!(
r.accessor("title", "csharp", "result"),
"result.Metadata.Document.Title"
);
}
#[test]
fn test_accessor_php() {
let r = make_resolver();
assert_eq!(
r.accessor("title", "php", "$result"),
"$result->metadata->document->title"
);
}
#[test]
fn test_accessor_r() {
let r = make_resolver();
assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
}
#[test]
fn test_accessor_c() {
let r = make_resolver();
assert_eq!(
r.accessor("title", "c", "result"),
"result_title(result_document(result_metadata(result)))"
);
}
#[test]
fn test_rust_unwrap_binding() {
let r = make_resolver();
let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
assert_eq!(var, "_metadata_document_title");
assert!(binding.starts_with("let _metadata_document_title ="));
assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
}
#[test]
fn test_rust_unwrap_binding_non_optional() {
let r = make_resolver();
assert!(r.rust_unwrap_binding("content", "result").is_none());
}
#[test]
fn test_rust_unwrap_binding_collapses_double_underscore() {
let mut aliases = HashMap::new();
aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
let mut optional = HashSet::new();
optional.insert("json_ld[].name".to_string());
let mut array = HashSet::new();
array.insert("json_ld".to_string());
let result_fields = HashSet::new();
let method_calls = HashSet::new();
let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
assert_eq!(var, "_json_ld_name");
}
#[test]
fn test_direct_field_no_alias() {
let r = make_resolver();
assert_eq!(r.accessor("content", "rust", "result"), "result.content");
assert_eq!(r.accessor("content", "go", "result"), "result.Content");
}
#[test]
fn test_accessor_rust_with_optionals() {
let r = make_resolver_with_doc_optional();
assert_eq!(
r.accessor("title", "rust", "result"),
"result.metadata.document.as_ref().unwrap().title"
);
}
#[test]
fn test_accessor_csharp_with_optionals() {
let r = make_resolver_with_doc_optional();
assert_eq!(
r.accessor("title", "csharp", "result"),
"result.Metadata.Document!.Title"
);
}
#[test]
fn test_accessor_rust_non_optional_field() {
let r = make_resolver();
assert_eq!(r.accessor("content", "rust", "result"), "result.content");
}
#[test]
fn test_accessor_csharp_non_optional_field() {
let r = make_resolver();
assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
}
#[test]
fn test_accessor_rust_method_call() {
let mut fields = HashMap::new();
fields.insert(
"excel_sheet_count".to_string(),
"metadata.format.excel.sheet_count".to_string(),
);
let mut optional = HashSet::new();
optional.insert("metadata.format".to_string());
optional.insert("metadata.format.excel".to_string());
let mut method_calls = HashSet::new();
method_calls.insert("metadata.format.excel".to_string());
let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
assert_eq!(
r.accessor("excel_sheet_count", "rust", "result"),
"result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
);
}
fn make_php_getter_resolver() -> FieldResolver {
let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
getters.insert(
"Root".to_string(),
["metadata".to_string(), "links".to_string()].into_iter().collect(),
);
let map = PhpGetterMap {
getters,
field_types: HashMap::new(),
root_type: Some("Root".to_string()),
all_fields: HashMap::new(),
};
FieldResolver::new_with_php_getters(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashMap::new(),
map,
)
}
#[test]
fn render_php_uses_getter_method_for_non_scalar_field() {
let r = make_php_getter_resolver();
assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
}
#[test]
fn render_php_uses_property_for_scalar_field() {
let r = make_php_getter_resolver();
assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
}
#[test]
fn render_php_nested_non_scalar_uses_getter_then_property() {
let mut fields = HashMap::new();
fields.insert("title".to_string(), "metadata.title".to_string());
let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
getters.insert("Root".to_string(), ["metadata".to_string()].into_iter().collect());
getters.insert("Metadata".to_string(), HashSet::new());
let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
field_types.insert(
"Root".to_string(),
[("metadata".to_string(), "Metadata".to_string())].into_iter().collect(),
);
let map = PhpGetterMap {
getters,
field_types,
root_type: Some("Root".to_string()),
all_fields: HashMap::new(),
};
let r = FieldResolver::new_with_php_getters(
&fields,
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashMap::new(),
map,
);
assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
}
#[test]
fn render_php_array_field_uses_getter_when_non_scalar() {
let mut fields = HashMap::new();
fields.insert("first_link".to_string(), "links[0]".to_string());
let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
getters.insert("Root".to_string(), ["links".to_string()].into_iter().collect());
let map = PhpGetterMap {
getters,
field_types: HashMap::new(),
root_type: Some("Root".to_string()),
all_fields: HashMap::new(),
};
let r = FieldResolver::new_with_php_getters(
&fields,
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashMap::new(),
map,
);
assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
}
#[test]
fn render_php_falls_back_to_property_when_getter_fields_empty() {
let r = FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
);
assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
}
#[test]
fn render_php_with_getters_distinguishes_same_field_name_on_different_types() {
let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
getters.insert("A".to_string(), ["content".to_string()].into_iter().collect());
getters.insert("B".to_string(), HashSet::new());
let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
all_fields.insert("A".to_string(), ["content".to_string()].into_iter().collect());
all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
let map_a = PhpGetterMap {
getters: getters.clone(),
field_types: HashMap::new(),
root_type: Some("A".to_string()),
all_fields: all_fields.clone(),
};
let map_b = PhpGetterMap {
getters,
field_types: HashMap::new(),
root_type: Some("B".to_string()),
all_fields,
};
let r_a = FieldResolver::new_with_php_getters(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashMap::new(),
map_a,
);
let r_b = FieldResolver::new_with_php_getters(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashMap::new(),
map_b,
);
assert_eq!(r_a.accessor("content", "php", "$a"), "$a->getContent()");
assert_eq!(r_b.accessor("content", "php", "$b"), "$b->content");
}
#[test]
fn render_php_with_getters_chains_through_correct_type() {
let mut fields = HashMap::new();
fields.insert("nested_content".to_string(), "inner.content".to_string());
let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
getters.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
getters.insert("B".to_string(), HashSet::new());
getters.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
field_types.insert(
"Outer".to_string(),
[("inner".to_string(), "B".to_string())].into_iter().collect(),
);
let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
all_fields.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
all_fields.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
let map = PhpGetterMap {
getters,
field_types,
root_type: Some("Outer".to_string()),
all_fields,
};
let r = FieldResolver::new_with_php_getters(
&fields,
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashMap::new(),
map,
);
assert_eq!(
r.accessor("nested_content", "php", "$result"),
"$result->getInner()->content"
);
}
fn make_resolver_with_result_fields(result_fields: &[&str]) -> FieldResolver {
let rf: HashSet<String> = result_fields.iter().map(|s| s.to_string()).collect();
FieldResolver::new(&HashMap::new(), &HashSet::new(), &rf, &HashSet::new(), &HashSet::new())
}
#[test]
fn is_valid_for_result_accepts_virtual_namespace_prefix() {
let r = make_resolver_with_result_fields(&["browser_used", "js_render_hint", "status_code"]);
assert!(
r.is_valid_for_result("browser.browser_used"),
"browser.browser_used should be valid via namespace-prefix stripping"
);
assert!(
r.is_valid_for_result("browser.js_render_hint"),
"browser.js_render_hint should be valid via namespace-prefix stripping"
);
}
#[test]
fn is_valid_for_result_accepts_namespace_prefix_before_array_field() {
let r = make_resolver_with_result_fields(&["action_results", "final_html", "final_url"]);
assert!(
r.is_valid_for_result("interaction.action_results[0].action_type"),
"interaction. prefix should be stripped so action_results is recognised"
);
}
#[test]
fn is_valid_for_result_rejects_unknown_field_even_after_namespace_strip() {
let r = make_resolver_with_result_fields(&["pages", "final_url"]);
assert!(
!r.is_valid_for_result("browser.browser_used"),
"browser_used is not in result_fields so should be rejected"
);
assert!(
!r.is_valid_for_result("ns.unknown_field"),
"unknown_field is not in result_fields so should be rejected"
);
}
#[test]
fn accessor_strips_namespace_prefix_for_python() {
let r = make_resolver_with_result_fields(&["browser_used", "js_render_hint"]);
assert_eq!(
r.accessor("browser.browser_used", "python", "result"),
"result.browser_used"
);
assert_eq!(
r.accessor("browser.js_render_hint", "python", "result"),
"result.js_render_hint"
);
}
#[test]
fn accessor_strips_namespace_prefix_for_csharp() {
let r = make_resolver_with_result_fields(&["browser_used"]);
assert_eq!(
r.accessor("browser.browser_used", "csharp", "result"),
"result.BrowserUsed"
);
}
#[test]
fn accessor_strips_namespace_prefix_for_indexed_array_field() {
let r = make_resolver_with_result_fields(&["action_results", "final_html", "final_url"]);
assert_eq!(
r.accessor("interaction.action_results[0].action_type", "python", "result"),
"result.action_results[0].action_type"
);
assert_eq!(
r.accessor("interaction.action_results[0].action_type", "typescript", "result"),
"result.actionResults[0].actionType"
);
}
#[test]
fn is_valid_for_result_is_permissive_when_result_fields_empty() {
let r = make_resolver_with_result_fields(&[]);
assert!(r.is_valid_for_result("browser.browser_used"));
assert!(r.is_valid_for_result("anything.at.all"));
}
#[test]
fn accessor_does_not_strip_real_first_segment() {
let r = make_resolver_with_result_fields(&["metadata", "status_code"]);
assert_eq!(
r.accessor("metadata.title", "python", "result"),
"result.metadata.title"
);
}
#[test]
fn namespace_stripped_path_returns_none_when_result_fields_empty() {
let r = make_resolver_with_result_fields(&[]);
assert_eq!(r.namespace_stripped_path("metrics.total_lines"), None);
assert_eq!(r.namespace_stripped_path("anything.deeply.nested.path"), None);
}
#[test]
fn render_rust_with_result_fields_overrides_method_calls() {
let result_fields: HashSet<String> = ["content".to_string(), "mime_type".to_string()].into_iter().collect();
let method_calls: HashSet<String> = [
"content".to_string(),
"mime_type".to_string(),
"other_accessor".to_string(),
]
.into_iter()
.collect();
let r = FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&result_fields,
&HashSet::new(),
&method_calls,
);
assert_eq!(r.accessor("content", "rust", "result"), "result.content");
assert_eq!(r.accessor("mime_type", "rust", "result"), "result.mime_type");
assert_eq!(
r.accessor("other_accessor", "rust", "result"),
"result.other_accessor()"
);
}
#[test]
fn render_php_needs_getter_returns_false_when_owner_has_no_getter_entry() {
let getters: HashMap<String, HashSet<String>> = {
let mut m = HashMap::new();
m.insert("Chunk".to_string(), ["content".to_string()].into_iter().collect());
m
};
let all_fields: HashMap<String, HashSet<String>> = {
let mut m = HashMap::new();
m.insert(
"ProcessingResult".to_string(),
["content".to_string()].into_iter().collect(),
);
m.insert("Chunk".to_string(), ["content".to_string()].into_iter().collect());
m
};
let map = PhpGetterMap {
getters,
field_types: HashMap::new(),
root_type: Some("ProcessingResult".to_string()),
all_fields,
};
assert!(!map.needs_getter(Some("ProcessingResult"), "content"));
assert!(map.needs_getter(Some("Chunk"), "content"));
assert!(map.needs_getter(None, "content"));
let r = FieldResolver::new_with_php_getters(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashMap::new(),
map,
);
assert_eq!(r.accessor("content", "php", "$result"), "$result->content");
}