#![cfg_attr(
not(test),
deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)
)]
use std::sync::Arc;
use async_trait::async_trait;
use base64::Engine;
use pmcp::types::{Content, ListResourcesResult, ReadResourceResult, ResourceInfo};
use pmcp::ResourceHandler;
use pmcp_workbook_runtime::render::render_xlsx;
use super::input::validate_input;
use super::render_uri::{self, WORKBOOK_XLSX_MIME};
use super::WorkbookBundle;
pub const RENDER_RESOURCE_LIST_URI: &str = render_uri::RENDER_URI_PREFIX;
pub struct RenderWorkbookResource {
bundle: Arc<WorkbookBundle>,
}
impl RenderWorkbookResource {
#[must_use]
pub fn new(bundle: Arc<WorkbookBundle>) -> Self {
Self { bundle }
}
fn list_entry(&self) -> ResourceInfo {
ResourceInfo::new(RENDER_RESOURCE_LIST_URI, "Rendered workbook (.xlsx)")
.with_description(
"Download the computed workbook as an .xlsx. Read a workbook://render/<...> \
URI minted by the render_workbook tool; the spreadsheet is regenerated \
statelessly from the URI on each read.",
)
.with_mime_type(WORKBOOK_XLSX_MIME)
}
fn regenerate(&self, uri: &str) -> Result<String, RegenError> {
let decoded = render_uri::decode(uri).map_err(|e| RegenError::BadUri(e.reason))?;
let lock = &self.bundle.stamp;
if decoded.provenance.bundle_id != lock.bundle_id
|| decoded.provenance.version != lock.version
|| decoded.provenance.combined_hash != lock.combined
{
return Err(RegenError::CrossProvenance);
}
let validated = validate_input(decoded.dto, &self.bundle.manifest, &self.bundle.cell_map)
.map_err(|e| RegenError::Invalid(e.reason))?;
let run = super::handler::run_bundle(&self.bundle, validated.seeds)
.map_err(|e| RegenError::Invalid(e.reason))?;
let bytes = render_xlsx(&self.bundle.layout, &run)
.map_err(|e| RegenError::Render(e.to_string()))?;
Ok(base64::engine::general_purpose::STANDARD.encode(bytes))
}
}
#[derive(Debug)]
enum RegenError {
BadUri(String),
CrossProvenance,
Invalid(String),
Render(String),
}
impl RegenError {
fn into_protocol(self) -> pmcp::Error {
match self {
RegenError::BadUri(r) => pmcp::Error::protocol(
pmcp::ErrorCode::INVALID_PARAMS,
format!("invalid workbook:// resource URI: {r}"),
),
RegenError::CrossProvenance => pmcp::Error::protocol(
pmcp::ErrorCode::INVALID_PARAMS,
"workbook:// URI provenance does not match the served bundle".to_string(),
),
RegenError::Invalid(r) => pmcp::Error::protocol(
pmcp::ErrorCode::INVALID_PARAMS,
format!("workbook:// URI inputs failed re-validation: {r}"),
),
RegenError::Render(r) => pmcp::Error::protocol(
pmcp::ErrorCode::INTERNAL_ERROR,
format!("workbook render failed: {r}"),
),
}
}
}
#[async_trait]
impl ResourceHandler for RenderWorkbookResource {
async fn list(
&self,
_cursor: Option<String>,
_extra: pmcp::RequestHandlerExtra,
) -> pmcp::Result<ListResourcesResult> {
Ok(ListResourcesResult::new(vec![self.list_entry()]))
}
async fn read(
&self,
uri: &str,
_extra: pmcp::RequestHandlerExtra,
) -> pmcp::Result<ReadResourceResult> {
let b64 = self.regenerate(uri).map_err(RegenError::into_protocol)?;
Ok(ReadResourceResult::new(vec![Content::resource_with_text(
uri.to_string(),
b64,
WORKBOOK_XLSX_MIME,
)]))
}
}
#[cfg(test)]
mod tests {
use super::super::ProvStamp;
use super::*;
use std::path::{Path, PathBuf};
use pmcp_workbook_runtime::{load_bundle, LocalDirSource};
use serde_json::json;
fn golden_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/tax-calc@1.1.0")
}
fn golden_bundle() -> Arc<WorkbookBundle> {
let source = LocalDirSource::new(golden_dir());
Arc::new(load_bundle(&source).expect("golden bundle boots"))
}
fn valid_uri(bundle: &Arc<WorkbookBundle>, inputs: serde_json::Value) -> String {
let validated = validate_input(inputs, &bundle.manifest, &bundle.cell_map)
.expect("inputs validate for fixture");
render_uri::encode(&validated.canonical_dto, &ProvStamp::from_bundle(bundle))
.expect("encode fixture uri")
}
#[test]
fn read_returns_base64_xlsx_and_is_byte_identical_across_reads() {
let bundle = golden_bundle();
let res = RenderWorkbookResource::new(bundle.clone());
let uri = valid_uri(
&bundle,
json!({ "inputs": { "gross_income": 60000.0, "filing_status": "single" } }),
);
let first = res.regenerate(&uri).expect("first read renders");
let second = res.regenerate(&uri).expect("second read renders");
let bytes = base64::engine::general_purpose::STANDARD
.decode(&first)
.expect("valid base64 xlsx");
assert_eq!(
&bytes[..2],
b"PK",
"rendered payload is an xlsx (ZIP) container"
);
assert_eq!(first, second, "regen-on-read is byte-identical (stateless)");
}
#[test]
fn cross_provenance_uri_errors_before_rendering() {
let bundle = golden_bundle();
let res = RenderWorkbookResource::new(bundle.clone());
let forged = ProvStamp {
bundle_id: "tax-calc".to_string(),
version: "1.1.0".to_string(),
combined_hash: "f".repeat(64), };
let dto = json!({ "inputs": { "gross_income": 60000.0, "filing_status": "single" }, "overrides": {} });
let uri = render_uri::encode(&dto, &forged).expect("encode forged uri");
let err = res.regenerate(&uri).expect_err("cross-provenance rejected");
assert!(
matches!(err, RegenError::CrossProvenance),
"rejected as cross-provenance BEFORE rendering, got {err:?}"
);
}
#[test]
fn out_of_range_decoded_input_errors_via_revalidation_not_render() {
let bundle = golden_bundle();
let res = RenderWorkbookResource::new(bundle.clone());
let dto = json!({ "inputs": { "filing_status": "alien" }, "overrides": {} });
let uri = render_uri::encode(&dto, &ProvStamp::from_bundle(&bundle))
.expect("encode out-of-range uri");
let err = res.regenerate(&uri).expect_err("out-of-range rejected");
assert!(
matches!(err, RegenError::Invalid(_)),
"rejected by re-validation (injection guard), not rendered: {err:?}"
);
}
#[test]
fn oversized_uri_errors_as_bad_uri() {
let bundle = golden_bundle();
let res = RenderWorkbookResource::new(bundle);
let oversized = format!(
"{}{}",
render_uri::RENDER_URI_PREFIX,
"A".repeat(render_uri::MAX_ENCODED_URI_LEN + 1)
);
let err = res.regenerate(&oversized).expect_err("oversized rejected");
assert!(matches!(err, RegenError::BadUri(_)), "size-guard rejection");
}
#[tokio::test]
async fn list_returns_the_single_workbook_resource_entry() {
let res = RenderWorkbookResource::new(golden_bundle());
let extra = pmcp::RequestHandlerExtra::default();
let listed = res.list(None, extra).await.expect("list");
assert_eq!(listed.resources.len(), 1, "exactly one resource (A3)");
assert_eq!(listed.resources[0].uri, RENDER_RESOURCE_LIST_URI);
assert_eq!(
listed.resources[0].mime_type.as_deref(),
Some(WORKBOOK_XLSX_MIME)
);
}
#[tokio::test]
async fn read_via_trait_returns_resource_content_with_xlsx_mime() {
let bundle = golden_bundle();
let res = RenderWorkbookResource::new(bundle.clone());
let uri = valid_uri(
&bundle,
json!({ "inputs": { "gross_income": 60000.0, "filing_status": "single" } }),
);
let extra = pmcp::RequestHandlerExtra::default();
let result = res.read(&uri, extra).await.expect("read renders");
assert_eq!(result.contents.len(), 1);
}
}