use std::fs;
use std::io::{self, Read};
use std::path::Path;
use crate::cli::LibraryArgs;
use crate::lang::ast::{BodyItem, DocItem};
use crate::transform::{self, FieldMapping, MapConfig, WhereClause};
pub fn run(
name: &str,
file: &Path,
input: Option<&Path>,
output: Option<&Path>,
_params: &[String],
lib_args: &LibraryArgs,
) -> Result<(), String> {
let source =
fs::read_to_string(file).map_err(|e| format!("cannot read {}: {}", file.display(), e))?;
let mut options = crate::ParseOptions {
root_dir: file.parent().unwrap_or(Path::new(".")).to_path_buf(),
..Default::default()
};
lib_args.apply(&mut options);
let doc = crate::parse(&source, options);
let real_errors: Vec<_> = doc
.errors()
.into_iter()
.filter(|d| !matches!(d.code.as_deref(), Some("E040") | Some("E060")))
.collect();
if !real_errors.is_empty() {
for diag in &real_errors {
eprintln!("{}", super::format_diagnostic(diag, &doc.source_map, file));
}
return Err("document has errors".to_string());
}
let transform_block = doc
.ast
.items
.iter()
.find_map(|item| {
if let DocItem::Body(BodyItem::Block(block)) = item {
if block.kind.name == "transform" {
if let Some(ref id) = block.inline_id {
let id_str = match id {
crate::lang::ast::InlineId::Literal(lit) => lit.value.clone(),
crate::lang::ast::InlineId::Interpolated(_) => return None,
};
if id_str == name {
return Some(block);
}
}
}
}
None
})
.ok_or_else(|| format!("transform '{}' not found in {}", name, file.display()))?;
let (input_codec, output_codec) = extract_codec_names_from_ast(transform_block)?;
let map_config = build_map_config(transform_block)?;
let input_data: Box<dyn Read> = match input {
Some(path) => Box::new(
fs::File::open(path)
.map_err(|e| format!("cannot open input {}: {}", path.display(), e))?,
),
None => Box::new(io::stdin()),
};
let mut input_bytes = Vec::new();
let mut reader: Box<dyn Read> = input_data;
reader
.read_to_end(&mut input_bytes)
.map_err(|e| format!("cannot read input: {}", e))?;
let mut output_buf = Vec::new();
let mut input_options: indexmap::IndexMap<String, crate::eval::value::Value> =
indexmap::IndexMap::new();
let mut output_options: indexmap::IndexMap<String, crate::eval::value::Value> =
indexmap::IndexMap::new();
for item in &transform_block.body {
if let crate::lang::ast::BodyItem::Attribute(attr) = item {
let value_str = extract_string_value(&attr.value);
match attr.name.name.as_str() {
"input_layout" => {
if let Some(s) = value_str {
input_options
.insert("layout".to_string(), crate::eval::value::Value::String(s));
}
}
"output_layout" => {
if let Some(s) = value_str {
output_options
.insert("layout".to_string(), crate::eval::value::Value::String(s));
}
}
"separator" => {
if let Some(s) = value_str {
input_options.insert(
"separator".to_string(),
crate::eval::value::Value::String(s.clone()),
);
output_options.insert(
"separator".to_string(),
crate::eval::value::Value::String(s),
);
}
}
"has_header" => {
if let crate::lang::ast::Expr::BoolLit(b, _) = &attr.value {
input_options.insert(
"has_header".to_string(),
crate::eval::value::Value::Bool(*b),
);
}
}
"header" => {
if let crate::lang::ast::Expr::BoolLit(b, _) = &attr.value {
output_options
.insert("header".to_string(), crate::eval::value::Value::Bool(*b));
}
}
_ => {}
}
}
}
let ctx = transform::TransformContext {
struct_registry: &doc.struct_registry,
layout_registry: &doc.layout_registry,
};
let stats = transform::execute(
&input_codec,
input_bytes.as_slice(),
&output_codec,
&mut output_buf,
&map_config,
&input_options,
&output_options,
Some(&ctx),
)
.map_err(|e| format!("transform error: {}", e))?;
match output {
Some(path) => {
fs::write(path, &output_buf)
.map_err(|e| format!("cannot write {}: {}", path.display(), e))?;
}
None => {
io::Write::write_all(&mut io::stdout(), &output_buf)
.map_err(|e| format!("cannot write output: {}", e))?;
}
}
eprintln!(
"transform '{}': {} records read, {} written, {} filtered",
name, stats.records_read, stats.records_written, stats.records_filtered
);
Ok(())
}
fn extract_codec_names_from_ast(
block: &crate::lang::ast::Block,
) -> Result<(String, String), String> {
let mut input_codec = "json".to_string();
let mut output_codec = "json".to_string();
for item in &block.body {
if let BodyItem::Attribute(attr) = item {
let value_str = extract_string_value(&attr.value);
if let Some(ref s) = value_str {
let codec_name = s.strip_prefix("codec::").unwrap_or(s).to_string();
match attr.name.name.as_str() {
"input" => input_codec = codec_name,
"output" => output_codec = codec_name,
_ => {}
}
}
}
}
Ok((input_codec, output_codec))
}
fn extract_string_value(expr: &crate::lang::ast::Expr) -> Option<String> {
if let crate::lang::ast::Expr::StringLit(s) = expr {
let mut result = String::new();
for part in &s.parts {
if let crate::lang::ast::StringPart::Literal(lit) = part {
result.push_str(lit);
} else {
return None; }
}
Some(result)
} else {
None
}
}
fn build_map_config(block: &crate::lang::ast::Block) -> Result<MapConfig, String> {
let mut mappings = Vec::new();
let mut where_clauses = Vec::new();
for item in &block.body {
match item {
BodyItem::Block(sub_block) if sub_block.kind.name == "map" => {
for sub_item in &sub_block.body {
if let BodyItem::Attribute(attr) = sub_item {
mappings.push(FieldMapping {
output_name: attr.name.name.clone(),
expr: attr.value.clone(),
});
}
}
for dec in &sub_block.decorators {
if dec.name.name == "where" {
if let Some(crate::lang::ast::DecoratorArg::Positional(expr)) =
dec.args.first()
{
where_clauses.push(WhereClause { expr: expr.clone() });
}
}
}
}
BodyItem::Attribute(attr)
if !matches!(
attr.name.name.as_str(),
"input"
| "output"
| "auto_map"
| "input_layout"
| "output_layout"
| "separator"
| "has_header"
| "header"
| "on_parse_error"
| "on_output_error"
) =>
{
mappings.push(FieldMapping {
output_name: attr.name.name.clone(),
expr: attr.value.clone(),
});
}
_ => {}
}
}
Ok(MapConfig {
mappings,
where_clauses,
})
}