use std::{
env,
ffi::OsString,
fmt::{self, Display, Formatter},
fs,
io::{self, Read},
net::{AddrParseError, SocketAddr},
path::{Path, PathBuf},
str::FromStr,
};
#[cfg(feature = "postgres")]
use sec::Secret;
use serde::{de::DeserializeOwned, Deserialize};
#[cfg(feature = "postgres")]
use sqlx::postgres::PgConnectOptions;
use thiserror::Error;
use tracing_subscriber::{filter::ParseError, EnvFilter};
#[derive(Debug, Eq, PartialEq)]
pub enum Location {
StandardInput,
File(PathBuf),
}
impl Location {
fn read_to_string(&self) -> io::Result<String> {
match self {
Self::StandardInput => {
let mut serialized = String::new();
io::stdin()
.lock()
.read_to_string(&mut serialized)
.map(|_| serialized)
}
Self::File(path) => fs::read_to_string(path),
}
}
}
impl From<PathBuf> for Location {
fn from(path: PathBuf) -> Self {
if path.as_path() == Path::new("-") {
Self::StandardInput
} else {
Self::File(path)
}
}
}
impl From<OsString> for Location {
fn from(value: OsString) -> Self {
PathBuf::from(value).into()
}
}
impl Display for Location {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::StandardInput => formatter.write_str("standard input"),
Self::File(path) => path.display().fmt(formatter),
}
}
}
#[derive(Debug, Deserialize, Eq, PartialEq)]
#[serde(try_from = "String")]
pub enum ListenAddress {
Tcp(SocketAddr),
Unix(PathBuf),
}
impl FromStr for ListenAddress {
type Err = ParseListenAddressError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if Path::new(value).is_absolute() {
Ok(Self::Unix(PathBuf::from(value)))
} else {
value
.parse()
.map(Self::Tcp)
.map_err(|source| ParseListenAddressError { source })
}
}
}
impl TryFrom<String> for ListenAddress {
type Error = ParseListenAddressError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
impl Display for ListenAddress {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Tcp(address) => address.fmt(formatter),
Self::Unix(path) => path.display().fmt(formatter),
}
}
}
#[derive(Debug, Error)]
#[error("expected a TCP socket address or absolute Unix socket path")]
pub struct ParseListenAddressError {
#[source]
source: AddrParseError,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(try_from = "String")]
pub struct LogFilter(EnvFilter);
impl FromStr for LogFilter {
type Err = ParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
EnvFilter::try_new(value).map(Self)
}
}
impl TryFrom<String> for LogFilter {
type Error = ParseError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
impl From<LogFilter> for EnvFilter {
fn from(filter: LogFilter) -> Self {
filter.0
}
}
impl Display for LogFilter {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
#[cfg(feature = "postgres")]
#[cfg_attr(docsrs, doc(cfg(feature = "postgres")))]
#[derive(Clone, Debug, Deserialize)]
#[serde(try_from = "String")]
pub struct DatabaseUrl(Secret<PgConnectOptions>);
#[cfg(feature = "postgres")]
impl DatabaseUrl {
pub fn into_connect_options(self) -> PgConnectOptions {
self.0.reveal_into()
}
}
#[cfg(feature = "postgres")]
impl FromStr for DatabaseUrl {
type Err = ParseDatabaseUrlError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let scheme = value
.split_once(':')
.map(|(scheme, _)| scheme)
.ok_or(ParseDatabaseUrlError::Scheme)?;
if !scheme.eq_ignore_ascii_case("postgres") && !scheme.eq_ignore_ascii_case("postgresql") {
return Err(ParseDatabaseUrlError::Scheme);
}
value
.parse()
.map(Secret::new)
.map(Self)
.map_err(|source| ParseDatabaseUrlError::Invalid { source })
}
}
#[cfg(feature = "postgres")]
impl TryFrom<String> for DatabaseUrl {
type Error = ParseDatabaseUrlError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
#[cfg(feature = "postgres")]
#[cfg_attr(docsrs, doc(cfg(feature = "postgres")))]
#[derive(Debug, Error)]
pub enum ParseDatabaseUrlError {
#[error("expected a postgres or postgresql URL")]
Scheme,
#[error("invalid PostgreSQL connection URL")]
Invalid {
#[source]
source: sqlx::Error,
},
}
#[derive(Debug, Deserialize)]
pub struct Core {
pub listen_address: ListenAddress,
pub log_filter: LogFilter,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("configuration file path is required")]
MissingPath,
#[error("unexpected argument after configuration file path")]
UnexpectedArgument,
#[error("failed to read configuration from {location}")]
Read {
location: Location,
#[source]
source: io::Error,
},
#[error("failed to parse configuration from {location}")]
Parse {
location: Location,
#[source]
source: toml::de::Error,
},
}
pub fn from_args<T>() -> Result<T, Error>
where
T: DeserializeOwned,
{
let mut arguments = env::args_os().skip(1);
let path = arguments.next().ok_or(Error::MissingPath)?;
if arguments.next().is_some() {
return Err(Error::UnexpectedArgument);
}
load_location(path.into())
}
pub fn load<T>(path: &Path) -> Result<T, Error>
where
T: DeserializeOwned,
{
load_location(path.to_owned().into())
}
fn load_location<T>(location: Location) -> Result<T, Error>
where
T: DeserializeOwned,
{
let serialized = match location.read_to_string() {
Ok(serialized) => serialized,
Err(source) => return Err(Error::Read { location, source }),
};
deserialize(&serialized, location)
}
fn deserialize<T>(serialized: &str, location: Location) -> Result<T, Error>
where
T: DeserializeOwned,
{
toml::from_str(serialized).map_err(|source| Error::Parse { location, source })
}
#[cfg(test)]
mod tests {
use std::{net::SocketAddr, path::PathBuf};
use serde::Deserialize;
#[cfg(feature = "postgres")]
use super::DatabaseUrl;
use super::{deserialize, Core, ListenAddress, Location};
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct Config {
#[serde(flatten)]
core: Core,
frontend: PathBuf,
}
#[test]
fn deserializes_flattened_core_configuration() {
let config: Config = deserialize(
concat!(
"listen_address = '127.0.0.1:3000'\n",
"log_filter = 'twelve=debug,tower_http=info'\n",
"frontend = '/srv/frontend'\n",
),
Location::File(PathBuf::from("test")),
)
.expect("configuration should deserialize");
assert_eq!(
config.core.listen_address,
ListenAddress::Tcp(SocketAddr::from(([127, 0, 0, 1], 3000)))
);
assert_eq!(
config.core.log_filter.to_string(),
"tower_http=info,twelve=debug"
);
assert_eq!(config.frontend, PathBuf::from("/srv/frontend"));
}
#[test]
fn parses_listener_addresses() {
let ipv6: ListenAddress = "[::1]:3000".parse().expect("IPv6 listener should parse");
let unix: ListenAddress = "/run/myapp/http.sock"
.parse()
.expect("Unix listener should parse");
assert_eq!(
ipv6,
ListenAddress::Tcp(SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], 3000)))
);
assert_eq!(
unix,
ListenAddress::Unix(PathBuf::from("/run/myapp/http.sock"))
);
assert!("systemd".parse::<ListenAddress>().is_err());
assert!("myapp.sock".parse::<ListenAddress>().is_err());
}
#[cfg(feature = "postgres")]
#[test]
fn validates_and_redacts_database_urls() {
let url: DatabaseUrl = "postgresql://user:password@localhost/database"
.parse()
.expect("database URL should parse");
assert_eq!(format!("{url:?}"), "DatabaseUrl(...)");
assert!("http://localhost/database".parse::<DatabaseUrl>().is_err());
}
}