use crate::escape::rust_raw_string;
#[allow(clippy::too_many_arguments)]
pub fn render_rust_arg(
name: &str,
value: &serde_json::Value,
arg_type: &str,
optional: bool,
module: &str,
fixture_id: &str,
mock_base_url: Option<&str>,
owned: bool,
element_type: Option<&str>,
) -> (Vec<String>, String) {
if arg_type == "mock_url" {
let lines = vec![format!(
"let {name} = format!(\"{{}}/fixtures/{{}}\", std::env::var(\"MOCK_SERVER_URL\").expect(\"MOCK_SERVER_URL not set\"), \"{fixture_id}\");"
)];
return (lines, format!("&{name}"));
}
if arg_type == "base_url" {
if let Some(url_expr) = mock_base_url {
return (vec![], url_expr.to_string());
}
}
if arg_type == "handle" {
use heck::ToSnakeCase;
let constructor_name = format!("create_{}", name.to_snake_case());
let mut lines = Vec::new();
if value.is_null() || value.is_object() && value.as_object().unwrap().is_empty() {
lines.push(format!(
"let {name} = {constructor_name}(None).expect(\"handle creation should succeed\");"
));
} else {
let json_literal = serde_json::to_string(value).unwrap_or_default();
let escaped = json_literal.replace('\\', "\\\\").replace('"', "\\\"");
lines.push(format!(
"let {name}_config: CrawlConfig = serde_json::from_str(\"{escaped}\").expect(\"config should parse\");"
));
lines.push(format!(
"let {name} = {constructor_name}(Some({name}_config)).expect(\"handle creation should succeed\");"
));
}
return (lines, format!("&{name}"));
}
if arg_type == "json_object" {
return render_json_object_arg(name, value, optional, owned, element_type, module);
}
if value.is_null() && !optional {
let default_val = match arg_type {
"string" => "String::new()".to_string(),
"int" | "integer" => "0".to_string(),
"float" | "number" => "0.0_f64".to_string(),
"bool" | "boolean" => "false".to_string(),
_ => "Default::default()".to_string(),
};
let expr = if arg_type == "string" {
format!("&{name}")
} else {
name.to_string()
};
return (vec![format!("let {name} = {default_val};")], expr);
}
if arg_type == "bytes" {
if let serde_json::Value::String(path_str) = value {
let binding = format!(
"let {name} = std::fs::read(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/../../test_documents/{path_str}\")).expect(\"test_documents/{path_str} must exist\");"
);
let call_expr = if owned { name.to_string() } else { format!("&{name}") };
return (vec![binding], call_expr);
}
if value.is_null() && optional {
return (
vec![format!("let {name}: Option<Vec<u8>> = None;")],
format!("{name}.as_deref().map(|v| v.as_slice())"),
);
}
}
if arg_type == "file_path" {
if let serde_json::Value::String(path_str) = value {
let binding = format!(
"let {name}: &str = concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/../../test_documents/\", \"{path_str}\");"
);
return (vec![binding], name.to_string());
}
}
let literal = json_to_rust_literal(value, arg_type);
let optional_expr = |n: &str| {
if arg_type == "string" {
format!("{n}.as_deref()")
} else if arg_type == "bytes" {
format!("{n}.as_deref().map(|v| v.as_slice())")
} else {
n.to_string()
}
};
let expr = |n: &str| {
if arg_type == "bytes" {
format!("{n}.as_bytes()")
} else if arg_type == "string" && owned {
format!("{n}.to_string()")
} else {
n.to_string()
}
};
if optional && value.is_null() {
let none_decl = match arg_type {
"string" => format!("let {name}: Option<String> = None;"),
"bytes" => format!("let {name}: Option<Vec<u8>> = None;"),
_ => format!("let {name} = None;"),
};
(vec![none_decl], optional_expr(name))
} else if optional {
(vec![format!("let {name} = Some({literal});")], optional_expr(name))
} else {
(vec![format!("let {name} = {literal};")], expr(name))
}
}
fn render_json_object_arg(
name: &str,
value: &serde_json::Value,
optional: bool,
owned: bool,
element_type: Option<&str>,
_module: &str,
) -> (Vec<String>, String) {
let pass_by_ref = !owned;
if value.is_null() && optional {
let expr = if pass_by_ref {
format!("&{name}")
} else {
name.to_string()
};
return (vec![format!("let {name} = Default::default();")], expr);
}
let normalized = super::super::normalize_json_keys_to_snake_case(value);
let json_literal = json_value_to_macro_literal(&normalized);
let mut lines = Vec::new();
lines.push(format!("let {name}_json = serde_json::json!({json_literal});"));
let deser_expr = if let Some(elem) = element_type {
format!("serde_json::from_value::<Vec<{elem}>>({name}_json).unwrap()")
} else {
format!("serde_json::from_value({name}_json).unwrap()")
};
lines.push(format!("let {name} = {deser_expr};"));
let expr = if pass_by_ref {
format!("&{name}")
} else {
name.to_string()
};
(lines, expr)
}
pub fn json_value_to_macro_literal(value: &serde_json::Value) -> String {
match value {
serde_json::Value::Null => "null".to_string(),
serde_json::Value::Bool(b) => format!("{b}"),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::String(s) => {
let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
}
serde_json::Value::Array(arr) => {
let items: Vec<String> = arr.iter().map(json_value_to_macro_literal).collect();
format!("[{}]", items.join(", "))
}
serde_json::Value::Object(obj) => {
let entries: Vec<String> = obj
.iter()
.map(|(k, v)| {
let escaped_key = k.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped_key}\": {}", json_value_to_macro_literal(v))
})
.collect();
format!("{{{}}}", entries.join(", "))
}
}
}
pub fn json_to_rust_literal(value: &serde_json::Value, arg_type: &str) -> String {
match value {
serde_json::Value::Null => "None".to_string(),
serde_json::Value::Bool(b) => format!("{b}"),
serde_json::Value::Number(n) => {
if arg_type.contains("float") || arg_type.contains("f64") || arg_type.contains("f32") {
if let Some(f) = n.as_f64() {
return format!("{f}_f64");
}
}
n.to_string()
}
serde_json::Value::String(s) => rust_raw_string(s),
serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
let json_str = serde_json::to_string(value).unwrap_or_default();
let literal = rust_raw_string(&json_str);
format!("serde_json::from_str({literal}).unwrap()")
}
}
}
pub fn resolve_visitor_trait(module: &str) -> String {
if module.contains("html_to_markdown") {
"HtmlVisitor".to_string()
} else {
"Visitor".to_string()
}
}
pub fn emit_rust_visitor_method(out: &mut String, method_name: &str, action: &crate::fixture::CallbackAction) {
use std::fmt::Write as FmtWrite;
let raw_params: &[(&str, &str)] = match method_name {
"visit_link" => &[
("ctx", "&NodeContext"),
("href", "&str"),
("text", "&str"),
("title", "Option<&str>"),
],
"visit_image" => &[
("ctx", "&NodeContext"),
("src", "&str"),
("alt", "&str"),
("title", "Option<&str>"),
],
"visit_heading" => &[
("ctx", "&NodeContext"),
("level", "u32"),
("text", "&str"),
("id", "Option<&str>"),
],
"visit_code_block" => &[("ctx", "&NodeContext"), ("lang", "Option<&str>"), ("code", "&str")],
"visit_code_inline"
| "visit_strong"
| "visit_emphasis"
| "visit_strikethrough"
| "visit_underline"
| "visit_subscript"
| "visit_superscript"
| "visit_mark"
| "visit_button"
| "visit_summary"
| "visit_figcaption"
| "visit_definition_term"
| "visit_definition_description" => &[("ctx", "&NodeContext"), ("text", "&str")],
"visit_text" => &[("ctx", "&NodeContext"), ("text", "&str")],
"visit_list_item" => &[
("ctx", "&NodeContext"),
("ordered", "bool"),
("marker", "&str"),
("text", "&str"),
],
"visit_blockquote" => &[("ctx", "&NodeContext"), ("content", "&str"), ("depth", "usize")],
"visit_table_row" => &[("ctx", "&NodeContext"), ("cells", "&[String]"), ("is_header", "bool")],
"visit_custom_element" => &[("ctx", "&NodeContext"), ("tag_name", "&str"), ("html", "&str")],
"visit_form" => &[
("ctx", "&NodeContext"),
("action", "Option<&str>"),
("method", "Option<&str>"),
],
"visit_input" => &[
("ctx", "&NodeContext"),
("input_type", "&str"),
("name", "Option<&str>"),
("value", "Option<&str>"),
],
"visit_audio" | "visit_video" | "visit_iframe" => &[("ctx", "&NodeContext"), ("src", "Option<&str>")],
"visit_details" => &[("ctx", "&NodeContext"), ("open", "bool")],
"visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
&[("ctx", "&NodeContext"), ("output", "&str")]
}
"visit_list_start" => &[("ctx", "&NodeContext"), ("ordered", "bool")],
"visit_list_end" => &[("ctx", "&NodeContext"), ("ordered", "bool"), ("output", "&str")],
_ => &[("ctx", "&NodeContext")],
};
let is_template = matches!(action, crate::fixture::CallbackAction::CustomTemplate { .. });
let template_vars: std::collections::HashSet<String> =
if let crate::fixture::CallbackAction::CustomTemplate { template } = action {
let mut vars = std::collections::HashSet::new();
let mut chars = template.chars().peekable();
while let Some(c) = chars.next() {
if c == '{' {
let mut var = String::new();
for inner in chars.by_ref() {
if inner == '}' {
break;
}
var.push(inner);
}
if !var.is_empty() {
vars.insert(var);
}
}
}
vars
} else {
std::collections::HashSet::new()
};
let params_str: String = raw_params
.iter()
.map(|(name, ty)| {
if is_template && template_vars.contains(*name) {
format!("{name}: {ty}")
} else {
format!("_{name}: {ty}")
}
})
.collect::<Vec<_>>()
.join(", ");
let _ = writeln!(
out,
" fn {method_name}(&mut self, {params_str}) -> VisitResult {{"
);
match action {
crate::fixture::CallbackAction::Skip => {
let _ = writeln!(out, " VisitResult::Skip");
}
crate::fixture::CallbackAction::Continue => {
let _ = writeln!(out, " VisitResult::Continue");
}
crate::fixture::CallbackAction::PreserveHtml => {
let _ = writeln!(out, " VisitResult::PreserveHtml");
}
crate::fixture::CallbackAction::Custom { output } => {
let escaped = crate::escape::escape_rust(output);
let _ = writeln!(out, " VisitResult::Custom(\"{escaped}\".to_string())");
}
crate::fixture::CallbackAction::CustomTemplate { template } => {
for (name, ty) in raw_params {
if template_vars.contains(*name) && ty.starts_with("Option<") {
let _ = writeln!(out, " let {name} = {name}.unwrap_or_default();");
}
}
let escaped = crate::escape::escape_rust(template);
let _ = writeln!(out, " VisitResult::Custom(format!(\"{escaped}\"))");
}
}
let _ = writeln!(out, " }}");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn json_value_to_macro_literal_null() {
let v = serde_json::Value::Null;
assert_eq!(json_value_to_macro_literal(&v), "null");
}
#[test]
fn json_value_to_macro_literal_string_escapes_quotes() {
let v = serde_json::Value::String("hello \"world\"".to_string());
let out = json_value_to_macro_literal(&v);
assert!(out.contains("\\\""));
}
#[test]
fn json_to_rust_literal_null_returns_none() {
let out = json_to_rust_literal(&serde_json::Value::Null, "string");
assert_eq!(out, "None");
}
#[test]
fn resolve_visitor_trait_html_to_markdown() {
assert_eq!(resolve_visitor_trait("html_to_markdown"), "HtmlVisitor");
assert_eq!(resolve_visitor_trait("other_module"), "Visitor");
}
}