use regex::Regex;
use serde_json::Value;
use super::data::NdtData;
use super::resolver;
use crate::ndf::{
audit::{Actor, AuditEvent, EventType, NdfAudit},
integrity::{canonical_hash, NdfIntegrity},
NdfDocument, NdfMeta, NdfOrigin, NDF_VERSION,
};
use crate::{NormaxisPdfError, Result};
#[derive(Debug, Clone)]
pub struct CompileOptions {
pub document_id: Option<String>,
pub generated_by: Actor,
pub ndt_template_id: Option<String>,
pub ndt_template_hash: Option<String>,
pub validate_resolved: bool,
}
impl Default for CompileOptions {
fn default() -> Self {
Self {
document_id: None,
generated_by: Actor::System {
id: "normordis-pdf".into(),
version: Some(env!("CARGO_PKG_VERSION").into()),
instance_id: None,
},
ndt_template_id: None,
ndt_template_hash: None,
validate_resolved: true,
}
}
}
pub fn compile_ndt(ndt: &str, data: &NdtData, options: CompileOptions) -> Result<NdfDocument> {
let doc = super::parse_ndt(ndt)
.map_err(|e| NormaxisPdfError::NdfCompileError(e.to_string()))?;
if let Some(ref placeholders) = doc.placeholders {
super::validator::validate(placeholders, data)
.map_err(|e| NormaxisPdfError::NdfCompileError(e.to_string()))?;
}
let body_val = serde_json::to_value(&doc.body)
.map_err(|e| NormaxisPdfError::SerdeError(e.to_string()))?;
let resolved_content = resolve_value_placeholders(body_val, data);
let styles_val = serde_json::to_value(&doc.style)
.map_err(|e| NormaxisPdfError::SerdeError(e.to_string()))?;
if options.validate_resolved {
let content_str = serde_json::to_string(&resolved_content)
.map_err(|e| NormaxisPdfError::SerdeError(e.to_string()))?;
let re = Regex::new(r"\{\{[a-zA-Z0-9_.]+\}\}").expect("static regex");
if let Some(m) = re.find(&content_str) {
return Err(NormaxisPdfError::NdfCompileError(format!(
"unresolved placeholder '{}' in content after substitution",
m.as_str()
)));
}
}
let now = chrono::Utc::now().to_rfc3339();
let meta_title = doc
.meta
.as_ref()
.and_then(|m| m.title.clone())
.unwrap_or_default();
let meta_compat = doc.meta.as_ref().and_then(|m| m.compat_mode);
let meta = NdfMeta {
title: meta_title,
entity: String::new(),
entity_id: None,
lang: "pt-PT".into(),
document_ref: None,
document_type: None,
classification: "public".into(),
subject: None,
keywords: None,
created_at: now.clone(),
valid_from: None,
valid_until: None,
supersedes: None,
compat_mode: meta_compat,
numbering: None,
};
let meta_val = serde_json::to_value(&meta)
.map_err(|e| NormaxisPdfError::SerdeError(e.to_string()))?;
let integrity = NdfIntegrity::compute(&resolved_content, &styles_val, &meta_val)?;
let document_id = options
.document_id
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
let ndt_template_hash = options.ndt_template_hash.unwrap_or_else(|| {
let v = serde_json::from_str::<Value>(ndt).unwrap_or(Value::Null);
canonical_hash(&v)
});
let first_event = AuditEvent {
seq: 1,
event_type: EventType::DocumentGenerated,
timestamp: now.clone(),
actor: options.generated_by.clone(),
content_hash: Some(integrity.content_hash.clone()),
note: None,
extra: Default::default(),
};
Ok(NdfDocument {
ndf: NDF_VERSION.into(),
origin: NdfOrigin {
ndt_template_id: options.ndt_template_id,
ndt_version: None,
ndt_template_hash: Some(ndt_template_hash),
ndt_data_hash: None,
engine_version: env!("CARGO_PKG_VERSION").into(),
engine_backend: "normordis-pdf".into(),
generated_at: now,
generated_by: options.generated_by,
},
revision: None,
meta,
output: serde_json::to_value(&doc.style).ok(),
styles: styles_val,
content: resolved_content,
integrity,
audit: NdfAudit {
document_id,
events: vec![first_event],
},
outputs: vec![],
signatures: vec![],
})
}
pub fn parse_ndf(json: &str) -> Result<NdfDocument> {
serde_json::from_str(json).map_err(|e| NormaxisPdfError::SerdeError(e.to_string()))
}
pub fn verify_ndf(json: &str) -> Result<crate::ndf::integrity::IntegrityReport> {
let ndf = parse_ndf(json)?;
ndf.verify_integrity()
}
pub fn render_ndf(ndf_json: &str) -> Result<Vec<u8>> {
let ndf = parse_ndf(ndf_json)?;
let body: Vec<super::model::BodyElement> =
serde_json::from_value(ndf.content.clone())
.map_err(|e| NormaxisPdfError::SerdeError(e.to_string()))?;
let ndt_doc = super::model::NdtDocument {
ndt: "1.1.0".into(),
id: None,
meta: Some(super::model::NdtMeta {
title: Some(ndf.meta.title.clone()),
compat_mode: ndf.meta.compat_mode,
..Default::default()
}),
style: serde_json::from_value(ndf.styles.clone()).ok(),
fonts: None,
page: None,
output: None,
signature: None,
placeholders: None,
zones: None,
body,
};
let empty_data = NdtData {
ndt_data: "1.0.0".into(),
template_id: None,
template_version: None,
data: Default::default(),
};
let style = crate::styles::DocumentStyle::default();
let elements = super::renderer::render_template(&ndt_doc, &empty_data, &style)
.map_err(|e| NormaxisPdfError::Template(e.to_string()))?;
crate::document::Document {
title: ndf.meta.title,
style,
fonts: crate::fonts::FontRegistry::default(),
header: None,
sectioned_header: None,
footer: None,
sectioned_footer: None,
watermark: None,
elements,
footnotes: vec![],
toc_entries: None,
compression: crate::document::CompressionLevel::Default,
standard: crate::document::PdfStandard::Pdf17,
signature: None,
traceability: None,
accessibility: crate::compliance::ua::AccessibilityConfig::default(),
}
.render_to_bytes()
}
pub fn render_ndf_prepared_for_signing(
ndf_json: &str,
opts: crate::signing::SignatureOptions,
) -> Result<crate::signing::PreparedPdf> {
let ndf = parse_ndf(ndf_json)?;
let body: Vec<super::model::BodyElement> =
serde_json::from_value(ndf.content.clone())
.map_err(|e| NormaxisPdfError::SerdeError(e.to_string()))?;
let ndt_doc = super::model::NdtDocument {
ndt: "1.1.0".into(),
id: None,
meta: Some(super::model::NdtMeta {
title: Some(ndf.meta.title.clone()),
compat_mode: ndf.meta.compat_mode,
..Default::default()
}),
style: serde_json::from_value(ndf.styles.clone()).ok(),
fonts: None,
page: None,
output: None,
signature: None,
placeholders: None,
zones: None,
body,
};
let empty_data = NdtData {
ndt_data: "1.0.0".into(),
template_id: None,
template_version: None,
data: Default::default(),
};
let style = crate::styles::DocumentStyle::default();
let elements = super::renderer::render_template(&ndt_doc, &empty_data, &style)
.map_err(|e| NormaxisPdfError::Template(e.to_string()))?;
crate::document::Document {
title: ndf.meta.title,
style,
fonts: crate::fonts::FontRegistry::default(),
header: None,
sectioned_header: None,
footer: None,
sectioned_footer: None,
watermark: None,
elements,
footnotes: vec![],
toc_entries: None,
compression: crate::document::CompressionLevel::Default,
standard: crate::document::PdfStandard::Pdf17,
signature: None,
traceability: None,
accessibility: crate::compliance::ua::AccessibilityConfig::default(),
}
.render_prepared_for_signing(opts)
}
fn resolve_value_placeholders(value: Value, data: &NdtData) -> Value {
match value {
Value::String(s) => Value::String(resolver::resolve_string(&s, data)),
Value::Array(arr) => {
Value::Array(arr.into_iter().map(|v| resolve_value_placeholders(v, data)).collect())
}
Value::Object(map) => {
let mut new_map = serde_json::Map::new();
for (k, v) in map {
new_map.insert(k, resolve_value_placeholders(v, data));
}
Value::Object(new_map)
}
other => other,
}
}