1use crate::{NodeKind, Workflow};
7use serde::{Deserialize, Serialize};
8use serde_json::{json, Value};
9use std::collections::HashMap;
10
11#[cfg(feature = "openapi")]
12use utoipa::ToSchema;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16#[cfg_attr(feature = "openapi", derive(ToSchema))]
17pub struct JsonSchema {
18 #[serde(rename = "$schema")]
20 pub schema: String,
21
22 #[serde(rename = "$id", skip_serializing_if = "Option::is_none")]
24 pub id: Option<String>,
25
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub title: Option<String>,
29
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub description: Option<String>,
33
34 #[serde(rename = "type")]
36 pub schema_type: String,
37
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub properties: Option<HashMap<String, JsonSchema>>,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub required: Option<Vec<String>>,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub items: Option<Box<JsonSchema>>,
49
50 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
52 pub enum_values: Option<Vec<Value>>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub additional_properties: Option<Box<JsonSchema>>,
57
58 #[serde(rename = "$defs", skip_serializing_if = "Option::is_none")]
60 pub definitions: Option<HashMap<String, JsonSchema>>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub minimum: Option<f64>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub maximum: Option<f64>,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub pattern: Option<String>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub format: Option<String>,
77
78 #[serde(rename = "oneOf", skip_serializing_if = "Option::is_none")]
80 pub one_of: Option<Vec<JsonSchema>>,
81
82 #[serde(rename = "anyOf", skip_serializing_if = "Option::is_none")]
84 pub any_of: Option<Vec<JsonSchema>>,
85
86 #[serde(rename = "allOf", skip_serializing_if = "Option::is_none")]
88 pub all_of: Option<Vec<JsonSchema>>,
89
90 #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
92 pub reference: Option<String>,
93}
94
95impl JsonSchema {
96 pub fn new(schema_type: &str) -> Self {
98 Self {
99 schema: "https://json-schema.org/draft/2020-12/schema".to_string(),
100 id: None,
101 title: None,
102 description: None,
103 schema_type: schema_type.to_string(),
104 properties: None,
105 required: None,
106 items: None,
107 enum_values: None,
108 additional_properties: None,
109 definitions: None,
110 minimum: None,
111 maximum: None,
112 pattern: None,
113 format: None,
114 one_of: None,
115 any_of: None,
116 all_of: None,
117 reference: None,
118 }
119 }
120
121 pub fn string() -> Self {
123 Self::new("string")
124 }
125
126 pub fn number() -> Self {
128 Self::new("number")
129 }
130
131 pub fn integer() -> Self {
133 Self::new("integer")
134 }
135
136 pub fn boolean() -> Self {
138 Self::new("boolean")
139 }
140
141 pub fn array(items: JsonSchema) -> Self {
143 let mut schema = Self::new("array");
144 schema.items = Some(Box::new(items));
145 schema
146 }
147
148 pub fn object() -> Self {
150 let mut schema = Self::new("object");
151 schema.properties = Some(HashMap::new());
152 schema
153 }
154
155 pub fn reference(ref_path: &str) -> Self {
157 let mut schema = Self::new("object");
158 schema.reference = Some(ref_path.to_string());
159 schema
160 }
161
162 pub fn with_title(mut self, title: String) -> Self {
164 self.title = Some(title);
165 self
166 }
167
168 pub fn with_description(mut self, description: String) -> Self {
170 self.description = Some(description);
171 self
172 }
173
174 pub fn with_id(mut self, id: String) -> Self {
176 self.id = Some(id);
177 self
178 }
179
180 pub fn add_property(&mut self, name: String, schema: JsonSchema) {
182 if self.properties.is_none() {
183 self.properties = Some(HashMap::new());
184 }
185 if let Some(props) = &mut self.properties {
186 props.insert(name, schema);
187 }
188 }
189
190 pub fn add_required(&mut self, name: String) {
192 if self.required.is_none() {
193 self.required = Some(Vec::new());
194 }
195 if let Some(req) = &mut self.required {
196 req.push(name);
197 }
198 }
199
200 pub fn with_enum(mut self, values: Vec<Value>) -> Self {
202 self.enum_values = Some(values);
203 self
204 }
205
206 pub fn with_pattern(mut self, pattern: String) -> Self {
208 self.pattern = Some(pattern);
209 self
210 }
211
212 pub fn with_format(mut self, format: String) -> Self {
214 self.format = Some(format);
215 self
216 }
217
218 pub fn add_definition(&mut self, name: String, schema: JsonSchema) {
220 if self.definitions.is_none() {
221 self.definitions = Some(HashMap::new());
222 }
223 if let Some(defs) = &mut self.definitions {
224 defs.insert(name, schema);
225 }
226 }
227}
228
229pub struct WorkflowSchemaGenerator {
231 pub include_optional: bool,
233
234 pub include_examples: bool,
236
237 pub include_descriptions: bool,
239}
240
241impl WorkflowSchemaGenerator {
242 pub fn new() -> Self {
244 Self {
245 include_optional: true,
246 include_examples: false,
247 include_descriptions: true,
248 }
249 }
250
251 pub fn generate_workflow_schema(&self) -> JsonSchema {
253 let mut schema = JsonSchema::object()
254 .with_id("https://oxify.dev/schemas/workflow.json".to_string())
255 .with_title("Workflow".to_string());
256
257 if self.include_descriptions {
258 schema =
259 schema.with_description("A workflow defining a sequence of operations".to_string());
260 }
261
262 schema.add_property(
264 "id".to_string(),
265 JsonSchema::string().with_format("uuid".to_string()),
266 );
267 schema.add_required("id".to_string());
268
269 schema.add_property("metadata".to_string(), self.generate_metadata_schema());
270 schema.add_required("metadata".to_string());
271
272 schema.add_property(
273 "nodes".to_string(),
274 JsonSchema::array(self.generate_node_schema()),
275 );
276 schema.add_required("nodes".to_string());
277
278 schema.add_property(
279 "edges".to_string(),
280 JsonSchema::array(self.generate_edge_schema()),
281 );
282 schema.add_required("edges".to_string());
283
284 self.add_node_type_definitions(&mut schema);
286
287 schema
288 }
289
290 fn generate_metadata_schema(&self) -> JsonSchema {
292 let mut schema = JsonSchema::object();
293
294 if self.include_descriptions {
295 schema = schema.with_description("Workflow metadata".to_string());
296 }
297
298 schema.add_property("name".to_string(), JsonSchema::string());
299 schema.add_required("name".to_string());
300
301 schema.add_property("description".to_string(), JsonSchema::string());
302
303 schema.add_property("version".to_string(), JsonSchema::string());
304 schema.add_required("version".to_string());
305
306 schema.add_property(
307 "created_at".to_string(),
308 JsonSchema::string().with_format("date-time".to_string()),
309 );
310 schema.add_required("created_at".to_string());
311
312 schema.add_property(
313 "updated_at".to_string(),
314 JsonSchema::string().with_format("date-time".to_string()),
315 );
316 schema.add_required("updated_at".to_string());
317
318 schema.add_property("tags".to_string(), JsonSchema::array(JsonSchema::string()));
319
320 schema
321 }
322
323 fn generate_node_schema(&self) -> JsonSchema {
325 let mut schema = JsonSchema::object();
326
327 if self.include_descriptions {
328 schema = schema.with_description("A workflow node".to_string());
329 }
330
331 schema.add_property(
332 "id".to_string(),
333 JsonSchema::string().with_format("uuid".to_string()),
334 );
335 schema.add_required("id".to_string());
336
337 schema.add_property("name".to_string(), JsonSchema::string());
338 schema.add_required("name".to_string());
339
340 schema.add_property("kind".to_string(), self.generate_node_kind_schema());
341 schema.add_required("kind".to_string());
342
343 schema
344 }
345
346 fn generate_node_kind_schema(&self) -> JsonSchema {
348 JsonSchema::string().with_enum(vec![
349 json!("Start"),
350 json!("End"),
351 json!("LLM"),
352 json!("Retriever"),
353 json!("Code"),
354 json!("IfElse"),
355 json!("Tool"),
356 json!("Loop"),
357 json!("TryCatch"),
358 json!("SubWorkflow"),
359 json!("Switch"),
360 json!("Parallel"),
361 json!("Approval"),
362 json!("Form"),
363 ])
364 }
365
366 fn generate_edge_schema(&self) -> JsonSchema {
368 let mut schema = JsonSchema::object();
369
370 if self.include_descriptions {
371 schema = schema.with_description("A workflow edge connecting two nodes".to_string());
372 }
373
374 schema.add_property(
375 "id".to_string(),
376 JsonSchema::string().with_format("uuid".to_string()),
377 );
378 schema.add_required("id".to_string());
379
380 schema.add_property(
381 "from".to_string(),
382 JsonSchema::string().with_format("uuid".to_string()),
383 );
384 schema.add_required("from".to_string());
385
386 schema.add_property(
387 "to".to_string(),
388 JsonSchema::string().with_format("uuid".to_string()),
389 );
390 schema.add_required("to".to_string());
391
392 schema
393 }
394
395 fn add_node_type_definitions(&self, schema: &mut JsonSchema) {
397 let mut llm_config = JsonSchema::object();
399 llm_config.add_property("model".to_string(), JsonSchema::string());
400 llm_config.add_property("prompt".to_string(), JsonSchema::string());
401 llm_config.add_property("temperature".to_string(), JsonSchema::number());
402 llm_config.add_property("max_tokens".to_string(), JsonSchema::integer());
403 schema.add_definition("LlmConfig".to_string(), llm_config);
404
405 let mut script_config = JsonSchema::object();
407 script_config.add_property(
408 "language".to_string(),
409 JsonSchema::string().with_enum(vec![
410 json!("Python"),
411 json!("JavaScript"),
412 json!("TypeScript"),
413 json!("Bash"),
414 ]),
415 );
416 script_config.add_property("code".to_string(), JsonSchema::string());
417 schema.add_definition("ScriptConfig".to_string(), script_config);
418
419 let mut condition = JsonSchema::object();
421 condition.add_property("expression".to_string(), JsonSchema::string());
422 schema.add_definition("Condition".to_string(), condition);
423 }
424
425 pub fn generate_node_type_schema(&self, node_kind: &NodeKind) -> JsonSchema {
427 match node_kind {
428 NodeKind::Start | NodeKind::End => {
429 JsonSchema::object().with_description(format!("{:?} node", node_kind))
430 }
431 NodeKind::LLM(_) => JsonSchema::reference("#/$defs/LlmConfig"),
432 NodeKind::Code(_) => JsonSchema::reference("#/$defs/ScriptConfig"),
433 _ => {
434 JsonSchema::object().with_description(format!("{:?} node configuration", node_kind))
435 }
436 }
437 }
438
439 pub fn validate_workflow(&self, workflow: &Workflow) -> Result<(), Vec<String>> {
441 let mut errors = Vec::new();
442
443 if workflow.nodes.is_empty() {
445 errors.push("Workflow must have at least one node".to_string());
446 }
447
448 if !workflow
450 .nodes
451 .iter()
452 .any(|n| matches!(n.kind, NodeKind::Start))
453 {
454 errors.push("Workflow must have a Start node".to_string());
455 }
456
457 if !workflow
459 .nodes
460 .iter()
461 .any(|n| matches!(n.kind, NodeKind::End))
462 {
463 errors.push("Workflow must have an End node".to_string());
464 }
465
466 if errors.is_empty() {
467 Ok(())
468 } else {
469 Err(errors)
470 }
471 }
472}
473
474impl Default for WorkflowSchemaGenerator {
475 fn default() -> Self {
476 Self::new()
477 }
478}
479
480pub fn generate_workflow_schema() -> JsonSchema {
482 WorkflowSchemaGenerator::new().generate_workflow_schema()
483}
484
485pub fn schema_to_json(schema: &JsonSchema) -> Result<String, serde_json::Error> {
487 serde_json::to_string_pretty(schema)
488}
489
490pub fn schema_to_value(schema: &JsonSchema) -> Result<Value, serde_json::Error> {
492 serde_json::to_value(schema)
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use crate::{Edge, Node, NodeKind};
499
500 #[test]
501 fn test_create_basic_schema() {
502 let schema = JsonSchema::string();
503 assert_eq!(schema.schema_type, "string");
504 assert_eq!(
505 schema.schema,
506 "https://json-schema.org/draft/2020-12/schema"
507 );
508 }
509
510 #[test]
511 fn test_create_object_schema() {
512 let mut schema = JsonSchema::object();
513 schema.add_property("name".to_string(), JsonSchema::string());
514 schema.add_required("name".to_string());
515
516 assert_eq!(schema.schema_type, "object");
517 assert!(schema.properties.is_some());
518 assert_eq!(schema.properties.as_ref().unwrap().len(), 1);
519 assert_eq!(schema.required.as_ref().unwrap().len(), 1);
520 }
521
522 #[test]
523 fn test_create_array_schema() {
524 let schema = JsonSchema::array(JsonSchema::string());
525 assert_eq!(schema.schema_type, "array");
526 assert!(schema.items.is_some());
527 }
528
529 #[test]
530 fn test_enum_schema() {
531 let schema = JsonSchema::string().with_enum(vec![
532 json!("option1"),
533 json!("option2"),
534 json!("option3"),
535 ]);
536
537 assert!(schema.enum_values.is_some());
538 assert_eq!(schema.enum_values.as_ref().unwrap().len(), 3);
539 }
540
541 #[test]
542 fn test_generate_workflow_schema() {
543 let generator = WorkflowSchemaGenerator::new();
544 let schema = generator.generate_workflow_schema();
545
546 assert_eq!(schema.schema_type, "object");
547 assert!(schema.properties.is_some());
548 assert!(schema.required.is_some());
549
550 let props = schema.properties.as_ref().unwrap();
551 assert!(props.contains_key("id"));
552 assert!(props.contains_key("metadata"));
553 assert!(props.contains_key("nodes"));
554 assert!(props.contains_key("edges"));
555
556 let required = schema.required.as_ref().unwrap();
557 assert!(required.contains(&"id".to_string()));
558 assert!(required.contains(&"metadata".to_string()));
559 assert!(required.contains(&"nodes".to_string()));
560 assert!(required.contains(&"edges".to_string()));
561 }
562
563 #[test]
564 fn test_schema_serialization() {
565 let schema = JsonSchema::string()
566 .with_title("Name".to_string())
567 .with_description("A person's name".to_string());
568
569 let json = schema_to_json(&schema).unwrap();
570 assert!(json.contains("Name"));
571 assert!(json.contains("person's name"));
572 }
573
574 #[test]
575 fn test_validate_workflow_missing_start() {
576 let mut workflow = Workflow::new("Test".to_string());
577 let end_node = Node::new("End".to_string(), NodeKind::End);
578 workflow.add_node(end_node);
579
580 let generator = WorkflowSchemaGenerator::new();
581 let result = generator.validate_workflow(&workflow);
582
583 assert!(result.is_err());
584 let errors = result.unwrap_err();
585 assert!(errors.iter().any(|e| e.contains("Start")));
586 }
587
588 #[test]
589 fn test_validate_workflow_missing_end() {
590 let mut workflow = Workflow::new("Test".to_string());
591 let start_node = Node::new("Start".to_string(), NodeKind::Start);
592 workflow.add_node(start_node);
593
594 let generator = WorkflowSchemaGenerator::new();
595 let result = generator.validate_workflow(&workflow);
596
597 assert!(result.is_err());
598 let errors = result.unwrap_err();
599 assert!(errors.iter().any(|e| e.contains("End")));
600 }
601
602 #[test]
603 fn test_validate_valid_workflow() {
604 let mut workflow = Workflow::new("Test".to_string());
605
606 let start_node = Node::new("Start".to_string(), NodeKind::Start);
607 let start_id = start_node.id;
608 workflow.add_node(start_node);
609
610 let end_node = Node::new("End".to_string(), NodeKind::End);
611 let end_id = end_node.id;
612 workflow.add_node(end_node);
613
614 workflow.add_edge(Edge::new(start_id, end_id));
615
616 let generator = WorkflowSchemaGenerator::new();
617 let result = generator.validate_workflow(&workflow);
618
619 assert!(result.is_ok());
620 }
621
622 #[test]
623 fn test_node_kind_schema() {
624 let generator = WorkflowSchemaGenerator::new();
625 let schema = generator.generate_node_kind_schema();
626
627 assert_eq!(schema.schema_type, "string");
628 assert!(schema.enum_values.is_some());
629
630 let enums = schema.enum_values.as_ref().unwrap();
631 assert!(enums.contains(&json!("Start")));
632 assert!(enums.contains(&json!("End")));
633 assert!(enums.contains(&json!("LLM")));
634 }
635
636 #[test]
637 fn test_metadata_schema() {
638 let generator = WorkflowSchemaGenerator::new();
639 let schema = generator.generate_metadata_schema();
640
641 assert_eq!(schema.schema_type, "object");
642 let props = schema.properties.as_ref().unwrap();
643 assert!(props.contains_key("name"));
644 assert!(props.contains_key("version"));
645 assert!(props.contains_key("created_at"));
646 assert!(props.contains_key("updated_at"));
647 }
648
649 #[test]
650 fn test_reference_schema() {
651 let schema = JsonSchema::reference("#/$defs/LlmConfig");
652 assert!(schema.reference.is_some());
653 assert_eq!(schema.reference.unwrap(), "#/$defs/LlmConfig");
654 }
655
656 #[test]
657 fn test_schema_with_pattern() {
658 let schema = JsonSchema::string().with_pattern("^[a-zA-Z0-9_-]+$".to_string());
659
660 assert!(schema.pattern.is_some());
661 assert_eq!(schema.pattern.unwrap(), "^[a-zA-Z0-9_-]+$");
662 }
663
664 #[test]
665 fn test_schema_with_format() {
666 let schema = JsonSchema::string().with_format("uuid".to_string());
667
668 assert!(schema.format.is_some());
669 assert_eq!(schema.format.unwrap(), "uuid");
670 }
671
672 #[test]
673 fn test_schema_definitions() {
674 let mut schema = JsonSchema::object();
675 schema.add_definition("CustomType".to_string(), JsonSchema::string());
676
677 assert!(schema.definitions.is_some());
678 assert_eq!(schema.definitions.as_ref().unwrap().len(), 1);
679 }
680}