Skip to main content

krabka_protocol_codegen/emit/
mod_rs.rs

1//! Generate the `mod.rs` files for `crates/protocol/src/{owned,borrowed}/`.
2
3use quote::{format_ident, quote};
4
5use crate::{
6    emit::{
7        EmitError,
8        common::{banner, validate},
9        wrappers::Flavor,
10    },
11    ir::MessageSpec,
12    name_conv,
13};
14
15/// Emit a `mod.rs` that declares one `pub mod` per active spec, sorted
16/// alphabetically by snake-case module name.
17///
18/// Pass only the specs whose wrapper files actually exist, which is the set
19/// that covers all active schemas.
20/// If `has_common` is true, this function also emits a `pub mod common;`
21/// entry.
22/// # Errors
23/// Returns an error when the generated code is not valid Rust, which is an
24/// emitter bug.
25pub fn emit(
26    specs: &[&MessageSpec],
27    flavor: Flavor,
28    schemas_version: &str,
29    has_common: bool,
30) -> Result<String, EmitError> {
31    let flavor_comment = match flavor {
32        Flavor::Owned => "Owned (heap-allocated) message types.",
33        Flavor::Borrowed => "Borrowed-flavor generated message types.",
34    };
35    let banner = banner(schemas_version);
36
37    // Collect snake-case names, sort, deduplicate.
38    let mut entries: Vec<String> = specs
39        .iter()
40        .filter(|s| !s.valid_versions.is_empty())
41        .map(|s| name_conv::module_name(&s.name))
42        .collect();
43    // Include `common` in the sorted list if this flavor has a common submodule.
44    if has_common {
45        entries.push("common".to_owned());
46    }
47    entries.sort();
48    entries.dedup();
49
50    let doc = format!(" {flavor_comment}");
51    let mods = entries.iter().map(|s| {
52        let id = format_ident!("{s}");
53        quote!(pub mod #id;)
54    });
55    let tokens = quote! {
56        #![doc = #doc]
57        #(#mods)*
58    };
59
60    // Validate at generation time.
61    validate::<syn::File>(&tokens, "mod.rs")?;
62
63    Ok(format!("{banner}{tokens}"))
64}
65
66#[cfg(test)]
67mod tests {
68    use assert2::assert;
69
70    use super::*;
71    use crate::ir::{FlexibleVersions, MessageSpec, MessageType, VersionRange};
72
73    fn make_spec(name: &str, min: i16, max: i16, ty: MessageType) -> MessageSpec {
74        MessageSpec {
75            name: name.to_owned(),
76            message_type: ty,
77            api_key: None,
78            valid_versions: VersionRange { min, max },
79            latest_version_unstable: false,
80            flexible_versions: FlexibleVersions::None,
81            fields: vec![],
82            common_structs: vec![],
83            internal: false,
84        }
85    }
86
87    #[test]
88    fn owned_mod_rs_sorted_alphabetically() {
89        let specs = [
90            make_spec("ProduceRequest", 0, 10, MessageType::Request),
91            make_spec("ApiVersionsRequest", 0, 3, MessageType::Request),
92            make_spec("MetadataRequest", 0, 13, MessageType::Request),
93        ];
94        let refs: Vec<&MessageSpec> = specs.iter().collect();
95        let out = emit(&refs, Flavor::Owned, "test-sha", false).unwrap();
96        // Must contain all three, in alphabetical order.
97        // Note: quote! may emit `pub mod api_versions_request ;` (space before `;`),
98        // so we search for the ident without the semicolon.
99        let api_pos = out.find("pub mod api_versions_request").unwrap();
100        let meta_pos = out.find("pub mod metadata_request").unwrap();
101        let prod_pos = out.find("pub mod produce_request").unwrap();
102        assert!(api_pos < meta_pos, "api before metadata");
103        assert!(meta_pos < prod_pos, "metadata before produce");
104    }
105
106    #[test]
107    fn skips_specs_with_empty_valid_versions() {
108        let specs = [
109            make_spec("ActiveRequest", 0, 5, MessageType::Request),
110            make_spec("RemovedRequest", i16::MAX, i16::MIN, MessageType::Request),
111        ];
112        let refs: Vec<&MessageSpec> = specs.iter().collect();
113        let out = emit(&refs, Flavor::Owned, "test-sha", false).unwrap();
114        assert!(out.contains("pub mod active_request"));
115        assert!(!out.contains("pub mod removed_request"));
116    }
117
118    #[test]
119    fn borrowed_flavor_has_correct_doc_comment() {
120        let refs: Vec<&MessageSpec> = vec![];
121        let out = emit(&refs, Flavor::Borrowed, "sha123", false).unwrap();
122        // Output now uses `#![doc = "..."]` form instead of `//! ...` comment,
123        // so we check for the doc text itself rather than the `//!` prefix.
124        assert!(out.contains("Borrowed-flavor generated message types."));
125    }
126}