ironflow_ops_gitlab/operation.rs
1//! [`GitLabOp`] -- wraps any [`Endpoint`] as a tracked [`Operation`].
2
3use async_trait::async_trait;
4use gitlab::AsyncGitlab;
5use gitlab::api::{AsyncQuery, Endpoint, raw};
6use ironflow_core::error::OperationError;
7use ironflow_core::operation::{Operation, OperationContext};
8use serde_json::Value;
9
10/// A GitLab endpoint wrapped as an Ironflow [`Operation`].
11///
12/// Created via [`GitLab::op`](crate::GitLab::op). Implements `Operation` so it
13/// can be passed to `WorkflowContext::operation()` for step lifecycle tracking
14/// (step record, status transitions, duration, output persistence).
15///
16/// The endpoint is executed against the cloned [`AsyncGitlab`] client and the
17/// response is deserialized as a JSON [`Value`].
18///
19/// # Examples
20///
21/// ```no_run
22/// use ironflow_ops_gitlab::GitLab;
23/// use ironflow_core::operation::{Operation, OperationContext, NoopSecretResolver};
24/// use gitlab::api::projects;
25/// use std::sync::Arc;
26///
27/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
28/// let ctx = OperationContext::new(Arc::new(NoopSecretResolver));
29/// let gitlab = GitLab::from_context(&ctx).await?;
30///
31/// let endpoint = projects::Project::builder().project(42).build()?;
32/// let op = gitlab.op(endpoint);
33///
34/// assert_eq!(op.kind(), "gitlab");
35/// # Ok(())
36/// # }
37/// ```
38pub struct GitLabOp<E> {
39 client: AsyncGitlab,
40 endpoint: E,
41}
42
43impl<E> GitLabOp<E> {
44 pub(crate) fn new(client: AsyncGitlab, endpoint: E) -> Self {
45 Self { client, endpoint }
46 }
47}
48
49#[async_trait]
50impl<E> Operation for GitLabOp<E>
51where
52 E: Endpoint + Sync + Send,
53{
54 fn kind(&self) -> &str {
55 "gitlab"
56 }
57
58 async fn execute(&self, _ctx: &OperationContext) -> Result<Value, OperationError> {
59 // Read the raw body: a `204 No Content` (a DELETE) has none, and the
60 // JSON query would reject it.
61 let body: Vec<u8> = raw(&self.endpoint)
62 .query_async(&self.client)
63 .await
64 .map_err(|e| OperationError::Http {
65 status: None,
66 message: e.to_string(),
67 })?;
68 if body.iter().all(u8::is_ascii_whitespace) {
69 return Ok(Value::Null);
70 }
71 serde_json::from_slice(&body).map_err(|e| OperationError::Http {
72 status: None,
73 message: format!("could not parse the GitLab response: {e}"),
74 })
75 }
76
77 fn input(&self) -> Option<Value> {
78 Some(Value::Object(serde_json::Map::from_iter([(
79 "endpoint".to_string(),
80 Value::String(self.endpoint.endpoint().into_owned()),
81 )])))
82 }
83}