atproto-static-web 0.3.0

A simple web viewer for AT Proto.
//! atproto-static-web   A simple web viewer for AT Proto.
//! Copyright (C) 2025  AverageHelper
//!
//! This program is free software: you can redistribute it and/or modify
//! it under the terms of the GNU General Public License as published by
//! the Free Software Foundation, either version 3 of the License, or
//! (at your option) any later version.
//!
//! This program is distributed in the hope that it will be useful,
//! but WITHOUT ANY WARRANTY; without even the implied warranty of
//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//! GNU General Public License for more details.
//!
//! You should have received a copy of the GNU General Public License
//! along with this program.  If not, see <https://www.gnu.org/licenses/>.

use std::{
	env,
	net::{AddrParseError, IpAddr, Ipv6Addr},
	num::ParseIntError,
};
use url::Url;

pub struct Env {
	address: IpAddr,
	base_url: Url,
	log_level: log::Level,
	pds_url: Url,
	http_port: u16,
	site_name: String,
}

#[derive(Debug, Clone, Copy)]
pub enum EnvKey {
	Address,
	Host,
	HttpPort,
	LogLevel,
	PdsUrl,
	SiteName,
}

impl AsRef<std::ffi::OsStr> for EnvKey {
	fn as_ref(&self) -> &std::ffi::OsStr {
		match self {
			Self::Address => "ADDRESS".as_ref(),
			Self::Host => "HOST".as_ref(),
			Self::HttpPort => "HTTP_PORT".as_ref(),
			Self::LogLevel => "LOG_LEVEL".as_ref(),
			Self::PdsUrl => "PDS_URL".as_ref(),
			Self::SiteName => "SITE_NAME".as_ref(),
		}
	}
}

impl core::fmt::Display for EnvKey {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		write!(f, "{}", self.as_ref().display())
	}
}

impl Env {
	/// Gathers configuration values from environment vars.
	pub fn new() -> Result<Self, EnvError> {
		// In dev mode, load env vars from a local file
		#[cfg(debug_assertions)]
		{
			_ = dotenvy::dotenv();
		}

		let key = EnvKey::Address;
		let address: IpAddr = match env::var(key) {
			Err(std::env::VarError::NotUnicode(_)) => return Err(EnvError::NotUnicode(key)),
			Err(std::env::VarError::NotPresent) => Ipv6Addr::UNSPECIFIED.into(),
			Ok(a) => a.parse().map_err(|e| EnvError::IpAddr(key, e))?,
		};

		let key = EnvKey::HttpPort;
		let http_port: u16 = match env::var(key) {
			Err(std::env::VarError::NotUnicode(_)) => return Err(EnvError::NotUnicode(key)),
			Err(std::env::VarError::NotPresent) => 8080,
			Ok(p) => p.parse().map_err(|e| EnvError::Port(key, e))?,
		};

		let key = EnvKey::Host;
		let base_url: Url = match env::var(key) {
			Err(std::env::VarError::NotUnicode(_)) => return Err(EnvError::NotUnicode(key)),
			Err(std::env::VarError::NotPresent) => return Err(EnvError::Missing(key)),
			Ok(d) => {
				let domain = url::Host::parse(&d).map_err(|e| EnvError::Host(key, e))?;
				format!("https://{domain}")
					.parse()
					.map_err(|e| EnvError::Url(key, e))?
			}
		};

		let key = EnvKey::LogLevel;
		let log_level: log::Level = match env::var(key) {
			Err(std::env::VarError::NotUnicode(_)) => return Err(EnvError::NotUnicode(key)),
			#[cfg(debug_assertions)]
			Err(std::env::VarError::NotPresent) => log::Level::Debug,
			#[cfg(not(debug_assertions))]
			Err(std::env::VarError::NotPresent) => log::Level::Warn,
			Ok(l) => l.parse().map_err(|_| EnvError::LogLevel(key))?,
		};

		let key = EnvKey::PdsUrl;
		let pds_url: Url = match env::var(key) {
			Err(std::env::VarError::NotUnicode(_)) => return Err(EnvError::NotUnicode(key)),
			Err(std::env::VarError::NotPresent) => return Err(EnvError::Missing(key)),
			Ok(u) => u.parse().map_err(|e| EnvError::Url(key, e))?,
		};

		let key = EnvKey::SiteName;
		let site_name: String = match env::var(key) {
			Err(std::env::VarError::NotUnicode(_)) => return Err(EnvError::NotUnicode(key)),
			Err(std::env::VarError::NotPresent) => "AT Proto Static".to_owned(),
			Ok(s) => s.trim().to_owned(),
		};

		Ok(Self {
			address,
			base_url,
			http_port,
			log_level,
			pds_url,
			site_name,
		})
	}

	pub const fn address(&self) -> IpAddr {
		self.address
	}

	pub const fn base_url(&self) -> &Url {
		&self.base_url
	}

	pub const fn http_port(&self) -> u16 {
		self.http_port
	}

	pub const fn log_level(&self) -> log::Level {
		self.log_level
	}

	pub const fn pds_url(&self) -> &Url {
		&self.pds_url
	}

	pub const fn site_name(&self) -> &str {
		self.site_name.as_str()
	}
}

#[derive(Debug)]
pub enum EnvError {
	Host(EnvKey, url::ParseError),
	IpAddr(EnvKey, AddrParseError),
	LogLevel(EnvKey),
	Missing(EnvKey),
	NotUnicode(EnvKey),
	Port(EnvKey, ParseIntError),
	Url(EnvKey, url::ParseError),
}

impl core::fmt::Display for EnvError {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		write!(f, "value for env key ")?;
		match self {
			Self::Host(key, err) => write!(f, "{key} is not a domain name: {err}"),
			Self::IpAddr(key, err) => write!(f, "{key} is not a valid IP address: {err}"),
			Self::LogLevel(key) => write!(f, "{key} is not a valid log level"),
			Self::Missing(key) => write!(f, "{key} not found"),
			Self::NotUnicode(key) => write!(f, "{key} is not unicode"),
			Self::Port(key, err) => write!(f, "{key} is not a port number: {err}"),
			Self::Url(key, err) => write!(f, "{key} is not a URL: {err}"),
		}
	}
}

impl core::error::Error for EnvError {}