Skip to main content

wowlab_engine/
composition.rs

1//! Engine host composition and deterministic build metadata.
2
3use wowlab_engine_ports::{ContentCatalog, ContentCatalogError, content_catalog};
4
5/// Fully linked engine composition used by native and browser hosts.
6#[derive(Clone, Copy, Debug)]
7#[doc(alias = "simulation engine", alias = "WoW simulator")]
8pub struct EngineComposition {
9    catalog: &'static ContentCatalog,
10}
11
12impl EngineComposition {
13    /// Link and validate the complete engine composition.
14    ///
15    /// # Errors
16    /// Returns an error if the content catalog cannot be assembled or fails validation.
17    pub fn initialize() -> Result<Self, ContentCatalogError> {
18        let linked_specs = wowlab_engine_content::force_link_generated_specs();
19
20        std::hint::black_box(linked_specs);
21
22        Self::from_catalog(content_catalog()?)
23    }
24
25    fn from_catalog(catalog: &'static ContentCatalog) -> Result<Self, ContentCatalogError> {
26        catalog.validate()?;
27
28        Ok(Self { catalog })
29    }
30
31    /// The validated content catalog assembled into this engine.
32    #[must_use]
33    pub const fn catalog(self) -> &'static ContentCatalog {
34        self.catalog
35    }
36
37    /// Deterministic metadata embedded in this engine build.
38    #[must_use]
39    pub const fn build_metadata(self) -> EngineBuildMetadata {
40        EngineBuildMetadata::CURRENT
41    }
42}
43
44/// Deterministic version and source revision embedded at build time.
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub struct EngineBuildMetadata {
47    version: &'static str,
48    git_hash: &'static str,
49}
50
51impl EngineBuildMetadata {
52    /// Metadata for the current engine artifact.
53    pub const CURRENT: Self = Self {
54        version: env!("CARGO_PKG_VERSION"),
55        git_hash: env!("GIT_HASH"),
56    };
57
58    /// Cargo package version for the composed engine artifact.
59    #[must_use]
60    pub const fn version(self) -> &'static str {
61        self.version
62    }
63
64    /// Deterministic source revision captured by the Engine build script.
65    #[must_use]
66    pub const fn git_hash(self) -> &'static str {
67        self.git_hash
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use std::error::Error as _;
74
75    use googletest::prelude::*;
76    use wowlab_engine_ports::ContentCatalog;
77
78    use super::{EngineBuildMetadata, EngineComposition};
79
80    static EMPTY_CATALOG: ContentCatalog = ContentCatalog::new(&[], &[], &[]);
81    // #t(rust_duplicate_strings) the regression assertion deliberately mirrors the production error message
82    const EMPTY_CATALOG_MESSAGE: &str = "engine-content catalog contains no spec descriptors";
83
84    #[gtest]
85    fn initialization_exposes_the_single_validated_catalog() -> Result<()> {
86        let composition = EngineComposition::initialize().or_fail()?;
87
88        verify_that!(composition.catalog().descriptors().len(), eq(27))?;
89        verify_true!(std::ptr::eq(
90            composition.catalog(),
91            EngineComposition::initialize().or_fail()?.catalog(),
92        ))?;
93
94        Ok(())
95    }
96
97    #[gtest]
98    fn invalid_catalog_returns_the_typed_error_without_erasing_it() -> Result<()> {
99        let error = EngineComposition::from_catalog(&EMPTY_CATALOG)
100            .err()
101            .or_fail()?;
102
103        verify_that!(error.to_string(), eq(EMPTY_CATALOG_MESSAGE))?;
104        verify_that!(
105            error.source().map(ToString::to_string).as_deref(),
106            eq(Some(EMPTY_CATALOG_MESSAGE))
107        )?;
108
109        Ok(())
110    }
111
112    #[gtest]
113    fn build_metadata_is_copyable_exact_and_nonempty() -> Result<()> {
114        let metadata = EngineBuildMetadata::CURRENT;
115
116        verify_that!(metadata, eq(metadata))?;
117        verify_that!(metadata.version(), eq(env!("CARGO_PKG_VERSION")))?;
118        verify_that!(metadata.git_hash(), eq(env!("GIT_HASH")))?;
119        verify_true!(!metadata.version().is_empty())?;
120        verify_true!(!metadata.git_hash().is_empty())?;
121
122        Ok(())
123    }
124}