Skip to main content

systemprompt_provider_contracts/
job.rs

1//! [`Job`] contract for scheduled / on-startup background jobs registered
2//! via the `inventory` crate.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use async_trait::async_trait;
11use systemprompt_identifiers::Actor;
12
13use crate::error::ProviderResult;
14
15#[derive(Debug, Clone)]
16pub struct JobResult {
17    pub success: bool,
18    pub message: Option<String>,
19    pub items_processed: Option<u64>,
20    pub items_failed: Option<u64>,
21    pub duration_ms: u64,
22}
23
24impl JobResult {
25    #[must_use]
26    pub const fn success() -> Self {
27        Self {
28            success: true,
29            message: None,
30            items_processed: None,
31            items_failed: None,
32            duration_ms: 0,
33        }
34    }
35
36    #[must_use]
37    pub fn with_message(mut self, message: impl Into<String>) -> Self {
38        self.message = Some(message.into());
39        self
40    }
41
42    #[must_use]
43    pub const fn with_stats(mut self, processed: u64, failed: u64) -> Self {
44        self.items_processed = Some(processed);
45        self.items_failed = Some(failed);
46        self
47    }
48
49    #[must_use]
50    pub const fn with_duration(mut self, duration_ms: u64) -> Self {
51        self.duration_ms = duration_ms;
52        self
53    }
54
55    #[must_use]
56    pub fn failure(message: impl Into<String>) -> Self {
57        Self {
58            success: false,
59            message: Some(message.into()),
60            items_processed: None,
61            items_failed: None,
62            duration_ms: 0,
63        }
64    }
65}
66
67pub struct JobContext {
68    actor: Actor,
69    db_pool: Arc<dyn std::any::Any + Send + Sync>,
70    app_context: Arc<dyn std::any::Any + Send + Sync>,
71    app_paths: Arc<dyn std::any::Any + Send + Sync>,
72    parameters: HashMap<String, String>,
73}
74
75impl std::fmt::Debug for JobContext {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        f.debug_struct("JobContext")
78            .field("actor", &self.actor)
79            .field("db_pool", &"<type-erased>")
80            .field("app_context", &"<type-erased>")
81            .field("app_paths", &"<type-erased>")
82            .field("parameters", &self.parameters)
83            .finish()
84    }
85}
86
87impl JobContext {
88    #[must_use]
89    pub fn new(
90        actor: Actor,
91        db_pool: Arc<dyn std::any::Any + Send + Sync>,
92        app_context: Arc<dyn std::any::Any + Send + Sync>,
93        app_paths: Arc<dyn std::any::Any + Send + Sync>,
94    ) -> Self {
95        Self {
96            actor,
97            db_pool,
98            app_context,
99            app_paths,
100            parameters: HashMap::new(),
101        }
102    }
103
104    #[must_use]
105    pub const fn actor(&self) -> &Actor {
106        &self.actor
107    }
108
109    #[must_use]
110    pub fn with_parameters(mut self, parameters: HashMap<String, String>) -> Self {
111        self.parameters = parameters;
112        self
113    }
114
115    #[must_use]
116    pub fn db_pool<T: 'static>(&self) -> Option<&T> {
117        self.db_pool.as_ref().downcast_ref::<T>()
118    }
119
120    #[must_use]
121    pub fn app_context<T: 'static>(&self) -> Option<&T> {
122        self.app_context.as_ref().downcast_ref::<T>()
123    }
124
125    #[must_use]
126    pub fn app_paths<T: 'static>(&self) -> Option<&T> {
127        self.app_paths.as_ref().downcast_ref::<T>()
128    }
129
130    #[must_use]
131    pub fn db_pool_arc(&self) -> Arc<dyn std::any::Any + Send + Sync> {
132        Arc::clone(&self.db_pool)
133    }
134
135    #[must_use]
136    pub fn app_context_arc(&self) -> Arc<dyn std::any::Any + Send + Sync> {
137        Arc::clone(&self.app_context)
138    }
139
140    #[must_use]
141    pub fn app_paths_arc(&self) -> Arc<dyn std::any::Any + Send + Sync> {
142        Arc::clone(&self.app_paths)
143    }
144
145    #[must_use]
146    pub const fn parameters(&self) -> &HashMap<String, String> {
147        &self.parameters
148    }
149
150    #[must_use]
151    pub fn get_parameter(&self, key: &str) -> Option<&String> {
152        self.parameters.get(key)
153    }
154}
155
156// Why: jobs are collected as `&'static dyn Job` via `inventory`; an async fn
157// in a bare trait is not dyn-compatible, so #[async_trait] is required.
158#[async_trait]
159pub trait Job: Send + Sync + 'static {
160    fn name(&self) -> &'static str;
161
162    fn description(&self) -> &'static str {
163        ""
164    }
165
166    fn schedule(&self) -> &'static str;
167
168    fn tags(&self) -> Vec<&'static str> {
169        vec![]
170    }
171
172    async fn execute(&self, ctx: &JobContext) -> ProviderResult<JobResult>;
173
174    fn enabled(&self) -> bool {
175        true
176    }
177}
178
179inventory::collect!(&'static dyn Job);
180
181#[macro_export]
182macro_rules! submit_job {
183    ($job:expr) => {
184        inventory::submit!($job as &'static dyn $crate::Job);
185    };
186}