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 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub on_migrate: Option<String>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
35#[serde(rename_all = "lowercase")]
36pub enum ServiceMode {
37 #[default]
38 Crud,
39 Wasm,
40 Container,
41 Web,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct TableDefinition {
46 pub name: String,
47 pub columns: Vec<ColumnDefinition>,
48 #[serde(default, skip_serializing_if = "Vec::is_empty")]
49 pub indexes: Vec<IndexDefinition>,
50 #[serde(default)]
51 pub soft_delete: bool,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub owner_field: Option<String>,
56 #[serde(default)]
59 pub auth_required: bool,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub permission_area: Option<String>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub hooks: Option<TableHooks>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ColumnDefinition {
72 pub name: String,
73 pub column_type: ColumnType,
74 #[serde(default)]
75 pub primary_key: bool,
76 #[serde(default)]
77 pub nullable: bool,
78 #[serde(default)]
79 pub auto_generate: bool,
80 pub default_value: Option<String>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub references: Option<String>,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub on_delete: Option<ForeignKeyAction>,
87 #[serde(default)]
89 pub unique: bool,
90 #[serde(default, skip_serializing_if = "Vec::is_empty")]
92 pub validations: Vec<ValidationRule>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
96#[serde(tag = "rule", rename_all = "snake_case")]
97pub enum ValidationRule {
99 Regex { pattern: String },
101 Min { value: f64 },
103 Max { value: f64 },
105 MinLength { value: usize },
107 MaxLength { value: usize },
109 OneOf { values: Vec<String> },
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114pub struct IndexDefinition {
115 pub name: String,
116 pub columns: Vec<String>,
117 #[serde(default)]
118 pub unique: bool,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
122#[serde(rename_all = "snake_case")]
123pub enum ForeignKeyAction {
124 Cascade,
125 SetNull,
126 Restrict,
127 NoAction,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
131#[serde(rename_all = "lowercase")]
132pub enum ColumnType {
133 Uuid,
134 Text,
135 Integer,
136 BigInteger,
137 Float,
138 Double,
139 Boolean,
140 Timestamp,
141 Date,
142 Jsonb,
143}
144
145impl ColumnType {
146 pub fn to_sql(&self) -> &str {
147 match self {
148 ColumnType::Uuid => "UUID",
149 ColumnType::Text => "TEXT",
150 ColumnType::Integer => "INTEGER",
151 ColumnType::BigInteger => "BIGINT",
152 ColumnType::Float => "REAL",
153 ColumnType::Double => "DOUBLE PRECISION",
154 ColumnType::Boolean => "BOOLEAN",
155 ColumnType::Timestamp => "TIMESTAMPTZ",
156 ColumnType::Date => "DATE",
157 ColumnType::Jsonb => "JSONB",
158 }
159 }
160
161 pub fn filter_type(&self) -> &str {
163 match self {
164 ColumnType::Uuid => "uuid",
165 ColumnType::Text => "text",
166 ColumnType::Integer | ColumnType::BigInteger => "integer",
167 ColumnType::Float | ColumnType::Double => "double",
168 ColumnType::Boolean => "boolean",
169 ColumnType::Timestamp => "timestamp",
170 ColumnType::Date => "date",
171 ColumnType::Jsonb => "jsonb",
172 }
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct CellDefinition {
178 pub name: String,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct EventDefinition {
183 pub name: String,
184 pub table: String,
185 pub action: EventAction,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(rename_all = "lowercase")]
190pub enum EventAction {
191 Create,
192 Update,
193 Delete,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct SubscriptionDefinition {
198 pub subject: String,
199 pub handler: String,
200 pub handler_type: HandlerType,
201 #[serde(default)]
202 pub config: HashMap<String, String>,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
206#[serde(rename_all = "snake_case")]
207pub enum HandlerType {
208 DeleteCascade,
209 UpdateField,
210 Webhook,
211 Wasm,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct CustomRouteDefinition {
216 pub method: String,
217 pub path: String,
218 pub handler: String,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, Default)]
223pub struct TableHooks {
224 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub before_create: Option<String>,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub after_create: Option<String>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub before_update: Option<String>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub after_update: Option<String>,
232 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub before_delete: Option<String>,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub after_delete: Option<String>,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct AuthorizationConfig {
241 #[serde(default)]
243 pub areas: Vec<PermissionAreaDef>,
244 #[serde(default)]
246 pub default_roles: Vec<DefaultRoleDef>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct PermissionAreaDef {
252 pub name: String,
253 #[serde(default)]
254 pub operations: Vec<String>,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct DefaultRoleDef {
260 pub name: String,
261 #[serde(default, skip_serializing_if = "Option::is_none")]
262 pub description: Option<String>,
263 #[serde(default)]
264 pub permissions: Vec<String>,
265}
266
267#[derive(Debug, Clone, Default, Serialize, Deserialize)]
271pub struct SchemaDiff {
272 #[serde(default)]
273 pub added_tables: Vec<String>,
274 #[serde(default)]
275 pub dropped_tables: Vec<String>,
276 #[serde(default)]
280 pub added_columns: Vec<(String, String)>,
281 #[serde(default)]
285 pub dropped_columns: Vec<(String, String)>,
286 #[serde(default)]
288 pub type_changes: Vec<(String, String, String, String)>,
289 #[serde(default)]
291 pub nullability_changes: Vec<(String, String, bool)>,
292}
293
294impl SchemaDiff {
295 pub fn added_column(&self, table: &str, col: &str) -> bool {
296 self.added_columns
297 .iter()
298 .any(|(t, c)| t == table && c == col)
299 }
300
301 pub fn dropped_column(&self, table: &str, col: &str) -> bool {
302 self.dropped_columns
303 .iter()
304 .any(|(t, c)| t == table && c == col)
305 }
306
307 pub fn added_table(&self, table: &str) -> bool {
308 self.added_tables.iter().any(|t| t == table)
309 }
310
311 pub fn dropped_table(&self, table: &str) -> bool {
312 self.dropped_tables.iter().any(|t| t == table)
313 }
314
315 pub fn is_empty(&self) -> bool {
316 self.added_tables.is_empty()
317 && self.dropped_tables.is_empty()
318 && self.added_columns.is_empty()
319 && self.dropped_columns.is_empty()
320 && self.type_changes.is_empty()
321 && self.nullability_changes.is_empty()
322 }
323}
324
325impl ServiceManifest {
326 pub fn get_table(&self, name: &str) -> Option<&TableDefinition> {
328 self.tables.iter().find(|t| t.name == name)
329 }
330
331 pub fn requires_wasm(&self) -> bool {
339 if self.mode == ServiceMode::Wasm {
340 return true;
341 }
342 if self.on_migrate.is_some() {
343 return true;
344 }
345 if self.tables.iter().any(|t| t.hooks.is_some()) {
346 return true;
347 }
348 if !self.custom_routes.is_empty() {
349 return true;
350 }
351 if self
352 .subscriptions
353 .iter()
354 .any(|s| matches!(s.handler_type, HandlerType::Wasm))
355 {
356 return true;
357 }
358 false
359 }
360
361 pub fn declared_wasm_handlers(&self) -> Vec<&str> {
365 let mut handlers: Vec<&str> = Vec::new();
366 if let Some(ref h) = self.on_migrate {
367 handlers.push(h.as_str());
368 }
369 for route in &self.custom_routes {
370 handlers.push(route.handler.as_str());
371 }
372 for sub in &self.subscriptions {
373 if matches!(sub.handler_type, HandlerType::Wasm) {
374 handlers.push(sub.handler.as_str());
375 }
376 }
377 for table in &self.tables {
378 if let Some(ref hooks) = table.hooks {
379 for h in [
380 hooks.before_create.as_deref(),
381 hooks.after_create.as_deref(),
382 hooks.before_update.as_deref(),
383 hooks.after_update.as_deref(),
384 hooks.before_delete.as_deref(),
385 hooks.after_delete.as_deref(),
386 ]
387 .iter()
388 .flatten()
389 {
390 handlers.push(h);
391 }
392 }
393 }
394 handlers.sort();
395 handlers.dedup();
396 handlers
397 }
398
399 pub fn schema_hash(&self) -> String {
401 use sha2::{Digest, Sha256};
402 let mut hasher = Sha256::new();
403 hasher.update(
404 serde_json::to_string(&self.tables)
405 .unwrap_or_default()
406 .as_bytes(),
407 );
408 hasher.update(
409 serde_json::to_string(&self.custom_routes)
410 .unwrap_or_default()
411 .as_bytes(),
412 );
413 hasher.update(
414 serde_json::to_string(&self.subscriptions)
415 .unwrap_or_default()
416 .as_bytes(),
417 );
418 hasher.update(
419 serde_json::to_string(&self.authorization)
420 .unwrap_or_default()
421 .as_bytes(),
422 );
423 hasher.update(
424 serde_json::to_string(&self.on_migrate)
425 .unwrap_or_default()
426 .as_bytes(),
427 );
428 hex::encode(hasher.finalize())
429 }
430}
431
432impl TableDefinition {
433 pub fn primary_key(&self) -> Option<&ColumnDefinition> {
435 self.columns.iter().find(|c| c.primary_key)
436 }
437
438 pub fn writable_columns(&self) -> Vec<&ColumnDefinition> {
440 self.columns
441 .iter()
442 .filter(|c| !c.auto_generate || !c.primary_key)
443 .collect()
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450
451 fn sample_manifest() -> ServiceManifest {
452 ServiceManifest {
453 name: "test-service".into(),
454 version: Some("1.0.0".into()),
455 tables: vec![TableDefinition {
456 name: "todos".into(),
457 columns: vec![
458 ColumnDefinition {
459 name: "id".into(),
460 column_type: ColumnType::Uuid,
461 primary_key: true,
462 nullable: false,
463 auto_generate: true,
464 default_value: None,
465 references: None,
466 on_delete: None,
467 unique: false,
468 validations: vec![],
469 },
470 ColumnDefinition {
471 name: "title".into(),
472 column_type: ColumnType::Text,
473 primary_key: false,
474 nullable: false,
475 auto_generate: false,
476 default_value: None,
477 references: None,
478 on_delete: None,
479 unique: false,
480 validations: vec![],
481 },
482 ColumnDefinition {
483 name: "user_id".into(),
484 column_type: ColumnType::Uuid,
485 primary_key: false,
486 nullable: false,
487 auto_generate: false,
488 default_value: None,
489 references: Some("users.id".into()),
490 on_delete: Some(ForeignKeyAction::Cascade),
491 unique: false,
492 validations: vec![],
493 },
494 ],
495 indexes: vec![],
496 soft_delete: false,
497 owner_field: None,
498 auth_required: false,
499 permission_area: None,
500 hooks: None,
501 }],
502 cells: vec![],
503 events: vec![],
504 subscriptions: vec![],
505 custom_routes: vec![],
506 mode: ServiceMode::Crud,
507 authorization: None,
508 on_migrate: None,
509 }
510 }
511
512 #[test]
513 fn test_manifest_serialization_roundtrip() {
514 let manifest = sample_manifest();
515 let json = serde_json::to_string(&manifest).unwrap();
516 let parsed: ServiceManifest = serde_json::from_str(&json).unwrap();
517 assert_eq!(parsed.name, "test-service");
518 assert_eq!(parsed.tables.len(), 1);
519 assert_eq!(parsed.tables[0].columns.len(), 3);
520 }
521
522 #[test]
523 fn test_get_table() {
524 let manifest = sample_manifest();
525 assert!(manifest.get_table("todos").is_some());
526 assert!(manifest.get_table("nonexistent").is_none());
527 }
528
529 #[test]
530 fn test_primary_key() {
531 let manifest = sample_manifest();
532 let table = manifest.get_table("todos").unwrap();
533 let pk = table.primary_key().unwrap();
534 assert_eq!(pk.name, "id");
535 assert!(pk.auto_generate);
536 }
537
538 #[test]
539 fn test_schema_hash_deterministic() {
540 let m1 = sample_manifest();
541 let m2 = sample_manifest();
542 assert_eq!(m1.schema_hash(), m2.schema_hash());
543 }
544
545 #[test]
546 fn test_schema_hash_changes() {
547 let mut m1 = sample_manifest();
548 let m2 = sample_manifest();
549 m1.tables[0].columns.push(ColumnDefinition {
550 name: "extra".into(),
551 column_type: ColumnType::Text,
552 primary_key: false,
553 nullable: true,
554 auto_generate: false,
555 default_value: None,
556 references: None,
557 on_delete: None,
558 unique: false,
559 validations: vec![],
560 });
561 assert_ne!(m1.schema_hash(), m2.schema_hash());
562 }
563
564 #[test]
565 fn test_column_type_to_sql() {
566 assert_eq!(ColumnType::Uuid.to_sql(), "UUID");
567 assert_eq!(ColumnType::Text.to_sql(), "TEXT");
568 assert_eq!(ColumnType::Integer.to_sql(), "INTEGER");
569 assert_eq!(ColumnType::BigInteger.to_sql(), "BIGINT");
570 assert_eq!(ColumnType::Boolean.to_sql(), "BOOLEAN");
571 assert_eq!(ColumnType::Timestamp.to_sql(), "TIMESTAMPTZ");
572 assert_eq!(ColumnType::Date.to_sql(), "DATE");
573 assert_eq!(ColumnType::Jsonb.to_sql(), "JSONB");
574 }
575
576 #[test]
577 fn test_fk_serialization() {
578 let manifest = sample_manifest();
579 let json = serde_json::to_string(&manifest).unwrap();
580 assert!(json.contains("\"references\":\"users.id\""));
581 assert!(json.contains("\"on_delete\":\"cascade\""));
582
583 let parsed: ServiceManifest = serde_json::from_str(&json).unwrap();
584 let user_id_col = &parsed.tables[0].columns[2];
585 assert_eq!(user_id_col.references.as_deref(), Some("users.id"));
586 assert_eq!(user_id_col.on_delete, Some(ForeignKeyAction::Cascade));
587 }
588
589 #[test]
590 fn test_fk_not_serialized_when_none() {
591 let col = ColumnDefinition {
592 name: "title".into(),
593 column_type: ColumnType::Text,
594 primary_key: false,
595 nullable: false,
596 auto_generate: false,
597 default_value: None,
598 references: None,
599 on_delete: None,
600 unique: false,
601 validations: vec![],
602 };
603 let json = serde_json::to_string(&col).unwrap();
604 assert!(!json.contains("references"));
605 assert!(!json.contains("on_delete"));
606 }
607
608 #[test]
609 fn test_unique_column_serialization() {
610 let col = ColumnDefinition {
611 name: "email".into(),
612 column_type: ColumnType::Text,
613 primary_key: false,
614 nullable: false,
615 auto_generate: false,
616 default_value: None,
617 references: None,
618 on_delete: None,
619 unique: true,
620 validations: vec![],
621 };
622 let json = serde_json::to_string(&col).unwrap();
623 assert!(json.contains("\"unique\":true"));
624 let parsed: ColumnDefinition = serde_json::from_str(&json).unwrap();
625 assert!(parsed.unique);
626 }
627
628 #[test]
629 fn test_index_serialization() {
630 let table = TableDefinition {
631 name: "users".into(),
632 columns: vec![],
633 indexes: vec![IndexDefinition {
634 name: "idx_users_email".into(),
635 columns: vec!["email".into()],
636 unique: true,
637 }],
638 soft_delete: false,
639 owner_field: None,
640 auth_required: false,
641 permission_area: None,
642 hooks: None,
643 };
644 let json = serde_json::to_string(&table).unwrap();
645 assert!(json.contains("idx_users_email"));
646 let parsed: TableDefinition = serde_json::from_str(&json).unwrap();
647 assert_eq!(parsed.indexes.len(), 1);
648 assert!(parsed.indexes[0].unique);
649 }
650
651 #[test]
652 fn test_indexes_not_serialized_when_empty() {
653 let table = TableDefinition {
654 name: "users".into(),
655 columns: vec![],
656 indexes: vec![],
657 soft_delete: false,
658 owner_field: None,
659 auth_required: false,
660 permission_area: None,
661 hooks: None,
662 };
663 let json = serde_json::to_string(&table).unwrap();
664 assert!(!json.contains("indexes"));
665 }
666
667 #[test]
668 fn test_service_mode_default() {
669 let json = r#"{"name":"svc","tables":[]}"#;
670 let m: ServiceManifest = serde_json::from_str(json).unwrap();
671 assert_eq!(m.mode, ServiceMode::Crud);
672 }
673
674 #[test]
675 fn test_owner_field_serialization() {
676 let table = TableDefinition {
677 name: "notes".into(),
678 columns: vec![],
679 indexes: vec![],
680 soft_delete: false,
681 owner_field: Some("user_id".into()),
682 auth_required: false,
683 permission_area: None,
684 hooks: None,
685 };
686 let json = serde_json::to_string(&table).unwrap();
687 assert!(json.contains("\"owner_field\":\"user_id\""));
688 let parsed: TableDefinition = serde_json::from_str(&json).unwrap();
689 assert_eq!(parsed.owner_field.as_deref(), Some("user_id"));
690 }
691
692 #[test]
693 fn test_owner_field_not_serialized_when_none() {
694 let table = TableDefinition {
695 name: "notes".into(),
696 columns: vec![],
697 indexes: vec![],
698 soft_delete: false,
699 owner_field: None,
700 auth_required: false,
701 permission_area: None,
702 hooks: None,
703 };
704 let json = serde_json::to_string(&table).unwrap();
705 assert!(!json.contains("owner_field"));
706 }
707
708 #[test]
709 fn test_owner_field_defaults_to_none() {
710 let json = r#"{"name":"notes","columns":[]}"#;
711 let table: TableDefinition = serde_json::from_str(json).unwrap();
712 assert!(table.owner_field.is_none());
713 }
714
715 #[test]
716 fn test_auth_required_serialization() {
717 let table = TableDefinition {
718 name: "orders".into(),
719 columns: vec![],
720 indexes: vec![],
721 soft_delete: false,
722 owner_field: None,
723 auth_required: true,
724 permission_area: None,
725 hooks: None,
726 };
727 let json = serde_json::to_string(&table).unwrap();
728 assert!(json.contains("\"auth_required\":true"));
729 let parsed: TableDefinition = serde_json::from_str(&json).unwrap();
730 assert!(parsed.auth_required);
731 }
732
733 #[test]
734 fn test_auth_required_defaults_to_false() {
735 let json = r#"{"name":"orders","columns":[]}"#;
736 let table: TableDefinition = serde_json::from_str(json).unwrap();
737 assert!(!table.auth_required);
738 }
739
740 #[test]
741 fn test_on_migrate_defaults_to_none() {
742 let json = r#"{"name":"svc","tables":[]}"#;
743 let m: ServiceManifest = serde_json::from_str(json).unwrap();
744 assert!(m.on_migrate.is_none());
745 }
746
747 #[test]
748 fn test_on_migrate_serialization_roundtrip() {
749 let mut m = sample_manifest();
750 m.on_migrate = Some("handle_on_migrate".into());
751 let json = serde_json::to_string(&m).unwrap();
752 assert!(json.contains("\"on_migrate\":\"handle_on_migrate\""));
753 let parsed: ServiceManifest = serde_json::from_str(&json).unwrap();
754 assert_eq!(parsed.on_migrate.as_deref(), Some("handle_on_migrate"));
755 }
756
757 #[test]
758 fn test_on_migrate_skipped_when_none() {
759 let m = sample_manifest();
760 let json = serde_json::to_string(&m).unwrap();
761 assert!(!json.contains("on_migrate"));
762 }
763
764 #[test]
765 fn test_on_migrate_changes_schema_hash() {
766 let m1 = sample_manifest();
767 let mut m2 = sample_manifest();
768 m2.on_migrate = Some("handle_on_migrate".into());
769 assert_ne!(m1.schema_hash(), m2.schema_hash());
770 }
771
772 #[test]
773 fn test_schema_diff_helpers() {
774 let diff = SchemaDiff {
775 added_tables: vec!["batches".into()],
776 dropped_tables: vec!["legacy_batches".into()],
777 added_columns: vec![("pickups".into(), "min".into())],
778 dropped_columns: vec![("pickups".into(), "midpoint".into())],
779 type_changes: vec![],
780 nullability_changes: vec![],
781 };
782 assert!(!diff.is_empty());
783 assert!(diff.added_table("batches"));
784 assert!(!diff.added_table("nope"));
785 assert!(diff.dropped_table("legacy_batches"));
786 assert!(diff.added_column("pickups", "min"));
787 assert!(!diff.added_column("pickups", "max"));
788 assert!(diff.dropped_column("pickups", "midpoint"));
789 }
790
791 #[test]
792 fn test_schema_diff_is_empty_default() {
793 assert!(SchemaDiff::default().is_empty());
794 }
795
796 #[test]
797 fn test_schema_diff_serialization_roundtrip() {
798 let diff = SchemaDiff {
799 added_tables: vec![],
800 dropped_tables: vec![],
801 added_columns: vec![("t".into(), "c".into())],
802 dropped_columns: vec![],
803 type_changes: vec![("t".into(), "c".into(), "TEXT".into(), "INTEGER".into())],
804 nullability_changes: vec![("t".into(), "c".into(), false)],
805 };
806 let json = serde_json::to_string(&diff).unwrap();
807 let parsed: SchemaDiff = serde_json::from_str(&json).unwrap();
808 assert!(parsed.added_column("t", "c"));
809 assert_eq!(parsed.type_changes.len(), 1);
810 assert_eq!(parsed.nullability_changes.len(), 1);
811 }
812
813 #[test]
814 fn test_requires_wasm_false_for_plain_crud() {
815 let m = sample_manifest();
816 assert!(!m.requires_wasm());
817 }
818
819 #[test]
820 fn test_requires_wasm_true_for_wasm_mode() {
821 let mut m = sample_manifest();
822 m.mode = ServiceMode::Wasm;
823 assert!(m.requires_wasm());
824 }
825
826 #[test]
827 fn test_requires_wasm_true_for_on_migrate() {
828 let mut m = sample_manifest();
829 m.on_migrate = Some("handle_on_migrate".into());
830 assert!(m.requires_wasm());
831 }
832
833 #[test]
834 fn test_requires_wasm_true_for_table_hooks() {
835 let mut m = sample_manifest();
836 m.tables[0].hooks = Some(TableHooks {
837 before_create: Some("validate".into()),
838 ..Default::default()
839 });
840 assert!(m.requires_wasm());
841 }
842
843 #[test]
844 fn test_requires_wasm_true_for_custom_routes() {
845 let mut m = sample_manifest();
846 m.custom_routes.push(CustomRouteDefinition {
847 method: "GET".into(),
848 path: "/hello".into(),
849 handler: "hello".into(),
850 });
851 assert!(m.requires_wasm());
852 }
853
854 #[test]
855 fn test_requires_wasm_true_for_wasm_subscription() {
856 let mut m = sample_manifest();
857 m.subscriptions.push(SubscriptionDefinition {
858 subject: "events.foo".into(),
859 handler: "on_foo".into(),
860 handler_type: HandlerType::Wasm,
861 config: HashMap::new(),
862 });
863 assert!(m.requires_wasm());
864 }
865
866 #[test]
867 fn test_requires_wasm_false_for_non_wasm_subscription() {
868 let mut m = sample_manifest();
869 m.subscriptions.push(SubscriptionDefinition {
870 subject: "events.foo".into(),
871 handler: "on_foo".into(),
872 handler_type: HandlerType::Webhook,
873 config: HashMap::new(),
874 });
875 assert!(!m.requires_wasm());
876 }
877
878 #[test]
879 fn test_declared_wasm_handlers_collects_all_sources() {
880 let mut m = sample_manifest();
881 m.on_migrate = Some("on_mig".into());
882 m.custom_routes.push(CustomRouteDefinition {
883 method: "GET".into(),
884 path: "/h".into(),
885 handler: "route_h".into(),
886 });
887 m.subscriptions.push(SubscriptionDefinition {
888 subject: "x".into(),
889 handler: "sub_h".into(),
890 handler_type: HandlerType::Wasm,
891 config: HashMap::new(),
892 });
893 m.subscriptions.push(SubscriptionDefinition {
894 subject: "y".into(),
895 handler: "wh_ignored".into(),
896 handler_type: HandlerType::Webhook,
897 config: HashMap::new(),
898 });
899 m.tables[0].hooks = Some(TableHooks {
900 before_create: Some("bc".into()),
901 after_create: Some("ac".into()),
902 before_delete: Some("bc".into()), ..Default::default()
904 });
905 let handlers = m.declared_wasm_handlers();
906 assert!(handlers.contains(&"on_mig"));
907 assert!(handlers.contains(&"route_h"));
908 assert!(handlers.contains(&"sub_h"));
909 assert!(handlers.contains(&"bc"));
910 assert!(handlers.contains(&"ac"));
911 assert!(!handlers.contains(&"wh_ignored"));
912 assert_eq!(handlers.iter().filter(|h| **h == "bc").count(), 1);
914 }
915}