use std::collections::BTreeMap;
use serde::Serialize;
use hwpforge::ops;
use hwpforge_smithy_hwpx::FilledField;
use crate::compat::{self, Tool};
use crate::output::{read_file_bytes, write_output_file, ToolErrorInfo, ToolWarningInfo};
#[derive(Debug, Serialize)]
pub struct FillData {
pub output_path: String,
pub filled: Vec<FilledField>,
pub size_bytes: u64,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<ToolWarningInfo>,
}
pub fn run_fill(
file_path: &str,
values: &BTreeMap<String, String>,
output_path: &str,
) -> Result<FillData, ToolErrorInfo> {
if !output_path.ends_with(".hwpx") {
return Err(ToolErrorInfo::new(
"INVALID_EXTENSION",
format!("Output path must end with .hwpx: {output_path}"),
"Use a .hwpx extension for the output file.",
));
}
if values.is_empty() {
return Err(ToolErrorInfo::new(
"NO_VALUES",
"values map is empty",
"Pass at least one name→value pair. Use hwpforge_fields to discover names.",
));
}
let bytes = read_file_bytes(file_path)?;
let pairs: Vec<(String, String)> =
values.iter().map(|(name, value)| (name.clone(), value.clone())).collect();
let outcome = ops::fill(&bytes, &pairs, &ops::FillOptions::default())
.map_err(|e| compat::tool_error(Tool::Fill, e))?;
write_output_file(output_path, &outcome.bytes)?;
let size_bytes = outcome.bytes.len() as u64;
let warnings: Vec<ToolWarningInfo> = outcome.warnings.iter().map(compat::warning).collect();
Ok(FillData {
output_path: output_path.to_string(),
filled: outcome.filled,
size_bytes,
warnings,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture(rel: &str) -> String {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/fixtures")
.join(rel)
.to_str()
.unwrap()
.to_string()
}
#[test]
fn fill_via_mcp_surface_has_no_warnings_on_a_clean_document() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("probe.hwpx");
crate::tools::convert::run_convert("성명: ( )", false, path.to_str().unwrap(), "default")
.unwrap();
let values = std::collections::BTreeMap::from([("없는이름".to_string(), "x".to_string())]);
let out = dir.path().join("out.hwpx");
let err = run_fill(path.to_str().unwrap(), &values, out.to_str().unwrap()).unwrap_err();
assert_eq!(err.code, "FIELD_NOT_FOUND");
}
#[test]
fn fill_via_mcp_surface_reports_no_warnings_on_a_successful_clean_fill() {
let path = fixture("fields/clickhere_named.hwpx");
let dir = tempfile::tempdir().unwrap();
let values =
std::collections::BTreeMap::from([("user_email".to_string(), "a@b.c".to_string())]);
let out = dir.path().join("out.hwpx");
let data = run_fill(&path, &values, out.to_str().unwrap()).unwrap();
assert_eq!(
data.filled.len(),
1,
"the one existing field must be filled: {:?}",
data.filled
);
assert!(
data.warnings.is_empty(),
"a clean successful fill must not warn: {:?}",
data.warnings
);
let value = serde_json::to_value(&data).unwrap();
assert!(
value.get("warnings").is_none(),
"empty warnings must be omitted from the wire shape, not an empty array: {value}"
);
}
#[test]
fn fill_surfaces_decode_warnings() {
let path = fixture("layout/stale-line-cache.hwpx");
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.hwpx");
let values =
std::collections::BTreeMap::from([("user_email".to_string(), "a@b.c".to_string())]);
let data = run_fill(&path, &values, out.to_str().unwrap()).unwrap();
assert!(
data.warnings.iter().any(|w| w.code == "LAYOUT_CACHE_DROPPED"),
"fill must surface the decode warning: {:?}",
data.warnings
);
let value = serde_json::to_value(&data).unwrap();
assert_eq!(value["warnings"][0]["code"], "LAYOUT_CACHE_DROPPED");
assert!(!value["warnings"][0]["message"].as_str().unwrap_or_default().is_empty());
}
}