potenv 0.2.0

Rust implementation of the POSIX-compliant dotenv file format specification.
Documentation
use std::collections::HashMap;

use thiserror::Error;

use crate::{
    env::EnvProvider,
    parser::ast::{Assignment, Expansion, Expression, Operator},
};

#[cfg(test)]
mod tests;

pub type Scope = HashMap<String, String>;

#[derive(Debug, Error)]
#[non_exhaustive]
pub enum EvaluationError {
    #[error("Undefined variable ${0} {1}")]
    UndefinedVariable(String, String),
    #[error("Missing required value for variable ${0} {1}")]
    EmptyValue(String, String),
}

type EvaluationResult<T> = Result<T, EvaluationError>;

#[derive(Debug)]
pub struct Evaluator<'a, T>
where
    T: EnvProvider,
{
    env: &'a T,
    scope: Scope,
    override_env: bool,
}

impl<'a, T> Evaluator<'a, T>
where
    T: EnvProvider,
{
    pub fn new(env: &'a T, override_env: bool) -> Self {
        Self {
            env,
            override_env,
            scope: HashMap::new(),
        }
    }

    pub fn evaluate(&mut self, ast: Vec<Assignment>) -> EvaluationResult<()> {
        for node in ast {
            self.evaluate_assignment(node)?;
        }
        Ok(())
    }

    pub fn into_scope(self) -> HashMap<String, String> {
        self.scope
    }

    fn evaluate_assignment(&mut self, node: Assignment) -> EvaluationResult<()> {
        let name = node.name;
        let value = if let Some(v) = (!self.override_env).then(|| self.env.var(&name)).flatten() {
            v
        } else {
            self.evaluate_expression(node.value)?
        };
        self.scope.insert(name, value);
        Ok(())
    }

    fn evaluate_expression(&mut self, nodes: Vec<Expression>) -> EvaluationResult<String> {
        let mut result = String::with_capacity(64);
        for node in nodes {
            let value = match node {
                Expression::Characters(chars) => chars,
                Expression::Expansion(expr) => self.evaluate_expansion(expr)?,
            };
            result.push_str(&value);
        }
        Ok(result)
    }

    fn evaluate_expansion(&mut self, expr: Expansion) -> EvaluationResult<String> {
        let value = self.resolve(&expr.name);
        let result = match expr.operator {
            Operator::IfUnset => match value {
                None => self.evaluate_expression(expr.rhs)?,
                _ => value.unwrap(),
            },
            Operator::IfUnsetOrNull => match value.as_deref() {
                None | Some("") => self.evaluate_expression(expr.rhs)?,
                _ => value.unwrap(),
            },
            Operator::IfSet => match value {
                None => "".to_owned(),
                _ => self.evaluate_expression(expr.rhs)?,
            },
            Operator::IfSetAndNotNull => match value.as_deref() {
                None | Some("") => "".to_owned(),
                _ => self.evaluate_expression(expr.rhs)?,
            },
            Operator::AssignIfUnset => match value {
                None => self.assign_op(expr.name, expr.rhs)?,
                _ => value.unwrap(),
            },
            Operator::AssignIfUnsetOrNull => match value.as_deref() {
                None | Some("") => self.assign_op(expr.name, expr.rhs)?,
                _ => value.unwrap(),
            },
            Operator::ErrorIfUnset => match value {
                None => self.error_op(expr.name, expr.rhs, false)?,
                _ => value.unwrap(),
            },
            Operator::ErrorIfUnsetOrNull => match value.as_deref() {
                None => self.error_op(expr.name, expr.rhs, false)?,
                Some("") => self.error_op(expr.name, expr.rhs, true)?,
                _ => value.unwrap(),
            },
        };
        Ok(result)
    }

    fn resolve(&self, name: &str) -> Option<String> {
        if self.override_env {
            self.scope
                .get(name)
                .map(ToOwned::to_owned)
                .or_else(|| self.env.var(name))
        } else {
            self.env
                .var(name)
                .or_else(|| self.scope.get(name).map(ToOwned::to_owned))
        }
    }

    fn assign_op(&mut self, name: String, expr: Vec<Expression>) -> EvaluationResult<String> {
        let value = self.evaluate_expression(expr)?;
        self.scope.insert(name, value.clone());
        Ok(value)
    }

    fn error_op(
        &mut self,
        name: String,
        expr: Vec<Expression>,
        require_value: bool,
    ) -> EvaluationResult<String> {
        let message = self.evaluate_expression(expr)?;
        if require_value {
            Err(EvaluationError::EmptyValue(name, message))
        } else {
            Err(EvaluationError::UndefinedVariable(name, message))
        }
    }
}