openauth-cli 0.0.4

Command-line tools for OpenAuth.
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};
use thiserror::Error;
use toml_edit::{value, Array, DocumentMut};

#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("failed to read {path}: {source}")]
    Read {
        path: PathBuf,
        source: std::io::Error,
    },
    #[error("failed to write {path}: {source}")]
    Write {
        path: PathBuf,
        source: std::io::Error,
    },
    #[error("failed to parse openauth.toml: {0}")]
    ParseToml(#[from] toml_edit::de::Error),
    #[error("failed to parse openauth.toml document: {0}")]
    ParseDocument(#[from] toml_edit::TomlError),
    #[error("failed to render openauth.toml: {0}")]
    SerializeToml(#[from] toml_edit::ser::Error),
    #[error("plugins.enabled must be an array")]
    InvalidPlugins,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct CliConfig {
    pub project: ProjectConfig,
    pub database: DatabaseConfig,
    pub security: SecurityConfig,
    pub plugins: PluginsConfig,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct ProjectConfig {
    pub framework: Option<String>,
    pub base_url: String,
    pub base_path: String,
    pub production: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct DatabaseConfig {
    pub adapter: String,
    pub provider: Option<String>,
    pub url_env: String,
    pub migrations_dir: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct SecurityConfig {
    pub secret_env: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct PluginsConfig {
    pub enabled: Vec<String>,
}

impl Default for ProjectConfig {
    fn default() -> Self {
        Self {
            framework: None,
            base_url: "http://localhost:3000/api/auth".to_owned(),
            base_path: "/api/auth".to_owned(),
            production: false,
        }
    }
}

impl Default for DatabaseConfig {
    fn default() -> Self {
        Self {
            adapter: "sqlx".to_owned(),
            provider: None,
            url_env: "DATABASE_URL".to_owned(),
            migrations_dir: "migrations/openauth".to_owned(),
        }
    }
}

impl Default for SecurityConfig {
    fn default() -> Self {
        Self {
            secret_env: "OPENAUTH_SECRET".to_owned(),
        }
    }
}

impl CliConfig {
    pub fn parse_str(source: &str) -> Result<Self, ConfigError> {
        source.parse()
    }

    pub fn load(path: &Path) -> Result<Self, ConfigError> {
        let source = fs::read_to_string(path).map_err(|source| ConfigError::Read {
            path: path.to_path_buf(),
            source,
        })?;
        Self::parse_str(&source)
    }

    pub fn load_optional(path: &Path) -> Result<Option<Self>, ConfigError> {
        if !path.exists() {
            return Ok(None);
        }
        Self::load(path).map(Some)
    }

    pub fn to_toml_string(&self) -> Result<String, ConfigError> {
        Ok(toml_edit::ser::to_string_pretty(self)?)
    }

    pub fn write(&self, path: &Path) -> Result<(), ConfigError> {
        let rendered = self.to_toml_string()?;
        fs::write(path, rendered).map_err(|source| ConfigError::Write {
            path: path.to_path_buf(),
            source,
        })
    }

    pub fn add_plugin_to_document(source: &str, plugin: &str) -> Result<String, ConfigError> {
        let mut document = source.parse::<DocumentMut>()?;
        ensure_plugin_array(&mut document)?;
        let enabled = document["plugins"]["enabled"]
            .as_array_mut()
            .ok_or(ConfigError::InvalidPlugins)?;
        if !enabled.iter().any(|item| item.as_str() == Some(plugin)) {
            enabled.push(plugin);
        }
        Ok(document.to_string())
    }

    pub fn remove_plugin_from_document(source: &str, plugin: &str) -> Result<String, ConfigError> {
        let mut document = source.parse::<DocumentMut>()?;
        ensure_plugin_array(&mut document)?;
        let enabled = document["plugins"]["enabled"]
            .as_array_mut()
            .ok_or(ConfigError::InvalidPlugins)?;
        enabled.retain(|item| item.as_str() != Some(plugin));
        Ok(document.to_string())
    }
}

impl std::str::FromStr for CliConfig {
    type Err = ConfigError;

    fn from_str(source: &str) -> Result<Self, Self::Err> {
        Ok(toml_edit::de::from_str(source)?)
    }
}

fn ensure_plugin_array(document: &mut DocumentMut) -> Result<(), ConfigError> {
    if document["plugins"].is_none() {
        document["plugins"] = toml_edit::table();
    }
    if document["plugins"]["enabled"].is_none() {
        document["plugins"]["enabled"] = value(Array::default());
    }
    if !document["plugins"]["enabled"].is_array() {
        return Err(ConfigError::InvalidPlugins);
    }
    Ok(())
}