Skip to main content

nautilus_rs/resources/clockwork/
mod.rs

1pub mod types;
2
3use std::sync::Arc;
4
5use crate::{error::Error, http::HttpClient};
6use types::{
7    CreateCronJobParams, CreateDelayedJobParams, CronJob, DelayedJob, Execution,
8    UpdateCronJobParams,
9};
10
11/// Clockwork service client — Cron-as-a-Service.
12///
13/// Schedules recurring cron jobs and one-off delayed jobs that invoke your HTTP
14/// endpoints, and reports on their execution history.
15///
16/// Obtain a `Clockwork` instance either as part of the unified [`Verne`] client
17/// or standalone:
18///
19/// ```no_run
20/// // Standalone
21/// use nautilus_rs::Clockwork;
22/// let clockwork = Clockwork::new("vrn_clockwork_live_sk_…");
23///
24/// // Via unified client
25/// use nautilus_rs::Verne;
26/// # fn run() -> Result<(), nautilus_rs::Error> {
27/// let verne = Verne::builder().clockwork("vrn_clockwork_live_sk_…").build()?;
28/// let clockwork = verne.clockwork()?;
29/// # Ok(())
30/// # }
31/// ```
32///
33/// [`Verne`]: crate::Verne
34pub struct Clockwork {
35    http: Arc<HttpClient>,
36}
37
38impl std::fmt::Debug for Clockwork {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct("Clockwork").finish_non_exhaustive()
41    }
42}
43
44impl Clockwork {
45    /// Create a `Clockwork` client with default settings.
46    ///
47    /// Panics if the API key is empty or the HTTP client cannot be
48    /// initialised. Use [`Clockwork::builder`] for fallible construction.
49    pub fn new(api_key: impl Into<String>) -> Self {
50        Self::builder()
51            .api_key(api_key)
52            .build()
53            .expect("failed to build Clockwork client")
54    }
55
56    /// Return a [`ClockworkBuilder`] for fine-grained configuration.
57    pub fn builder() -> ClockworkBuilder {
58        ClockworkBuilder::default()
59    }
60
61    pub(crate) fn from_http(http: Arc<HttpClient>) -> Self {
62        Self { http }
63    }
64
65    /// Return a [`CronJobsClient`] for managing recurring cron jobs.
66    pub fn jobs(&self) -> CronJobsClient {
67        CronJobsClient {
68            http: Arc::clone(&self.http),
69        }
70    }
71
72    /// Return a [`DelayedJobsClient`] for managing one-off delayed jobs.
73    pub fn delayed(&self) -> DelayedJobsClient {
74        DelayedJobsClient {
75            http: Arc::clone(&self.http),
76        }
77    }
78}
79
80/// Builder for a standalone [`Clockwork`] client.
81///
82/// # Example
83///
84/// ```no_run
85/// use nautilus_rs::Clockwork;
86///
87/// let clockwork = Clockwork::builder()
88///     .api_key("vrn_clockwork_live_sk_…")
89///     .timeout_secs(15)
90///     .build()
91///     .expect("invalid configuration");
92/// ```
93#[derive(Default)]
94pub struct ClockworkBuilder {
95    api_key: Option<String>,
96    base_url: Option<String>,
97    timeout_secs: Option<u64>,
98}
99
100impl ClockworkBuilder {
101    /// Set the Clockwork API key (**required**).
102    pub fn api_key(mut self, key: impl Into<String>) -> Self {
103        self.api_key = Some(key.into());
104        self
105    }
106
107    /// Override the API base URL (default: `https://api.vernesoft.com`).
108    pub fn base_url(mut self, url: impl Into<String>) -> Self {
109        self.base_url = Some(url.into());
110        self
111    }
112
113    /// Set the HTTP request timeout in seconds (default: `30`).
114    pub fn timeout_secs(mut self, secs: u64) -> Self {
115        self.timeout_secs = Some(secs);
116        self
117    }
118
119    /// Consume the builder and return a configured [`Clockwork`].
120    ///
121    /// # Errors
122    ///
123    /// Returns [`Error::Config`] if the API key was not set.
124    pub fn build(self) -> Result<Clockwork, Error> {
125        let key = self
126            .api_key
127            .ok_or_else(|| Error::Config("clockwork API key is required".into()))?;
128        let http = HttpClient::new(key, self.base_url, self.timeout_secs)?;
129        Ok(Clockwork {
130            http: Arc::new(http),
131        })
132    }
133}
134
135/// Access to the `/v1/clockwork/jobs` endpoints.
136///
137/// Obtain via [`Clockwork::jobs`].
138pub struct CronJobsClient {
139    http: Arc<HttpClient>,
140}
141
142impl CronJobsClient {
143    /// List all cron jobs.
144    ///
145    /// Maps to `GET /v1/clockwork/jobs`.
146    ///
147    /// # Example
148    ///
149    /// ```no_run
150    /// use nautilus_rs::Clockwork;
151    ///
152    /// # async fn run() -> Result<(), nautilus_rs::Error> {
153    /// let clockwork = Clockwork::new("vrn_clockwork_live_sk_…");
154    /// for job in clockwork.jobs().list().await? {
155    ///     println!("{} — {}", job.name, job.schedule);
156    /// }
157    /// # Ok(())
158    /// # }
159    /// ```
160    pub async fn list(&self) -> Result<Vec<CronJob>, Error> {
161        self.http.get("/v1/clockwork/jobs").await
162    }
163
164    /// Create a new cron job.
165    ///
166    /// Maps to `POST /v1/clockwork/jobs`.
167    ///
168    /// # Example
169    ///
170    /// ```no_run
171    /// use nautilus_rs::{Clockwork, CreateCronJobParams};
172    ///
173    /// # async fn run() -> Result<(), nautilus_rs::Error> {
174    /// let clockwork = Clockwork::new("vrn_clockwork_live_sk_…");
175    /// let job = clockwork.jobs().create(CreateCronJobParams {
176    ///     name: "nightly-report".into(),
177    ///     schedule: "0 2 * * *".into(),
178    ///     url: "https://example.com/hooks/report".into(),
179    ///     ..Default::default()
180    /// }).await?;
181    /// println!("created job {}", job.id);
182    /// # Ok(())
183    /// # }
184    /// ```
185    pub async fn create(&self, params: CreateCronJobParams) -> Result<CronJob, Error> {
186        self.http.post("/v1/clockwork/jobs", &params, false).await
187    }
188
189    /// Partially update an existing cron job.
190    ///
191    /// Only the fields set on `params` are sent; the rest are left unchanged.
192    /// Maps to `PATCH /v1/clockwork/jobs/{id}`.
193    pub async fn update(
194        &self,
195        job_id: &str,
196        params: UpdateCronJobParams,
197    ) -> Result<CronJob, Error> {
198        self.http
199            .patch(&format!("/v1/clockwork/jobs/{job_id}"), &params)
200            .await
201    }
202
203    /// Permanently delete a cron job.
204    ///
205    /// Maps to `DELETE /v1/clockwork/jobs/{id}`.
206    pub async fn delete(&self, job_id: &str) -> Result<(), Error> {
207        self.http
208            .delete(&format!("/v1/clockwork/jobs/{job_id}"))
209            .await
210    }
211
212    /// List the execution history for a cron job.
213    ///
214    /// Maps to `GET /v1/clockwork/jobs/{id}/executions`.
215    pub async fn executions(&self, job_id: &str) -> Result<Vec<Execution>, Error> {
216        self.http
217            .get(&format!("/v1/clockwork/jobs/{job_id}/executions"))
218            .await
219    }
220}
221
222/// Access to the `/v1/clockwork/delayed` endpoints.
223///
224/// Obtain via [`Clockwork::delayed`].
225pub struct DelayedJobsClient {
226    http: Arc<HttpClient>,
227}
228
229impl DelayedJobsClient {
230    /// List all delayed jobs.
231    ///
232    /// Maps to `GET /v1/clockwork/delayed`.
233    ///
234    /// # Example
235    ///
236    /// ```no_run
237    /// use nautilus_rs::Clockwork;
238    ///
239    /// # async fn run() -> Result<(), nautilus_rs::Error> {
240    /// let clockwork = Clockwork::new("vrn_clockwork_live_sk_…");
241    /// for job in clockwork.delayed().list().await? {
242    ///     println!("{} runs at {}", job.name, job.run_at);
243    /// }
244    /// # Ok(())
245    /// # }
246    /// ```
247    pub async fn list(&self) -> Result<Vec<DelayedJob>, Error> {
248        self.http.get("/v1/clockwork/delayed").await
249    }
250
251    /// Schedule a new one-off delayed job.
252    ///
253    /// Maps to `POST /v1/clockwork/delayed`.
254    ///
255    /// # Example
256    ///
257    /// ```no_run
258    /// use nautilus_rs::{Clockwork, CreateDelayedJobParams};
259    ///
260    /// # async fn run() -> Result<(), nautilus_rs::Error> {
261    /// let clockwork = Clockwork::new("vrn_clockwork_live_sk_…");
262    /// let job = clockwork.delayed().create(CreateDelayedJobParams {
263    ///     name: "send-reminder".into(),
264    ///     run_at: "2026-01-01T12:00:00Z".into(),
265    ///     url: "https://example.com/hooks/reminder".into(),
266    ///     ..Default::default()
267    /// }).await?;
268    /// println!("scheduled job {}", job.id);
269    /// # Ok(())
270    /// # }
271    /// ```
272    pub async fn create(&self, params: CreateDelayedJobParams) -> Result<DelayedJob, Error> {
273        self.http
274            .post("/v1/clockwork/delayed", &params, false)
275            .await
276    }
277
278    /// Cancel a pending delayed job.
279    ///
280    /// Maps to `DELETE /v1/clockwork/delayed/{id}`.
281    pub async fn cancel(&self, job_id: &str) -> Result<(), Error> {
282        self.http
283            .delete(&format!("/v1/clockwork/delayed/{job_id}"))
284            .await
285    }
286
287    /// List the execution history for a delayed job.
288    ///
289    /// Maps to `GET /v1/clockwork/delayed/{id}/executions`.
290    pub async fn executions(&self, job_id: &str) -> Result<Vec<Execution>, Error> {
291        self.http
292            .get(&format!("/v1/clockwork/delayed/{job_id}/executions"))
293            .await
294    }
295}