Skip to main content

sova_grpc/
plugin.rs

1//! Plugin: client / fake / server.
2
3use 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
27/// Connect-JSON unary RPC. Prefer [`Grpc::client`] / [`Grpc::fake`].
28pub 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    /// Unary handler with access to the incoming HTTP [`sova_core::Request`].
77    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    /// Also install an outbound client (BFF: serve + call remote RPC).
89    pub fn client(mut self, base: impl Into<String>) -> Self {
90        self.client_base = Some(base.into());
91        self
92    }
93
94    /// Outbound client base URL from `GRPC_URL`.
95    pub fn client_from_env(mut self) -> Self {
96        self.client_from_env = true;
97        self
98    }
99
100    /// Also listen on a dedicated socket (BackgroundService).
101    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    /// Mount unary paths on the main HTTP app (default true).
108    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
249// Allow `app.install(Grpc::server().unary(...))` without `.build()`
250impl 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}