bular 0.0.2

CLI for managing Bular deployments
use std::{collections::HashMap, path::PathBuf};

use serde::{Deserialize, Serialize};

// TODO: What if multiple frontend apps?
// TODO: Custom Rust flags
// TODO: Multiple Rust crates
// TODO: Configure AWS account id?
// TODO: Configure Lambda memory, timeout, role, etc.

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    pub name: String,
    #[serde(default)]
    pub bular_id: Option<String>,
    #[serde(default, rename = "static", skip_serializing_if = "Option::is_none")]
    pub _static: Option<StaticConfig>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub paths: Vec<Path>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StaticConfig {
    /// Command to build the static files
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub build: Option<String>,
    /// Directory where the static files are located
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dist: Option<PathBuf>,
    /// Command to start the development server
    /// It's important this command listens on the `PORT` environment variable
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dev: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Path {
    /// Path to match against the incoming HTTP request
    pub path: String,
    /// Headers to set on the response if the path matches
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub status: Option<u16>,
    /// Headers to set on the response if the path matches
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub headers: HashMap<String, String>,
    /// Place to send the request
    #[serde(default, flatten)]
    pub destination: Option<Destination>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Destination {
    /// Rewrite the incoming path.
    /// This could be used for SPA routing or to proxy to another service
    Rewrite { to: String },
    /// Redirect the user to another URL
    /// This is a nicer wrapper on
    Redirect { redirect: String },
    /// Route the request to a specific Rust crate
    Rust {
        #[serde(rename = "crate")]
        _crate: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        bin: Option<String>,
    },
    /// Used to signify that matching should stop and the response should be sent
    /// Is useful for implementing a redirect or the like which doesn't have a response
    Final { r#final: bool },
}

pub fn default_config(name: String, bular_id: Option<String>) -> Config {
    Config {
        name: name.clone(),
        bular_id,
        _static: None,
        paths: vec![Path {
            path: "/*".into(),
            status: None,
            headers: Default::default(),
            destination: Some(Destination::Rust {
                _crate: name, // TODO: Should we transform the name to the right casing?
                bin: None,
            }),
        }],
    }
}