1use crate::client::GrpcClient;
4use crate::fake::FakeGrpc;
5use crate::router::MethodRouter;
6use crate::server::{mount_on_app, GrpcBindService};
7use serde_json::json;
8use sova_core::{App, DevToolsConfigRegistry, Plugin};
9use std::net::SocketAddr;
10
11enum Mode {
12 Client {
13 base: String,
14 },
15 Fake {
16 fake: FakeGrpc,
17 },
18 Server {
19 router: MethodRouter,
20 bind: Option<SocketAddr>,
21 mount: bool,
22 client_base: Option<String>,
23 client_from_env: bool,
24 },
25}
26
27pub struct Grpc {
29 mode: Mode,
30}
31
32impl Grpc {
33 pub fn client(base: impl Into<String>) -> Self {
34 Self {
35 mode: Mode::Client { base: base.into() },
36 }
37 }
38
39 pub fn fake(fake: FakeGrpc) -> Self {
40 Self {
41 mode: Mode::Fake { fake },
42 }
43 }
44
45 pub fn server() -> GrpcServerBuilder {
46 GrpcServerBuilder {
47 router: MethodRouter::new(),
48 bind: None,
49 mount: true,
50 client_base: None,
51 client_from_env: false,
52 }
53 }
54}
55
56pub struct GrpcServerBuilder {
57 router: MethodRouter,
58 bind: Option<SocketAddr>,
59 mount: bool,
60 client_base: Option<String>,
61 client_from_env: bool,
62}
63
64impl GrpcServerBuilder {
65 pub fn unary<Req, Res, F, Fut>(self, method: impl Into<String>, f: F) -> Self
66 where
67 Req: serde::de::DeserializeOwned + Send + 'static,
68 Res: serde::Serialize + Send + 'static,
69 F: Fn(Req) -> Fut + Send + Sync + 'static,
70 Fut: std::future::Future<Output = Result<Res, crate::GrpcError>> + Send + 'static,
71 {
72 self.router.unary(method, f);
73 self
74 }
75
76 pub fn unary_with_request<Req, Res, F, Fut>(self, method: impl Into<String>, f: F) -> Self
78 where
79 Req: serde::de::DeserializeOwned + Send + 'static,
80 Res: serde::Serialize + Send + 'static,
81 F: Fn(sova_core::Request, Req) -> Fut + Send + Sync + 'static,
82 Fut: std::future::Future<Output = Result<Res, crate::GrpcError>> + Send + 'static,
83 {
84 self.router.unary_with_request(method, f);
85 self
86 }
87
88 pub fn client(mut self, base: impl Into<String>) -> Self {
90 self.client_base = Some(base.into());
91 self
92 }
93
94 pub fn client_from_env(mut self) -> Self {
96 self.client_from_env = true;
97 self
98 }
99
100 pub fn bind(mut self, addr: impl Into<String>) -> Self {
102 let s = addr.into();
103 self.bind = s.parse().ok();
104 self
105 }
106
107 pub fn mount(mut self, enabled: bool) -> Self {
109 self.mount = enabled;
110 self
111 }
112
113 pub fn build(self) -> Grpc {
114 Grpc {
115 mode: Mode::Server {
116 router: self.router,
117 bind: self.bind,
118 mount: self.mount,
119 client_base: self.client_base,
120 client_from_env: self.client_from_env,
121 },
122 }
123 }
124}
125
126impl From<GrpcServerBuilder> for Grpc {
127 fn from(b: GrpcServerBuilder) -> Self {
128 b.build()
129 }
130}
131
132impl Plugin for Grpc {
133 fn id(&self) -> &'static str {
134 "grpc"
135 }
136
137 fn meta(&self) -> sova_core::PluginMeta {
138 sova_core::PluginMeta::new("gRPC")
139 .description("Connect-JSON unary RPC client (+ optional server)")
140 .version(env!("CARGO_PKG_VERSION"))
141 }
142
143 fn install(mut self, app: &mut App) {
144 if let Some(doc) = app.config_doc() {
145 if let Some(section) = doc.section("grpc") {
146 if let Mode::Client { base } = &mut self.mode {
147 if base.is_empty() {
148 if let Some(u) = section.get("client_url").and_then(|v| v.as_str()) {
149 *base = u.to_string();
150 }
151 }
152 }
153 if let Mode::Server {
154 bind,
155 client_base,
156 client_from_env,
157 ..
158 } = &mut self.mode
159 {
160 if bind.is_none() {
161 if let Some(b) = section.get("bind").and_then(|v| v.as_str()) {
162 *bind = b.parse().ok();
163 }
164 }
165 if client_base.is_none() && !*client_from_env {
166 if let Some(u) = section.get("client_url").and_then(|v| v.as_str()) {
167 *client_base = Some(u.to_string());
168 }
169 }
170 }
171 }
172 }
173
174 match self.mode {
175 Mode::Client { base } => {
176 let base = if base.is_empty() {
177 std::env::var("GRPC_URL").unwrap_or_default()
178 } else {
179 base
180 };
181 app.state(GrpcClient::http(base.clone()));
182 register_devtools_mount(app, &base, &[], None);
183 }
184 Mode::Fake { fake } => {
185 app.state(GrpcClient::with_fake("fake://grpc", fake));
186 register_devtools_mount(app, "fake://grpc", &[], None);
187 }
188 Mode::Server {
189 router,
190 bind,
191 mount,
192 client_base,
193 client_from_env,
194 } => {
195 let outbound = if client_from_env {
196 std::env::var("GRPC_URL").unwrap_or_default()
197 } else {
198 client_base.unwrap_or_default()
199 };
200 if !outbound.is_empty() {
201 app.state(GrpcClient::http(outbound.clone()));
202 }
203 let methods = router.methods();
204 app.state(router.clone());
205 if mount {
206 mount_on_app(app, router.clone());
207 }
208 let bind_label = bind.map(|a| a.to_string());
209 if let Some(addr) = bind {
210 app.service(GrpcBindService::new(addr, router));
211 }
212 register_devtools_mount(
213 app,
214 if outbound.is_empty() {
215 "in-process"
216 } else {
217 &outbound
218 },
219 &methods,
220 bind_label,
221 );
222 }
223 }
224 }
225}
226
227fn register_devtools_mount(
228 app: &mut App,
229 client_base: &str,
230 methods: &[String],
231 bind: Option<String>,
232) {
233 if app.try_state::<DevToolsConfigRegistry>().is_none() {
234 app.state(DevToolsConfigRegistry::default());
235 }
236 let reg = app
237 .try_state::<DevToolsConfigRegistry>()
238 .expect("DevToolsConfigRegistry");
239 reg.set(
240 "grpc",
241 json!({
242 "client_base": client_base,
243 "methods": methods,
244 "bind": bind,
245 }),
246 );
247}
248
249impl Plugin for GrpcServerBuilder {
251 fn id(&self) -> &'static str {
252 "grpc"
253 }
254
255 fn meta(&self) -> sova_core::PluginMeta {
256 sova_core::PluginMeta::new("gRPC")
257 .description("Connect-JSON unary RPC client (+ optional server)")
258 .version(env!("CARGO_PKG_VERSION"))
259 }
260
261 fn install(self, app: &mut App) {
262 Grpc::from(self).install(app);
263 }
264}