#![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, path::Path,
str::FromStr,
};
use typed_builder::TypedBuilder;
#[derive(Clone, Debug, serde::Deserialize)]
#[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))]
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,
}
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::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 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 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 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 fn get_config_from_str(text: &str) -> Result<ConfFile, 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, "[leptos-options]");
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()))
}
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)?;
get_config_from_str(&text)
}
pub fn get_config_from_env() -> Result<ConfFile, LeptosConfigError> {
Ok(ConfFile {
leptos_options: LeptosOptions::try_from_env()?,
})
}
#[path = "tests.rs"]
#[cfg(test)]
mod tests;