1use std::{
4 collections::BTreeMap,
5 net::SocketAddr,
6 ops::Deref,
7 path::{Path, PathBuf},
8 str::FromStr,
9};
10
11use derive_more::{Constructor, Display};
12use kxio::fs::FileSystem;
13use secrecy::SecretString;
14use serde::{Deserialize, Serialize};
15use tracing::info;
16
17use crate::{
18 config::{ForgeAlias, ForgeConfig, RepoAlias},
19 newtype, s,
20};
21
22#[derive(Debug, thiserror::Error)]
23pub enum Error {
24 #[error("fs: {0}")]
25 KxioFs(#[from] kxio::fs::Error),
26
27 #[error("deserialise toml: {0}")]
28 TomlDe(#[from] toml::de::Error),
29
30 #[error("parse IP addres/port: {0}")]
31 AddressParse(#[from] std::net::AddrParseError),
32}
33
34type Result<T> = core::result::Result<T, Error>;
35
36#[derive(
38 Clone,
39 Debug,
40 derive_more::From,
41 PartialEq,
42 Eq,
43 PartialOrd,
44 Ord,
45 derive_more::AsRef,
46 serde::Deserialize,
47 derive_more::Constructor,
48)]
49pub struct AppConfig {
50 listen: Listen,
51 shout: Shout,
52 storage: Storage,
53 pub forge: BTreeMap<String, ForgeConfig>,
54}
55impl AppConfig {
56 #[tracing::instrument(skip_all)]
57 pub fn load(fs: &FileSystem) -> Result<Self> {
58 let file = fs.base().join("git-next-server.toml");
59 info!(?file, "");
60 let str = fs.file(&file).reader()?;
61 Ok(toml::from_str(&s!(str))?)
62 }
63
64 pub fn forges(&self) -> impl Iterator<Item = (ForgeAlias, &ForgeConfig)> {
65 self.forge
66 .iter()
67 .map(|(alias, forge)| (ForgeAlias::new(alias.clone()), forge))
68 }
69
70 #[must_use]
71 pub const fn storage(&self) -> &Storage {
72 &self.storage
73 }
74
75 #[must_use]
76 pub const fn shout(&self) -> &Shout {
77 &self.shout
78 }
79
80 #[must_use]
81 pub const fn listen(&self) -> &Listen {
82 &self.listen
83 }
84
85 pub fn listen_socket_addr(&self) -> Result<SocketAddr> {
91 self.listen.http.socket_addr()
92 }
93}
94
95#[derive(
97 Clone,
98 Debug,
99 derive_more::From,
100 PartialEq,
101 Eq,
102 PartialOrd,
103 Ord,
104 derive_more::AsRef,
105 serde::Deserialize,
106 derive_more::Constructor,
107)]
108pub struct Listen {
109 http: Http,
110 url: ListenUrl,
111}
112impl Listen {
113 #[must_use]
119 pub const fn url(&self) -> &ListenUrl {
120 &self.url
121 }
122}
123
124newtype!(
125 ListenUrl,
126 String,
127 Serialize,
128 Deserialize,
129 PartialOrd,
130 Ord,
131 Display,
132 "The base url for receiving all webhooks from all forges"
133);
134impl ListenUrl {
135 #[must_use]
136 pub fn repo_url(&self, forge_alias: ForgeAlias, repo_alias: RepoAlias) -> RepoListenUrl {
137 RepoListenUrl::new((self.clone(), forge_alias, repo_alias))
138 }
139}
140
141newtype!(ForgeWebhookUrl, String, "Raw URL from a forge Webhook");
142
143newtype!(
144 RepoListenUrl,
145 (ListenUrl, ForgeAlias, RepoAlias),
146 "URL to listen for webhook from forges"
147);
148impl Display for RepoListenUrl {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 write!(
151 f,
152 "{}/{}/{}",
153 self.deref().0,
154 self.deref().1,
155 self.deref().2
156 )
157 }
158}
159impl From<RepoListenUrl> for ForgeWebhookUrl {
160 fn from(value: RepoListenUrl) -> Self {
161 Self::new(s!(value))
162 }
163}
164
165#[derive(
167 Clone,
168 Debug,
169 derive_more::From,
170 PartialEq,
171 Eq,
172 PartialOrd,
173 Ord,
174 derive_more::AsRef,
175 serde::Deserialize,
176 derive_more::Constructor,
177)]
178pub struct Http {
179 addr: String,
180 port: u16,
181}
182impl Http {
183 fn socket_addr(&self) -> Result<SocketAddr> {
184 Ok(SocketAddr::from_str(&format!(
185 "{}:{}",
186 self.addr, self.port
187 ))?)
188 }
189}
190
191#[derive(
193 Clone,
194 Debug,
195 derive_more::From,
196 PartialEq,
197 Eq,
198 PartialOrd,
199 Ord,
200 derive_more::AsRef,
201 serde::Deserialize,
202 derive_more::Constructor,
203)]
204pub struct Storage {
205 path: PathBuf,
206}
207impl Storage {
208 #[must_use]
209 pub fn path(&self) -> &Path {
210 self.path.as_path()
211 }
212}
213
214#[derive(
217 Clone,
218 Debug,
219 Default,
220 derive_more::From,
221 PartialEq,
222 Eq,
223 PartialOrd,
224 Ord,
225 derive_more::AsRef,
226 serde::Deserialize,
227 Constructor,
228)]
229pub struct Shout {
230 webhook: Option<OutboundWebhook>,
231 email: Option<EmailConfig>,
232 desktop: Option<bool>,
233}
234impl Shout {
235 #[must_use]
236 pub const fn webhook(&self) -> Option<&OutboundWebhook> {
237 self.webhook.as_ref()
238 }
239
240 #[cfg(test)]
241 pub(crate) fn webhook_url(&self) -> Option<String> {
242 self.webhook.clone().map(|x| x.url)
243 }
244
245 pub fn webhook_secret(&self) -> Option<SecretString> {
246 self.webhook
247 .clone()
248 .map(|x| x.secret)
249 .map(SecretString::from)
250 }
251
252 #[must_use]
253 pub const fn email(&self) -> Option<&EmailConfig> {
254 self.email.as_ref()
255 }
256
257 #[must_use]
258 pub const fn desktop(&self) -> Option<bool> {
259 self.desktop
260 }
261}
262
263#[derive(
264 Clone,
265 Debug,
266 derive_more::From,
267 PartialEq,
268 Eq,
269 PartialOrd,
270 Ord,
271 serde::Deserialize,
272 derive_more::Constructor,
273)]
274pub struct OutboundWebhook {
275 url: String,
276 secret: String,
277}
278impl OutboundWebhook {
279 #[must_use]
280 pub fn url(&self) -> &str {
281 self.url.as_ref()
282 }
283 #[must_use]
284 pub fn secret(&self) -> SecretString {
285 SecretString::from(self.secret.clone())
286 }
287}
288
289#[derive(
290 Clone,
291 Debug,
292 derive_more::From,
293 PartialEq,
294 Eq,
295 PartialOrd,
296 Ord,
297 serde::Deserialize,
298 derive_more::Constructor,
299)]
300pub struct EmailConfig {
301 from: String,
302 to: String,
303 smtp: Option<SmtpConfig>,
305}
306impl EmailConfig {
307 #[must_use]
308 pub fn from(&self) -> &str {
309 &self.from
310 }
311
312 #[must_use]
313 pub fn to(&self) -> &str {
314 &self.to
315 }
316
317 #[must_use]
318 pub const fn smtp(&self) -> Option<&SmtpConfig> {
319 self.smtp.as_ref()
320 }
321}
322
323#[derive(
324 Clone,
325 Debug,
326 derive_more::From,
327 PartialEq,
328 Eq,
329 PartialOrd,
330 Ord,
331 serde::Deserialize,
332 derive_more::Constructor,
333)]
334pub struct SmtpConfig {
335 hostname: String,
336 username: String,
337 password: String,
338}
339impl SmtpConfig {
340 #[must_use]
341 pub fn username(&self) -> &str {
342 &self.username
343 }
344
345 #[must_use]
346 pub fn password(&self) -> &str {
347 &self.password
348 }
349
350 #[must_use]
351 pub fn hostname(&self) -> &str {
352 &self.hostname
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 const fn is_sendable<T: Send>() {}
359
360 #[test]
361 const fn normal() {
362 is_sendable::<super::AppConfig>();
363 }
364}