Skip to main content

actix_cloud/
response_build.rs

1//! Build-time code generation for response codes (feature `response-build`).
2//!
3//! Call [`generate_response`] from `build.rs` to walk a directory of YAML files and
4//! emit one enum per file implementing [`ResponseCodeTrait`](crate::response::ResponseCodeTrait).
5//! See the `examples/response` example for the full setup.
6use anyhow::Result;
7use quote::{format_ident, quote};
8use std::{
9    env,
10    fs::{read_to_string, File},
11    io::Write,
12    path::Path,
13};
14use walkdir::WalkDir;
15use yaml_rust2::YamlLoader;
16
17/// Errors raised while parsing response YAML files.
18#[derive(thiserror::Error, Debug)]
19pub enum BuildError {
20    /// A YAML document does not follow the expected `name: {code, message}` shape.
21    #[error("response file format invalid")]
22    Format,
23
24    /// A file name cannot be turned into a valid enum name.
25    #[error("response file name invalid")]
26    File,
27}
28
29/// Generate response enums from YAML files.
30///
31/// Walks `input` (relative to the crate root) and writes one formatted Rust enum per
32/// YAML file into `OUT_DIR/output`. Each YAML entry is `name: {code, message}`; the
33/// enum is named after the file stem (e.g. `general.yml` → `GeneralResponse`).
34///
35/// - `import_prefix`: prefix prepended to the `actix_cloud` import, e.g. `crate::` when
36///   re-exported by the calling crate.
37/// - `output`: generated file name inside `OUT_DIR`, included via `include!`.
38///
39/// This function should be used in `build.rs`.
40/// ```ignore
41/// [build-dependencies]
42/// actix-cloud = { version = "xx", features = ["response-build"] }
43/// ```
44///
45/// ```no_run
46/// use actix_cloud::response_build::generate_response;
47///
48/// generate_response("", "response", "response.rs").unwrap();
49/// ```
50pub fn generate_response(import_prefix: &str, input: &str, output: &str) -> Result<()> {
51    let outfile = Path::new(&env::var("OUT_DIR")?).join(output);
52    let mut output = File::create(&outfile)?;
53    writeln!(
54        output,
55        "use {}actix_cloud::response::ResponseCodeTrait;",
56        import_prefix
57    )?;
58    for entry in WalkDir::new(input) {
59        let entry = entry?;
60        if entry.file_type().is_file() {
61            let file = read_to_string(entry.path())?;
62            let yaml = YamlLoader::load_from_str(&file)?;
63            let doc = &yaml[0];
64            let mut name_vec = Vec::new();
65            let mut code_vec = Vec::new();
66            let mut message_vec = Vec::new();
67            for (name, field) in doc.as_hash().ok_or(BuildError::Format)? {
68                name_vec.push(format_ident!(
69                    "{}",
70                    name.as_str().ok_or(BuildError::Format)?
71                ));
72                code_vec.push(field["code"].as_i64().ok_or(BuildError::Format)?);
73                message_vec.push(field["message"].as_str().ok_or(BuildError::Format)?);
74            }
75
76            let file_stem = entry.path().file_stem().ok_or(BuildError::File)?;
77            let stem = file_stem.to_str().ok_or(BuildError::File)?;
78            if stem.is_empty()
79                || !stem.starts_with(|c: char| c.is_alphabetic() || c == '_')
80                || !stem.chars().all(|c| c.is_alphanumeric() || c == '_')
81            {
82                return Err(BuildError::File.into());
83            }
84            let mut chars = stem.chars();
85            let s = chars.next().unwrap().to_uppercase().collect::<String>() + chars.as_str();
86            let enum_name = format_ident!("{}Response", s);
87            let mut enum_code = Vec::new();
88            for i in 0..code_vec.len() {
89                let s = &name_vec[i];
90                let c = code_vec[i];
91                enum_code.push(quote! {#enum_name::#s => #c});
92            }
93            let mut enum_message = Vec::new();
94            for i in 0..code_vec.len() {
95                let s = &name_vec[i];
96                let c = message_vec[i];
97                enum_message.push(quote! {#enum_name::#s => #c});
98            }
99            let content = quote! {
100                pub enum #enum_name {
101                    #(#name_vec),*
102                }
103
104                impl ResponseCodeTrait for #enum_name {
105                    fn code(&self) -> i64 {
106                        match self {
107                            #(#enum_code),*
108                        }
109                    }
110
111                    fn message(&self) -> &'static str {
112                        match self {
113                            #(#enum_message),*
114                        }
115                    }
116                }
117            };
118
119            write!(
120                output,
121                "{}",
122                prettyplease::unparse(&syn::parse_file(&content.to_string())?)
123            )?;
124        }
125    }
126    Ok(())
127}