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