use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use uuid::Uuid;
use crate::pipeline::PipelineError;
use crate::pipeline::model::{DslFlag, DslFlagKind, NodeFieldDef, NodeFieldType, SelectOptionDef};
use crate::pipeline::nodes::shared::file_ref::{BACKEND_ZEBFS, FILE_REF_TYPE, LIFECYCLE_DURABLE};
use crate::pipeline::nodes::shared::util::filename_stem;
use crate::platform::services::PlatformService;
pub mod code128;
pub mod qr;
pub mod render;
pub struct Output<'a> {
pub owner: &'a str,
pub project: &'a str,
pub folder: &'a str,
pub filename: Option<&'a str>,
pub format: &'a str,
pub bytes: Vec<u8>,
pub origin: &'a str,
}
pub fn save(platform: &PlatformService, out: Output<'_>) -> Result<Value, PipelineError> {
let err = |m: String| PipelineError::new("FS_BARCODE", m);
let layout = platform.file.ensure_project_layout(out.owner, out.project).map_err(|e| err(e.to_string()))?;
let folder = sanitize_folder(out.folder);
let stem = out.filename.map(filename_stem).filter(|s| !s.is_empty()).unwrap_or_else(|| Uuid::new_v4().to_string());
let name = format!("{stem}.{}", out.format);
let rel = if folder.is_empty() { name.clone() } else { format!("{folder}/{name}") };
let dest = layout.local_files_dir()?.join(&rel);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent).map_err(|e| err(format!("mkdir: {e}")))?;
}
let tmp = dest.with_extension(format!("{}.tmp", out.format));
std::fs::write(&tmp, &out.bytes).map_err(|e| err(format!("write: {e}")))?;
std::fs::rename(&tmp, &dest).map_err(|e| err(format!("rename: {e}")))?;
Ok(json!({
"__zf_type": FILE_REF_TYPE,
"backend": BACKEND_ZEBFS,
"ref": rel,
"filename": name,
"mime": if out.format == "svg" { "image/svg+xml" } else { "image/png" },
"kind": "image",
"size": out.bytes.len(),
"sha256": format!("sha256:{:x}", Sha256::digest(&out.bytes)),
"lifecycle": LIFECYCLE_DURABLE,
"origin": out.origin,
"trust": "generated",
}))
}
fn sanitize_folder(folder: &str) -> String {
folder
.split('/')
.filter(|seg| !seg.is_empty() && *seg != "." && *seg != "..")
.collect::<Vec<_>>()
.join("/")
}
pub(super) fn flag(flag: &str, key: &str, description: &str, kind: DslFlagKind, required: bool) -> DslFlag {
DslFlag { flag: flag.into(), config_key: key.into(), description: description.into(), kind, required }
}
pub(super) fn field(name: &str, label: &str, help: &str, default: Option<Value>) -> NodeFieldDef {
NodeFieldDef {
name: name.into(),
label: label.into(),
field_type: NodeFieldType::Text,
help: Some(help.into()),
default_value: default,
..Default::default()
}
}
pub(super) fn select(name: &str, label: &str, help: &str, default: &str, options: &[(&str, &str)]) -> NodeFieldDef {
NodeFieldDef {
field_type: NodeFieldType::Select,
options: options.iter().map(|(v, l)| SelectOptionDef { value: (*v).into(), label: (*l).into() }).collect(),
..field(name, label, help, Some(json!(default)))
}
}