meld-core 0.1.0

Core domain types, state, and error model for the Meld framework.
Documentation
use std::sync::Arc;

use thiserror::Error;

pub mod auth;

pub type MeldResult<T> = Result<T, MeldError>;
pub type AlloyResult<T> = MeldResult<T>;

#[derive(Debug, Clone)]
pub struct AppConfig {
    pub service_name: String,
    pub environment: String,
}

impl AppConfig {
    pub fn local(service_name: impl Into<String>) -> Self {
        Self {
            service_name: service_name.into(),
            environment: "local".to_string(),
        }
    }
}

#[derive(Debug, Error)]
pub enum MeldError {
    #[error("validation error: {0}")]
    Validation(String),
    #[error("internal error: {0}")]
    Internal(String),
}

pub type AlloyError = MeldError;

pub trait GreetingEngine: Send + Sync {
    fn greet(&self, name: &str) -> MeldResult<String>;
}

pub trait MetricsSink: Send + Sync {
    fn incr_counter(&self, name: &str);
}

#[derive(Debug, Default)]
pub struct NoopMetrics;

impl MetricsSink for NoopMetrics {
    fn incr_counter(&self, _name: &str) {}
}

#[derive(Debug, Clone)]
pub struct StaticGreetingEngine {
    prefix: String,
}

impl StaticGreetingEngine {
    pub fn new(prefix: impl Into<String>) -> Self {
        Self {
            prefix: prefix.into(),
        }
    }
}

impl GreetingEngine for StaticGreetingEngine {
    fn greet(&self, name: &str) -> MeldResult<String> {
        if name.trim().is_empty() {
            return Err(MeldError::Validation("name must not be empty".to_string()));
        }
        Ok(format!("{}, {}!", self.prefix, name))
    }
}

#[derive(Clone)]
pub struct AppState {
    pub config: AppConfig,
    pub greeter: Arc<dyn GreetingEngine>,
    pub metrics: Arc<dyn MetricsSink>,
}

impl AppState {
    pub fn new(
        config: AppConfig,
        greeter: Arc<dyn GreetingEngine>,
        metrics: Arc<dyn MetricsSink>,
    ) -> Self {
        Self {
            config,
            greeter,
            metrics,
        }
    }

    pub fn local(service_name: impl Into<String>) -> Self {
        Self {
            config: AppConfig::local(service_name),
            greeter: Arc::new(StaticGreetingEngine::new("Hello")),
            metrics: Arc::new(NoopMetrics),
        }
    }

    pub fn greet(&self, name: &str) -> MeldResult<String> {
        self.metrics.incr_counter("greet.requests");
        self.greeter.greet(name)
    }
}

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

    #[test]
    fn greet_rejects_empty_name() {
        let state = AppState::local("meld-test");
        let result = state.greet("  ");
        assert!(matches!(result, Err(MeldError::Validation(_))));
    }

    #[test]
    fn greet_returns_message() {
        let state = AppState::local("meld-test");
        let result = state.greet("Rust");
        assert_eq!(result.expect("must greet"), "Hello, Rust!");
    }
}