soda_pool_build/
lib.rs

1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
2
3//! This crate generates pooled gRPC clients (that uses
4//! [soda-pool](https://docs.rs/soda-pool) crate) from the original gRPC clients
5//! generated by [tonic-build](https://docs.rs/tonic-build) crate.
6//!
7//! # Usage
8//!
9//! ```no_run
10//! fn main() -> Result<(), Box<dyn std::error::Error>> {
11//!     soda_pool_build::configure()
12//!         .dir("./protobuf.gen/src")
13//!         .build_all_clients()?;
14//!     Ok(())
15//! }
16//! ```
17//!
18//! # Output Example
19//!
20//! Please see
21//! [example/protobuf.gen](https://github.com/Makinami/soda-pool/blob/main/example/protobuf.gen/src/health_pool.rs)
22//! for an example of generated file and
23//! [example/client](https://github.com/Makinami/soda-pool/blob/main/example/client/src/main.rs)
24//! for its usage.
25//!
26
27use std::{
28    fs,
29    path::{Path, PathBuf},
30};
31
32mod error;
33pub use error::*;
34
35mod parser;
36use parser::parse_grpc_client_file;
37
38mod generator;
39mod model;
40
41/// Create a default [`SodaPoolBuilder`].
42// Emulates the `tonic_build` interface.
43#[must_use]
44pub fn configure() -> SodaPoolBuilder {
45    SodaPoolBuilder { dir: None }
46}
47
48/// Pooled gRPC clients generator.
49#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
50pub struct SodaPoolBuilder {
51    dir: Option<PathBuf>,
52}
53
54impl SodaPoolBuilder {
55    /// Create a new [`SodaPoolBuilder`].
56    #[must_use]
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    /// Set the input/output directory of gRPC clients' files.
62    pub fn dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
63        self.dir = Some(dir.as_ref().to_path_buf());
64        self
65    }
66
67    /// Build pooled gRPC clients.
68    ///
69    /// Generate pooled version of gRPC clients from the specified files.
70    /// `services` should be a list of files (with or without `.rs` extension)
71    /// containing the original gRPC client code. Files will be searched in the
72    /// directory specified by `dir`. For each input file, a new file will be
73    /// created with the same name but with `_pool` suffix.
74    ///
75    /// # Errors
76    /// Will return [`BuilderError`] on any errors encountered during pooled clients generation.
77    #[allow(clippy::missing_panics_doc)]
78    pub fn build_clients(
79        &self,
80        services: impl IntoIterator<Item = impl AsRef<Path>>,
81    ) -> BuilderResult<()> {
82        let dir = self
83            .dir
84            .as_ref()
85            .ok_or_else(|| BuilderError::missing_configuration("dir"))?;
86
87        services.into_iter().try_for_each(|service| {
88            let service_filename = service.as_ref().with_extension("rs");
89
90            let service_file = if service_filename.is_relative() { dir.join(&service_filename) } else { service_filename };
91            let service_file_structure = parse_grpc_client_file(&service_file)?;
92
93            if service_file_structure.client_modules.iter().all(|module| module.clients.is_empty()) {
94                return Err(BuilderError::GrpcClientNotFound);
95            }
96
97            let output = service_file_structure.generate_pooled_version();
98            let file = syn::parse2(output)?;
99            let formatted = format!(
100                "// This file is @generated by soda-pool-build.\n{}",
101                prettyplease::unparse(&file),
102            );
103
104            let output_file = {
105                let mut filename = service_file
106                .file_stem()
107                .expect("`service_file` is already certain to hold path to a file by previous check")
108                .to_owned();
109                filename.push("_pool.rs");
110                service_file.with_file_name(filename)
111            };
112
113            fs::write(output_file, formatted).unwrap();
114
115            Ok(())
116        })
117    }
118
119    /// Build pooled gRPC clients for all files in the specified directory.
120    ///
121    /// This method will search for all files in the directory specified by
122    /// `dir` and attempt to build pooled gRPC clients for each file. It will
123    /// skip any files that do not contain any recognized gRPC clients.
124    ///
125    /// # Errors
126    /// Will return [`BuilderError`] on any errors encountered during pooled clients generation.
127    #[allow(clippy::missing_panics_doc)]
128    pub fn build_all_clients(&self) -> BuilderResult<()> {
129        let dir = self
130            .dir
131            .as_ref()
132            .ok_or_else(|| BuilderError::missing_configuration("dir"))?;
133
134        let entries = fs::read_dir(dir)?;
135
136        for entry in entries {
137            let entry = entry?;
138            if !entry.file_type()?.is_file() {
139                continue;
140            }
141
142            if entry
143                .path()
144                .extension()
145                .is_some_and(|ext| ext.eq_ignore_ascii_case("rs"))
146            {
147                match self.build_clients([entry
148                    .path()
149                    .file_name()
150                    .expect("We have checked that this is a file so it must have a name")])
151                {
152                    Ok(()) | Err(BuilderError::GrpcClientNotFound) => {}
153                    Err(e) => return Err(e),
154                }
155            }
156        }
157
158        Ok(())
159    }
160}