1use std::{
2 collections::HashMap,
3 ops::{Add, Deref},
4 pin::Pin,
5 sync::Arc,
6};
7
8use crate::core::graphql_client::DynGraphQLClient;
9use futures::{future, Future};
10use serde::{Deserialize, Serialize};
11
12use crate::errors::{DaggerError, DaggerUnpackError};
13
14pub fn query() -> Selection {
15 Selection::default()
16}
17
18#[derive(Clone)]
19struct LazyResolve(Arc<dyn Fn() -> Pin<Box<dyn Future<Output = String> + Send>> + Send + Sync>);
20
21impl LazyResolve {
22 pub fn new(
23 func: Box<dyn Fn() -> Pin<Box<dyn Future<Output = String> + Send>> + Send + Sync>,
24 ) -> Self {
25 Self(Arc::new(func))
26 }
27
28 pub fn from_string(val: impl Into<String>) -> Self {
29 let val: String = val.into();
30 Self(Arc::new(move || Box::pin(future::ready(val.clone()))))
31 }
32}
33
34impl Deref for LazyResolve {
35 type Target = Arc<dyn Fn() -> Pin<Box<dyn Future<Output = String> + Send>> + Send + Sync>;
36
37 fn deref(&self) -> &Self::Target {
38 &self.0
39 }
40}
41
42impl From<String> for LazyResolve {
43 fn from(value: String) -> Self {
44 LazyResolve::from_string(value)
45 }
46}
47
48#[derive(Clone, Default)]
49pub struct Selection {
50 name: Option<String>,
51 alias: Option<String>,
52 args: Option<HashMap<String, LazyResolve>>,
53
54 inline_fragment: Option<String>,
57
58 prev: Option<Arc<Selection>>,
59}
60
61impl Selection {
62 pub fn root(&self) -> Selection {
66 Selection::default()
67 }
68
69 pub fn select_with_alias(&self, alias: &str, name: &str) -> Selection {
70 Self {
71 name: Some(name.to_string()),
72 alias: Some(alias.to_string()),
73 args: None,
74 inline_fragment: None,
75 prev: Some(Arc::new(self.clone())),
76 }
77 }
78
79 pub fn select(&self, name: &str) -> Selection {
80 Self {
81 name: Some(name.to_string()),
82 alias: None,
83 args: None,
84 inline_fragment: None,
85 prev: Some(Arc::new(self.clone())),
86 }
87 }
88
89 pub fn inline_fragment(&self, type_name: &str) -> Selection {
94 Self {
95 name: None,
96 alias: None,
97 args: None,
98 inline_fragment: Some(type_name.to_string()),
99 prev: Some(Arc::new(self.clone())),
100 }
101 }
102
103 pub fn arg<S>(&self, name: &str, value: S) -> Selection
104 where
105 S: Serialize,
106 {
107 let mut s = self.clone();
108
109 let val = serde_graphql_input::to_string_pretty(&value).unwrap();
110
111 match s.args.as_mut() {
112 Some(args) => {
113 let _ = args.insert(name.to_string(), val.into());
114 }
115 None => {
116 let mut hm = HashMap::new();
117 let _ = hm.insert(name.to_string(), val.into());
118 s.args = Some(hm);
119 }
120 }
121
122 s
123 }
124
125 pub fn arg_lazy(
126 &self,
127 name: &str,
128 value: Box<dyn Fn() -> Pin<Box<dyn Future<Output = String> + Send>> + Send + Sync>,
129 ) -> Selection {
130 let mut s = self.clone();
131
132 match s.args.as_mut() {
133 Some(args) => {
134 let _ = args.insert(name.to_string(), LazyResolve::new(Box::new(value)));
135 }
136 None => {
137 let mut hm = HashMap::new();
138 let _ = hm.insert(name.to_string(), LazyResolve::new(Box::new(value)));
139 s.args = Some(hm);
140 }
141 }
142
143 s
144 }
145
146 pub async fn build(&self) -> Result<String, DaggerError> {
147 let mut fields = vec!["query".to_string()];
148
149 for sel in self.path() {
150 if let Some(type_name) = sel.inline_fragment {
151 fields.push(format!("... on {}", type_name));
152 } else if let Some(mut query) = sel.name {
153 if let Some(args) = sel.args {
154 let mut actualargs = Vec::new();
155 for (name, arg) in args.iter() {
156 let arg = arg().await;
157 actualargs.push(format!("{name}:{arg}"));
158 }
159
160 query = query.add(&format!("({})", actualargs.join(", ")));
161 }
162
163 if let Some(alias) = sel.alias {
164 query = format!("{}:{}", alias, query);
165 }
166
167 fields.push(query);
168 }
169 }
170
171 Ok(fields.join("{") + &"}".repeat(fields.len() - 1))
172 }
173
174 pub async fn execute<D>(&self, gql_client: DynGraphQLClient) -> Result<D, DaggerError>
175 where
176 D: for<'de> Deserialize<'de>,
177 {
178 let query = self.build().await?;
179
180 tracing::trace!(query = query.as_str(), "dagger-query");
181
182 let resp: Option<serde_json::Value> = match gql_client.query(&query).await {
183 Ok(r) => r,
184 Err(e) => return Err(DaggerError::Query(e)),
185 };
186
187 let resp: Option<D> = self.unpack_resp(resp)?;
188
189 Ok(resp.unwrap())
190 }
191
192 fn path(&self) -> Vec<Selection> {
193 let mut selections: Vec<Selection> = vec![];
194 let mut cur = self;
195
196 while cur.prev.is_some() {
197 selections.push(cur.clone());
198
199 if let Some(prev) = cur.prev.as_ref() {
200 cur = prev;
201 }
202 }
203
204 selections.reverse();
205 selections
206 }
207
208 pub(crate) fn unpack_resp<D>(
209 &self,
210 resp: Option<serde_json::Value>,
211 ) -> Result<Option<D>, DaggerError>
212 where
213 D: for<'de> Deserialize<'de>,
214 {
215 match resp {
216 Some(r) => self.unpack_resp_value::<D>(r).map(|v| Some(v)),
217 None => Ok(None),
218 }
219 }
220
221 fn unpack_resp_value<D>(&self, r: serde_json::Value) -> Result<D, DaggerError>
222 where
223 D: for<'de> Deserialize<'de>,
224 {
225 let mut data = r;
226
227 for sel in self.path() {
228 if sel.inline_fragment.is_some() {
230 continue;
231 }
232
233 if let Some(o) = data.as_object() {
234 let key = sel.alias.as_ref().or(sel.name.as_ref());
235 if let Some(key) = key {
236 data = o.get(key).cloned().unwrap_or(serde_json::Value::Null);
237 }
238 }
239 }
240
241 if let serde_json::Value::Array(arr) = data {
242 let unwrapped: Vec<serde_json::Value> = arr
243 .into_iter()
244 .map(|v| match v {
245 serde_json::Value::Object(mut o) if o.len() == 1 => {
246 let key = o.keys().next().unwrap().clone();
247 o.remove(&key).unwrap()
248 }
249 other => other,
250 })
251 .collect();
252 return serde_json::from_value::<D>(serde_json::Value::Array(unwrapped))
253 .map_err(DaggerUnpackError::Deserialize)
254 .map_err(DaggerError::Unpack);
255 }
256
257 serde_json::from_value::<D>(data)
258 .map_err(DaggerUnpackError::Deserialize)
259 .map_err(DaggerError::Unpack)
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use pretty_assertions::assert_eq;
266 use serde::Serialize;
267
268 use super::query;
269
270 #[tokio::test]
271 async fn test_query() {
272 let root = query()
273 .select("core")
274 .select("image")
275 .arg("ref", "alpine")
276 .select("file")
277 .arg("path", "/etc/alpine-release");
278
279 let query = root.build().await.unwrap();
280
281 assert_eq!(
282 query,
283 r#"query{core{image(ref:"alpine"){file(path:"/etc/alpine-release")}}}"#.to_string()
284 )
285 }
286
287 #[tokio::test]
288 async fn test_query_alias() {
289 let root = query()
290 .select("core")
291 .select("image")
292 .arg("ref", "alpine")
293 .select_with_alias("foo", "file")
294 .arg("path", "/etc/alpine-release");
295
296 let query = root.build().await.unwrap();
297
298 assert_eq!(
299 query,
300 r#"query{core{image(ref:"alpine"){foo:file(path:"/etc/alpine-release")}}}"#.to_string()
301 )
302 }
303
304 #[tokio::test]
305 async fn test_arg_collision() {
306 let root = query()
307 .select("a")
308 .arg("arg", "one")
309 .select("b")
310 .arg("arg", "two");
311
312 let query = root.build().await.unwrap();
313
314 assert_eq!(query, r#"query{a(arg:"one"){b(arg:"two")}}"#.to_string())
315 }
316
317 #[tokio::test]
318 async fn test_vec_arg() {
319 let input = vec!["some-string"];
320
321 let root = query().select("a").arg("arg", input);
322 let query = root.build().await.unwrap();
323
324 assert_eq!(query, r#"query{a(arg:["some-string"])}"#.to_string())
325 }
326
327 #[tokio::test]
328 async fn test_ref_slice_arg() {
329 let input = &["some-string"];
330
331 let root = query().select("a").arg("arg", input);
332 let query = root.build().await.unwrap();
333
334 assert_eq!(query, r#"query{a(arg:["some-string"])}"#.to_string())
335 }
336
337 #[tokio::test]
338 async fn test_stringb_arg() {
339 let input = "some-string".to_string();
340
341 let root = query().select("a").arg("arg", input);
342 let query = root.build().await.unwrap();
343
344 assert_eq!(query, r#"query{a(arg:"some-string")}"#.to_string())
345 }
346
347 #[tokio::test]
348 async fn test_field_immutability() {
349 let root = query().select("test");
350
351 let a = root.select("a").build().await.unwrap();
352 assert_eq!(a, r#"query{test{a}}"#.to_string());
353
354 let b = root.select("b").build().await.unwrap();
355 assert_eq!(b, r#"query{test{b}}"#.to_string());
356 }
357
358 #[derive(Serialize)]
359 struct CustomType {
360 pub name: String,
361 pub s: Option<Box<CustomType>>,
362 }
363
364 #[tokio::test]
365 async fn test_arg_custom_type() {
366 let input = CustomType {
367 name: "some-name".to_string(),
368 s: Some(Box::new(CustomType {
369 name: "some-other-name".to_string(),
370 s: None,
371 })),
372 };
373
374 let root = query().select("a").arg("arg", input);
375 let query = root.build().await.unwrap();
376
377 assert_eq!(
378 query,
379 r#"query{a(arg:{name:"some-name",s:{name:"some-other-name",s:null}})}"#.to_string()
380 )
381 }
382
383 #[tokio::test]
384 async fn test_inline_fragment_build() {
385 let root = query()
387 .select("node")
388 .arg("id", "abc")
389 .inline_fragment("Container")
390 .select("imageRef");
391
392 let query = root.build().await.unwrap();
393
394 assert_eq!(
395 query,
396 r#"query{node(id:"abc"){... on Container{imageRef}}}"#.to_string()
397 )
398 }
399
400 #[tokio::test]
401 async fn test_inline_fragment_nested_fields() {
402 let root = query()
404 .select("node")
405 .arg("id", "abc")
406 .inline_fragment("Container")
407 .select("withExec")
408 .arg("args", vec!["echo"])
409 .select("stdout");
410
411 let query = root.build().await.unwrap();
412
413 assert_eq!(
414 query,
415 r#"query{node(id:"abc"){... on Container{withExec(args:["echo"]){stdout}}}}"#
416 .to_string()
417 )
418 }
419
420 #[tokio::test]
421 async fn test_inline_fragment_unpack() {
422 let root = query()
427 .select("node")
428 .arg("id", "abc")
429 .inline_fragment("Container")
430 .select("imageRef");
431
432 let resp: Option<serde_json::Value> =
433 serde_json::from_str(r#"{"node": {"imageRef": "alpine:3.18"}}"#).unwrap();
434
435 let result: Option<String> = root.unpack_resp(resp).unwrap();
436 assert_eq!(result, Some("alpine:3.18".to_string()));
437 }
438
439 #[tokio::test]
440 async fn test_inline_fragment_unpack_nested() {
441 let root = query()
444 .select("node")
445 .arg("id", "abc")
446 .inline_fragment("Container")
447 .select("file")
448 .arg("path", "/x")
449 .select("contents");
450
451 let resp: Option<serde_json::Value> =
452 serde_json::from_str(r#"{"node": {"file": {"contents": "hello"}}}"#).unwrap();
453
454 let result: Option<String> = root.unpack_resp(resp).unwrap();
455 assert_eq!(result, Some("hello".to_string()));
456 }
457
458 #[tokio::test]
459 async fn test_inline_fragment_immutability() {
460 let base = query()
462 .select("node")
463 .arg("id", "abc")
464 .inline_fragment("Container");
465
466 let a = base.select("imageRef").build().await.unwrap();
467 assert_eq!(
468 a,
469 r#"query{node(id:"abc"){... on Container{imageRef}}}"#.to_string()
470 );
471
472 let b = base.select("stdout").build().await.unwrap();
473 assert_eq!(
474 b,
475 r#"query{node(id:"abc"){... on Container{stdout}}}"#.to_string()
476 );
477 }
478}