Skip to main content

baukit_core/
lib.rs

1//! Dependency-light shared vocabulary for Baukit crates and services.
2//!
3//! This crate is the canonical home for process identity and configuration
4//! vocabulary shared across crate boundaries. It deliberately contains no
5//! telemetry exporters, async runtime, HTTP framework, or operational routing,
6//! so configuration-only consumers do not inherit those dependency graphs.
7//! Higher-level Baukit crates re-export these types from their established APIs.
8
9#![deny(missing_docs)]
10
11use std::{fmt, str::FromStr};
12
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15
16/// The deployment environment attached to configuration and telemetry signals.
17#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
18#[serde(rename_all = "lowercase")]
19pub enum DeploymentEnvironment {
20    /// A developer workstation or other local process.
21    #[default]
22    Local,
23    /// A deployed environment used for integration and acceptance testing.
24    Testing,
25    /// A deployed pre-production process.
26    Staging,
27    /// A deployed production process.
28    Production,
29}
30
31impl DeploymentEnvironment {
32    const fn as_str(self) -> &'static str {
33        match self {
34            Self::Local => "local",
35            Self::Testing => "testing",
36            Self::Staging => "staging",
37            Self::Production => "production",
38        }
39    }
40}
41
42impl fmt::Display for DeploymentEnvironment {
43    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44        formatter.write_str(self.as_str())
45    }
46}
47
48impl FromStr for DeploymentEnvironment {
49    type Err = ParseEnvironmentError;
50
51    fn from_str(value: &str) -> Result<Self, Self::Err> {
52        match value {
53            "local" => Ok(Self::Local),
54            "testing" => Ok(Self::Testing),
55            "staging" => Ok(Self::Staging),
56            "production" => Ok(Self::Production),
57            _ => Err(ParseEnvironmentError(value.to_owned())),
58        }
59    }
60}
61
62/// An error returned when a deployment environment name is not supported.
63#[derive(Clone, Debug, Error, Eq, PartialEq)]
64#[error("unsupported environment `{0}`; expected local, testing, staging, or production")]
65pub struct ParseEnvironmentError(String);
66
67/// Selection policy for the stdout log formatter.
68#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
69#[serde(rename_all = "lowercase")]
70pub enum LogFormat {
71    /// Select JSON for deployed environments, and pretty output locally.
72    #[default]
73    Auto,
74    /// Always emit newline-delimited JSON.
75    Json,
76    /// Always emit human-readable pretty output.
77    Pretty,
78}
79
80impl LogFormat {
81    /// Resolves [`LogFormat::Auto`] for a deployment environment.
82    #[must_use]
83    pub const fn resolve(self, environment: DeploymentEnvironment) -> Self {
84        match (self, environment) {
85            (Self::Auto, DeploymentEnvironment::Local) => Self::Pretty,
86            (
87                Self::Auto,
88                DeploymentEnvironment::Testing
89                | DeploymentEnvironment::Staging
90                | DeploymentEnvironment::Production,
91            ) => Self::Json,
92            (explicit, _) => explicit,
93        }
94    }
95}
96
97/// The kind of process being identified and instrumented.
98#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum ProcessKind {
100    /// A public or internal HTTP API process.
101    Api,
102    /// A background worker process.
103    Worker,
104    /// A database migration process.
105    Migrate,
106    /// A data seeding process.
107    Seed,
108}
109
110impl ProcessKind {
111    const fn as_str(self) -> &'static str {
112        match self {
113            Self::Api => "api",
114            Self::Worker => "worker",
115            Self::Migrate => "migrate",
116            Self::Seed => "seed",
117        }
118    }
119}
120
121impl fmt::Display for ProcessKind {
122    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123        formatter.write_str(self.as_str())
124    }
125}
126
127/// Stable identity and build metadata for one running process.
128#[derive(Clone, Debug, Eq, PartialEq)]
129pub struct ServiceIdentity {
130    product: String,
131    process: ProcessKind,
132    version: String,
133    commit: String,
134    environment: DeploymentEnvironment,
135}
136
137impl ServiceIdentity {
138    /// Creates a service identity.
139    pub fn new(
140        product: impl Into<String>,
141        process: ProcessKind,
142        version: impl Into<String>,
143        commit: impl Into<String>,
144        environment: DeploymentEnvironment,
145    ) -> Self {
146        Self {
147            product: product.into(),
148            process,
149            version: version.into(),
150            commit: commit.into(),
151            environment,
152        }
153    }
154
155    /// Returns the stable product identifier.
156    pub fn product(&self) -> &str {
157        &self.product
158    }
159
160    /// Returns this process's kind.
161    pub const fn process(&self) -> ProcessKind {
162        self.process
163    }
164
165    /// Returns the Cargo package version.
166    pub fn version(&self) -> &str {
167        &self.version
168    }
169
170    /// Returns the short source-control commit identifier.
171    pub fn commit(&self) -> &str {
172        &self.commit
173    }
174
175    /// Returns the deployment environment.
176    pub const fn environment(&self) -> DeploymentEnvironment {
177        self.environment
178    }
179
180    /// Composes the OpenTelemetry `service.name` value.
181    pub fn service_name(&self) -> String {
182        format!("{}-{}", self.product, self.process)
183    }
184}
185
186/// Immutable version-control and compiler metadata for a process build.
187#[derive(Clone, Debug, Eq, PartialEq)]
188pub struct BuildInfo {
189    version: String,
190    commit: String,
191    rust_version: String,
192}
193
194impl BuildInfo {
195    /// Creates build metadata from values supplied by a build pipeline.
196    pub fn new(
197        version: impl Into<String>,
198        commit: impl Into<String>,
199        rust_version: impl Into<String>,
200    ) -> Self {
201        Self {
202            version: version.into(),
203            commit: commit.into(),
204            rust_version: rust_version.into(),
205        }
206    }
207
208    /// Returns the Cargo package version.
209    pub fn version(&self) -> &str {
210        &self.version
211    }
212
213    /// Returns the source-control commit identifier.
214    pub fn commit(&self) -> &str {
215        &self.commit
216    }
217
218    /// Returns the Rust compiler version used for the build.
219    pub fn rust_version(&self) -> &str {
220        &self.rust_version
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn shared_vocabulary_follows_the_telemetry_contract() {
230        let identity = ServiceIdentity::new(
231            "orders",
232            ProcessKind::Api,
233            "1.2.3",
234            "abc123",
235            DeploymentEnvironment::Production,
236        );
237
238        assert_eq!(identity.service_name(), "orders-api");
239        assert_eq!(identity.environment().to_string(), "production");
240        assert_eq!(
241            LogFormat::Auto.resolve(DeploymentEnvironment::Production),
242            LogFormat::Json
243        );
244    }
245
246    #[test]
247    fn deployment_environments_parse_display_and_serialize() {
248        for (text, environment) in [
249            ("local", DeploymentEnvironment::Local),
250            ("testing", DeploymentEnvironment::Testing),
251            ("staging", DeploymentEnvironment::Staging),
252            ("production", DeploymentEnvironment::Production),
253        ] {
254            assert_eq!(text.parse::<DeploymentEnvironment>(), Ok(environment));
255            assert_eq!(environment.to_string(), text);
256            assert_eq!(
257                serde_json::to_string(&environment).expect("serialize environment"),
258                format!("\"{text}\"")
259            );
260            assert_eq!(
261                serde_json::from_str::<DeploymentEnvironment>(&format!("\"{text}\""))
262                    .expect("deserialize environment"),
263                environment
264            );
265        }
266    }
267}
268
269// Compiles the README's examples so they cannot drift from the API.
270#[doc = include_str!("../README.md")]
271#[cfg(doctest)]
272struct ReadmeDoctests;