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#![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}