horkos 0.2.0

Cloud infrastructure language where insecure code won't compile
Documentation
//! Resource definitions for Horkos.
//!
//! Resources are defined in YAML (`resources/aws/`) and code-generated.
//! Run `cargo run -p horkos-codegen` to regenerate.
//!
//! Each resource definition includes:
//! - Required parameters (must provide)
//! - Optional parameters (can override defaults)
//! - Preferred parameters (recommended defaults, can override without unsafe)
//! - Security parameters (weakening requires `unsafe`)

pub mod generated;
pub mod registry;

use crate::types::ResolvedType;

/// A complete resource definition.
#[derive(Debug, Clone)]
pub struct ResourceDefinition {
    /// Full name like "S3.createBucket"
    pub module: &'static str,
    pub function: &'static str,

    /// Parameters that must be provided
    pub required_params: Vec<Param>,

    /// Parameters with defaults (can be omitted)
    pub optional_params: Vec<Param>,

    /// Preferred parameters (recommended defaults, can override freely)
    pub preferred_params: Vec<PreferredParam>,

    /// Security-sensitive parameters (weakening requires unsafe)
    pub security_params: Vec<SecurityParam>,

    /// Return type
    pub returns: ResolvedType,
}

impl ResourceDefinition {
    /// Get the minimum number of required arguments.
    pub fn min_args(&self) -> usize {
        self.required_params.len()
    }

    /// Get the maximum number of arguments (required + optional + preferred + security).
    pub fn max_args(&self) -> usize {
        self.required_params.len()
            + self.optional_params.len()
            + self.preferred_params.len()
            + self.security_params.len()
    }

    /// Get all parameters in order: required, optional, preferred, security.
    pub fn all_params(&self) -> Vec<&Param> {
        let mut params: Vec<&Param> = self.required_params.iter().collect();
        params.extend(self.optional_params.iter());
        params.extend(self.preferred_params.iter().map(|pp| &pp.param));
        params.extend(self.security_params.iter().map(|sp| &sp.param));
        params
    }

    /// Find a security parameter by name.
    pub fn get_security_param(&self, name: &str) -> Option<&SecurityParam> {
        self.security_params.iter().find(|sp| sp.param.name == name)
    }

    /// Find a preferred parameter by name.
    pub fn get_preferred_param(&self, name: &str) -> Option<&PreferredParam> {
        self.preferred_params
            .iter()
            .find(|pp| pp.param.name == name)
    }

    /// Check if a named argument weakens security.
    /// Returns the parameter name if it requires unsafe.
    pub fn check_security(&self, name: &str, value: &ParamValue) -> Option<&'static str> {
        if let Some(sp) = self.get_security_param(name) {
            if sp.is_weakened(value) {
                return Some(sp.param.name);
            }
        }
        None
    }

    /// Check if a named argument overrides a preferred default.
    /// Returns the param name and recommended value if overridden.
    pub fn check_preferred(
        &self,
        name: &str,
        value: &ParamValue,
    ) -> Option<(&'static str, &ParamValue)> {
        if let Some(pp) = self.get_preferred_param(name) {
            if pp.is_overridden(value) {
                return Some((pp.param.name, &pp.recommended_default));
            }
        }
        None
    }
}

/// A parameter definition.
#[derive(Debug, Clone)]
pub struct Param {
    pub name: &'static str,
    pub param_type: ResolvedType,
}

impl Param {
    pub fn new(name: &'static str, param_type: ResolvedType) -> Self {
        Self { name, param_type }
    }

    pub fn string(name: &'static str) -> Self {
        Self::new(name, ResolvedType::String)
    }

    pub fn bool(name: &'static str) -> Self {
        Self::new(name, ResolvedType::Bool)
    }

    pub fn number(name: &'static str) -> Self {
        Self::new(name, ResolvedType::Number)
    }

    pub fn record(name: &'static str) -> Self {
        Self::new(name, ResolvedType::Record(std::collections::HashMap::new()))
    }

    /// Create a tags parameter (Record<String, String> - AWS compatible)
    pub fn tags(name: &'static str) -> Self {
        Self::new(name, ResolvedType::Tags)
    }
}

/// A preferred parameter (recommended default, can override without unsafe).
#[derive(Debug, Clone)]
pub struct PreferredParam {
    pub param: Param,
    pub recommended_default: ParamValue,
}

impl PreferredParam {
    /// Create a bool preferred param.
    pub fn bool(name: &'static str, recommended: bool) -> Self {
        Self {
            param: Param::new(name, ResolvedType::Bool),
            recommended_default: ParamValue::Bool(recommended),
        }
    }

    /// Create a number preferred param.
    pub fn number(name: &'static str, recommended: f64) -> Self {
        Self {
            param: Param::new(name, ResolvedType::Number),
            recommended_default: ParamValue::Number(recommended),
        }
    }

    /// Check if the value differs from the recommended default.
    pub fn is_overridden(&self, value: &ParamValue) -> bool {
        &self.recommended_default != value
    }
}

/// A security-sensitive parameter.
#[derive(Debug, Clone)]
pub struct SecurityParam {
    pub param: Param,
    pub secure_default: ParamValue,
}

impl SecurityParam {
    /// Create a bool security param with secure default and weakened value.
    /// Example: `SecurityParam::new("encryption", true, false)` means:
    /// - `encryption: true` is secure (default)
    /// - `encryption: false` weakens security (requires unsafe)
    pub fn new(name: &'static str, secure_default: bool, _weakened_when: bool) -> Self {
        Self {
            param: Param::new(name, ResolvedType::Bool),
            secure_default: ParamValue::Bool(secure_default),
        }
    }

    pub fn with_type(
        name: &'static str,
        param_type: ResolvedType,
        secure_default: ParamValue,
    ) -> Self {
        Self {
            param: Param::new(name, param_type),
            secure_default,
        }
    }

    /// Check if a value weakens security (differs from secure default).
    pub fn is_weakened(&self, value: &ParamValue) -> bool {
        match (&self.secure_default, value) {
            // None means "no value is secure" - any provided value is weakening
            (ParamValue::None, _) => true,
            (ParamValue::Bool(secure), ParamValue::Bool(actual)) => secure != actual,
            (ParamValue::String(secure), ParamValue::String(actual)) => secure != actual,
            _ => false,
        }
    }

    /// Create a security param where any value requires unsafe.
    /// Example: `password` - providing any password requires unsafe.
    pub fn presence(name: &'static str) -> Self {
        Self {
            param: Param::new(name, ResolvedType::String),
            secure_default: ParamValue::None,
        }
    }

    /// Create a bool security param where `true` is secure.
    pub fn bool_secure_true(name: &'static str) -> Self {
        Self::new(name, true, false)
    }

    /// Create a bool security param where `false` is secure.
    pub fn bool_secure_false(name: &'static str) -> Self {
        Self::new(name, false, true)
    }
}

/// A parameter value for security checks.
#[derive(Debug, Clone, PartialEq)]
pub enum ParamValue {
    /// No value is the secure default (any value requires unsafe)
    None,
    Bool(bool),
    String(String),
    Number(f64),
}

impl ParamValue {
    pub fn from_bool(b: bool) -> Self {
        Self::Bool(b)
    }

    pub fn from_string(s: impl Into<String>) -> Self {
        Self::String(s.into())
    }
}

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

    #[test]
    fn test_security_param_weakened() {
        let sp = SecurityParam::bool_secure_true("encrypted");

        assert!(!sp.is_weakened(&ParamValue::Bool(true)), "true is secure");
        assert!(sp.is_weakened(&ParamValue::Bool(false)), "false weakens");
    }

    #[test]
    fn test_security_param_public_access() {
        let sp = SecurityParam::bool_secure_false("publicAccess");

        assert!(!sp.is_weakened(&ParamValue::Bool(false)), "false is secure");
        assert!(sp.is_weakened(&ParamValue::Bool(true)), "true weakens");
    }
}