1mod enum_gen;
7mod generator;
8mod store_gen;
9mod struct_gen;
10pub mod type_utils;
11mod view_gen;
12
13use std::path::Path;
14
15pub fn generate_from_schema(
28 schema_path: &str,
29 output_path: &str,
30) -> Result<(), Box<dyn std::error::Error>> {
31 let schema = grounddb::schema::parse_schema(Path::new(schema_path))?;
32 let tokens = generator::generate_all(&schema);
33 let formatted = generator::format_token_stream(&tokens);
34 std::fs::write(output_path, formatted)?;
35 Ok(())
36}
37
38pub fn generate_from_schema_str(
43 schema_yaml: &str,
44) -> Result<String, Box<dyn std::error::Error>> {
45 let schema = grounddb::schema::parse_schema_str(schema_yaml)?;
46 let tokens = generator::generate_all(&schema);
47 let formatted = generator::format_token_stream(&tokens);
48 Ok(formatted)
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 const TEST_SCHEMA: &str = r#"
56types:
57 address:
58 street: { type: string, required: true }
59 city: { type: string, required: true }
60 state: { type: string }
61 zip: { type: string }
62
63collections:
64 users:
65 path: "users/{name}.md"
66 fields:
67 name: { type: string, required: true }
68 email: { type: string, required: true }
69 role: { type: string, enum: [admin, member, guest], default: member }
70 address: { type: address }
71 additional_properties: false
72 strict: true
73 on_delete: error
74
75 posts:
76 path: "posts/{status}/{date:YYYY-MM-DD}-{title}.md"
77 fields:
78 title: { type: string, required: true }
79 author_id: { type: ref, target: users, required: true }
80 date: { type: date, required: true }
81 tags: { type: list, items: string }
82 status: { type: string, enum: [draft, published, archived], default: draft }
83 content: true
84 additional_properties: false
85 strict: true
86
87 comments:
88 path: "comments/{parent:type}/{parent:id}/{user:id}.md"
89 fields:
90 user: { type: ref, target: users, required: true }
91 parent: { type: ref, target: [posts, comments], required: true }
92 content: true
93
94 events:
95 path: "events/{id}.md"
96 fields:
97 type: { type: string, required: true }
98 severity: { type: string, enum: [info, warn, error], default: info }
99 payload: { type: object }
100 additional_properties: true
101 strict: false
102
103views:
104 post_feed:
105 query: |
106 SELECT p.title, p.date, p.tags, u.name AS author_name, u.id AS author_id
107 FROM posts p
108 JOIN users u ON p.author_id = u.id
109 WHERE p.status = 'published'
110 ORDER BY p.date DESC
111 LIMIT 100
112 materialize: true
113 buffer: 2x
114
115 user_lookup:
116 query: |
117 SELECT id, name, email, role
118 FROM users
119 ORDER BY name ASC
120 materialize: true
121
122 recent_activity:
123 query: |
124 SELECT id, title, modified_at, status
125 FROM posts
126 ORDER BY modified_at DESC
127 LIMIT 50
128 materialize: true
129 buffer: 2x
130
131 post_comments:
132 type: query
133 query: |
134 SELECT c.id, c.created_at, c.content, u.name AS commenter_name
135 FROM comments c
136 JOIN users u ON c.user = u.id
137 WHERE c.parent = :post_id
138 ORDER BY c.created_at ASC
139 params:
140 post_id: { type: string }
141"#;
142
143 #[test]
144 fn test_generate_from_schema_str_full() {
145 let result = generate_from_schema_str(TEST_SCHEMA);
146 assert!(result.is_ok(), "Generation failed: {:?}", result.err());
147
148 let code = result.unwrap();
149
150 assert!(
152 syn::parse_file(&code).is_ok(),
153 "Generated code is not valid Rust:\n{}",
154 &code[..code.len().min(2000)]
155 );
156
157 assert!(code.contains("UserRole"), "Missing UserRole enum");
159 assert!(code.contains("PostStatus"), "Missing PostStatus enum");
160 assert!(code.contains("EventSeverity"), "Missing EventSeverity enum");
161
162 assert!(code.contains("Admin"), "Missing Admin variant");
164 assert!(code.contains("Member"), "Missing Member variant");
165 assert!(code.contains("Guest"), "Missing Guest variant");
166 assert!(code.contains("Draft"), "Missing Draft variant");
167 assert!(code.contains("Published"), "Missing Published variant");
168
169 assert!(
171 code.contains("impl Default for UserRole"),
172 "Missing UserRole Default impl"
173 );
174 assert!(
175 code.contains("impl Default for PostStatus"),
176 "Missing PostStatus Default impl"
177 );
178
179 assert!(code.contains("pub struct User"), "Missing User struct");
181 assert!(code.contains("pub struct Post"), "Missing Post struct");
182 assert!(code.contains("pub struct Comment"), "Missing Comment struct");
183 assert!(code.contains("pub struct Event"), "Missing Event struct");
184
185 assert!(code.contains("pub struct Address"), "Missing Address struct");
187
188 assert!(code.contains("ParentRef"), "Missing ParentRef enum");
190
191 assert!(code.contains("pub struct UserPartial"), "Missing UserPartial");
193 assert!(code.contains("pub struct PostPartial"), "Missing PostPartial");
194 assert!(code.contains("pub struct CommentPartial"), "Missing CommentPartial");
195 assert!(code.contains("pub struct EventPartial"), "Missing EventPartial");
196
197 assert!(code.contains("PostFeedRow"), "Missing PostFeedRow");
199 assert!(code.contains("UserLookupRow"), "Missing UserLookupRow");
200 assert!(code.contains("RecentActivityRow"), "Missing RecentActivityRow");
201 assert!(code.contains("PostCommentsRow"), "Missing PostCommentsRow");
202
203 assert!(code.contains("PostCommentsParams"), "Missing PostCommentsParams");
205
206 assert!(code.contains("StoreExt"), "Missing StoreExt trait");
208 assert!(code.contains("fn users"), "Missing users accessor");
209 assert!(code.contains("fn posts"), "Missing posts accessor");
210 assert!(code.contains("fn comments"), "Missing comments accessor");
211 assert!(code.contains("fn events"), "Missing events accessor");
212 }
213
214 #[test]
215 fn test_generate_minimal_schema() {
216 let schema = r#"
217collections:
218 items:
219 path: "items/{name}.md"
220 fields:
221 name: { type: string, required: true }
222"#;
223 let result = generate_from_schema_str(schema);
224 assert!(result.is_ok(), "Generation failed: {:?}", result.err());
225
226 let code = result.unwrap();
227 assert!(syn::parse_file(&code).is_ok(), "Not valid Rust");
228 assert!(code.contains("pub struct Item"));
229 assert!(code.contains("pub struct ItemPartial"));
230 }
231
232 #[test]
233 fn test_generate_all_field_types() {
234 let schema = r#"
235collections:
236 records:
237 path: "records/{id}.md"
238 fields:
239 name: { type: string, required: true }
240 count: { type: number, required: true }
241 active: { type: boolean, required: true }
242 birthday: { type: date }
243 updated: { type: datetime }
244 tags: { type: list, items: string }
245 metadata: { type: object }
246 owner: { type: ref, target: records }
247"#;
248 let result = generate_from_schema_str(schema);
249 assert!(result.is_ok(), "Generation failed: {:?}", result.err());
250
251 let code = result.unwrap();
252 assert!(syn::parse_file(&code).is_ok(), "Not valid Rust:\n{}", &code[..code.len().min(2000)]);
253
254 assert!(code.contains("String"), "Missing String type");
255 assert!(code.contains("f64"), "Missing f64 type");
256 assert!(code.contains("bool"), "Missing bool type");
257 assert!(code.contains("NaiveDate"), "Missing NaiveDate type");
258 assert!(code.contains("DateTime"), "Missing DateTime type");
259 assert!(code.contains("Vec"), "Missing Vec type");
260 assert!(code.contains("serde_json"), "Missing serde_json::Value type");
261 }
262
263 #[test]
264 fn test_rust_keyword_field_names() {
265 let schema = r#"
266collections:
267 events:
268 path: "events/{id}.md"
269 fields:
270 type: { type: string, required: true }
271 ref: { type: string }
272"#;
273 let result = generate_from_schema_str(schema);
274 assert!(result.is_ok(), "Generation failed: {:?}", result.err());
275
276 let code = result.unwrap();
277 assert!(syn::parse_file(&code).is_ok(), "Not valid Rust:\n{}", &code[..code.len().min(2000)]);
278 }
279}