rtb-app 0.9.0

Application context, tool metadata, runtime features, and the Command plugin trait. Part of the phpboyscout Rust toolkit.
Documentation
//! Build-time version information.

use semver::Version;
use serde::{Deserialize, Serialize};

/// Capture the **calling crate's** version into a [`VersionInfo`].
///
/// Expands to [`VersionInfo::from_pkg_version`] applied to
/// `env!("CARGO_PKG_VERSION")`. Because `macro_rules!` expands at the
/// call site, the `env!` is evaluated while *your* crate is compiled and
/// therefore yields *your* version.
///
/// This has to be a macro. A plain function cannot do it: the `env!`
/// inside a function body is expanded when the *defining* crate is
/// compiled, so it would always report rtb-app's version. That is
/// exactly the bug [`VersionInfo::from_env`] has, and why it is
/// deprecated.
///
/// ```
/// let v = rtb_app::version_info!();
/// assert_eq!(v.version.to_string(), env!("CARGO_PKG_VERSION"));
/// ```
///
/// Chain the fluent setters to add build metadata:
///
/// ```
/// let v = rtb_app::version_info!()
///     .with_commit("deadbeef")
///     .with_date("2026-08-02T00:00:00Z");
/// assert_eq!(v.commit.as_deref(), Some("deadbeef"));
/// ```
#[macro_export]
macro_rules! version_info {
    () => {
        $crate::version::VersionInfo::from_pkg_version(::core::env!("CARGO_PKG_VERSION"))
    };
}

/// Version information captured at build time.
///
/// Populate the `version` field with the [`version_info!`] macro, and
/// inject `commit` / `date` via your `build.rs` (the `vergen` or `built`
/// crates are canonical).
///
/// [`version_info!`]: crate::version_info
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionInfo {
    /// Parsed semantic version.
    pub version: Version,

    /// Short commit SHA, if known at build time.
    #[serde(default)]
    pub commit: Option<String>,

    /// ISO-8601 build timestamp, if known at build time.
    #[serde(default)]
    pub date: Option<String>,
}

impl VersionInfo {
    /// Construct from a parsed semver. `commit` and `date` start unset
    /// — add them with [`Self::with_commit`] / [`Self::with_date`].
    #[must_use]
    pub const fn new(version: Version) -> Self {
        Self { version, commit: None, date: None }
    }

    /// Fluent setter for the commit SHA.
    #[must_use]
    pub fn with_commit(mut self, commit: impl Into<String>) -> Self {
        self.commit = Some(commit.into());
        self
    }

    /// Fluent setter for the build timestamp.
    #[must_use]
    pub fn with_date(mut self, date: impl Into<String>) -> Self {
        self.date = Some(date.into());
        self
    }

    /// Parse a `CARGO_PKG_VERSION`-shaped string, with a silent fallback
    /// to `0.0.0` when parsing fails (which in turn is flagged by
    /// [`Self::is_development`]).
    ///
    /// Prefer the [`version_info!`] macro, which passes the *calling*
    /// crate's version for you. Call this directly only when the version
    /// string comes from somewhere else — a `build.rs` output, say.
    ///
    /// [`version_info!`]: crate::version_info
    #[must_use]
    pub fn from_pkg_version(raw: &str) -> Self {
        let version = Version::parse(raw).unwrap_or_else(|_| Version::new(0, 0, 0));
        Self::new(version)
    }

    /// Parse *rtb-app's own* `CARGO_PKG_VERSION`.
    ///
    /// # This is almost certainly not what you want
    ///
    /// `env!` is expanded by the compiler wherever it is written — and it
    /// is written *here*, inside rtb-app. So this returns the version of
    /// the framework, never the version of the tool calling it. Every
    /// downstream tool using it reported rtb-app's version as its own.
    ///
    /// That is not merely cosmetic: [`crate::metadata`]-driven self-update
    /// verifies a freshly staged binary by checking the version it
    /// reports against the release tag it came from. A binary that
    /// misreports its version fails that check, and the update is
    /// refused.
    ///
    /// Use the [`version_info!`] macro instead — it expands `env!` at
    /// *your* call site.
    ///
    /// [`version_info!`]: crate::version_info
    #[must_use]
    #[deprecated(
        since = "0.9.0",
        note = "returns rtb-app's version, not the calling tool's — `env!` expands where it is written. Use the `rtb_app::version_info!()` macro instead."
    )]
    pub fn from_env() -> Self {
        Self::from_pkg_version(env!("CARGO_PKG_VERSION"))
    }

    /// `true` when this build is a development / pre-release build.
    ///
    /// Development is any of:
    ///
    /// * `major == 0` (pre-1.0 builds are always considered development),
    /// * a non-empty pre-release identifier (`-alpha`, `-dev.5`, …),
    /// * version exactly `0.0.0` (the [`Self::from_pkg_version`] fallback).
    #[must_use]
    pub fn is_development(&self) -> bool {
        self.version.major == 0 || !self.version.pre.is_empty()
    }
}