Skip to main content

doido_generators/generators/
storage_adapter.rs

1//! `doido generate storage:adapter <Name>` — scaffolds a custom storage adapter
2//! (an external file-service integration) implementing `doido_storage::Service`.
3//!
4//! Emits `app/storage/<snake>_service.rs` with a `<Pascal>Service` skeleton plus a
5//! `register()` that wires it into the storage adapter registry, so the app can
6//! select it from `config/<env>.yml` with `type: <snake>`.
7
8use crate::generator::{GeneratedFile, Generator};
9use crate::generators::{to_pascal, to_snake};
10use doido_core::Result;
11
12pub struct StorageAdapterGenerator;
13
14fn render(name: &str) -> String {
15    let snake = to_snake(name);
16    let pascal = to_pascal(name);
17    format!(
18        r#"//! Custom storage adapter `{pascal}Service` — integrate an external file service.
19//!
20//! Wire it up: declare this module in your app, call `{snake}_service::register()`
21//! once at boot, then select it in `config/<env>.yml`:
22//!
23//! ```yaml
24//! storage:
25//!   service: files
26//!   services:
27//!     files: {{ type: {snake}, token: "...", root: "/app" }}
28//! ```
29
30use doido::core::Result;
31use doido::storage::{{register_adapter, Service, ServiceConfig}};
32use std::sync::Arc;
33
34/// Adapter for your external file service.
35pub struct {pascal}Service {{
36    name: String,
37    // TODO: hold your HTTP/SDK client + credentials here.
38}}
39
40impl {pascal}Service {{
41    /// Build the adapter from a named service's config. Read custom keys with
42    /// `cfg.option_str("...")` and the shared fields (`cfg.bucket`, `cfg.endpoint`).
43    pub fn connect(name: &str, cfg: &ServiceConfig) -> Result<Self> {{
44        let _token = cfg.option_str("token");
45        Ok(Self {{ name: name.to_string() }})
46    }}
47}}
48
49/// Register this adapter under `type: {snake}`. Call once at boot.
50pub fn register() {{
51    register_adapter("{snake}", |name: &str, cfg: &ServiceConfig| {{
52        Ok(Arc::new({pascal}Service::connect(name, cfg)?) as Arc<dyn Service>)
53    }});
54}}
55
56#[doido::core::async_trait]
57impl Service for {pascal}Service {{
58    fn name(&self) -> &str {{
59        &self.name
60    }}
61
62    async fn upload(&self, key: &str, _data: Vec<u8>, _content_type: Option<&str>) -> Result<()> {{
63        todo!("upload object {{key}} to your service")
64    }}
65
66    async fn download(&self, key: &str) -> Result<Vec<u8>> {{
67        todo!("download object {{key}} from your service")
68    }}
69
70    async fn delete(&self, key: &str) -> Result<()> {{
71        todo!("delete object {{key}} from your service")
72    }}
73
74    async fn exists(&self, key: &str) -> Result<bool> {{
75        todo!("check whether object {{key}} exists")
76    }}
77
78    async fn size(&self, key: &str) -> Result<u64> {{
79        todo!("return the byte size of object {{key}}")
80    }}
81
82    // Optionally override `url`/`presigned_put` to return a native (presigned)
83    // URL. The defaults return `None`, so blobs are served through the app proxy.
84}}
85"#
86    )
87}
88
89impl Generator for StorageAdapterGenerator {
90    fn name(&self) -> &str {
91        "storage:adapter"
92    }
93
94    fn generate(&self, args: &[&str]) -> Result<Vec<GeneratedFile>> {
95        let name = args.first().copied().ok_or_else(|| {
96            doido_core::anyhow::anyhow!("storage:adapter generator requires a name argument")
97        })?;
98        let snake = to_snake(name);
99        Ok(vec![GeneratedFile {
100            path: format!("app/storage/{snake}_service.rs"),
101            content: render(name),
102        }])
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn emits_service_skeleton_and_register() {
112        let files = StorageAdapterGenerator.generate(&["Dropbox"]).unwrap();
113        assert_eq!(files.len(), 1);
114        assert_eq!(files[0].path, "app/storage/dropbox_service.rs");
115        let content = &files[0].content;
116        assert!(content.contains("pub struct DropboxService"));
117        assert!(content.contains("impl Service for DropboxService"));
118        assert!(content.contains("register_adapter(\"dropbox\""));
119        assert!(content.contains("pub fn register()"));
120    }
121
122    #[test]
123    fn requires_a_name() {
124        assert!(StorageAdapterGenerator.generate(&[]).is_err());
125    }
126}