use std::fs;
use std::path::Path;
use crate::error::SsgError;
use crate::plugin::{Plugin, PluginContext};
pub const RPC_DTS_RELATIVE_PATH: &str = ".ssg/rpc.d.ts";
#[derive(Debug, Clone, Copy, Default)]
pub struct RpcSchemaPlugin;
impl RpcSchemaPlugin {
#[must_use]
pub const fn new() -> Self {
Self
}
}
impl Plugin for RpcSchemaPlugin {
fn name(&self) -> &'static str {
"rpc-schema"
}
fn after_compile(&self, ctx: &PluginContext) -> Result<(), SsgError> {
if ctx.dry_run {
return Ok(());
}
if ssg_rpc::dispatch::iter_descriptors().next().is_none() {
return Ok(());
}
let opts = ssg_rpc::EmitOptions::default();
let ts = ssg_rpc::emit_typescript(&opts);
let out_path = ctx.site_dir.join(RPC_DTS_RELATIVE_PATH);
ensure_parent(&out_path)?;
fs::write(&out_path, ts).map_err(|e| SsgError::Io {
path: out_path.clone(),
source: e,
})?;
Ok(())
}
}
fn ensure_parent(path: &Path) -> Result<(), SsgError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| SsgError::Io {
path: parent.to_path_buf(),
source: e,
})?;
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::plugin::PluginContext;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ssg_rpc::{ssg_rpc, RpcError};
use tempfile::tempdir;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct CovInput {
v: u32,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct CovOutput {
out: u32,
}
#[ssg_rpc]
#[doc = "Coverage probe: only exists so the inventory is non-empty."]
fn _ssg_rpc_schema_coverage_probe(
input: CovInput,
) -> Result<CovOutput, RpcError> {
Ok(CovOutput { out: input.v + 1 })
}
fn ctx_for(site_dir: &Path) -> PluginContext {
PluginContext {
content_dir: site_dir.to_path_buf(),
build_dir: site_dir.to_path_buf(),
site_dir: site_dir.to_path_buf(),
template_dir: site_dir.to_path_buf(),
config: None,
cache: None,
memory_budget: None,
html_files: None,
dep_graph: None,
dry_run: false,
}
}
#[test]
fn plugin_name_is_stable() {
assert_eq!(RpcSchemaPlugin::new().name(), "rpc-schema");
}
#[test]
fn dry_run_short_circuits() {
let dir = tempdir().unwrap();
let mut ctx = ctx_for(dir.path());
ctx.dry_run = true;
RpcSchemaPlugin::new().after_compile(&ctx).unwrap();
assert!(!dir.path().join(RPC_DTS_RELATIVE_PATH).exists());
}
#[test]
fn coverage_probe_dispatches_and_increments() {
let out = ssg_rpc::dispatch::dispatch(
"_ssg_rpc_schema_coverage_probe",
r#"{"v":41}"#,
)
.expect("dispatch");
assert!(out.contains("\"out\":42"));
}
#[test]
fn writes_typescript_when_inventory_nonempty() {
let dir = tempdir().unwrap();
let ctx = ctx_for(dir.path());
RpcSchemaPlugin::new().after_compile(&ctx).unwrap();
let path = dir.path().join(RPC_DTS_RELATIVE_PATH);
assert!(path.exists(), "rpc.d.ts must be written");
let txt = fs::read_to_string(&path).unwrap();
assert!(
txt.contains("AUTO-GENERATED"),
"emitted file should carry the header: {txt}"
);
}
#[test]
fn ensure_parent_creates_missing_directory() {
let dir = tempdir().unwrap();
let nested = dir.path().join("a/b/c/file.d.ts");
ensure_parent(&nested).unwrap();
assert!(nested.parent().unwrap().is_dir());
}
#[test]
fn ensure_parent_path_without_parent_is_ok() {
ensure_parent(Path::new("")).unwrap();
}
#[test]
fn after_compile_fails_when_ssg_dir_squatted_by_file() {
let dir = tempdir().unwrap();
fs::write(dir.path().join(".ssg"), "not a dir").unwrap();
let ctx = ctx_for(dir.path());
let err = RpcSchemaPlugin::new().after_compile(&ctx).unwrap_err();
assert!(!format!("{err}").is_empty());
}
#[test]
fn after_compile_fails_when_dts_path_squatted_by_dir() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join(RPC_DTS_RELATIVE_PATH)).unwrap();
let ctx = ctx_for(dir.path());
let err = RpcSchemaPlugin::new().after_compile(&ctx).unwrap_err();
assert!(!format!("{err}").is_empty());
}
}