1use crate::client::GraphQlClient;
4use crate::fake::FakeGraphql;
5use sova_core::{App, Plugin};
6
7#[cfg(feature = "server")]
8use crate::server::{
9 default_graphiql_path, default_subscriptions_path, install_server, SchemaHandle,
10 ServerMountConfig,
11};
12#[cfg(feature = "server")]
13use async_graphql::{ObjectType, Schema, SubscriptionType};
14
15enum Mode {
16 Client {
17 endpoint: String,
18 },
19 Fake {
20 fake: FakeGraphql,
21 endpoint: String,
22 },
23 #[cfg(feature = "server")]
24 Server {
25 handle: SchemaHandle,
26 },
27}
28
29enum Outbound {
31 Http(String),
32 FromEnv,
33}
34
35pub struct GraphQl {
37 mode: Mode,
38 path: String,
39 path_explicit: bool,
40 graphiql: bool,
41 graphiql_explicit: bool,
42 enabled: bool,
43 #[cfg(feature = "server")]
44 outbound: Option<Outbound>,
45 #[cfg(feature = "server")]
46 graphiql_path: Option<String>,
47 #[cfg(feature = "server")]
48 graphiql_path_explicit: bool,
49 #[cfg(feature = "server")]
50 subscriptions_path: Option<String>,
51 #[cfg(feature = "server")]
52 subscriptions_disabled: bool,
53 #[cfg(feature = "server")]
54 allow_get_queries: bool,
55 #[cfg(feature = "server")]
56 sdl_path: Option<String>,
57 #[cfg(feature = "server")]
58 sdl_path_explicit: bool,
59}
60
61impl GraphQl {
62 fn server_defaults() -> Self {
63 Self {
64 mode: Mode::Client {
65 endpoint: String::new(),
66 },
67 path: "/graphql".into(),
68 path_explicit: false,
69 graphiql: cfg!(debug_assertions),
70 graphiql_explicit: false,
71 enabled: true,
72 #[cfg(feature = "server")]
73 outbound: None,
74 #[cfg(feature = "server")]
75 graphiql_path: None,
76 #[cfg(feature = "server")]
77 graphiql_path_explicit: false,
78 #[cfg(feature = "server")]
79 subscriptions_path: None,
80 #[cfg(feature = "server")]
81 subscriptions_disabled: false,
82 #[cfg(feature = "server")]
83 allow_get_queries: false,
84 #[cfg(feature = "server")]
85 sdl_path: None,
86 #[cfg(feature = "server")]
87 sdl_path_explicit: false,
88 }
89 }
90
91 pub fn client(endpoint: impl Into<String>) -> Self {
93 Self {
94 mode: Mode::Client {
95 endpoint: endpoint.into(),
96 },
97 path: "/graphql".into(),
98 path_explicit: false,
99 graphiql: false,
100 graphiql_explicit: true,
101 enabled: true,
102 #[cfg(feature = "server")]
103 outbound: None,
104 #[cfg(feature = "server")]
105 graphiql_path: None,
106 #[cfg(feature = "server")]
107 graphiql_path_explicit: false,
108 #[cfg(feature = "server")]
109 subscriptions_path: None,
110 #[cfg(feature = "server")]
111 subscriptions_disabled: true,
112 #[cfg(feature = "server")]
113 allow_get_queries: false,
114 #[cfg(feature = "server")]
115 sdl_path: None,
116 #[cfg(feature = "server")]
117 sdl_path_explicit: false,
118 }
119 }
120
121 pub fn fake(fake: FakeGraphql) -> Self {
123 Self {
124 mode: Mode::Fake {
125 fake,
126 endpoint: "fake://graphql".into(),
127 },
128 path: "/graphql".into(),
129 path_explicit: false,
130 graphiql: false,
131 graphiql_explicit: true,
132 enabled: true,
133 #[cfg(feature = "server")]
134 outbound: None,
135 #[cfg(feature = "server")]
136 graphiql_path: None,
137 #[cfg(feature = "server")]
138 graphiql_path_explicit: false,
139 #[cfg(feature = "server")]
140 subscriptions_path: None,
141 #[cfg(feature = "server")]
142 subscriptions_disabled: true,
143 #[cfg(feature = "server")]
144 allow_get_queries: false,
145 #[cfg(feature = "server")]
146 sdl_path: None,
147 #[cfg(feature = "server")]
148 sdl_path_explicit: false,
149 }
150 }
151
152 #[cfg(feature = "server")]
154 pub fn server<Q, M, S>(schema: Schema<Q, M, S>) -> Self
155 where
156 Q: ObjectType + 'static,
157 M: ObjectType + 'static,
158 S: SubscriptionType + 'static,
159 {
160 let mut this = Self::server_defaults();
161 this.mode = Mode::Server {
162 handle: SchemaHandle::from_schema(schema),
163 };
164 this
165 }
166
167 #[cfg(feature = "server")]
169 pub fn with_client(mut self, endpoint: impl Into<String>) -> Self {
170 self.outbound = Some(Outbound::Http(endpoint.into()));
171 self
172 }
173
174 #[cfg(feature = "server")]
176 pub fn with_client_from_env(mut self) -> Self {
177 self.outbound = Some(Outbound::FromEnv);
178 self
179 }
180
181 pub fn path(mut self, path: impl Into<String>) -> Self {
182 self.path = path.into();
183 self.path_explicit = true;
184 self
185 }
186
187 pub fn graphiql(mut self, enabled: bool) -> Self {
188 self.graphiql = enabled;
189 self.graphiql_explicit = true;
190 self
191 }
192
193 #[cfg(feature = "server")]
195 pub fn graphiql_path(mut self, path: impl Into<String>) -> Self {
196 self.graphiql_path = Some(path.into());
197 self.graphiql_path_explicit = true;
198 self
199 }
200
201 #[cfg(feature = "server")]
203 pub fn subscriptions(mut self, path: impl Into<String>) -> Self {
204 self.subscriptions_path = Some(path.into());
205 self.subscriptions_disabled = false;
206 self
207 }
208
209 #[cfg(feature = "server")]
211 pub fn without_subscriptions(mut self) -> Self {
212 self.subscriptions_disabled = true;
213 self
214 }
215
216 #[cfg(feature = "server")]
218 pub fn allow_get_queries(mut self, enabled: bool) -> Self {
219 self.allow_get_queries = enabled;
220 self
221 }
222
223 #[cfg(feature = "server")]
225 pub fn sdl_path(mut self, path: impl Into<String>) -> Self {
226 self.sdl_path = Some(path.into());
227 self.sdl_path_explicit = true;
228 self
229 }
230
231 #[cfg(feature = "server")]
232 fn install_outbound(&self, app: &mut App) {
233 let Some(outbound) = &self.outbound else {
234 return;
235 };
236 let endpoint = match outbound {
237 Outbound::Http(url) => url.clone(),
238 Outbound::FromEnv => std::env::var("GRAPHQL_URL").unwrap_or_default(),
239 };
240 if !endpoint.is_empty() {
241 app.state(GraphQlClient::http(endpoint));
242 }
243 }
244
245 #[cfg(feature = "server")]
246 fn server_mount_config(&self) -> ServerMountConfig {
247 let graphiql_path = if self.graphiql_path_explicit {
248 self.graphiql_path
249 .clone()
250 .unwrap_or_else(|| default_graphiql_path(&self.path))
251 } else {
252 default_graphiql_path(&self.path)
253 };
254 let subscriptions_path = if self.subscriptions_disabled {
255 None
256 } else if let Some(p) = &self.subscriptions_path {
257 Some(p.clone())
258 } else {
259 Some(default_subscriptions_path(&self.path))
260 };
261 let sdl_path = if self.sdl_path_explicit {
262 self.sdl_path.clone()
263 } else {
264 None
265 };
266 ServerMountConfig {
267 path: self.path.clone(),
268 graphiql: self.graphiql,
269 graphiql_path,
270 subscriptions_path,
271 allow_get_queries: self.allow_get_queries,
272 sdl_path,
273 }
274 }
275}
276
277impl Plugin for GraphQl {
278 fn id(&self) -> &'static str {
279 "graphql"
280 }
281
282 fn meta(&self) -> sova_core::PluginMeta {
283 sova_core::PluginMeta::new("GraphQL")
284 .description("Outbound GraphQL client (+ optional schema mount)")
285 .version(env!("CARGO_PKG_VERSION"))
286 }
287
288 fn install(mut self, app: &mut App) {
289 if let Some(doc) = app.config_doc() {
290 if let Some(section) = doc.section("graphql") {
291 if let Some(en) = section.get("enabled").and_then(|v| v.as_bool()) {
292 self.enabled = en;
293 }
294 if !self.path_explicit {
295 if let Some(p) = section.get("path").and_then(|v| v.as_str()) {
296 self.path = p.to_string();
297 }
298 }
299 if !self.graphiql_explicit {
300 if let Some(g) = section.get("graphiql").and_then(|v| v.as_bool()) {
301 self.graphiql = g;
302 }
303 }
304 #[cfg(feature = "server")]
305 {
306 if !self.graphiql_path_explicit {
307 if let Some(p) = section.get("graphiql_path").and_then(|v| v.as_str()) {
308 self.graphiql_path = Some(p.to_string());
309 self.graphiql_path_explicit = true;
310 }
311 }
312 if self.subscriptions_path.is_none() && !self.subscriptions_disabled {
313 if let Some(p) = section.get("subscriptions_path").and_then(|v| v.as_str())
314 {
315 self.subscriptions_path = Some(p.to_string());
316 }
317 }
318 if let Some(v) = section.get("allow_get_queries").and_then(|v| v.as_bool()) {
319 self.allow_get_queries = v;
320 }
321 if !self.sdl_path_explicit {
322 if let Some(p) = section.get("sdl_path").and_then(|v| v.as_str()) {
323 self.sdl_path = Some(p.to_string());
324 self.sdl_path_explicit = true;
325 }
326 }
327 }
328 if let Mode::Client { endpoint } = &mut self.mode {
329 if endpoint.is_empty() {
330 if let Some(u) = section.get("url").and_then(|v| v.as_str()) {
331 *endpoint = u.to_string();
332 }
333 }
334 }
335 }
336 }
337
338 if !self.enabled {
339 return;
340 }
341
342 #[cfg(feature = "server")]
343 if matches!(self.mode, Mode::Server { .. }) {
344 self.install_outbound(app);
345 }
346
347 #[cfg(feature = "server")]
348 let server_cfg = if matches!(&self.mode, Mode::Server { .. }) {
349 Some(self.server_mount_config())
350 } else {
351 None
352 };
353
354 match self.mode {
355 Mode::Client { endpoint } => {
356 let endpoint = if endpoint.is_empty() {
357 std::env::var("GRAPHQL_URL").unwrap_or_default()
358 } else {
359 endpoint
360 };
361 app.state(GraphQlClient::http(endpoint));
362 }
363 Mode::Fake { fake, endpoint } => {
364 app.state(GraphQlClient::with_fake(endpoint, fake));
365 }
366 #[cfg(feature = "server")]
367 Mode::Server { handle } => {
368 install_server(app, handle, server_cfg.expect("server cfg"));
369 }
370 }
371 }
372}