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