1use crate::compiler::models::*;
6
7pub fn generate_graphql(schema: &CompiledSchema) -> String {
9 let mut out = String::new();
10
11 out.push_str(&format!(
12 "# Auto-generated Cortex GraphQL schema for {}\n",
13 schema.domain
14 ));
15 out.push_str(&format!(
16 "# {} models, {} actions, {} relationships\n\n",
17 schema.models.len(),
18 schema.actions.len(),
19 schema.relationships.len()
20 ));
21
22 for model in &schema.models {
24 generate_graphql_type(&mut out, model, schema);
25 }
26
27 out.push_str("type Query {\n");
29 for model in &schema.models {
30 if model.instance_count <= 1 {
31 let name_lower = model.name.to_lowercase();
33 out.push_str(&format!(" {name_lower}: {name}\n", name = model.name));
34 } else {
35 let name_lower = pluralize_lower(&model.name);
37 out.push_str(&format!(
38 " {name_lower}(query: String, limit: Int = 20): [{name}!]!\n",
39 name = model.name
40 ));
41 out.push_str(&format!(
42 " {single}(nodeId: Int!): {name}\n",
43 single = model.name.to_lowercase(),
44 name = model.name
45 ));
46 }
47 }
48 out.push_str("}\n\n");
49
50 let mutations: Vec<&CompiledAction> = schema
52 .actions
53 .iter()
54 .filter(|a| a.http_method == "POST")
55 .collect();
56
57 if !mutations.is_empty() {
58 out.push_str("type Mutation {\n");
59 for action in &mutations {
60 let fn_name = to_camel_case(&action.name);
61 let mut params: Vec<String> = Vec::new();
62
63 if action.is_instance_method {
64 params.push("nodeId: Int!".to_string());
65 }
66
67 for param in &action.params {
68 if param.name == "node_id" {
69 continue;
70 }
71 let gql_type = param.param_type.to_graphql_type();
72 let required = if param.required { "!" } else { "" };
73 let default = if let Some(ref d) = param.default_value {
74 format!(" = {}", graphql_default(d, ¶m.param_type))
75 } else {
76 String::new()
77 };
78 params.push(format!(
79 "{}: {gql_type}{required}{default}",
80 to_camel_case(¶m.name)
81 ));
82 }
83
84 let params_str = if params.is_empty() {
85 String::new()
86 } else {
87 format!("({})", params.join(", "))
88 };
89
90 out.push_str(&format!(" {fn_name}{params_str}: Boolean!\n"));
91 }
92 out.push_str("}\n\n");
93 }
94
95 out
96}
97
98fn generate_graphql_type(out: &mut String, model: &DataModel, schema: &CompiledSchema) {
100 out.push_str(&format!("type {} {{\n", model.name));
101
102 for field in &model.fields {
103 let gql_type = field.field_type.to_graphql_type();
104 let required = if !field.nullable { "!" } else { "" };
105 out.push_str(&format!(
106 " {}: {gql_type}{required}\n",
107 to_camel_case(&field.name)
108 ));
109 }
110
111 for rel in &schema.relationships {
113 if rel.from_model == model.name {
114 match rel.cardinality {
115 Cardinality::BelongsTo | Cardinality::HasOne => {
116 out.push_str(&format!(
117 " {}: {}\n",
118 to_camel_case(&rel.name),
119 rel.to_model
120 ));
121 }
122 Cardinality::HasMany | Cardinality::ManyToMany => {
123 out.push_str(&format!(
124 " {}(limit: Int = 10): [{}!]!\n",
125 to_camel_case(&rel.name),
126 rel.to_model
127 ));
128 }
129 }
130 }
131 }
132
133 out.push_str("}\n\n");
134}
135
136fn to_camel_case(s: &str) -> String {
138 let parts: Vec<&str> = s.split('_').collect();
139 if parts.is_empty() {
140 return s.to_string();
141 }
142 let mut result = parts[0].to_string();
143 for part in &parts[1..] {
144 if let Some(first) = part.chars().next() {
145 result.push(first.to_uppercase().next().unwrap_or(first));
146 result.push_str(&part[first.len_utf8()..]);
147 }
148 }
149 result
150}
151
152fn pluralize_lower(name: &str) -> String {
154 let lower = name.to_lowercase();
155 if lower.ends_with('s') {
156 format!("{lower}es")
157 } else if lower.ends_with('y') {
158 format!("{}ies", &lower[..lower.len() - 1])
159 } else {
160 format!("{lower}s")
161 }
162}
163
164fn graphql_default(value: &str, field_type: &FieldType) -> String {
166 match field_type {
167 FieldType::Integer | FieldType::Float | FieldType::Bool => value.to_string(),
168 FieldType::String | FieldType::Url => format!("\"{value}\""),
169 _ => format!("\"{value}\""),
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use chrono::Utc;
177
178 fn test_schema() -> CompiledSchema {
179 CompiledSchema {
180 domain: "test.com".to_string(),
181 compiled_at: Utc::now(),
182 models: vec![DataModel {
183 name: "Product".to_string(),
184 schema_org_type: "Product".to_string(),
185 fields: vec![
186 ModelField {
187 name: "url".to_string(),
188 field_type: FieldType::Url,
189 source: FieldSource::Inferred,
190 confidence: 1.0,
191 nullable: false,
192 example_values: vec![],
193 feature_dim: None,
194 },
195 ModelField {
196 name: "name".to_string(),
197 field_type: FieldType::String,
198 source: FieldSource::JsonLd,
199 confidence: 0.99,
200 nullable: false,
201 example_values: vec![],
202 feature_dim: None,
203 },
204 ModelField {
205 name: "price".to_string(),
206 field_type: FieldType::Float,
207 source: FieldSource::JsonLd,
208 confidence: 0.99,
209 nullable: true,
210 example_values: vec![],
211 feature_dim: Some(48),
212 },
213 ],
214 instance_count: 50,
215 example_urls: vec![],
216 search_action: None,
217 list_url: None,
218 }],
219 actions: vec![CompiledAction {
220 name: "add_to_cart".to_string(),
221 belongs_to: "Product".to_string(),
222 is_instance_method: true,
223 http_method: "POST".to_string(),
224 endpoint_template: "/cart/add".to_string(),
225 params: vec![ActionParam {
226 name: "quantity".to_string(),
227 param_type: FieldType::Integer,
228 required: false,
229 default_value: Some("1".to_string()),
230 source: "json_body".to_string(),
231 }],
232 requires_auth: false,
233 execution_path: "http".to_string(),
234 confidence: 0.9,
235 }],
236 relationships: vec![],
237 stats: SchemaStats {
238 total_models: 1,
239 total_fields: 3,
240 total_instances: 50,
241 avg_confidence: 0.99,
242 },
243 }
244 }
245
246 #[test]
247 fn test_generate_graphql_has_type() {
248 let gql = generate_graphql(&test_schema());
249 assert!(gql.contains("type Product {"));
250 assert!(gql.contains("url: String!"));
251 assert!(gql.contains("price: Float"));
252 }
253
254 #[test]
255 fn test_generate_graphql_has_query() {
256 let gql = generate_graphql(&test_schema());
257 assert!(gql.contains("type Query {"));
258 assert!(gql.contains("products(query: String"));
259 }
260
261 #[test]
262 fn test_generate_graphql_has_mutation() {
263 let gql = generate_graphql(&test_schema());
264 assert!(gql.contains("type Mutation {"));
265 assert!(gql.contains("addToCart("));
266 }
267
268 #[test]
269 fn test_pluralize_lower() {
270 assert_eq!(pluralize_lower("Product"), "products");
271 assert_eq!(pluralize_lower("Category"), "categories");
272 assert_eq!(pluralize_lower("Address"), "addresses");
273 }
274}