1use std::{
9 collections::{BTreeMap, BTreeSet},
10 fs, io,
11 path::{Path, PathBuf},
12};
13
14use proc_macro2::Ident;
15use quote::{format_ident, quote};
16
17use crate::{
18 emit::{self, wrappers::Flavor},
19 fmt::{self, Formatter},
20 ir, name_conv,
21 out::{WriteError, Written},
22 validate,
23};
24
25#[derive(Debug, thiserror::Error)]
27pub enum RunError {
28 #[error(transparent)]
29 Ir(#[from] ir::IrError),
30 #[error(transparent)]
31 Validate(#[from] validate::ValidateError),
32 #[error(transparent)]
33 Emit(#[from] emit::EmitError),
34 #[error(transparent)]
35 Io(#[from] io::Error),
36 #[error(transparent)]
37 Fmt(#[from] fmt::FmtError),
38 #[error(transparent)]
39 Write(#[from] WriteError),
40 #[error("schemas/VERSION must contain a `sha:` line")]
41 MissingSha,
42 #[error("usage: codegen [--namespace NAME] <schemas> <out>")]
43 Usage,
44 #[error("common-struct key `{0}` must be `<message_snake>/<struct_snake>`")]
45 CommonStem(String),
46 #[error("a namespaced output directory `{0}` must sit two levels below the protocol crate")]
47 NamespaceOut(PathBuf),
48}
49
50fn read_schemas_sha(schemas: &Path) -> Result<String, RunError> {
51 let version_text = fs::read_to_string(schemas.join("VERSION"))?;
52 let sha = version_text
53 .lines()
54 .find_map(|l| l.strip_prefix("sha: "))
55 .ok_or(RunError::MissingSha)?
56 .to_owned();
57 Ok(sha)
58}
59
60fn should_emit(spec: &ir::MessageSpec) -> bool {
62 !spec.valid_versions.is_empty()
63}
64
65fn protocol_src_from_out(out: &Path) -> PathBuf {
69 out.parent().unwrap_or(out).join("src")
70}
71
72fn write_wrapper(
73 written: &mut Written,
74 spec: &ir::MessageSpec,
75 flavor: Flavor,
76 schemas_version: &str,
77 protocol_src: &Path,
78 namespace: Option<&str>,
79) -> Result<(), RunError> {
80 let snake = name_conv::module_name(&spec.name);
81 let body = emit::wrappers::emit(spec, flavor, schemas_version, namespace)?;
82 let dir = protocol_src.join(match flavor {
83 Flavor::Owned => "owned",
84 Flavor::Borrowed => "borrowed",
85 });
86 fs::create_dir_all(&dir)?;
87 written.write(dir.join(format!("{snake}.rs")), &body)?;
88 Ok(())
89}
90
91fn split_common_stem(stem: &str) -> Result<(&str, &str), RunError> {
94 stem.split_once('/')
95 .ok_or_else(|| RunError::CommonStem(stem.to_owned()))
96}
97
98fn write_common_wrapper(
107 written: &mut Written,
108 message_snake: &str,
109 struct_snake: &str,
110 flavor: Flavor,
111 schemas_version: &str,
112 message_src_dir: &Path,
113 namespace: Option<&str>,
114) -> io::Result<()> {
115 let suffix = match flavor {
116 Flavor::Owned => "owned",
117 Flavor::Borrowed => "borrowed",
118 };
119 let path_prefix = namespace.map_or_else(String::new, |ns| format!("{ns}/"));
120 let body = format!(
121 "{}include!(concat!(\n env!(\"CARGO_MANIFEST_DIR\"),\n \"/generated/{path_prefix}common/{flavor_dir}/{message_snake}/{struct_snake}.{suffix}.rs\"\n));\n",
122 emit::common::banner(schemas_version),
123 flavor_dir = flavor.dir(),
124 );
125 written.write(message_src_dir.join(format!("{struct_snake}.rs")), &body)?;
126 Ok(())
127}
128
129fn write_common_wrapper_tree(
139 written: &mut Written,
140 tree: &BTreeMap<String, BTreeSet<String>>,
141 flavor: Flavor,
142 schemas_version: &str,
143 protocol_src: &Path,
144 namespace: Option<&str>,
145) -> io::Result<usize> {
146 if tree.is_empty() {
147 return Ok(0);
148 }
149 let mut count = 0;
150 let flavor_doc = match flavor {
151 Flavor::Owned => "Owned",
152 Flavor::Borrowed => "Borrowed",
153 };
154 let src_common = protocol_src.join(flavor.dir()).join("common");
155 fs::create_dir_all(&src_common)?;
156
157 let mut message_mods: Vec<Ident> = Vec::new();
158 for (message_snake, structs) in tree {
159 message_mods.push(format_ident!("{message_snake}"));
160 let message_dir = src_common.join(message_snake);
161 fs::create_dir_all(&message_dir)?;
162
163 let mut struct_mods: Vec<Ident> = Vec::new();
164 for struct_snake in structs {
165 write_common_wrapper(
166 written,
167 message_snake,
168 struct_snake,
169 flavor,
170 schemas_version,
171 &message_dir,
172 namespace,
173 )?;
174 struct_mods.push(format_ident!("{struct_snake}"));
175 count += 1;
176 }
177 let message_mod_tokens = quote!(#(pub mod #struct_mods;)*);
178 let message_mod = format!(
179 "{}{message_mod_tokens}",
180 emit::common::banner(schemas_version)
181 );
182 written.write(message_dir.join("mod.rs"), &message_mod)?;
183 count += 1;
184 }
185 let top_doc = format!(" {flavor_doc} common structs, scoped per owning message schema.");
186 let top_tokens = quote!(#![doc = #top_doc] #(pub mod #message_mods;)*);
187 let top_mod = format!("{}{top_tokens}", emit::common::banner(schemas_version));
188 written.write(src_common.join("mod.rs"), &top_mod)?;
189 count += 1;
190 Ok(count)
191}
192
193pub fn run(
205 schemas: &Path,
206 out: &Path,
207 namespace: Option<&str>,
208 format: Formatter,
209) -> Result<usize, RunError> {
210 let mut written = Written::default();
211 let schemas_sha = read_schemas_sha(schemas)?;
212 let specs = ir::load_dir(schemas)?;
213 validate::validate(&specs)?;
214 fs::create_dir_all(out)?;
215 let common_owned_dir = out.join("common").join("owned");
217 let common_borrowed_dir = out.join("common").join("borrowed");
218
219 let protocol_src = match namespace {
221 None => protocol_src_from_out(out),
222 Some(ns) => out
224 .parent()
225 .and_then(Path::parent)
226 .ok_or_else(|| RunError::NamespaceOut(out.to_path_buf()))?
227 .join("src")
228 .join(ns),
229 };
230
231 let mut all_common_owned = BTreeMap::<String, BTreeSet<String>>::new();
234 let mut all_common_borrowed = BTreeMap::<String, BTreeSet<String>>::new();
235
236 let mut count = 0;
237 for s in &specs {
238 if !should_emit(s) {
239 continue;
240 }
241 let owned_em = emit::owned_quote::emit(s, &schemas_sha)?;
242 let borrowed_em = emit::borrowed_quote::emit(s, &schemas_sha, namespace)?;
243 written.write_rs(
244 out.join(format!("{}.owned.rs", s.name)),
245 &owned_em.primary,
246 format,
247 )?;
248 written.write_rs(
249 out.join(format!("{}.borrowed.rs", s.name)),
250 &borrowed_em.primary,
251 format,
252 )?;
253 count += 2;
254 for (stem, body) in &owned_em.commons {
258 let (message_snake, struct_snake) = split_common_stem(stem)?;
259 let dir = common_owned_dir.join(message_snake);
260 fs::create_dir_all(&dir)?;
261 written.write_rs(dir.join(format!("{struct_snake}.owned.rs")), body, format)?;
262 all_common_owned
263 .entry(message_snake.to_owned())
264 .or_default()
265 .insert(struct_snake.to_owned());
266 count += 1;
267 }
268 for (stem, body) in &borrowed_em.commons {
269 let (message_snake, struct_snake) = split_common_stem(stem)?;
270 let dir = common_borrowed_dir.join(message_snake);
271 fs::create_dir_all(&dir)?;
272 written.write_rs(
273 dir.join(format!("{struct_snake}.borrowed.rs")),
274 body,
275 format,
276 )?;
277 all_common_borrowed
278 .entry(message_snake.to_owned())
279 .or_default()
280 .insert(struct_snake.to_owned());
281 count += 1;
282 }
283
284 if emit::wrappers::should_emit_wrapper(s) {
286 write_wrapper(
287 &mut written,
288 s,
289 Flavor::Owned,
290 &schemas_sha,
291 &protocol_src,
292 namespace,
293 )?;
294 write_wrapper(
295 &mut written,
296 s,
297 Flavor::Borrowed,
298 &schemas_sha,
299 &protocol_src,
300 namespace,
301 )?;
302 count += 2;
303 }
304 }
305
306 count += write_module_files(
307 &mut written,
308 &specs,
309 &all_common_owned,
310 &all_common_borrowed,
311 &schemas_sha,
312 &protocol_src,
313 namespace,
314 )?;
315
316 if namespace.is_none() {
320 let api_key_src = emit::api_key_enum_quote::emit(&specs, &schemas_sha)?;
321 written.write_rs(out.join("api_key.rs"), &api_key_src, format)?;
322 count += 1;
323
324 let diff_table = emit::differential_table::emit(&specs, &schemas_sha)?;
326 written.write_rs(out.join("differential_table.rs"), &diff_table, format)?;
327 count += 1;
328 }
329
330 written.prune(out, false)?;
334 written.prune(&out.join("common"), true)?;
335 written.prune(&protocol_src.join("owned"), true)?;
336 written.prune(&protocol_src.join("borrowed"), true)?;
337
338 Ok(count)
339}
340
341fn write_module_files(
342 written: &mut Written,
343 specs: &[ir::MessageSpec],
344 common_owned: &BTreeMap<String, BTreeSet<String>>,
345 common_borrowed: &BTreeMap<String, BTreeSet<String>>,
346 schemas_sha: &str,
347 protocol_src: &Path,
348 namespace: Option<&str>,
349) -> Result<usize, RunError> {
350 let mut count = write_common_wrapper_tree(
351 written,
352 common_owned,
353 Flavor::Owned,
354 schemas_sha,
355 protocol_src,
356 namespace,
357 )?;
358 count += write_common_wrapper_tree(
359 written,
360 common_borrowed,
361 Flavor::Borrowed,
362 schemas_sha,
363 protocol_src,
364 namespace,
365 )?;
366 let active: Vec<_> = specs.iter().filter(|spec| should_emit(spec)).collect();
367 let owned = emit::mod_rs::emit(
368 &active,
369 Flavor::Owned,
370 schemas_sha,
371 !common_owned.is_empty(),
372 )?;
373 let borrowed = emit::mod_rs::emit(
374 &active,
375 Flavor::Borrowed,
376 schemas_sha,
377 !common_borrowed.is_empty(),
378 )?;
379 written.write(protocol_src.join("owned/mod.rs"), &owned)?;
380 written.write(protocol_src.join("borrowed/mod.rs"), &borrowed)?;
381 count += 2;
382 if namespace.is_some() {
383 written.write(
384 protocol_src.join("mod.rs"),
385 "pub mod borrowed;\npub mod owned;\n",
386 )?;
387 }
388 Ok(count)
389}