ssg/plugins/
rpc_schema.rs1use std::fs;
27use std::path::Path;
28
29use crate::error::SsgError;
30use crate::plugin::{Plugin, PluginContext};
31
32pub const RPC_DTS_RELATIVE_PATH: &str = ".ssg/rpc.d.ts";
34
35#[derive(Debug, Clone, Copy, Default)]
49pub struct RpcSchemaPlugin;
50
51impl RpcSchemaPlugin {
52 #[must_use]
61 pub const fn new() -> Self {
62 Self
63 }
64}
65
66impl Plugin for RpcSchemaPlugin {
67 fn name(&self) -> &'static str {
68 "rpc-schema"
69 }
70
71 fn after_compile(&self, ctx: &PluginContext) -> Result<(), SsgError> {
72 if ctx.dry_run {
73 return Ok(());
74 }
75
76 if ssg_rpc::dispatch::iter_descriptors().next().is_none() {
79 return Ok(());
80 }
81
82 let opts = ssg_rpc::EmitOptions::default();
83 let ts = ssg_rpc::emit_typescript(&opts);
84
85 let out_path = ctx.site_dir.join(RPC_DTS_RELATIVE_PATH);
86 ensure_parent(&out_path)?;
87 fs::write(&out_path, ts).map_err(|e| SsgError::Io {
88 path: out_path.clone(),
89 source: e,
90 })?;
91
92 Ok(())
93 }
94}
95
96fn ensure_parent(path: &Path) -> Result<(), SsgError> {
97 if let Some(parent) = path.parent() {
98 fs::create_dir_all(parent).map_err(|e| SsgError::Io {
99 path: parent.to_path_buf(),
100 source: e,
101 })?;
102 }
103 Ok(())
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::plugin::PluginContext;
110 use schemars::JsonSchema;
111 use serde::{Deserialize, Serialize};
112 use ssg_rpc::{ssg_rpc, RpcError};
113 use tempfile::tempdir;
114
115 #[derive(Debug, Serialize, Deserialize, JsonSchema)]
121 struct CovInput {
122 v: u32,
123 }
124
125 #[derive(Debug, Serialize, Deserialize, JsonSchema)]
126 struct CovOutput {
127 out: u32,
128 }
129
130 #[ssg_rpc]
131 #[doc = "Coverage probe: only exists so the inventory is non-empty."]
132 fn _ssg_rpc_schema_coverage_probe(
133 input: CovInput,
134 ) -> Result<CovOutput, RpcError> {
135 Ok(CovOutput { out: input.v + 1 })
136 }
137
138 fn ctx_for(site_dir: &Path) -> PluginContext {
139 PluginContext {
140 content_dir: site_dir.to_path_buf(),
141 build_dir: site_dir.to_path_buf(),
142 site_dir: site_dir.to_path_buf(),
143 template_dir: site_dir.to_path_buf(),
144 config: None,
145 cache: None,
146 memory_budget: None,
147 html_files: None,
148 dep_graph: None,
149 dry_run: false,
150 }
151 }
152
153 #[test]
154 fn plugin_name_is_stable() {
155 assert_eq!(RpcSchemaPlugin::new().name(), "rpc-schema");
156 }
157
158 #[test]
159 fn dry_run_short_circuits() {
160 let dir = tempdir().unwrap();
161 let mut ctx = ctx_for(dir.path());
162 ctx.dry_run = true;
163 RpcSchemaPlugin::new().after_compile(&ctx).unwrap();
164 assert!(!dir.path().join(RPC_DTS_RELATIVE_PATH).exists());
166 }
167
168 #[test]
169 fn coverage_probe_dispatches_and_increments() {
170 let out = ssg_rpc::dispatch::dispatch(
175 "_ssg_rpc_schema_coverage_probe",
176 r#"{"v":41}"#,
177 )
178 .expect("dispatch");
179 assert!(out.contains("\"out\":42"));
180 }
181
182 #[test]
183 fn writes_typescript_when_inventory_nonempty() {
184 let dir = tempdir().unwrap();
188 let ctx = ctx_for(dir.path());
189 RpcSchemaPlugin::new().after_compile(&ctx).unwrap();
190 let path = dir.path().join(RPC_DTS_RELATIVE_PATH);
191 assert!(path.exists(), "rpc.d.ts must be written");
192 let txt = fs::read_to_string(&path).unwrap();
193 assert!(
194 txt.contains("AUTO-GENERATED"),
195 "emitted file should carry the header: {txt}"
196 );
197 }
198
199 #[test]
200 fn ensure_parent_creates_missing_directory() {
201 let dir = tempdir().unwrap();
202 let nested = dir.path().join("a/b/c/file.d.ts");
203 ensure_parent(&nested).unwrap();
204 assert!(nested.parent().unwrap().is_dir());
205 }
206
207 #[test]
208 fn ensure_parent_path_without_parent_is_ok() {
209 ensure_parent(Path::new("")).unwrap();
211 }
212
213 #[test]
214 fn after_compile_fails_when_ssg_dir_squatted_by_file() {
215 let dir = tempdir().unwrap();
218 fs::write(dir.path().join(".ssg"), "not a dir").unwrap();
219 let ctx = ctx_for(dir.path());
220 let err = RpcSchemaPlugin::new().after_compile(&ctx).unwrap_err();
221 assert!(!format!("{err}").is_empty());
222 }
223
224 #[test]
225 fn after_compile_fails_when_dts_path_squatted_by_dir() {
226 let dir = tempdir().unwrap();
228 fs::create_dir_all(dir.path().join(RPC_DTS_RELATIVE_PATH)).unwrap();
229 let ctx = ctx_for(dir.path());
230 let err = RpcSchemaPlugin::new().after_compile(&ctx).unwrap_err();
231 assert!(!format!("{err}").is_empty());
232 }
233}