embers-client 0.1.0

Client rendering, input handling, configuration, and scripting support for Embers.
use std::path::Path;

use rhai::{EvalAltResult, ParseError, Position};
use thiserror::Error;

use crate::config::LoadedConfigSource;

#[derive(Debug, Error)]
pub enum ScriptError {
    #[error("failed to compile config '{path}'{location}: {message}")]
    Compile {
        path: String,
        location: String,
        message: String,
    },
    #[error("failed to evaluate config '{path}'{location}: {message}")]
    Runtime {
        path: String,
        location: String,
        message: String,
    },
    #[error("config '{path}' is invalid{location}: {message}")]
    Validation {
        path: String,
        location: String,
        message: String,
    },
}

impl ScriptError {
    pub fn compile(source: &LoadedConfigSource, error: ParseError) -> Self {
        Self::Compile {
            path: source_path(source),
            location: format_location(error.position()),
            message: error.to_string(),
        }
    }

    pub fn runtime(source: &LoadedConfigSource, error: Box<EvalAltResult>) -> Self {
        Self::runtime_path(source.path.as_deref(), error)
    }

    pub fn validation(
        source: &LoadedConfigSource,
        position: Position,
        message: impl Into<String>,
    ) -> Self {
        Self::validation_path(source.path.as_deref(), position, message)
    }

    pub fn runtime_path(path: Option<&Path>, error: Box<EvalAltResult>) -> Self {
        let position = error.position();
        Self::Runtime {
            path: format_path(path),
            location: format_location(position),
            message: error.to_string(),
        }
    }

    pub fn validation_path(
        path: Option<&Path>,
        position: Position,
        message: impl Into<String>,
    ) -> Self {
        Self::Validation {
            path: format_path(path),
            location: format_location(position),
            message: message.into(),
        }
    }
}

fn source_path(source: &LoadedConfigSource) -> String {
    format_path(source.path.as_deref())
}

fn format_path(path: Option<&Path>) -> String {
    path.unwrap_or_else(|| Path::new("<built-in>"))
        .display()
        .to_string()
}

fn format_location(position: Position) -> String {
    if position.is_none() {
        return String::new();
    }

    let line = position.line().unwrap_or(0);
    match position.position() {
        Some(column) => format!(" at {line}:{column}"),
        None => format!(" at {line}"),
    }
}