oxify_model/
graphql_schema.rs

1//! GraphQL schema generation for workflow models
2//!
3//! This module provides functionality to generate GraphQL Schema Definition Language (SDL)
4//! from workflow models for API integration.
5
6use std::collections::HashMap;
7
8#[cfg(feature = "openapi")]
9use utoipa::ToSchema;
10
11/// GraphQL type definition
12#[derive(Debug, Clone)]
13#[cfg_attr(feature = "openapi", derive(ToSchema))]
14pub struct GraphQLType {
15    /// Type name
16    pub name: String,
17
18    /// Type kind (Object, Enum, Interface, etc.)
19    pub kind: GraphQLTypeKind,
20
21    /// Description
22    pub description: Option<String>,
23
24    /// Fields for object types
25    pub fields: Vec<GraphQLField>,
26
27    /// Enum values for enum types
28    pub enum_values: Vec<String>,
29
30    /// Implemented interfaces
31    pub interfaces: Vec<String>,
32}
33
34/// GraphQL type kind
35#[derive(Debug, Clone, PartialEq, Eq)]
36#[cfg_attr(feature = "openapi", derive(ToSchema))]
37pub enum GraphQLTypeKind {
38    /// Object type
39    Object,
40    /// Enum type
41    Enum,
42    /// Interface type
43    Interface,
44    /// Input object type
45    Input,
46}
47
48/// GraphQL field definition
49#[derive(Debug, Clone)]
50#[cfg_attr(feature = "openapi", derive(ToSchema))]
51pub struct GraphQLField {
52    /// Field name
53    pub name: String,
54
55    /// Field type
56    pub field_type: String,
57
58    /// Whether the field is required (non-null)
59    pub required: bool,
60
61    /// Whether the field is a list
62    pub is_list: bool,
63
64    /// Description
65    pub description: Option<String>,
66
67    /// Arguments for the field
68    pub arguments: Vec<GraphQLArgument>,
69}
70
71/// GraphQL argument definition
72#[derive(Debug, Clone)]
73#[cfg_attr(feature = "openapi", derive(ToSchema))]
74pub struct GraphQLArgument {
75    /// Argument name
76    pub name: String,
77
78    /// Argument type
79    pub arg_type: String,
80
81    /// Whether the argument is required
82    pub required: bool,
83
84    /// Default value
85    pub default_value: Option<String>,
86}
87
88impl GraphQLType {
89    /// Create a new GraphQL type
90    pub fn new(name: String, kind: GraphQLTypeKind) -> Self {
91        Self {
92            name,
93            kind,
94            description: None,
95            fields: Vec::new(),
96            enum_values: Vec::new(),
97            interfaces: Vec::new(),
98        }
99    }
100
101    /// Set description
102    pub fn with_description(mut self, description: String) -> Self {
103        self.description = Some(description);
104        self
105    }
106
107    /// Add a field
108    pub fn add_field(&mut self, field: GraphQLField) {
109        self.fields.push(field);
110    }
111
112    /// Add an enum value
113    pub fn add_enum_value(&mut self, value: String) {
114        self.enum_values.push(value);
115    }
116
117    /// Add an interface
118    pub fn add_interface(&mut self, interface: String) {
119        self.interfaces.push(interface);
120    }
121
122    /// Convert to SDL string
123    pub fn to_sdl(&self) -> String {
124        let mut lines = Vec::new();
125
126        // Add description if present
127        if let Some(desc) = &self.description {
128            lines.push(format!("\"\"\"{}\"\"\"", desc));
129        }
130
131        // Add type definition
132        match self.kind {
133            GraphQLTypeKind::Object => {
134                let interfaces = if self.interfaces.is_empty() {
135                    String::new()
136                } else {
137                    format!(" implements {}", self.interfaces.join(" & "))
138                };
139                lines.push(format!("type {}{} {{", self.name, interfaces));
140                for field in &self.fields {
141                    lines.push(format!("  {}", field.to_sdl()));
142                }
143                lines.push("}".to_string());
144            }
145            GraphQLTypeKind::Enum => {
146                lines.push(format!("enum {} {{", self.name));
147                for value in &self.enum_values {
148                    lines.push(format!("  {}", value));
149                }
150                lines.push("}".to_string());
151            }
152            GraphQLTypeKind::Interface => {
153                lines.push(format!("interface {} {{", self.name));
154                for field in &self.fields {
155                    lines.push(format!("  {}", field.to_sdl()));
156                }
157                lines.push("}".to_string());
158            }
159            GraphQLTypeKind::Input => {
160                lines.push(format!("input {} {{", self.name));
161                for field in &self.fields {
162                    lines.push(format!("  {}", field.to_sdl()));
163                }
164                lines.push("}".to_string());
165            }
166        }
167
168        lines.join("\n")
169    }
170}
171
172impl GraphQLField {
173    /// Create a new GraphQL field
174    pub fn new(name: String, field_type: String) -> Self {
175        Self {
176            name,
177            field_type,
178            required: false,
179            is_list: false,
180            description: None,
181            arguments: Vec::new(),
182        }
183    }
184
185    /// Set required flag
186    pub fn required(mut self) -> Self {
187        self.required = true;
188        self
189    }
190
191    /// Set list flag
192    pub fn list(mut self) -> Self {
193        self.is_list = true;
194        self
195    }
196
197    /// Set description
198    pub fn with_description(mut self, description: String) -> Self {
199        self.description = Some(description);
200        self
201    }
202
203    /// Add an argument
204    pub fn add_argument(&mut self, argument: GraphQLArgument) {
205        self.arguments.push(argument);
206    }
207
208    /// Convert to SDL string
209    pub fn to_sdl(&self) -> String {
210        let mut result = String::new();
211
212        // Add description if present
213        if let Some(desc) = &self.description {
214            result.push_str(&format!("\"\"\"{}\"\"\" ", desc));
215        }
216
217        result.push_str(&self.name);
218
219        // Add arguments
220        if !self.arguments.is_empty() {
221            result.push('(');
222            let args: Vec<String> = self.arguments.iter().map(|a| a.to_sdl()).collect();
223            result.push_str(&args.join(", "));
224            result.push(')');
225        }
226
227        result.push_str(": ");
228
229        // Add type with list/required modifiers
230        let type_str = if self.is_list {
231            format!("[{}]", self.field_type)
232        } else {
233            self.field_type.clone()
234        };
235
236        result.push_str(&type_str);
237
238        if self.required {
239            result.push('!');
240        }
241
242        result
243    }
244}
245
246impl GraphQLArgument {
247    /// Create a new GraphQL argument
248    pub fn new(name: String, arg_type: String) -> Self {
249        Self {
250            name,
251            arg_type,
252            required: false,
253            default_value: None,
254        }
255    }
256
257    /// Set required flag
258    pub fn required(mut self) -> Self {
259        self.required = true;
260        self
261    }
262
263    /// Set default value
264    pub fn with_default(mut self, default: String) -> Self {
265        self.default_value = Some(default);
266        self
267    }
268
269    /// Convert to SDL string
270    pub fn to_sdl(&self) -> String {
271        let mut result = format!("{}: {}", self.name, self.arg_type);
272
273        if self.required {
274            result.push('!');
275        }
276
277        if let Some(default) = &self.default_value {
278            result.push_str(&format!(" = {}", default));
279        }
280
281        result
282    }
283}
284
285/// GraphQL schema generator for workflows
286pub struct GraphQLSchemaGenerator {
287    /// Include descriptions
288    pub include_descriptions: bool,
289
290    /// Generated types
291    types: HashMap<String, GraphQLType>,
292}
293
294impl GraphQLSchemaGenerator {
295    /// Create a new schema generator
296    pub fn new() -> Self {
297        Self {
298            include_descriptions: true,
299            types: HashMap::new(),
300        }
301    }
302
303    /// Generate GraphQL schema for workflows
304    pub fn generate_workflow_schema(&mut self) -> String {
305        // Generate core types
306        self.generate_workflow_type();
307        self.generate_metadata_type();
308        self.generate_node_type();
309        self.generate_edge_type();
310        self.generate_node_kind_enum();
311        self.generate_execution_state_enum();
312        self.generate_query_type();
313        self.generate_mutation_type();
314
315        // Generate SDL
316        self.to_sdl()
317    }
318
319    /// Generate the Workflow type
320    fn generate_workflow_type(&mut self) {
321        let mut workflow_type = GraphQLType::new("Workflow".to_string(), GraphQLTypeKind::Object);
322
323        if self.include_descriptions {
324            workflow_type = workflow_type
325                .with_description("A workflow defining a sequence of operations".to_string());
326        }
327
328        workflow_type.add_field(GraphQLField::new("id".to_string(), "ID".to_string()).required());
329
330        workflow_type.add_field(
331            GraphQLField::new("metadata".to_string(), "WorkflowMetadata".to_string()).required(),
332        );
333
334        workflow_type.add_field(
335            GraphQLField::new("nodes".to_string(), "Node".to_string())
336                .list()
337                .required(),
338        );
339
340        workflow_type.add_field(
341            GraphQLField::new("edges".to_string(), "Edge".to_string())
342                .list()
343                .required(),
344        );
345
346        self.types.insert("Workflow".to_string(), workflow_type);
347    }
348
349    /// Generate the WorkflowMetadata type
350    fn generate_metadata_type(&mut self) {
351        let mut metadata_type =
352            GraphQLType::new("WorkflowMetadata".to_string(), GraphQLTypeKind::Object);
353
354        if self.include_descriptions {
355            metadata_type =
356                metadata_type.with_description("Workflow metadata and description".to_string());
357        }
358
359        metadata_type
360            .add_field(GraphQLField::new("name".to_string(), "String".to_string()).required());
361
362        metadata_type.add_field(GraphQLField::new(
363            "description".to_string(),
364            "String".to_string(),
365        ));
366
367        metadata_type
368            .add_field(GraphQLField::new("version".to_string(), "String".to_string()).required());
369
370        metadata_type.add_field(
371            GraphQLField::new("createdAt".to_string(), "DateTime".to_string()).required(),
372        );
373
374        metadata_type.add_field(
375            GraphQLField::new("updatedAt".to_string(), "DateTime".to_string()).required(),
376        );
377
378        metadata_type.add_field(
379            GraphQLField::new("tags".to_string(), "String".to_string())
380                .list()
381                .required(),
382        );
383
384        self.types
385            .insert("WorkflowMetadata".to_string(), metadata_type);
386    }
387
388    /// Generate the Node type
389    fn generate_node_type(&mut self) {
390        let mut node_type = GraphQLType::new("Node".to_string(), GraphQLTypeKind::Object);
391
392        if self.include_descriptions {
393            node_type = node_type.with_description("A workflow node".to_string());
394        }
395
396        node_type.add_field(GraphQLField::new("id".to_string(), "ID".to_string()).required());
397
398        node_type.add_field(GraphQLField::new("name".to_string(), "String".to_string()).required());
399
400        node_type
401            .add_field(GraphQLField::new("kind".to_string(), "NodeKind".to_string()).required());
402
403        self.types.insert("Node".to_string(), node_type);
404    }
405
406    /// Generate the Edge type
407    fn generate_edge_type(&mut self) {
408        let mut edge_type = GraphQLType::new("Edge".to_string(), GraphQLTypeKind::Object);
409
410        if self.include_descriptions {
411            edge_type = edge_type.with_description("An edge connecting two nodes".to_string());
412        }
413
414        edge_type.add_field(GraphQLField::new("id".to_string(), "ID".to_string()).required());
415
416        edge_type.add_field(GraphQLField::new("from".to_string(), "ID".to_string()).required());
417
418        edge_type.add_field(GraphQLField::new("to".to_string(), "ID".to_string()).required());
419
420        self.types.insert("Edge".to_string(), edge_type);
421    }
422
423    /// Generate the NodeKind enum
424    fn generate_node_kind_enum(&mut self) {
425        let mut node_kind = GraphQLType::new("NodeKind".to_string(), GraphQLTypeKind::Enum);
426
427        if self.include_descriptions {
428            node_kind = node_kind.with_description("Types of nodes in a workflow".to_string());
429        }
430
431        node_kind.add_enum_value("START".to_string());
432        node_kind.add_enum_value("END".to_string());
433        node_kind.add_enum_value("LLM".to_string());
434        node_kind.add_enum_value("RETRIEVER".to_string());
435        node_kind.add_enum_value("CODE".to_string());
436        node_kind.add_enum_value("IF_ELSE".to_string());
437        node_kind.add_enum_value("TOOL".to_string());
438        node_kind.add_enum_value("LOOP".to_string());
439        node_kind.add_enum_value("TRY_CATCH".to_string());
440        node_kind.add_enum_value("SUB_WORKFLOW".to_string());
441        node_kind.add_enum_value("SWITCH".to_string());
442        node_kind.add_enum_value("PARALLEL".to_string());
443        node_kind.add_enum_value("APPROVAL".to_string());
444        node_kind.add_enum_value("FORM".to_string());
445
446        self.types.insert("NodeKind".to_string(), node_kind);
447    }
448
449    /// Generate the ExecutionState enum
450    fn generate_execution_state_enum(&mut self) {
451        let mut exec_state = GraphQLType::new("ExecutionState".to_string(), GraphQLTypeKind::Enum);
452
453        if self.include_descriptions {
454            exec_state = exec_state.with_description("State of a workflow execution".to_string());
455        }
456
457        exec_state.add_enum_value("PENDING".to_string());
458        exec_state.add_enum_value("RUNNING".to_string());
459        exec_state.add_enum_value("COMPLETED".to_string());
460        exec_state.add_enum_value("FAILED".to_string());
461        exec_state.add_enum_value("CANCELLED".to_string());
462
463        self.types.insert("ExecutionState".to_string(), exec_state);
464    }
465
466    /// Generate the Query type
467    fn generate_query_type(&mut self) {
468        let mut query_type = GraphQLType::new("Query".to_string(), GraphQLTypeKind::Object);
469
470        // Get workflow by ID
471        let mut get_workflow = GraphQLField::new("workflow".to_string(), "Workflow".to_string());
472        get_workflow
473            .add_argument(GraphQLArgument::new("id".to_string(), "ID".to_string()).required());
474        query_type.add_field(get_workflow);
475
476        // List workflows
477        let mut list_workflows = GraphQLField::new("workflows".to_string(), "Workflow".to_string())
478            .list()
479            .required();
480        list_workflows.add_argument(
481            GraphQLArgument::new("limit".to_string(), "Int".to_string())
482                .with_default("10".to_string()),
483        );
484        list_workflows.add_argument(
485            GraphQLArgument::new("offset".to_string(), "Int".to_string())
486                .with_default("0".to_string()),
487        );
488        query_type.add_field(list_workflows);
489
490        self.types.insert("Query".to_string(), query_type);
491    }
492
493    /// Generate the Mutation type
494    fn generate_mutation_type(&mut self) {
495        let mut mutation_type = GraphQLType::new("Mutation".to_string(), GraphQLTypeKind::Object);
496
497        // Create workflow
498        let mut create_workflow =
499            GraphQLField::new("createWorkflow".to_string(), "Workflow".to_string()).required();
500        create_workflow.add_argument(
501            GraphQLArgument::new("name".to_string(), "String".to_string()).required(),
502        );
503        create_workflow.add_argument(GraphQLArgument::new(
504            "description".to_string(),
505            "String".to_string(),
506        ));
507        mutation_type.add_field(create_workflow);
508
509        // Delete workflow
510        let mut delete_workflow =
511            GraphQLField::new("deleteWorkflow".to_string(), "Boolean".to_string()).required();
512        delete_workflow
513            .add_argument(GraphQLArgument::new("id".to_string(), "ID".to_string()).required());
514        mutation_type.add_field(delete_workflow);
515
516        self.types.insert("Mutation".to_string(), mutation_type);
517    }
518
519    /// Convert all types to SDL
520    pub fn to_sdl(&self) -> String {
521        let mut sdl = Vec::new();
522
523        // Add custom scalars
524        sdl.push("scalar DateTime".to_string());
525        sdl.push("".to_string());
526
527        // Add types in order: enums, objects, query, mutation
528        let type_order = vec![
529            "NodeKind",
530            "ExecutionState",
531            "Edge",
532            "Node",
533            "WorkflowMetadata",
534            "Workflow",
535            "Query",
536            "Mutation",
537        ];
538
539        for type_name in type_order {
540            if let Some(gql_type) = self.types.get(type_name) {
541                sdl.push(gql_type.to_sdl());
542                sdl.push("".to_string());
543            }
544        }
545
546        sdl.join("\n")
547    }
548}
549
550impl Default for GraphQLSchemaGenerator {
551    fn default() -> Self {
552        Self::new()
553    }
554}
555
556/// Generate a GraphQL schema for workflows
557pub fn generate_graphql_schema() -> String {
558    let mut generator = GraphQLSchemaGenerator::new();
559    generator.generate_workflow_schema()
560}
561
562#[cfg(test)]
563mod tests {
564    use super::*;
565
566    #[test]
567    fn test_graphql_field_sdl() {
568        let field = GraphQLField::new("name".to_string(), "String".to_string()).required();
569        assert_eq!(field.to_sdl(), "name: String!");
570    }
571
572    #[test]
573    fn test_graphql_field_list() {
574        let field = GraphQLField::new("tags".to_string(), "String".to_string())
575            .list()
576            .required();
577        assert_eq!(field.to_sdl(), "tags: [String]!");
578    }
579
580    #[test]
581    fn test_graphql_argument() {
582        let arg = GraphQLArgument::new("id".to_string(), "ID".to_string()).required();
583        assert_eq!(arg.to_sdl(), "id: ID!");
584    }
585
586    #[test]
587    fn test_graphql_argument_with_default() {
588        let arg = GraphQLArgument::new("limit".to_string(), "Int".to_string())
589            .with_default("10".to_string());
590        assert_eq!(arg.to_sdl(), "limit: Int = 10");
591    }
592
593    #[test]
594    fn test_enum_type_sdl() {
595        let mut enum_type = GraphQLType::new("Status".to_string(), GraphQLTypeKind::Enum);
596        enum_type.add_enum_value("ACTIVE".to_string());
597        enum_type.add_enum_value("INACTIVE".to_string());
598
599        let sdl = enum_type.to_sdl();
600        assert!(sdl.contains("enum Status"));
601        assert!(sdl.contains("ACTIVE"));
602        assert!(sdl.contains("INACTIVE"));
603    }
604
605    #[test]
606    fn test_object_type_sdl() {
607        let mut object_type = GraphQLType::new("User".to_string(), GraphQLTypeKind::Object);
608        object_type.add_field(GraphQLField::new("id".to_string(), "ID".to_string()).required());
609        object_type
610            .add_field(GraphQLField::new("name".to_string(), "String".to_string()).required());
611
612        let sdl = object_type.to_sdl();
613        assert!(sdl.contains("type User"));
614        assert!(sdl.contains("id: ID!"));
615        assert!(sdl.contains("name: String!"));
616    }
617
618    #[test]
619    fn test_generate_workflow_schema() {
620        let mut generator = GraphQLSchemaGenerator::new();
621        let schema = generator.generate_workflow_schema();
622
623        assert!(schema.contains("type Workflow"));
624        assert!(schema.contains("type Node"));
625        assert!(schema.contains("type Edge"));
626        assert!(schema.contains("enum NodeKind"));
627        assert!(schema.contains("type Query"));
628        assert!(schema.contains("type Mutation"));
629    }
630
631    #[test]
632    fn test_schema_has_node_kinds() {
633        let mut generator = GraphQLSchemaGenerator::new();
634        let schema = generator.generate_workflow_schema();
635
636        assert!(schema.contains("START"));
637        assert!(schema.contains("END"));
638        assert!(schema.contains("LLM"));
639        assert!(schema.contains("RETRIEVER"));
640    }
641
642    #[test]
643    fn test_schema_has_execution_states() {
644        let mut generator = GraphQLSchemaGenerator::new();
645        let schema = generator.generate_workflow_schema();
646
647        assert!(schema.contains("PENDING"));
648        assert!(schema.contains("RUNNING"));
649        assert!(schema.contains("COMPLETED"));
650        assert!(schema.contains("FAILED"));
651    }
652
653    #[test]
654    fn test_query_type_has_workflow_field() {
655        let mut generator = GraphQLSchemaGenerator::new();
656        generator.generate_query_type();
657
658        if let Some(query_type) = generator.types.get("Query") {
659            assert!(query_type.fields.iter().any(|f| f.name == "workflow"));
660            assert!(query_type.fields.iter().any(|f| f.name == "workflows"));
661        } else {
662            panic!("Query type not found");
663        }
664    }
665
666    #[test]
667    fn test_mutation_type_has_create_workflow() {
668        let mut generator = GraphQLSchemaGenerator::new();
669        generator.generate_mutation_type();
670
671        if let Some(mutation_type) = generator.types.get("Mutation") {
672            assert!(mutation_type
673                .fields
674                .iter()
675                .any(|f| f.name == "createWorkflow"));
676            assert!(mutation_type
677                .fields
678                .iter()
679                .any(|f| f.name == "deleteWorkflow"));
680        } else {
681            panic!("Mutation type not found");
682        }
683    }
684
685    #[test]
686    fn test_field_with_description() {
687        let field = GraphQLField::new("name".to_string(), "String".to_string())
688            .with_description("The name of the user".to_string())
689            .required();
690
691        let sdl = field.to_sdl();
692        assert!(sdl.contains("The name of the user"));
693    }
694
695    #[test]
696    fn test_type_with_description() {
697        let type_def = GraphQLType::new("User".to_string(), GraphQLTypeKind::Object)
698            .with_description("A user in the system".to_string());
699
700        let sdl = type_def.to_sdl();
701        assert!(sdl.contains("A user in the system"));
702    }
703
704    #[test]
705    fn test_field_with_arguments() {
706        let mut field = GraphQLField::new("users".to_string(), "User".to_string()).list();
707        field.add_argument(GraphQLArgument::new("limit".to_string(), "Int".to_string()));
708        field.add_argument(GraphQLArgument::new(
709            "offset".to_string(),
710            "Int".to_string(),
711        ));
712
713        let sdl = field.to_sdl();
714        assert!(sdl.contains("users("));
715        assert!(sdl.contains("limit: Int"));
716        assert!(sdl.contains("offset: Int"));
717    }
718}