use std::{convert::TryFrom, env::VarError, fs, net::AddrParseError, net::SocketAddr, num::ParseIntError, path::Path, str::FromStr};
use config::{Config, File, FileFormat};
use regex::Regex;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct GloryConfig {
pub output_name: String,
#[serde(default = "default_site_root")]
pub site_root: String,
#[serde(default = "default_site_pkg_dir")]
pub site_pkg_dir: String,
#[serde(default = "default_site_addr")]
pub site_addr: SocketAddr,
#[serde(default = "default_reload_port")]
pub reload_port: u32,
#[serde(default)]
pub reload_external_port: Option<u32>,
#[serde(default)]
pub reload_ws_protocol: ReloadWSProtocol,
#[serde(default = "default_not_found_path")]
pub not_found_path: String,
}
impl Default for GloryConfig {
fn default() -> Self {
Self::new()
}
}
impl GloryConfig {
pub fn new() -> Self {
Self::default()
}
pub async fn load(path: impl Into<Option<&str>>) -> Result<Self, GloryConfigError> {
if let Some(path) = path.into() {
Self::load_from_file(&path).await
} else {
Self::load_from_env()
}
}
pub async fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, GloryConfigError> {
let text = fs::read_to_string(path).map_err(|_| GloryConfigError::ConfigNotFound)?;
Self::load_from_str(&text)
}
pub fn load_from_env() -> Result<Self, GloryConfigError> {
Self::try_load_from_env()
}
fn try_load_from_env() -> Result<Self, GloryConfigError> {
let output_name = env_with_default("GLORY_OUTPUT_NAME", std::option_env!("GLORY_OUTPUT_NAME",).unwrap_or_default())?;
if output_name.is_empty() {
eprintln!(
"It looks like you're trying to compile Glory without the \
GLORY_OUTPUT_NAME environment variable being set. There are \
two options\n 1. glory-cli is not being used, but \
get_configuration() is being passed None. This needs to be \
changed to Some(\"Cargo.toml\")\n 2. You are compiling \
Glory without GLORY_OUTPUT_NAME being set with \
glory-cli. This shouldn't be possible!"
);
}
Ok(GloryConfig {
output_name,
site_root: env_with_default("GLORY_SITE_ROOT", "target/site")?,
site_pkg_dir: env_with_default("GLORY_SITE_PKG_DIR", "pkg")?,
site_addr: env_with_default("GLORY_SITE_ADDR", "127.0.0.1:8000")?.parse()?,
reload_port: env_with_default("GLORY_RELOAD_PORT", "3001")?.parse()?,
reload_external_port: match env_without_default("GLORY_RELOAD_EXTERNAL_PORT")? {
Some(val) => Some(val.parse()?),
None => None,
},
reload_ws_protocol: ws_from_str(env_with_default("GLORY_RELOAD_WS_PROTOCOL", "ws")?.as_str())?,
not_found_path: env_with_default("GLORY_NOT_FOUND_PATH", "/404")?,
})
}
pub fn load_from_str(text: &str) -> Result<Self, GloryConfigError> {
let re: Regex = Regex::new(r"(?m)^\[package.metadata.glory\]").unwrap();
let re_workspace: Regex = Regex::new(r"(?m)^\[\[workspace.metadata.glory\]\]").unwrap();
let metadata_name;
let start;
match re.find(text) {
Some(found) => {
metadata_name = "[package.metadata.glory]";
start = found.start();
}
None => match re_workspace.find(text) {
Some(found) => {
metadata_name = "[[workspace.metadata.glory]]";
start = found.start();
}
None => return Err(GloryConfigError::ConfigSectionNotFound),
},
};
let newlines = text[..start].matches('\n').count();
let input = "\n".repeat(newlines) + &text[start..];
let toml = input.replace(metadata_name, "[glory-web-config]");
let settings = Config::builder()
.add_source(File::from_str(&toml, FileFormat::Toml))
.add_source(config::Environment::with_prefix("GLORY").separator("_"))
.build()?;
settings.try_deserialize().map_err(|e| GloryConfigError::ConfigError(e.to_string()))
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub enum ReloadWSProtocol {
WS,
WSS,
}
impl Default for ReloadWSProtocol {
fn default() -> Self {
Self::WS
}
}
fn ws_from_str(input: &str) -> Result<ReloadWSProtocol, GloryConfigError> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
"ws" | "WS" => Ok(ReloadWSProtocol::WS),
"wss" | "WSS" => Ok(ReloadWSProtocol::WSS),
_ => Err(GloryConfigError::EnvVarError(format!(
"{input} is not a supported websocket protocol. Use only `ws` or \
`wss`.",
))),
}
}
impl FromStr for ReloadWSProtocol {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
ws_from_str(input).or_else(|_| Ok(Self::default()))
}
}
impl From<&str> for ReloadWSProtocol {
fn from(str: &str) -> Self {
ws_from_str(str).unwrap_or_else(|err| panic!("{}", err))
}
}
impl From<&Result<String, VarError>> for ReloadWSProtocol {
fn from(input: &Result<String, VarError>) -> Self {
match input {
Ok(str) => ws_from_str(str).unwrap_or_else(|err| panic!("{}", err)),
Err(_) => Self::default(),
}
}
}
impl TryFrom<String> for ReloadWSProtocol {
type Error = GloryConfigError;
fn try_from(s: String) -> Result<Self, Self::Error> {
ws_from_str(s.as_str())
}
}
#[derive(Debug, Error, Clone)]
pub enum GloryConfigError {
#[error("Cargo.toml not found in package root")]
ConfigNotFound,
#[error("package.metadata.glory section missing from Cargo.toml")]
ConfigSectionNotFound,
#[error("Failed to get Glory Environment. Did you set GLORY_ENV?")]
EnvError,
#[error("Config Error: {0}")]
ConfigError(String),
#[error("Config Error: {0}")]
EnvVarError(String),
}
impl From<config::ConfigError> for GloryConfigError {
fn from(e: config::ConfigError) -> Self {
Self::ConfigError(e.to_string())
}
}
impl From<ParseIntError> for GloryConfigError {
fn from(e: ParseIntError) -> Self {
Self::ConfigError(e.to_string())
}
}
impl From<AddrParseError> for GloryConfigError {
fn from(e: AddrParseError) -> Self {
Self::ConfigError(e.to_string())
}
}
fn default_site_root() -> String {
".".to_string()
}
fn default_site_pkg_dir() -> String {
"pkg".to_string()
}
fn default_site_addr() -> SocketAddr {
SocketAddr::from(([127, 0, 0, 1], 8000))
}
fn default_reload_port() -> u32 {
8001
}
fn default_not_found_path() -> String {
"/404".to_string()
}
fn env_with_default(key: &str, default: &str) -> Result<String, GloryConfigError> {
match std::env::var(key) {
Ok(val) => Ok(val),
Err(VarError::NotPresent) => Ok(default.to_string()),
Err(e) => Err(GloryConfigError::EnvVarError(format!("{key}: {e}"))),
}
}
fn env_without_default(key: &str) -> Result<Option<String>, GloryConfigError> {
match std::env::var(key) {
Ok(val) => Ok(Some(val)),
Err(VarError::NotPresent) => Ok(None),
Err(e) => Err(GloryConfigError::EnvVarError(format!("{key}: {e}"))),
}
}