use std::fmt;
use std::path::{Path, PathBuf};
use std::time::Duration;
use serde::Deserialize;
pub struct Token(String);
impl Token {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Debug for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Token({} bytes)", self.0.len())
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Limits {
pub frame_bytes: usize,
pub record_bytes: usize,
pub reply_timeout_ms: u64,
pub flow_steps: usize,
pub request_bytes: usize,
}
impl Limits {
#[must_use]
pub const fn reply_timeout(&self) -> Duration {
Duration::from_millis(self.reply_timeout_ms)
}
const fn validate(self) -> Result<Self, &'static str> {
if self.frame_bytes == 0 {
return Err("frame_bytes");
}
if self.record_bytes == 0 {
return Err("record_bytes");
}
if self.reply_timeout_ms == 0 {
return Err("reply_timeout_ms");
}
if self.flow_steps == 0 {
return Err("flow_steps");
}
if self.request_bytes == 0 {
return Err("request_bytes");
}
Ok(self)
}
}
#[derive(Debug)]
pub struct Config {
pub server: String,
pub channel: String,
pub credential: Credential,
pub limits: Limits,
}
#[derive(Debug)]
pub enum Credential {
Token(Token),
Lys(crate::lys::LysTable),
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("the configuration {path} cannot be read: {message}")]
Unreadable {
path: PathBuf,
message: String,
},
#[error("the configuration {path} is refused: {message}")]
Invalid {
path: PathBuf,
message: String,
},
#[error("the configuration {path} is refused: {field} must be positive")]
Zero {
path: PathBuf,
field: &'static str,
},
#[error("the token file {path} cannot be read: {message}")]
Token {
path: PathBuf,
message: String,
},
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Written {
server: String,
channel: String,
token_file: Option<PathBuf>,
lys: Option<crate::lys::LysTable>,
limits: Limits,
}
impl Config {
pub fn load(path: &Path) -> Result<Self, ConfigError> {
let text = std::fs::read_to_string(path).map_err(|e| ConfigError::Unreadable {
path: path.to_path_buf(),
message: e.to_string(),
})?;
let file: Written = toml::from_str(&text).map_err(|e| ConfigError::Invalid {
path: path.to_path_buf(),
message: e.message().to_owned(),
})?;
let limits = file.limits.validate().map_err(|field| ConfigError::Zero {
path: path.to_path_buf(),
field,
})?;
if file.server.is_empty() {
return Err(ConfigError::Zero {
path: path.to_path_buf(),
field: "server",
});
}
if file.channel.is_empty() {
return Err(ConfigError::Zero {
path: path.to_path_buf(),
field: "channel",
});
}
let token_file = match (file.token_file, file.lys) {
(None, Some(lys)) => {
return Ok(Self {
server: file.server,
channel: file.channel,
credential: Credential::Lys(lys),
limits,
});
}
(Some(token_file), None) if token_file.is_absolute() => token_file,
(Some(token_file), None) => path
.parent()
.map_or_else(|| token_file.clone(), |base| base.join(&token_file)),
_ => {
return Err(ConfigError::Invalid {
path: path.to_path_buf(),
message: "give exactly one of token_file and [lys]".to_owned(),
});
}
};
let token = std::fs::read_to_string(&token_file).map_err(|e| ConfigError::Token {
path: token_file.clone(),
message: e.to_string(),
})?;
let token = token.trim_end_matches(['\r', '\n']);
if token.is_empty() {
return Err(ConfigError::Token {
path: token_file,
message: "token file is empty".to_owned(),
});
}
Ok(Self {
server: file.server,
channel: file.channel,
credential: Credential::Token(Token(token.to_owned())),
limits,
})
}
}
#[cfg(test)]
#[path = "config_tests.rs"]
mod tests;