Skip to main content

shaperail_codegen/
feature_check.rs

1use shaperail_core::ResourceDefinition;
2
3/// A feature that a resource requires but may not be enabled.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct RequiredFeature {
6    /// The Cargo feature name (e.g., "storage").
7    pub feature: &'static str,
8    /// Why this feature is needed.
9    pub reason: String,
10    /// How to enable it.
11    pub enable_hint: String,
12}
13
14/// Check what Cargo features a set of resources require.
15///
16/// Returns a list of features that resources use. The caller can then
17/// compare against the project's enabled features to detect mismatches early
18/// (before the compile-time error).
19pub 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        // Check for upload endpoints → storage feature
26        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                // Check for WASM controllers → wasm-plugins feature
39                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        // Check for tenant_key → implicit (always available, no feature needed)
54        // Check for multi-db → multi-db feature
55        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    // Deduplicate by feature name
66    required.sort_by(|a, b| a.feature.cmp(b.feature));
67    required.dedup_by(|a, b| a.feature == b.feature);
68    required
69}
70
71/// Format required features as user-facing warnings.
72pub 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}