git_next_core/config/
server.rs

1//
2
3use 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/// Mapped from the `git-next-server.toml` file
37#[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    /// Returns the `SocketAddr` to listen to for incoming webhooks.
86    ///
87    /// # Errors
88    ///
89    /// Will return an `Err` if the IP address or port from the config file are invalid.
90    pub fn listen_socket_addr(&self) -> Result<SocketAddr> {
91        self.listen.http.socket_addr()
92    }
93}
94
95/// Defines how the server receives webhook notifications from forges.
96#[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    // /// Returns the URL a Repo will listen to for updates from the Forge
114    // pub fn repo_url(&self, forge_alias: ForgeAlias, repo_alias: RepoAlias) -> RepoListenUrl {
115    //     self.url.repo_url(forge_alias, repo_alias)
116    // }
117
118    #[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/// Defines the port the server will listen to for incoming webhooks messages
166#[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/// The directory to store server data, such as cloned repos
192#[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/// Defines the Webhook Forges should send updates to
215/// Must be an address that is accessible from the remote forge
216#[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    // email will be sent via sendmail, unless smtp is specified
304    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}