#![forbid(unsafe_code)]
pub mod errors;
use crate::errors::LeptosConfigError;
use config::{Case, Config, File, FileFormat};
use regex::Regex;
use std::{env::VarError, fs, net::SocketAddr, path::Path, str::FromStr};
use typed_builder::TypedBuilder;
#[derive(Clone, Debug, serde::Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct ConfFile {
pub leptos_options: LeptosOptions,
}
#[derive(TypedBuilder, Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct LeptosOptions {
#[builder(setter(into), default=default_output_name())]
pub output_name: String,
#[builder(setter(into), default=default_site_root())]
#[serde(default = "default_site_root")]
pub site_root: String,
#[builder(setter(into), default=default_site_pkg_dir())]
#[serde(default = "default_site_pkg_dir")]
pub site_pkg_dir: String,
#[builder(setter(into), default=default_env())]
#[serde(default = "default_env")]
pub env: Env,
#[builder(setter(into), default=default_site_addr())]
#[serde(default = "default_site_addr")]
pub site_addr: SocketAddr,
#[builder(default = default_reload_port())]
#[serde(default = "default_reload_port")]
pub reload_port: u32,
#[builder(default)]
#[serde(default)]
pub reload_external_port: Option<u32>,
#[builder(default)]
#[serde(default)]
pub reload_ws_protocol: ReloadWSProtocol,
#[builder(default = default_not_found_path())]
#[serde(default = "default_not_found_path")]
pub not_found_path: String,
#[builder(default = default_hash_file_name())]
#[serde(default = "default_hash_file_name")]
pub hash_file: String,
#[builder(default = default_hash_files())]
#[serde(default = "default_hash_files")]
pub hash_files: bool,
}
impl LeptosOptions {
fn try_from_env() -> Result<Self, LeptosConfigError> {
let output_name = env_w_default(
"LEPTOS_OUTPUT_NAME",
std::option_env!("LEPTOS_OUTPUT_NAME",).unwrap_or_default(),
)?;
if output_name.is_empty() {
eprintln!(
"It looks like you're trying to compile Leptos without the \
LEPTOS_OUTPUT_NAME environment variable being set. There are \
two options\n 1. cargo-leptos 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 \
Leptos without LEPTOS_OUTPUT_NAME being set with \
cargo-leptos. This shouldn't be possible!"
);
}
Ok(LeptosOptions {
output_name,
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?,
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?,
env: env_from_str(env_w_default("LEPTOS_ENV", "DEV")?.as_str())?,
site_addr: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?
.parse()?,
reload_port: env_w_default("LEPTOS_RELOAD_PORT", "3001")?
.parse()?,
reload_external_port: match env_wo_default(
"LEPTOS_RELOAD_EXTERNAL_PORT",
)? {
Some(val) => Some(val.parse()?),
None => None,
},
reload_ws_protocol: ws_from_str(
env_w_default("LEPTOS_RELOAD_WS_PROTOCOL", "ws")?.as_str(),
)?,
not_found_path: env_w_default("LEPTOS_NOT_FOUND_PATH", "/404")?,
hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?,
hash_files: env_w_default("LEPTOS_HASH_FILES", "false")?.parse()?,
})
}
}
impl Default for LeptosOptions {
fn default() -> Self {
LeptosOptions::builder().build()
}
}
fn default_output_name() -> String {
env!("CARGO_CRATE_NAME").replace('-', "_")
}
fn default_site_root() -> String {
".".to_string()
}
fn default_site_pkg_dir() -> String {
"pkg".to_string()
}
fn default_env() -> Env {
Env::DEV
}
fn default_site_addr() -> SocketAddr {
SocketAddr::from(([127, 0, 0, 1], 3000))
}
fn default_reload_port() -> u32 {
3001
}
fn default_not_found_path() -> String {
"/404".to_string()
}
fn default_hash_file_name() -> String {
"hash.txt".to_string()
}
fn default_hash_files() -> bool {
false
}
fn env_wo_default(key: &str) -> Result<Option<String>, LeptosConfigError> {
match std::env::var(key) {
Ok(val) => Ok(Some(val)),
Err(VarError::NotPresent) => Ok(None),
Err(e) => Err(LeptosConfigError::EnvVarError(format!("{key}: {e}"))),
}
}
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::Serialize, serde::Deserialize, PartialEq, Eq)]
pub enum Env {
PROD,
DEV,
}
impl Default for Env {
fn default() -> Self {
Self::DEV
}
}
fn env_from_str(input: &str) -> Result<Env, LeptosConfigError> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
"dev" | "development" => Ok(Env::DEV),
"prod" | "production" => Ok(Env::PROD),
_ => Err(LeptosConfigError::EnvVarError(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> {
env_from_str(input).or_else(|_| Ok(Self::default()))
}
}
impl From<&str> for Env {
fn from(str: &str) -> Self {
env_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) => {
env_from_str(str).unwrap_or_else(|err| panic!("{}", err))
}
Err(_) => Self::default(),
}
}
}
impl TryFrom<String> for Env {
type Error = LeptosConfigError;
fn try_from(s: String) -> Result<Self, Self::Error> {
env_from_str(s.as_str())
}
}
#[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, LeptosConfigError> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
"ws" | "WS" => Ok(ReloadWSProtocol::WS),
"wss" | "WSS" => Ok(ReloadWSProtocol::WSS),
_ => Err(LeptosConfigError::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 = LeptosConfigError;
fn try_from(s: String) -> Result<Self, Self::Error> {
ws_from_str(s.as_str())
}
}
pub fn get_config_from_str(
text: &str,
) -> Result<LeptosOptions, LeptosConfigError> {
let re: Regex = Regex::new(r"(?m)^\[package.metadata.leptos\]").unwrap();
let re_workspace: Regex =
Regex::new(r"(?m)^\[\[workspace.metadata.leptos\]\]").unwrap();
let metadata_name;
let start;
match re.find(text) {
Some(found) => {
metadata_name = "[package.metadata.leptos]";
start = found.start();
}
None => match re_workspace.find(text) {
Some(found) => {
metadata_name = "[[workspace.metadata.leptos]]";
start = 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(metadata_name, "");
let settings = Config::builder()
.add_source(File::from_str(&toml, FileFormat::Toml))
.add_source(
config::Environment::with_prefix("LEPTOS")
.convert_case(Case::Kebab),
)
.build()?;
settings
.try_deserialize()
.map_err(|e| LeptosConfigError::ConfigError(e.to_string()))
}
pub async fn get_configuration(
path: Option<&str>,
) -> Result<ConfFile, LeptosConfigError> {
if let Some(path) = path {
get_config_from_file(&path).await
} else {
get_config_from_env()
}
}
pub async fn get_config_from_file<P: AsRef<Path>>(
path: P,
) -> Result<ConfFile, LeptosConfigError> {
let text = fs::read_to_string(path)
.map_err(|_| LeptosConfigError::ConfigNotFound)?;
let leptos_options = get_config_from_str(&text)?;
Ok(ConfFile { leptos_options })
}
pub fn get_config_from_env() -> Result<ConfFile, LeptosConfigError> {
Ok(ConfFile {
leptos_options: LeptosOptions::try_from_env()?,
})
}
#[path = "tests.rs"]
#[cfg(test)]
mod tests;