ch-rds 0.1.5

Runtime resource loading and local service orchestration
Documentation
//! Runtime context and resource loading used by `ch_rds::run`.

use std::collections::HashMap;
use std::path::{Path, PathBuf};

use anyhow::Result;
use async_trait::async_trait;
use serde_json::Value;
use serde_json::map::Map;

/// Describes the execution environment used to resolve resources.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Environment {
    /// Resolve local resources only when explicit URLs and stack settings require them.
    Automatic,
    /// Local development mode where explicitly configured local stacks may start.
    Local,
    /// Production execution where remote resources are expected.
    Production,
}

impl Environment {
    /// Returns `true` when explicitly configured local resources are allowed.
    pub fn allows_local_resources(self) -> bool {
        !matches!(self, Environment::Production)
    }
}

/// Shared runtime context forwarded to resource builders and loaders.
#[derive(Debug, Clone)]
pub struct Context {
    environment: Environment,
    project_root: Option<PathBuf>,
    metadata: Map<String, Value>,
}

impl Context {
    /// Constructs a context in automatic mode without reading process environment.
    pub fn new() -> Self {
        Self::with_environment(Environment::Automatic)
    }

    /// Builds a context for a specific environment, primarily for tests.
    pub fn with_environment(environment: Environment) -> Self {
        Self {
            environment,
            project_root: None,
            metadata: Map::new(),
        }
    }

    /// Returns the selected execution environment.
    pub fn environment(&self) -> Environment {
        self.environment
    }

    /// Returns the application root discovered by [`crate::Runtime::bootstrap`].
    pub fn project_root(&self) -> Option<&Path> {
        self.project_root.as_deref()
    }

    pub(crate) fn set_project_root(&mut self, project_root: PathBuf) {
        self.project_root = Some(project_root);
    }

    /// Inserts metadata that can be reused by subsequent resources.
    pub fn insert_metadata(&mut self, key: impl Into<String>, value: Value) -> Option<Value> {
        self.metadata.insert(key.into(), value)
    }

    /// Fetches a metadata value written by a previous resource loader.
    pub fn metadata(&self, key: &str) -> Option<&Value> {
        self.metadata.get(key)
    }
}

impl Default for Context {
    fn default() -> Self {
        Self::new()
    }
}

/// Coordinates resource loading before executing the user supplied future.
#[derive(Debug, Default)]
pub struct LoaderAndRunner {
    context: Context,
}

impl LoaderAndRunner {
    /// Creates a new loader in automatic mode.
    pub fn new() -> Self {
        Self::default()
    }

    /// Creates a loader with a custom context, useful for tests.
    pub fn with_context(context: Context) -> Self {
        Self { context }
    }

    /// Borrows the runtime context shared with resource builders.
    pub fn context(&self) -> &Context {
        &self.context
    }

    /// Borrows the runtime context mutably for advanced integrations.
    pub fn context_mut(&mut self) -> &mut Context {
        &mut self.context
    }

    /// Loads a resource using the supplied builder and returns the value.
    pub async fn load<RB, Input, Output>(&self, builder: RB) -> Result<Output>
    where
        RB: ResourceInputBuilder<Input = Input>,
        Input: IntoResource<Output> + Send,
        Output: Send,
    {
        let resource_input = builder.build(self.context())?;
        resource_input.into_resource(self.context()).await
    }
}

/// Provides a synchronous hook that converts builder values into inputs.
pub trait ResourceInputBuilder {
    /// Intermediate resource representation forwarded to [`IntoResource`].
    type Input;

    /// Produces the intermediate representation for the given context.
    fn build(self, context: &Context) -> Result<Self::Input>;
}

/// Converts a builder input into the final resource asynchronously.
#[async_trait]
pub trait IntoResource<Output>: Sized + Send {
    /// Consumes the builder input and yields the resource instance.
    async fn into_resource(self, context: &Context) -> Result<Output>;
}

/// Map type alias exposed to simplify metadata manipulation.
pub type MetadataMap = HashMap<String, Value>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn context_defaults_to_automatic_resource_resolution() {
        assert_eq!(Context::new().environment(), Environment::Automatic);
    }

    #[test]
    fn only_production_forbids_local_resources() {
        assert!(Environment::Automatic.allows_local_resources());
        assert!(Environment::Local.allows_local_resources());
        assert!(!Environment::Production.allows_local_resources());
    }
}