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