Skip to main content

tensorlogic_adapters/codegen/
graphql.rs

1use std::fmt::Write as FmtWrite;
2
3use crate::{DomainInfo, PredicateInfo, SymbolTable};
4
5use super::rust::RustCodegen;
6
7/// Code generator for GraphQL schemas from symbol tables.
8///
9/// This generator creates GraphQL type definitions, queries, and mutations
10/// from TensorLogic schemas, enabling API development with type-safe schemas.
11pub struct GraphQLCodegen {
12    /// Schema name
13    schema_name: String,
14    /// Whether to include descriptions
15    include_descriptions: bool,
16    /// Whether to generate Query type
17    generate_queries: bool,
18    /// Whether to generate Mutation type
19    generate_mutations: bool,
20}
21
22impl GraphQLCodegen {
23    /// Create a new GraphQL code generator.
24    pub fn new(schema_name: impl Into<String>) -> Self {
25        Self {
26            schema_name: schema_name.into(),
27            include_descriptions: true,
28            generate_queries: true,
29            generate_mutations: false,
30        }
31    }
32
33    /// Set whether to include descriptions.
34    pub fn with_descriptions(mut self, enable: bool) -> Self {
35        self.include_descriptions = enable;
36        self
37    }
38
39    /// Set whether to generate Query type.
40    pub fn with_queries(mut self, enable: bool) -> Self {
41        self.generate_queries = enable;
42        self
43    }
44
45    /// Set whether to generate Mutation type.
46    pub fn with_mutations(mut self, enable: bool) -> Self {
47        self.generate_mutations = enable;
48        self
49    }
50
51    /// Generate complete GraphQL schema from a symbol table.
52    pub fn generate(&self, table: &SymbolTable) -> String {
53        let mut schema = String::new();
54
55        // Schema header
56        writeln!(schema, "# Generated GraphQL Schema").expect("writing to String is infallible");
57        writeln!(schema, "# Schema: {}", self.schema_name)
58            .expect("writing to String is infallible");
59        writeln!(schema, "#").expect("writing to String is infallible");
60        writeln!(
61            schema,
62            "# This schema was automatically generated from TensorLogic."
63        )
64        .expect("writing to String is infallible");
65        writeln!(schema, "# DO NOT EDIT MANUALLY.").expect("writing to String is infallible");
66        writeln!(schema).expect("writing to String is infallible");
67
68        // Generate domain types
69        writeln!(schema, "# ==========================================")
70            .expect("writing to String is infallible");
71        writeln!(schema, "# Domain Types").expect("writing to String is infallible");
72        writeln!(schema, "# ==========================================")
73            .expect("writing to String is infallible");
74        writeln!(schema).expect("writing to String is infallible");
75
76        for domain in table.domains.values() {
77            self.generate_domain_type(&mut schema, domain);
78            writeln!(schema).expect("writing to String is infallible");
79        }
80
81        // Generate predicate types
82        writeln!(schema, "# ==========================================")
83            .expect("writing to String is infallible");
84        writeln!(schema, "# Predicate Types").expect("writing to String is infallible");
85        writeln!(schema, "# ==========================================")
86            .expect("writing to String is infallible");
87        writeln!(schema).expect("writing to String is infallible");
88
89        for predicate in table.predicates.values() {
90            self.generate_predicate_type(&mut schema, predicate, table);
91            writeln!(schema).expect("writing to String is infallible");
92        }
93
94        // Generate Query type
95        if self.generate_queries {
96            writeln!(schema, "# ==========================================")
97                .expect("writing to String is infallible");
98            writeln!(schema, "# Query Operations").expect("writing to String is infallible");
99            writeln!(schema, "# ==========================================")
100                .expect("writing to String is infallible");
101            writeln!(schema).expect("writing to String is infallible");
102            self.generate_query_type(&mut schema, table);
103            writeln!(schema).expect("writing to String is infallible");
104        }
105
106        // Generate Mutation type
107        if self.generate_mutations {
108            writeln!(schema, "# ==========================================")
109                .expect("writing to String is infallible");
110            writeln!(schema, "# Mutation Operations").expect("writing to String is infallible");
111            writeln!(schema, "# ==========================================")
112                .expect("writing to String is infallible");
113            writeln!(schema).expect("writing to String is infallible");
114            self.generate_mutation_type(&mut schema, table);
115            writeln!(schema).expect("writing to String is infallible");
116        }
117
118        // Schema definition
119        writeln!(schema, "# ==========================================")
120            .expect("writing to String is infallible");
121        writeln!(schema, "# Schema Definition").expect("writing to String is infallible");
122        writeln!(schema, "# ==========================================")
123            .expect("writing to String is infallible");
124        writeln!(schema).expect("writing to String is infallible");
125        writeln!(schema, "schema {{").expect("writing to String is infallible");
126        if self.generate_queries {
127            writeln!(schema, "  query: Query").expect("writing to String is infallible");
128        }
129        if self.generate_mutations {
130            writeln!(schema, "  mutation: Mutation").expect("writing to String is infallible");
131        }
132        writeln!(schema, "}}").expect("writing to String is infallible");
133
134        schema
135    }
136
137    /// Generate GraphQL type for a domain.
138    fn generate_domain_type(&self, schema: &mut String, domain: &DomainInfo) {
139        let type_name = Self::to_graphql_type_name(&domain.name);
140
141        if self.include_descriptions {
142            if let Some(ref desc) = domain.description {
143                writeln!(schema, "\"\"\"\n{}\n\"\"\"", desc)
144                    .expect("writing to String is infallible");
145            } else {
146                writeln!(schema, "\"\"\"\nDomain: {}\n\"\"\"", domain.name)
147                    .expect("writing to String is infallible");
148            }
149        }
150
151        writeln!(schema, "type {} {{", type_name).expect("writing to String is infallible");
152        writeln!(schema, "  \"Unique identifier\"").expect("writing to String is infallible");
153        writeln!(schema, "  id: ID!").expect("writing to String is infallible");
154        writeln!(
155            schema,
156            "  \"Integer index (0 to {})\"",
157            domain.cardinality - 1
158        )
159        .expect("writing to String is infallible");
160        writeln!(schema, "  index: Int!").expect("writing to String is infallible");
161        writeln!(schema, "}}").expect("writing to String is infallible");
162    }
163
164    /// Generate GraphQL type for a predicate.
165    fn generate_predicate_type(
166        &self,
167        schema: &mut String,
168        predicate: &PredicateInfo,
169        _table: &SymbolTable,
170    ) {
171        let type_name = Self::to_graphql_type_name(&predicate.name);
172
173        if self.include_descriptions {
174            if let Some(ref desc) = predicate.description {
175                writeln!(schema, "\"\"\"\n{}\n\"\"\"", desc)
176                    .expect("writing to String is infallible");
177            } else {
178                writeln!(schema, "\"\"\"\nPredicate: {}\n\"\"\"", predicate.name)
179                    .expect("writing to String is infallible");
180            }
181        }
182
183        writeln!(schema, "type {} {{", type_name).expect("writing to String is infallible");
184
185        // Add ID field
186        writeln!(schema, "  \"Unique identifier\"").expect("writing to String is infallible");
187        writeln!(schema, "  id: ID!").expect("writing to String is infallible");
188
189        // Add argument fields
190        for (i, domain_name) in predicate.arg_domains.iter().enumerate() {
191            let field_name = format!("arg{}", i);
192            let field_type = Self::to_graphql_type_name(domain_name);
193            writeln!(schema, "  \"Argument {} of type {}\"", i, domain_name)
194                .expect("writing to String is infallible");
195            writeln!(schema, "  {}: {}!", field_name, field_type)
196                .expect("writing to String is infallible");
197        }
198
199        writeln!(schema, "}}").expect("writing to String is infallible");
200    }
201
202    /// Generate Query type.
203    fn generate_query_type(&self, schema: &mut String, table: &SymbolTable) {
204        writeln!(schema, "\"\"\"").expect("writing to String is infallible");
205        writeln!(schema, "Root query type for retrieving data")
206            .expect("writing to String is infallible");
207        writeln!(schema, "\"\"\"").expect("writing to String is infallible");
208        writeln!(schema, "type Query {{").expect("writing to String is infallible");
209
210        // Domain queries
211        for domain in table.domains.values() {
212            let type_name = Self::to_graphql_type_name(&domain.name);
213            let field_name = Self::to_graphql_field_name(&domain.name);
214
215            writeln!(schema, "  \"Get {} by ID\"", domain.name)
216                .expect("writing to String is infallible");
217            writeln!(schema, "  {}(id: ID!): {}", field_name, type_name)
218                .expect("writing to String is infallible");
219            writeln!(schema).expect("writing to String is infallible");
220
221            writeln!(schema, "  \"List all {}s\"", domain.name)
222                .expect("writing to String is infallible");
223            writeln!(schema, "  {}s: [{}!]!", field_name, type_name)
224                .expect("writing to String is infallible");
225            writeln!(schema).expect("writing to String is infallible");
226        }
227
228        // Predicate queries
229        for predicate in table.predicates.values() {
230            let type_name = Self::to_graphql_type_name(&predicate.name);
231            let field_name = Self::to_graphql_field_name(&predicate.name);
232
233            writeln!(schema, "  \"Query {} predicate\"", predicate.name)
234                .expect("writing to String is infallible");
235
236            // Build query with argument filters
237            write!(schema, "  {}(", field_name).expect("writing to String is infallible");
238            for (i, domain_name) in predicate.arg_domains.iter().enumerate() {
239                if i > 0 {
240                    write!(schema, ", ").expect("writing to String is infallible");
241                }
242                let arg_type = Self::to_graphql_type_name(domain_name);
243                write!(schema, "arg{}: {}", i, arg_type).expect("writing to String is infallible");
244            }
245            writeln!(schema, "): [{}!]!", type_name).expect("writing to String is infallible");
246            writeln!(schema).expect("writing to String is infallible");
247        }
248
249        writeln!(schema, "}}").expect("writing to String is infallible");
250    }
251
252    /// Generate Mutation type.
253    fn generate_mutation_type(&self, schema: &mut String, table: &SymbolTable) {
254        writeln!(schema, "\"\"\"").expect("writing to String is infallible");
255        writeln!(schema, "Root mutation type for modifying data")
256            .expect("writing to String is infallible");
257        writeln!(schema, "\"\"\"").expect("writing to String is infallible");
258        writeln!(schema, "type Mutation {{").expect("writing to String is infallible");
259
260        // Predicate mutations (add/remove)
261        for predicate in table.predicates.values() {
262            let type_name = Self::to_graphql_type_name(&predicate.name);
263
264            // Add mutation
265            writeln!(schema, "  \"Add {} instance\"", predicate.name)
266                .expect("writing to String is infallible");
267            write!(schema, "  add{}(", type_name).expect("writing to String is infallible");
268            for (i, domain_name) in predicate.arg_domains.iter().enumerate() {
269                if i > 0 {
270                    write!(schema, ", ").expect("writing to String is infallible");
271                }
272                let arg_type = Self::to_graphql_type_name(domain_name);
273                write!(schema, "arg{}: {}!", i, arg_type).expect("writing to String is infallible");
274            }
275            writeln!(schema, "): {}!", type_name).expect("writing to String is infallible");
276            writeln!(schema).expect("writing to String is infallible");
277
278            // Remove mutation
279            writeln!(schema, "  \"Remove {} instance\"", predicate.name)
280                .expect("writing to String is infallible");
281            writeln!(schema, "  remove{}(id: ID!): Boolean!", type_name)
282                .expect("writing to String is infallible");
283            writeln!(schema).expect("writing to String is infallible");
284        }
285
286        writeln!(schema, "}}").expect("writing to String is infallible");
287    }
288
289    /// Convert a name to GraphQL type name (PascalCase).
290    fn to_graphql_type_name(name: &str) -> String {
291        RustCodegen::to_type_name(name) // Reuse Rust converter
292    }
293
294    /// Convert a name to GraphQL field name (camelCase).
295    pub(super) fn to_graphql_field_name(name: &str) -> String {
296        let parts: Vec<&str> = name.split('_').collect();
297        if parts.is_empty() {
298            return String::new();
299        }
300
301        let mut result = parts[0].to_lowercase();
302        for part in &parts[1..] {
303            if let Some(first_char) = part.chars().next() {
304                result.push_str(&first_char.to_uppercase().to_string());
305                result.push_str(&part[first_char.len_utf8()..]);
306            }
307        }
308        result
309    }
310}