tinycongress_api/
build_info.rs

1use async_graphql::SimpleObject;
2use chrono::{DateTime, Utc};
3use serde::Serialize;
4use std::env;
5use utoipa::ToSchema;
6
7/// Build metadata exposed via GraphQL, REST, and logs.
8#[derive(Clone, Debug, PartialEq, Eq, Serialize, SimpleObject, ToSchema)]
9#[graphql(rename_fields = "camelCase")]
10#[serde(rename_all = "camelCase")]
11pub struct BuildInfo {
12    pub version: String,
13    pub git_sha: String,
14    pub build_time: String,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub message: Option<String>,
17}
18
19#[derive(Clone, Debug)]
20pub struct BuildInfoProvider {
21    info: BuildInfo,
22}
23
24impl BuildInfoProvider {
25    /// Construct a provider using environment variables, falling back to sensible defaults.
26    #[must_use]
27    pub fn from_env() -> Self {
28        Self::from_lookup(|key| env::var(key).ok())
29    }
30
31    /// Construct a provider using a custom lookup function (useful for tests).
32    #[must_use]
33    pub fn from_lookup<F>(mut lookup: F) -> Self
34    where
35        F: FnMut(&str) -> Option<String>,
36    {
37        let version = lookup("APP_VERSION")
38            .or_else(|| lookup("VERSION"))
39            .unwrap_or_else(|| "dev".to_string());
40
41        let git_sha = lookup("GIT_SHA").unwrap_or_else(|| "unknown".to_string());
42
43        let build_time = lookup("BUILD_TIME")
44            .and_then(|value| normalize_build_time(&value))
45            .unwrap_or_else(|| "unknown".to_string());
46
47        let message = lookup("BUILD_MESSAGE");
48
49        let info = BuildInfo {
50            version,
51            git_sha,
52            build_time,
53            message,
54        };
55
56        Self { info }
57    }
58
59    /// Fetch the resolved build info values.
60    #[must_use]
61    pub fn build_info(&self) -> BuildInfo {
62        self.info.clone()
63    }
64}
65
66fn normalize_build_time(value: &str) -> Option<String> {
67    DateTime::parse_from_rfc3339(value)
68        .or_else(|_| DateTime::parse_from_rfc3339(&format!("{value}Z")))
69        .map(|dt| dt.with_timezone(&Utc).to_rfc3339())
70        .ok()
71}