#![forbid(unsafe_code)]
pub mod errors;
use crate::errors::LeptosConfigError;
use config::{Config, File, FileFormat};
use regex::Regex;
use std::{convert::TryFrom, env::VarError, fs, net::SocketAddr, str::FromStr};
use typed_builder::TypedBuilder;
#[derive(Clone, Debug, serde::Deserialize)]
pub struct ConfFile {
pub leptos_options: LeptosOptions,
}
#[derive(TypedBuilder, Debug, Clone, serde::Deserialize)]
pub struct LeptosOptions {
#[builder(setter(into))]
pub output_name: String,
#[builder(setter(into), default=".".to_string())]
pub site_root: String,
#[builder(setter(into), default="pkg".to_string())]
pub site_pkg_dir: String,
#[builder(setter(into), default=Env::DEV)]
pub env: Env,
#[builder(setter(into), default=SocketAddr::from(([127,0,0,1], 3000)))]
pub site_addr: SocketAddr,
#[builder(default = 3001)]
pub reload_port: u32,
}
impl LeptosOptions {
fn try_from_env() -> Result<Self, LeptosConfigError> {
Ok(LeptosOptions {
output_name: std::env::var("LEPTOS_OUTPUT_NAME").map_err(|e| {
LeptosConfigError::EnvVarError(format!(
"LEPTOS_OUTPUT_NAME: {e}"
))
})?,
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?,
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?,
env: Env::default(),
site_addr: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?
.parse()?,
reload_port: env_w_default("LEPTOS_RELOAD_PORT", "3001")?
.parse()?,
})
}
}
fn env_w_default(
key: &str,
default: &str,
) -> Result<String, LeptosConfigError> {
match std::env::var(key) {
Ok(val) => Ok(val),
Err(VarError::NotPresent) => Ok(default.to_string()),
Err(e) => Err(LeptosConfigError::EnvVarError(format!("{key}: {e}"))),
}
}
#[derive(Debug, Clone, serde::Deserialize)]
pub enum Env {
PROD,
DEV,
}
impl Default for Env {
fn default() -> Self {
Self::DEV
}
}
fn from_str(input: &str) -> Result<Env, String> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
"dev" | "development" => Ok(Env::DEV),
"prod" | "production" => Ok(Env::PROD),
_ => Err(format!(
"{input} is not a supported environment. Use either `dev` or \
`production`.",
)),
}
}
impl FromStr for Env {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
from_str(input).or_else(|_| Ok(Self::default()))
}
}
impl From<&str> for Env {
fn from(str: &str) -> Self {
from_str(str).unwrap_or_else(|err| panic!("{}", err))
}
}
impl From<&Result<String, VarError>> for Env {
fn from(input: &Result<String, VarError>) -> Self {
match input {
Ok(str) => from_str(str).unwrap_or_else(|err| panic!("{}", err)),
Err(_) => Self::default(),
}
}
}
impl TryFrom<String> for Env {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
from_str(s.as_str())
}
}
pub async fn get_configuration(
path: Option<&str>,
) -> Result<ConfFile, LeptosConfigError> {
if let Some(path) = path {
let text = fs::read_to_string(path)
.map_err(|_| LeptosConfigError::ConfigNotFound)?;
let re: Regex =
Regex::new(r#"(?m)^\[package.metadata.leptos\]"#).unwrap();
let start = match re.find(&text) {
Some(found) => found.start(),
None => return Err(LeptosConfigError::ConfigSectionNotFound),
};
let newlines = text[..start].matches('\n').count();
let input = "\n".repeat(newlines) + &text[start..];
let toml = input
.replace("[package.metadata.leptos]", "[leptos_options]")
.replace('-', "_");
let settings = Config::builder()
.add_source(File::from_str(&toml, FileFormat::Toml))
.add_source(
config::Environment::with_prefix("LEPTOS").separator("_"),
)
.build()?;
settings
.try_deserialize()
.map_err(|e| LeptosConfigError::ConfigError(e.to_string()))
} else {
Ok(ConfFile {
leptos_options: LeptosOptions::try_from_env()?,
})
}
}