1use crate::prelude::*;
2use crate::{DryContext, WetContext};
3
4use serde_json::Value as JsonValue;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct OpMetadata {
9 pub name: String,
10 pub input_schema: Option<JsonValue>,
11 pub reference_schema: Option<JsonValue>,
12 pub output_schema: Option<JsonValue>,
13 pub description: Option<String>,
14}
15
16impl OpMetadata {
17 pub fn builder(name: impl Into<String>) -> OpMetadataBuilder {
18 OpMetadataBuilder {
19 name: name.into(),
20 input_schema: None,
21 reference_schema: None,
22 output_schema: None,
23 description: None,
24 }
25 }
26
27 pub fn validate_dry_context(&self, ctx: &DryContext) -> Result<ValidationReport, OpError> {
29 if let Some(schema) = &self.input_schema {
30 let values = ctx.values();
31 let context_json = serde_json::to_value(values)?;
32 validate_against_schema(&context_json, schema)
33 } else {
34 Ok(ValidationReport::success())
35 }
36 }
37
38 pub fn validate_wet_context(&self, ctx: &WetContext) -> Result<ValidationReport, OpError> {
40 if let Some(schema) = &self.reference_schema {
41 let mut context_keys = serde_json::Map::new();
44 for key in ctx.keys() {
45 context_keys.insert(
46 key.clone(),
47 serde_json::Value::String("present".to_string()),
48 );
49 }
50 let context_json = serde_json::Value::Object(context_keys);
51 validate_reference_schema(&context_json, schema)
52 } else {
53 Ok(ValidationReport::success())
54 }
55 }
56
57 pub fn validate_contexts(
59 &self,
60 dry: &DryContext,
61 wet: &WetContext,
62 ) -> Result<ValidationReport, OpError> {
63 let dry_report = self.validate_dry_context(dry)?;
64 let wet_report = self.validate_wet_context(wet)?;
65
66 Ok(ValidationReport {
67 is_valid: dry_report.is_valid && wet_report.is_valid,
68 errors: [dry_report.errors, wet_report.errors].concat(),
69 warnings: [dry_report.warnings, wet_report.warnings].concat(),
70 })
71 }
72
73 pub fn validate_output<T: Serialize>(&self, output: &T) -> Result<ValidationReport, OpError> {
75 if let Some(schema) = &self.output_schema {
76 let output_json = serde_json::to_value(output)?;
77 validate_against_schema(&output_json, schema)
78 } else {
79 Ok(ValidationReport::success())
80 }
81 }
82}
83
84pub struct OpMetadataBuilder {
85 name: String,
86 input_schema: Option<JsonValue>,
87 reference_schema: Option<JsonValue>,
88 output_schema: Option<JsonValue>,
89 description: Option<String>,
90}
91
92impl OpMetadataBuilder {
93 pub fn input_schema(mut self, schema: JsonValue) -> Self {
94 self.input_schema = Some(schema);
95 self
96 }
97
98 pub fn reference_schema(mut self, schema: JsonValue) -> Self {
99 self.reference_schema = Some(schema);
100 self
101 }
102
103 pub fn output_schema(mut self, schema: JsonValue) -> Self {
104 self.output_schema = Some(schema);
105 self
106 }
107
108 pub fn description(mut self, desc: impl Into<String>) -> Self {
109 self.description = Some(desc.into());
110 self
111 }
112
113 pub fn build(self) -> OpMetadata {
114 OpMetadata {
115 name: self.name,
116 input_schema: self.input_schema,
117 reference_schema: self.reference_schema,
118 output_schema: self.output_schema,
119 description: self.description,
120 }
121 }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct ValidationReport {
127 pub is_valid: bool,
128 pub errors: Vec<ValidationError>,
129 pub warnings: Vec<ValidationWarning>,
130}
131
132impl ValidationReport {
133 pub fn success() -> Self {
134 Self {
135 is_valid: true,
136 errors: vec![],
137 warnings: vec![],
138 }
139 }
140
141 pub fn is_fully_valid(&self) -> bool {
142 self.is_valid && self.warnings.is_empty()
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct ValidationError {
148 pub field: String,
149 pub message: String,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct ValidationWarning {
154 pub field: String,
155 pub message: String,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct TriggerFuse {
161 pub id: String,
162 pub trigger_name: String,
163 pub dry_context: DryContext,
164 pub created_at: chrono::DateTime<chrono::Utc>,
165 pub metadata: Option<OpMetadata>,
166}
167
168impl TriggerFuse {
169 pub fn new(trigger_name: impl Into<String>) -> Self {
170 Self {
171 id: uuid::Uuid::new_v4().to_string(),
172 trigger_name: trigger_name.into(),
173 dry_context: DryContext::new(),
174 created_at: chrono::Utc::now(),
175 metadata: None,
176 }
177 }
178
179 pub fn with_data<T: Serialize>(mut self, key: impl Into<String>, value: T) -> Self {
180 self.dry_context.insert(key, value);
181 self
182 }
183
184 pub fn with_metadata(mut self, metadata: OpMetadata) -> Self {
185 self.metadata = Some(metadata);
186 self
187 }
188
189 pub fn validate_and_get_dry_context(&self) -> Result<DryContext, OpError> {
190 if let Some(metadata) = &self.metadata {
191 let report = metadata.validate_dry_context(&self.dry_context)?;
192 if !report.is_valid {
193 return Err(OpError::Context(format!(
194 "Invalid dry context: {:?}",
195 report.errors
196 )));
197 }
198 }
199 Ok(self.dry_context.clone())
200 }
201}
202
203fn validate_against_schema(
205 value: &JsonValue,
206 schema: &JsonValue,
207) -> Result<ValidationReport, OpError> {
208 let mut errors = vec![];
212 let warnings = vec![];
213
214 if let (Some(schema_obj), Some(value_obj)) = (schema.as_object(), value.as_object()) {
215 if let Some(required) = schema_obj.get("required").and_then(|r| r.as_array()) {
217 for req_field in required {
218 if let Some(field_name) = req_field.as_str() {
219 if !value_obj.contains_key(field_name) {
220 errors.push(ValidationError {
221 field: field_name.to_string(),
222 message: format!("Required field '{}' is missing", field_name),
223 });
224 }
225 }
226 }
227 }
228 }
229
230 Ok(ValidationReport {
231 is_valid: errors.is_empty(),
232 errors,
233 warnings,
234 })
235}
236
237fn validate_reference_schema(
238 value: &JsonValue,
239 schema: &JsonValue,
240) -> Result<ValidationReport, OpError> {
241 let mut errors = vec![];
243
244 if let (Some(schema_obj), Some(value_obj)) = (schema.as_object(), value.as_object()) {
245 if let Some(required) = schema_obj.get("required").and_then(|r| r.as_array()) {
246 for req_field in required {
247 if let Some(field_name) = req_field.as_str() {
248 if !value_obj.contains_key(field_name) {
249 errors.push(ValidationError {
250 field: field_name.to_string(),
251 message: format!("Required reference '{}' is missing", field_name),
252 });
253 }
254 }
255 }
256 }
257 }
258
259 Ok(ValidationReport {
260 is_valid: errors.is_empty(),
261 errors,
262 warnings: vec![],
263 })
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use serde_json::json;
270
271 #[test]
273 fn test0021_metadata_builder() {
274 let metadata = OpMetadata::builder("TestOp")
275 .description("A test operation")
276 .input_schema(json!({
277 "type": "object",
278 "properties": {
279 "name": { "type": "string" }
280 },
281 "required": ["name"]
282 }))
283 .output_schema(json!({
284 "type": "string"
285 }))
286 .build();
287
288 assert_eq!(metadata.name, "TestOp");
289 assert_eq!(metadata.description.as_deref(), Some("A test operation"));
290 assert!(metadata.input_schema.is_some());
291 assert!(metadata.output_schema.is_some());
292 }
293
294 #[test]
296 fn test0022_trigger_fuse() {
297 let request = TriggerFuse::new("ProcessImage")
298 .with_data("image_path", "/tmp/test.jpg")
299 .with_data("width", 800);
300
301 assert_eq!(request.trigger_name, "ProcessImage");
302 assert_eq!(
303 request.dry_context.get::<String>("image_path").unwrap(),
304 "/tmp/test.jpg"
305 );
306 assert_eq!(request.dry_context.get::<i32>("width").unwrap(), 800);
307 }
308
309 #[test]
311 fn test0023_basic_validation() {
312 let metadata = OpMetadata::builder("TestOp")
313 .input_schema(json!({
314 "type": "object",
315 "required": ["name"]
316 }))
317 .build();
318
319 let ctx = DryContext::new().with_value("name", "test");
320 let report = metadata.validate_dry_context(&ctx).unwrap();
321 assert!(report.is_valid);
322
323 let empty_ctx = DryContext::new();
324 let report = metadata.validate_dry_context(&empty_ctx).unwrap();
325 assert!(!report.is_valid);
326 assert_eq!(report.errors.len(), 1);
327 }
328}