1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ServiceManifest {
8 pub name: String,
9 pub version: Option<String>,
10 pub tables: Vec<TableDefinition>,
11 #[serde(default)]
12 pub cells: Vec<CellDefinition>,
13 #[serde(default)]
14 pub events: Vec<EventDefinition>,
15 #[serde(default)]
16 pub subscriptions: Vec<SubscriptionDefinition>,
17 #[serde(default)]
18 pub custom_routes: Vec<CustomRouteDefinition>,
19 #[serde(default)]
20 pub mode: ServiceMode,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
24#[serde(rename_all = "lowercase")]
25pub enum ServiceMode {
26 #[default]
27 Crud,
28 Wasm,
29 Container,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct TableDefinition {
34 pub name: String,
35 pub columns: Vec<ColumnDefinition>,
36 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 pub indexes: Vec<IndexDefinition>,
38 #[serde(default)]
39 pub soft_delete: bool,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub owner_field: Option<String>,
44 #[serde(default)]
47 pub auth_required: bool,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ColumnDefinition {
52 pub name: String,
53 pub column_type: ColumnType,
54 #[serde(default)]
55 pub primary_key: bool,
56 #[serde(default)]
57 pub nullable: bool,
58 #[serde(default)]
59 pub auto_generate: bool,
60 pub default_value: Option<String>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub references: Option<String>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub on_delete: Option<ForeignKeyAction>,
67 #[serde(default)]
69 pub unique: bool,
70 #[serde(default, skip_serializing_if = "Vec::is_empty")]
72 pub validations: Vec<ValidationRule>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
76#[serde(tag = "rule", rename_all = "snake_case")]
77pub enum ValidationRule {
78 Regex { pattern: String },
80 Min { value: f64 },
82 Max { value: f64 },
84 MinLength { value: usize },
86 MaxLength { value: usize },
88 OneOf { values: Vec<String> },
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
93pub struct IndexDefinition {
94 pub name: String,
95 pub columns: Vec<String>,
96 #[serde(default)]
97 pub unique: bool,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
101#[serde(rename_all = "snake_case")]
102pub enum ForeignKeyAction {
103 Cascade,
104 SetNull,
105 Restrict,
106 NoAction,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
110#[serde(rename_all = "lowercase")]
111pub enum ColumnType {
112 Uuid,
113 Text,
114 Integer,
115 BigInteger,
116 Float,
117 Double,
118 Boolean,
119 Timestamp,
120 Date,
121 Jsonb,
122}
123
124impl ColumnType {
125 pub fn to_sql(&self) -> &str {
126 match self {
127 ColumnType::Uuid => "UUID",
128 ColumnType::Text => "TEXT",
129 ColumnType::Integer => "INTEGER",
130 ColumnType::BigInteger => "BIGINT",
131 ColumnType::Float => "REAL",
132 ColumnType::Double => "DOUBLE PRECISION",
133 ColumnType::Boolean => "BOOLEAN",
134 ColumnType::Timestamp => "TIMESTAMPTZ",
135 ColumnType::Date => "DATE",
136 ColumnType::Jsonb => "JSONB",
137 }
138 }
139
140 pub fn filter_type(&self) -> &str {
142 match self {
143 ColumnType::Uuid => "uuid",
144 ColumnType::Text => "text",
145 ColumnType::Integer | ColumnType::BigInteger => "integer",
146 ColumnType::Float | ColumnType::Double => "double",
147 ColumnType::Boolean => "boolean",
148 ColumnType::Timestamp => "timestamp",
149 ColumnType::Date => "date",
150 ColumnType::Jsonb => "jsonb",
151 }
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct CellDefinition {
157 pub name: String,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct EventDefinition {
162 pub name: String,
163 pub table: String,
164 pub action: EventAction,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
168#[serde(rename_all = "lowercase")]
169pub enum EventAction {
170 Create,
171 Update,
172 Delete,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct SubscriptionDefinition {
177 pub subject: String,
178 pub handler: String,
179 pub handler_type: HandlerType,
180 #[serde(default)]
181 pub config: HashMap<String, String>,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
185#[serde(rename_all = "snake_case")]
186pub enum HandlerType {
187 DeleteCascade,
188 UpdateField,
189 Webhook,
190 Wasm,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct CustomRouteDefinition {
195 pub method: String,
196 pub path: String,
197 pub handler: String,
198}
199
200impl ServiceManifest {
201 pub fn get_table(&self, name: &str) -> Option<&TableDefinition> {
203 self.tables.iter().find(|t| t.name == name)
204 }
205
206 pub fn schema_hash(&self) -> String {
208 use sha2::{Digest, Sha256};
209 let json = serde_json::to_string(&self.tables).unwrap_or_default();
210 let mut hasher = Sha256::new();
211 hasher.update(json.as_bytes());
212 hex::encode(hasher.finalize())
213 }
214}
215
216impl TableDefinition {
217 pub fn primary_key(&self) -> Option<&ColumnDefinition> {
219 self.columns.iter().find(|c| c.primary_key)
220 }
221
222 pub fn writable_columns(&self) -> Vec<&ColumnDefinition> {
224 self.columns
225 .iter()
226 .filter(|c| !c.auto_generate || !c.primary_key)
227 .collect()
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 fn sample_manifest() -> ServiceManifest {
236 ServiceManifest {
237 name: "test-service".into(),
238 version: Some("1.0.0".into()),
239 tables: vec![TableDefinition {
240 name: "todos".into(),
241 columns: vec![
242 ColumnDefinition {
243 name: "id".into(),
244 column_type: ColumnType::Uuid,
245 primary_key: true,
246 nullable: false,
247 auto_generate: true,
248 default_value: None,
249 references: None,
250 on_delete: None,
251 unique: false,
252 validations: vec![],
253 },
254 ColumnDefinition {
255 name: "title".into(),
256 column_type: ColumnType::Text,
257 primary_key: false,
258 nullable: false,
259 auto_generate: false,
260 default_value: None,
261 references: None,
262 on_delete: None,
263 unique: false,
264 validations: vec![],
265 },
266 ColumnDefinition {
267 name: "user_id".into(),
268 column_type: ColumnType::Uuid,
269 primary_key: false,
270 nullable: false,
271 auto_generate: false,
272 default_value: None,
273 references: Some("users.id".into()),
274 on_delete: Some(ForeignKeyAction::Cascade),
275 unique: false,
276 validations: vec![],
277 },
278 ],
279 indexes: vec![],
280 soft_delete: false,
281 owner_field: None,
282 auth_required: false,
283 }],
284 cells: vec![],
285 events: vec![],
286 subscriptions: vec![],
287 custom_routes: vec![],
288 mode: ServiceMode::Crud,
289 }
290 }
291
292 #[test]
293 fn test_manifest_serialization_roundtrip() {
294 let manifest = sample_manifest();
295 let json = serde_json::to_string(&manifest).unwrap();
296 let parsed: ServiceManifest = serde_json::from_str(&json).unwrap();
297 assert_eq!(parsed.name, "test-service");
298 assert_eq!(parsed.tables.len(), 1);
299 assert_eq!(parsed.tables[0].columns.len(), 3);
300 }
301
302 #[test]
303 fn test_get_table() {
304 let manifest = sample_manifest();
305 assert!(manifest.get_table("todos").is_some());
306 assert!(manifest.get_table("nonexistent").is_none());
307 }
308
309 #[test]
310 fn test_primary_key() {
311 let manifest = sample_manifest();
312 let table = manifest.get_table("todos").unwrap();
313 let pk = table.primary_key().unwrap();
314 assert_eq!(pk.name, "id");
315 assert!(pk.auto_generate);
316 }
317
318 #[test]
319 fn test_schema_hash_deterministic() {
320 let m1 = sample_manifest();
321 let m2 = sample_manifest();
322 assert_eq!(m1.schema_hash(), m2.schema_hash());
323 }
324
325 #[test]
326 fn test_schema_hash_changes() {
327 let mut m1 = sample_manifest();
328 let m2 = sample_manifest();
329 m1.tables[0].columns.push(ColumnDefinition {
330 name: "extra".into(),
331 column_type: ColumnType::Text,
332 primary_key: false,
333 nullable: true,
334 auto_generate: false,
335 default_value: None,
336 references: None,
337 on_delete: None,
338 unique: false,
339 validations: vec![],
340 });
341 assert_ne!(m1.schema_hash(), m2.schema_hash());
342 }
343
344 #[test]
345 fn test_column_type_to_sql() {
346 assert_eq!(ColumnType::Uuid.to_sql(), "UUID");
347 assert_eq!(ColumnType::Text.to_sql(), "TEXT");
348 assert_eq!(ColumnType::Integer.to_sql(), "INTEGER");
349 assert_eq!(ColumnType::BigInteger.to_sql(), "BIGINT");
350 assert_eq!(ColumnType::Boolean.to_sql(), "BOOLEAN");
351 assert_eq!(ColumnType::Timestamp.to_sql(), "TIMESTAMPTZ");
352 assert_eq!(ColumnType::Date.to_sql(), "DATE");
353 assert_eq!(ColumnType::Jsonb.to_sql(), "JSONB");
354 }
355
356 #[test]
357 fn test_fk_serialization() {
358 let manifest = sample_manifest();
359 let json = serde_json::to_string(&manifest).unwrap();
360 assert!(json.contains("\"references\":\"users.id\""));
361 assert!(json.contains("\"on_delete\":\"cascade\""));
362
363 let parsed: ServiceManifest = serde_json::from_str(&json).unwrap();
364 let user_id_col = &parsed.tables[0].columns[2];
365 assert_eq!(user_id_col.references.as_deref(), Some("users.id"));
366 assert_eq!(user_id_col.on_delete, Some(ForeignKeyAction::Cascade));
367 }
368
369 #[test]
370 fn test_fk_not_serialized_when_none() {
371 let col = ColumnDefinition {
372 name: "title".into(),
373 column_type: ColumnType::Text,
374 primary_key: false,
375 nullable: false,
376 auto_generate: false,
377 default_value: None,
378 references: None,
379 on_delete: None,
380 unique: false,
381 validations: vec![],
382 };
383 let json = serde_json::to_string(&col).unwrap();
384 assert!(!json.contains("references"));
385 assert!(!json.contains("on_delete"));
386 }
387
388 #[test]
389 fn test_unique_column_serialization() {
390 let col = ColumnDefinition {
391 name: "email".into(),
392 column_type: ColumnType::Text,
393 primary_key: false,
394 nullable: false,
395 auto_generate: false,
396 default_value: None,
397 references: None,
398 on_delete: None,
399 unique: true,
400 validations: vec![],
401 };
402 let json = serde_json::to_string(&col).unwrap();
403 assert!(json.contains("\"unique\":true"));
404 let parsed: ColumnDefinition = serde_json::from_str(&json).unwrap();
405 assert!(parsed.unique);
406 }
407
408 #[test]
409 fn test_index_serialization() {
410 let table = TableDefinition {
411 name: "users".into(),
412 columns: vec![],
413 indexes: vec![IndexDefinition {
414 name: "idx_users_email".into(),
415 columns: vec!["email".into()],
416 unique: true,
417 }],
418 soft_delete: false,
419 owner_field: None,
420 auth_required: false,
421 };
422 let json = serde_json::to_string(&table).unwrap();
423 assert!(json.contains("idx_users_email"));
424 let parsed: TableDefinition = serde_json::from_str(&json).unwrap();
425 assert_eq!(parsed.indexes.len(), 1);
426 assert!(parsed.indexes[0].unique);
427 }
428
429 #[test]
430 fn test_indexes_not_serialized_when_empty() {
431 let table = TableDefinition {
432 name: "users".into(),
433 columns: vec![],
434 indexes: vec![],
435 soft_delete: false,
436 owner_field: None,
437 auth_required: false,
438 };
439 let json = serde_json::to_string(&table).unwrap();
440 assert!(!json.contains("indexes"));
441 }
442
443 #[test]
444 fn test_service_mode_default() {
445 let json = r#"{"name":"svc","tables":[]}"#;
446 let m: ServiceManifest = serde_json::from_str(json).unwrap();
447 assert_eq!(m.mode, ServiceMode::Crud);
448 }
449
450 #[test]
451 fn test_owner_field_serialization() {
452 let table = TableDefinition {
453 name: "notes".into(),
454 columns: vec![],
455 indexes: vec![],
456 soft_delete: false,
457 owner_field: Some("user_id".into()),
458 auth_required: false,
459 };
460 let json = serde_json::to_string(&table).unwrap();
461 assert!(json.contains("\"owner_field\":\"user_id\""));
462 let parsed: TableDefinition = serde_json::from_str(&json).unwrap();
463 assert_eq!(parsed.owner_field.as_deref(), Some("user_id"));
464 }
465
466 #[test]
467 fn test_owner_field_not_serialized_when_none() {
468 let table = TableDefinition {
469 name: "notes".into(),
470 columns: vec![],
471 indexes: vec![],
472 soft_delete: false,
473 owner_field: None,
474 auth_required: false,
475 };
476 let json = serde_json::to_string(&table).unwrap();
477 assert!(!json.contains("owner_field"));
478 }
479
480 #[test]
481 fn test_owner_field_defaults_to_none() {
482 let json = r#"{"name":"notes","columns":[]}"#;
483 let table: TableDefinition = serde_json::from_str(json).unwrap();
484 assert!(table.owner_field.is_none());
485 }
486
487 #[test]
488 fn test_auth_required_serialization() {
489 let table = TableDefinition {
490 name: "orders".into(),
491 columns: vec![],
492 indexes: vec![],
493 soft_delete: false,
494 owner_field: None,
495 auth_required: true,
496 };
497 let json = serde_json::to_string(&table).unwrap();
498 assert!(json.contains("\"auth_required\":true"));
499 let parsed: TableDefinition = serde_json::from_str(&json).unwrap();
500 assert!(parsed.auth_required);
501 }
502
503 #[test]
504 fn test_auth_required_defaults_to_false() {
505 let json = r#"{"name":"orders","columns":[]}"#;
506 let table: TableDefinition = serde_json::from_str(json).unwrap();
507 assert!(!table.auth_required);
508 }
509}