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}