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