use crate::core::config::ResolvedCrateConfig;
use crate::e2e::escape::escape_r;
use crate::e2e::fixture::Fixture;
use super::values::{json_to_r, json_to_r_preserve_arrays};
pub(super) fn strip_options_arg(args_str: &str) -> String {
let mut parts: Vec<String> = Vec::new();
let mut current = String::new();
let mut paren_depth: i32 = 0;
let mut in_single = false;
let mut in_double = false;
for c in args_str.chars() {
if !in_single && !in_double {
match c {
'(' | '[' | '{' => paren_depth += 1,
')' | ']' | '}' => paren_depth -= 1,
'\'' => in_single = true,
'"' => in_double = true,
',' if paren_depth == 0 => {
parts.push(current.trim().to_string());
current.clear();
continue;
}
_ => {}
}
} else if in_single && c == '\'' {
in_single = false;
} else if in_double && c == '"' {
in_double = false;
}
current.push(c);
}
if !current.trim().is_empty() {
parts.push(current.trim().to_string());
}
parts
.into_iter()
.filter(|p| !p.starts_with("options ") && !p.starts_with("options="))
.collect::<Vec<_>>()
.join(", ")
}
pub(super) struct RArgsContext<'a> {
pub(super) arg_name_map: Option<&'a std::collections::HashMap<String, String>>,
pub(super) options_type: Option<&'a str>,
pub(super) fixture: &'a Fixture,
pub(super) config: &'a ResolvedCrateConfig,
pub(super) type_defs: &'a [crate::core::ir::TypeDef],
pub(super) setup_lines: &'a mut Vec<String>,
pub(super) teardown_block: &'a mut String,
}
pub(super) fn build_args_string(
input: &serde_json::Value,
args: &[crate::e2e::config::ArgMapping],
context: RArgsContext<'_>,
) -> String {
let RArgsContext {
arg_name_map,
options_type,
fixture,
config,
type_defs,
setup_lines,
teardown_block,
} = context;
if args.is_empty() {
return String::new();
}
let parts: Vec<String> = args
.iter()
.filter_map(|arg| {
let arg_name: &str = arg_name_map
.and_then(|m| m.get(&arg.name).map(String::as_str))
.unwrap_or(&arg.name);
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let val = input.get(field);
let val = match val {
Some(v) if !(v.is_null() && arg.optional) => v,
_ => {
if !arg.optional {
return None;
}
if arg.arg_type == "json_object" {
let r_value = r_default_for_config_arg(arg_name, options_type);
return Some(format!("{arg_name} = {r_value}"));
}
return Some(format!("{arg_name} = NULL"));
}
};
if arg.arg_type == "json_object" && (val.is_null() || val.as_object().is_some_and(|m| m.is_empty())) {
let r_value = r_default_for_config_arg(arg_name, options_type);
return Some(format!("{arg_name} = {r_value}"));
}
if arg.arg_type == "json_object" && val.is_object() {
if let Some(type_name) = options_type {
let r_list = json_to_r_preserve_arrays(val, true);
let r_value = format!("{type_name}$from_json(jsonlite::toJSON({r_list}, auto_unbox = TRUE))");
return Some(format!("{arg_name} = {r_value}"));
}
let r_value = json_to_r(val, true);
return Some(format!("{arg_name} = {r_value}"));
}
if arg.arg_type == "json_object" && val.is_array() {
if arg.element_type.as_deref() == Some("String") {
let r_value = if val.as_array().is_some_and(|arr| arr.is_empty()) {
"character(0)".to_string()
} else {
json_to_r(val, false)
};
return Some(format!("{arg_name} = {r_value}"));
}
let json_literal = serde_json::to_string(val).unwrap_or_else(|_| "[]".to_string());
let escaped = escape_r(&json_literal);
return Some(format!("{arg_name} = \"{escaped}\""));
}
if arg.arg_type == "bytes" {
if let Some(raw) = val.as_str() {
let r_value = render_bytes_value(raw);
return Some(format!("{arg_name} = {r_value}"));
}
}
if arg.arg_type == "file_path" {
if let Some(raw) = val.as_str() {
if !raw.starts_with('/') && !raw.is_empty() {
let escaped = escape_r(raw);
return Some(format!("{arg_name} = .resolve_fixture(\"{escaped}\")"));
}
}
}
if arg.arg_type == "test_backend" {
if let Some(trait_name) = &arg.trait_name {
if let Some(trait_bridge) = config.trait_bridges.iter().find(|tb| tb.trait_name == *trait_name) {
let methods: Vec<&crate::core::ir::MethodDef> = type_defs
.iter()
.find(|t| t.name == *trait_name)
.map(|t| t.methods.iter().collect())
.unwrap_or_default();
let emission = crate::e2e::codegen::emit_test_backend("r", trait_bridge, &methods, fixture);
if !emission.setup_block.is_empty() {
setup_lines.push(emission.setup_block.trim_end().to_string());
}
teardown_block.push_str(&emission.teardown_block);
return Some(format!("{arg_name} = {}", emission.arg_expr));
}
}
let emission = crate::e2e::codegen::TestBackendEmission::unimplemented("r");
return Some(format!("{arg_name} = NULL # {}", emission.arg_expr));
}
Some(format!("{arg_name} = {}", json_to_r(val, true)))
})
.collect();
parts.join(", ")
}
fn render_bytes_value(raw: &str) -> String {
if raw.starts_with('<') || raw.starts_with('{') || raw.starts_with('[') || raw.contains(' ') {
let escaped = escape_r(raw);
return format!("charToRaw(\"{escaped}\")");
}
let first = raw.chars().next().unwrap_or('\0');
if first.is_ascii_alphanumeric() || first == '_' {
if let Some(slash) = raw.find('/') {
if slash > 0 {
let after = &raw[slash + 1..];
if after.contains('.') && !after.is_empty() {
let escaped = escape_r(raw);
return format!(
"readBin(.resolve_fixture(\"{escaped}\"), what = \"raw\", n = file.info(.resolve_fixture(\"{escaped}\"))$size)"
);
}
}
}
}
let escaped = escape_r(raw);
format!("charToRaw(\"{escaped}\")")
}
fn r_default_for_config_arg(arg_name: &str, options_type: Option<&str>) -> String {
if let Some(type_name) = options_type {
return format!("{type_name}$default()");
}
let _ = arg_name;
"NULL".to_string()
}
#[cfg(test)]
mod tests {
use super::{RArgsContext, build_args_string, strip_options_arg};
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::ArgMapping;
use crate::e2e::fixture::Fixture;
use serde_json::json;
#[test]
fn strip_options_arg_preserves_nested_commas() {
let args = "source = \"a,b\", options = list(visitor = visitor, flags = c(\"x,y\")), limit = 2";
assert_eq!(strip_options_arg(args), "source = \"a,b\", limit = 2");
}
#[test]
fn build_args_string_wraps_typed_json_object_with_preserved_arrays() {
let input = json!({
"options": {
"formats": ["Pdf"],
"enabled": true
}
});
let args = vec![ArgMapping {
name: "options".to_string(),
field: "input.options".to_string(),
arg_type: "json_object".to_string(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}];
let fixture = Fixture {
id: "typed_options".to_string(),
..Fixture::default()
};
let config = ResolvedCrateConfig::default();
let mut setup_lines = Vec::new();
let mut teardown_block = String::new();
let rendered = build_args_string(
&input,
&args,
RArgsContext {
arg_name_map: None,
options_type: Some("ExtractOptions"),
fixture: &fixture,
config: &config,
type_defs: &[],
setup_lines: &mut setup_lines,
teardown_block: &mut teardown_block,
},
);
assert!(rendered.starts_with("options = ExtractOptions$from_json(jsonlite::toJSON(list("));
assert!(
rendered.ends_with("), auto_unbox = TRUE))"),
"typed options arg should round-trip through jsonlite::toJSON, got: {rendered}"
);
assert!(
rendered.contains("\"formats\" = I(c(\"pdf\"))"),
"array fields must be preserved with I(c(...)), got: {rendered}"
);
assert!(
rendered.contains("\"enabled\" = TRUE"),
"scalar fields should remain ordinary R literals, got: {rendered}"
);
assert!(setup_lines.is_empty());
assert!(teardown_block.is_empty());
}
}