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