shaperail_codegen/
feature_check.rs1use shaperail_core::ResourceDefinition;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct RequiredFeature {
6 pub feature: &'static str,
8 pub reason: String,
10 pub enable_hint: String,
12}
13
14pub fn check_required_features(resources: &[ResourceDefinition]) -> Vec<RequiredFeature> {
20 let mut required = Vec::new();
21
22 for resource in resources {
23 let res = &resource.resource;
24
25 if let Some(endpoints) = &resource.endpoints {
29 for (action, ep) in endpoints {
30 if let Some(controller) = &ep.controller {
32 if controller.has_wasm_before() || controller.has_wasm_after() {
33 required.push(RequiredFeature {
34 feature: "wasm-plugins",
35 reason: format!(
36 "resource '{res}' endpoint '{action}' uses WASM controller"
37 ),
38 enable_hint: "Add to Cargo.toml: shaperail-runtime = { features = [\"wasm-plugins\"] }".into(),
39 });
40 }
41 }
42 }
43 }
44
45 if resource.db.is_some() {
48 required.push(RequiredFeature {
49 feature: "multi-db",
50 reason: format!("resource '{res}' uses 'db' key for multi-database routing"),
51 enable_hint: "Add to Cargo.toml: shaperail-runtime = { features = [\"multi-db\"] }"
52 .into(),
53 });
54 }
55 }
56
57 required.sort_by(|a, b| a.feature.cmp(b.feature));
59 required.dedup_by(|a, b| a.feature == b.feature);
60 required
61}
62
63pub fn format_feature_warnings(required: &[RequiredFeature]) -> String {
65 if required.is_empty() {
66 return String::new();
67 }
68
69 let mut out = String::from("Feature requirements detected:\n");
70 for feat in required {
71 out.push_str(&format!(
72 " - feature '{}' needed: {}\n {}\n",
73 feat.feature, feat.reason, feat.enable_hint
74 ));
75 }
76 out
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::parser::parse_resource;
83
84 #[test]
85 fn no_features_for_basic_resource() {
86 let yaml = r#"
87resource: items
88version: 1
89schema:
90 id: { type: uuid, primary: true, generated: true }
91 name: { type: string, required: true }
92endpoints:
93 list:
94 auth: public
95 pagination: cursor
96"#;
97 let rd = parse_resource(yaml).unwrap();
98 let required = check_required_features(&[rd]);
99 assert!(required.is_empty());
100 }
101
102 #[test]
103 fn upload_requires_no_extra_feature() {
104 let yaml = r#"
105resource: assets
106version: 1
107schema:
108 id: { type: uuid, primary: true, generated: true }
109 file: { type: file, required: true }
110endpoints:
111 upload:
112 method: POST
113 path: /assets/upload
114 input: [file]
115 upload:
116 field: file
117 storage: s3
118 max_size: 5mb
119"#;
120 let rd = parse_resource(yaml).unwrap();
121 let required = check_required_features(&[rd]);
122 assert!(!required.iter().any(|f| f.feature == "storage"));
123 }
124
125 #[test]
126 fn wasm_controller_requires_wasm_plugins_feature() {
127 let yaml = r#"
128resource: items
129version: 1
130schema:
131 id: { type: uuid, primary: true, generated: true }
132 name: { type: string, required: true }
133endpoints:
134 create:
135 method: POST
136 path: /items
137 input: [name]
138 controller: { before: "wasm:./plugins/validator.wasm" }
139"#;
140 let rd = parse_resource(yaml).unwrap();
141 let required = check_required_features(&[rd]);
142 assert!(required.iter().any(|f| f.feature == "wasm-plugins"));
143 }
144
145 #[test]
146 fn multi_db_requires_feature() {
147 let yaml = r#"
148resource: events
149version: 1
150db: analytics
151schema:
152 id: { type: uuid, primary: true, generated: true }
153 name: { type: string, required: true }
154"#;
155 let rd = parse_resource(yaml).unwrap();
156 let required = check_required_features(&[rd]);
157 assert!(required.iter().any(|f| f.feature == "multi-db"));
158 }
159}