Skip to main content

cellos_server/routes/
meta.rs

1//! `/v1/version` — server build metadata.
2//!
3//! This is the canonical day-1 reachability probe for cellctl. A fresh
4//! operator types `cellctl version` to confirm the server is alive
5//! before doing anything else; the route MUST exist (E2E report
6//! SRV-001) and MUST require Bearer auth like every other route.
7//!
8//! ## Response shape (stable)
9//!
10//! ```json
11//! {
12//!   "server": {
13//!     "version":      "0.5.0",      // env!("CARGO_PKG_VERSION")
14//!     "gitSha":       "abc1234",     // optional; only present when CELLOS_GIT_SHA was set at build time
15//!     "buildProfile": "release"     // "release" or "debug"
16//!   },
17//!   "api": { "version": "v1" }
18//! }
19//! ```
20//!
21//! `gitSha` is best-effort: the field is *omitted* (via
22//! `#[serde(skip_serializing_if = "Option::is_none")]`) when the
23//! `CELLOS_GIT_SHA` env var is unset at build time. This keeps test
24//! output stable regardless of git state and avoids forcing a
25//! `build.rs` that would re-run on every commit.
26//!
27//! ## Why a typed response instead of `serde_json::json!`
28//!
29//! The response shape is a stable contract — cellctl pins on
30//! `server.version` today and future tooling will pin on more fields.
31//! A struct catches typos at compile time and gives us a place to hang
32//! doc comments that explain each field.
33
34use axum::extract::State;
35use axum::http::HeaderMap;
36use axum::Json;
37use serde::Serialize;
38
39use crate::auth::require_bearer;
40use crate::error::AppError;
41use crate::state::AppState;
42
43/// Compile-time build profile string. `cfg!(debug_assertions)` is
44/// evaluated at the call site, so this constant materialises to a
45/// single `&'static str` per build.
46const BUILD_PROFILE: &str = if cfg!(debug_assertions) {
47    "debug"
48} else {
49    "release"
50};
51
52/// Top-level wire shape returned by `GET /v1/version`.
53#[derive(Debug, Serialize)]
54pub struct VersionResponse {
55    pub server: ServerInfo,
56    pub api: ApiInfo,
57}
58
59#[derive(Debug, Serialize)]
60pub struct ServerInfo {
61    /// `env!("CARGO_PKG_VERSION")` — the cellos-server crate version.
62    pub version: &'static str,
63    /// Short git SHA captured at build time via the optional
64    /// `CELLOS_GIT_SHA` env var. Omitted from the JSON when unset so
65    /// downstream consumers can rely on "present means real".
66    #[serde(rename = "gitSha", skip_serializing_if = "Option::is_none")]
67    pub git_sha: Option<&'static str>,
68    /// `"release"` for non-debug builds, `"debug"` otherwise.
69    #[serde(rename = "buildProfile")]
70    pub build_profile: &'static str,
71}
72
73#[derive(Debug, Serialize)]
74pub struct ApiInfo {
75    /// API surface version. Stable string — bump only when the URL
76    /// prefix bumps (e.g. `/v2`).
77    pub version: &'static str,
78}
79
80/// `GET /v1/version` handler. Bearer auth required.
81pub async fn get_version(
82    State(state): State<AppState>,
83    headers: HeaderMap,
84) -> Result<Json<VersionResponse>, AppError> {
85    require_bearer(&headers, &state.api_token)?;
86    Ok(Json(VersionResponse {
87        server: ServerInfo {
88            version: env!("CARGO_PKG_VERSION"),
89            git_sha: option_env!("CELLOS_GIT_SHA"),
90            build_profile: BUILD_PROFILE,
91        },
92        api: ApiInfo { version: "v1" },
93    }))
94}