1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::environment::EnvironmentConfig;
10use crate::mounts::Mount;
11use crate::resources::ResourceConfig;
12use crate::runtime::RuntimeOverrides;
13use crate::secrets::SecretsConfig;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub struct ExecutionContext {
26 pub id: String,
28
29 pub name: String,
31
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub description: Option<String>,
35
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub inherits_from: Option<String>,
39
40 #[serde(default)]
42 pub mounts: Vec<Mount>,
43
44 #[serde(default)]
46 pub environment: EnvironmentConfig,
47
48 #[serde(default)]
50 pub secrets: SecretsConfig,
51
52 #[serde(default)]
54 pub resources: ResourceConfig,
55
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub runtime_overrides: Option<RuntimeOverrides>,
59
60 #[serde(default)]
62 pub metadata: ContextMetadata,
63}
64
65impl ExecutionContext {
66 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
68 Self {
69 id: id.into(),
70 name: name.into(),
71 description: None,
72 inherits_from: None,
73 mounts: Vec::new(),
74 environment: EnvironmentConfig::default(),
75 secrets: SecretsConfig::default(),
76 resources: ResourceConfig::default(),
77 runtime_overrides: None,
78 metadata: ContextMetadata::new(),
79 }
80 }
81
82 pub fn inheriting(
84 id: impl Into<String>,
85 name: impl Into<String>,
86 parent_id: impl Into<String>,
87 ) -> Self {
88 let mut ctx = Self::new(id, name);
89 ctx.inherits_from = Some(parent_id.into());
90 ctx
91 }
92
93 pub fn with_description(mut self, description: impl Into<String>) -> Self {
95 self.description = Some(description.into());
96 self
97 }
98
99 pub fn with_mount(mut self, mount: Mount) -> Self {
101 self.mounts.push(mount);
102 self
103 }
104
105 pub fn with_environment(mut self, environment: EnvironmentConfig) -> Self {
107 self.environment = environment;
108 self
109 }
110
111 pub fn with_secrets(mut self, secrets: SecretsConfig) -> Self {
113 self.secrets = secrets;
114 self
115 }
116
117 pub fn with_resources(mut self, resources: ResourceConfig) -> Self {
119 self.resources = resources;
120 self
121 }
122
123 pub fn with_runtime_overrides(mut self, overrides: RuntimeOverrides) -> Self {
125 self.runtime_overrides = Some(overrides);
126 self
127 }
128
129 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
131 self.metadata.tags.push(tag.into());
132 self
133 }
134
135 pub fn has_parent(&self) -> bool {
137 self.inherits_from.is_some()
138 }
139
140 pub fn required_secrets(&self) -> Vec<&str> {
142 self.secrets
143 .secrets
144 .iter()
145 .filter(|(_, def)| def.required)
146 .map(|(key, _)| key.as_str())
147 .collect()
148 }
149
150 pub fn required_mounts(&self) -> Vec<&Mount> {
152 self.mounts.iter().filter(|m| m.required).collect()
153 }
154
155 pub fn touch(&mut self) {
157 self.metadata.updated_at = Utc::now();
158 self.metadata.version += 1;
159 }
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164#[serde(rename_all = "snake_case")]
165pub struct ContextMetadata {
166 pub created_at: DateTime<Utc>,
168
169 pub updated_at: DateTime<Utc>,
171
172 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub created_by: Option<String>,
175
176 #[serde(default, skip_serializing_if = "Vec::is_empty")]
178 pub tags: Vec<String>,
179
180 #[serde(default = "default_version")]
182 pub version: u32,
183}
184
185fn default_version() -> u32 {
186 1
187}
188
189impl ContextMetadata {
190 pub fn new() -> Self {
192 let now = Utc::now();
193 Self {
194 created_at: now,
195 updated_at: now,
196 created_by: None,
197 tags: Vec::new(),
198 version: 1,
199 }
200 }
201
202 pub fn with_creator(mut self, creator: impl Into<String>) -> Self {
204 self.created_by = Some(creator.into());
205 self
206 }
207}
208
209impl Default for ContextMetadata {
210 fn default() -> Self {
211 Self::new()
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn test_context_creation() {
221 let ctx = ExecutionContext::new("test-ctx", "Test Context")
222 .with_description("A test context")
223 .with_tag("test")
224 .with_tag("development");
225
226 assert_eq!(ctx.id, "test-ctx");
227 assert_eq!(ctx.name, "Test Context");
228 assert_eq!(ctx.description, Some("A test context".to_string()));
229 assert_eq!(ctx.metadata.tags, vec!["test", "development"]);
230 assert!(!ctx.has_parent());
231 }
232
233 #[test]
234 fn test_context_inheritance() {
235 let ctx = ExecutionContext::inheriting("child-ctx", "Child Context", "parent-ctx");
236
237 assert!(ctx.has_parent());
238 assert_eq!(ctx.inherits_from, Some("parent-ctx".to_string()));
239 }
240
241 #[test]
242 fn test_context_serialization() {
243 let ctx = ExecutionContext::new("test", "Test");
244 let json = serde_json::to_string(&ctx).unwrap();
245 let deserialized: ExecutionContext = serde_json::from_str(&json).unwrap();
246
247 assert_eq!(ctx.id, deserialized.id);
248 assert_eq!(ctx.name, deserialized.name);
249 }
250
251 #[test]
252 fn test_context_toml_serialization() {
253 let ctx = ExecutionContext::new("test", "Test")
254 .with_description("Test context")
255 .with_tag("test");
256
257 let toml_str = toml::to_string_pretty(&ctx).unwrap();
258 let deserialized: ExecutionContext = toml::from_str(&toml_str).unwrap();
259
260 assert_eq!(ctx.id, deserialized.id);
261 assert_eq!(ctx.description, deserialized.description);
262 }
263
264 #[test]
265 fn test_metadata_versioning() {
266 let mut ctx = ExecutionContext::new("test", "Test");
267 assert_eq!(ctx.metadata.version, 1);
268
269 ctx.touch();
270 assert_eq!(ctx.metadata.version, 2);
271
272 ctx.touch();
273 assert_eq!(ctx.metadata.version, 3);
274 }
275}