Skip to main content

voce_adapter_core/
lib.rs

1//! Shared deployment adapter trait and types for Voce IR.
2//!
3//! All deployment adapters (static, Vercel, Cloudflare, Netlify)
4//! implement the [`Adapter`] trait to produce platform-specific bundles.
5
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9use anyhow::Result;
10use serde::{Deserialize, Serialize};
11
12/// The compiled output from the Voce compiler, ready for deployment.
13#[derive(Debug)]
14pub struct CompiledOutput {
15    /// The main HTML file content.
16    pub html: String,
17    /// Asset files: filename → bytes (images, fonts, etc.).
18    pub assets: HashMap<String, Vec<u8>>,
19    /// Server-side action handlers extracted from ActionNodes.
20    pub actions: Vec<ActionHandler>,
21    /// Project metadata.
22    pub meta: ProjectMeta,
23}
24
25/// A server-side action handler derived from an ActionNode.
26#[derive(Debug, Clone)]
27pub struct ActionHandler {
28    /// Route path (e.g., "/api/contact").
29    pub route: String,
30    /// HTTP method (e.g., "POST").
31    pub method: String,
32    /// Node ID from the IR.
33    pub node_id: String,
34    /// Handler body — JS/TS code stub for the serverless function.
35    pub handler_code: String,
36}
37
38/// Project metadata for deployment configuration.
39#[derive(Debug, Clone, Default)]
40pub struct ProjectMeta {
41    /// Project name (used for deployment naming).
42    pub name: String,
43    /// Custom domain, if configured.
44    pub domain: Option<String>,
45    /// Environment variables to set.
46    pub env_vars: HashMap<String, String>,
47}
48
49/// A deployment bundle — the files ready to upload/deploy.
50#[derive(Debug)]
51pub struct Bundle {
52    /// Output directory path.
53    pub output_dir: PathBuf,
54    /// All files in the bundle: relative path → content bytes.
55    pub files: HashMap<PathBuf, Vec<u8>>,
56    /// Human-readable summary of what was generated.
57    pub summary: String,
58}
59
60impl Bundle {
61    /// Write all bundle files to the output directory.
62    pub fn write_to_disk(&self) -> Result<()> {
63        for (rel_path, content) in &self.files {
64            let full_path = self.output_dir.join(rel_path);
65            if let Some(parent) = full_path.parent() {
66                std::fs::create_dir_all(parent)?;
67            }
68            std::fs::write(&full_path, content)?;
69        }
70        Ok(())
71    }
72
73    /// Total size of all files in bytes.
74    pub fn total_size(&self) -> usize {
75        self.files.values().map(|v| v.len()).sum()
76    }
77}
78
79/// Result of a deployment operation.
80#[derive(Debug)]
81pub struct DeployResult {
82    /// URL where the site is live (if available).
83    pub url: Option<String>,
84    /// Platform-specific deployment ID.
85    pub deployment_id: Option<String>,
86    /// Human-readable status message.
87    pub message: String,
88}
89
90/// Deployment configuration from `.voce/config.toml`.
91#[derive(Debug, Clone, Deserialize, Serialize, Default)]
92pub struct DeployConfig {
93    /// Default adapter name.
94    #[serde(default)]
95    pub adapter: String,
96    /// Custom domain.
97    pub domain: Option<String>,
98    /// Environment variables.
99    #[serde(default)]
100    pub env: HashMap<String, String>,
101    /// Adapter-specific settings.
102    #[serde(default)]
103    pub settings: HashMap<String, String>,
104}
105
106/// Load deployment config from `.voce/config.toml`.
107pub fn load_config(project_dir: &Path) -> Result<DeployConfig> {
108    let config_path = project_dir.join(".voce/config.toml");
109    if config_path.exists() {
110        let content = std::fs::read_to_string(&config_path)?;
111        let config: DeployConfig = toml::from_str(&content)?;
112        Ok(config)
113    } else {
114        Ok(DeployConfig::default())
115    }
116}
117
118/// The trait all deployment adapters must implement.
119pub trait Adapter {
120    /// Human-readable adapter name (e.g., "static", "vercel").
121    fn name(&self) -> &str;
122
123    /// Prepare a deployment bundle from compiled output.
124    fn prepare(&self, compiled: &CompiledOutput, config: &DeployConfig) -> Result<Bundle>;
125
126    /// Deploy the bundle to the target platform.
127    /// Returns `Err` if the platform CLI is not available or deployment fails.
128    fn deploy(&self, bundle: &Bundle, config: &DeployConfig) -> Result<DeployResult>;
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn bundle_total_size() {
137        let mut files = HashMap::new();
138        files.insert(PathBuf::from("index.html"), vec![0u8; 100]);
139        files.insert(PathBuf::from("style.css"), vec![0u8; 50]);
140        let bundle = Bundle {
141            output_dir: PathBuf::from("dist"),
142            files,
143            summary: "test".to_string(),
144        };
145        assert_eq!(bundle.total_size(), 150);
146    }
147
148    #[test]
149    fn default_config() {
150        let config = DeployConfig::default();
151        assert!(config.adapter.is_empty());
152        assert!(config.domain.is_none());
153    }
154}