doido_generators/generators/
storage_adapter.rs1use 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}