1use std::fmt;
9use std::path::{Path, PathBuf};
10use std::time::Duration;
11
12use serde::Deserialize;
13
14pub struct Token(String);
16
17impl Token {
18 #[must_use]
20 pub fn as_str(&self) -> &str {
21 &self.0
22 }
23}
24
25impl fmt::Debug for Token {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 write!(f, "Token({} bytes)", self.0.len())
28 }
29}
30
31#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
33#[serde(deny_unknown_fields)]
34pub struct Limits {
35 pub frame_bytes: usize,
37 pub record_bytes: usize,
39 pub reply_timeout_ms: u64,
41 pub flow_steps: usize,
43 pub request_bytes: usize,
45}
46
47impl Limits {
48 #[must_use]
50 pub const fn reply_timeout(&self) -> Duration {
51 Duration::from_millis(self.reply_timeout_ms)
52 }
53
54 const fn validate(self) -> Result<Self, &'static str> {
55 if self.frame_bytes == 0 {
56 return Err("frame_bytes");
57 }
58 if self.record_bytes == 0 {
59 return Err("record_bytes");
60 }
61 if self.reply_timeout_ms == 0 {
62 return Err("reply_timeout_ms");
63 }
64 if self.flow_steps == 0 {
65 return Err("flow_steps");
66 }
67 if self.request_bytes == 0 {
68 return Err("request_bytes");
69 }
70 Ok(self)
71 }
72}
73
74#[derive(Debug)]
76pub struct Config {
77 pub server: String,
79 pub channel: String,
81 pub credential: Credential,
83 pub limits: Limits,
85}
86
87#[derive(Debug)]
89pub enum Credential {
90 Token(Token),
92 Lys(crate::lys::LysTable),
94}
95
96#[derive(Debug, thiserror::Error)]
98pub enum ConfigError {
99 #[error("the configuration {path} cannot be read: {message}")]
101 Unreadable {
102 path: PathBuf,
104 message: String,
106 },
107 #[error("the configuration {path} is refused: {message}")]
109 Invalid {
110 path: PathBuf,
112 message: String,
114 },
115 #[error("the configuration {path} is refused: {field} must be positive")]
117 Zero {
118 path: PathBuf,
120 field: &'static str,
122 },
123 #[error("the token file {path} cannot be read: {message}")]
125 Token {
126 path: PathBuf,
128 message: String,
130 },
131}
132
133#[derive(Deserialize)]
135#[serde(deny_unknown_fields)]
136struct Written {
137 server: String,
138 channel: String,
139 token_file: Option<PathBuf>,
140 lys: Option<crate::lys::LysTable>,
141 limits: Limits,
142}
143
144impl Config {
145 pub fn load(path: &Path) -> Result<Self, ConfigError> {
152 let text = std::fs::read_to_string(path).map_err(|e| ConfigError::Unreadable {
153 path: path.to_path_buf(),
154 message: e.to_string(),
155 })?;
156 let file: Written = toml::from_str(&text).map_err(|e| ConfigError::Invalid {
157 path: path.to_path_buf(),
158 message: e.message().to_owned(),
159 })?;
160 let limits = file.limits.validate().map_err(|field| ConfigError::Zero {
161 path: path.to_path_buf(),
162 field,
163 })?;
164 if file.server.is_empty() {
165 return Err(ConfigError::Zero {
166 path: path.to_path_buf(),
167 field: "server",
168 });
169 }
170 if file.channel.is_empty() {
171 return Err(ConfigError::Zero {
172 path: path.to_path_buf(),
173 field: "channel",
174 });
175 }
176 let token_file = match (file.token_file, file.lys) {
177 (None, Some(lys)) => {
178 return Ok(Self {
179 server: file.server,
180 channel: file.channel,
181 credential: Credential::Lys(lys),
182 limits,
183 });
184 }
185 (Some(token_file), None) if token_file.is_absolute() => token_file,
186 (Some(token_file), None) => path
187 .parent()
188 .map_or_else(|| token_file.clone(), |base| base.join(&token_file)),
189 _ => {
190 return Err(ConfigError::Invalid {
191 path: path.to_path_buf(),
192 message: "give exactly one of token_file and [lys]".to_owned(),
193 });
194 }
195 };
196 let token = std::fs::read_to_string(&token_file).map_err(|e| ConfigError::Token {
197 path: token_file.clone(),
198 message: e.to_string(),
199 })?;
200 let token = token.trim_end_matches(['\r', '\n']);
201 if token.is_empty() {
202 return Err(ConfigError::Token {
203 path: token_file,
204 message: "token file is empty".to_owned(),
205 });
206 }
207 Ok(Self {
208 server: file.server,
209 channel: file.channel,
210 credential: Credential::Token(Token(token.to_owned())),
211 limits,
212 })
213 }
214}
215
216#[cfg(test)]
217#[path = "config_tests.rs"]
218mod tests;