xrml 0.1.0

eXtensible Rust Markup Language — recursive acronym: HRML (HRML Markup Language) and TRML (TOML-like Markup Language)
Documentation
use crate::template::Engine;
use serde_json::{json, Value};
use std::fs;
use std::path::{Path, PathBuf};

pub struct Runtime {
    endpoints_path: PathBuf,
}

impl Runtime {
    pub fn new(endpoints_path: &str) -> Self {
        Self {
            endpoints_path: PathBuf::from(endpoints_path),
        }
    }

    pub fn call_endpoint(&self, path: &str, data: &Value) -> Result<Value, String> {
        let parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
        if parts.len() < 2 || parts[0] != "api" {
            return Err("Invalid endpoint path; expected /api/<name>[/... ]".to_string());
        }

        let module = parts[1];
        let (id, action) = if parts.len() > 2 {
            if parts[2].parse::<i64>().is_ok() {
                let action = if parts.len() > 3 { parts[3] } else { "" };
                (parts[2], action)
            } else {
                ("", parts[2])
            }
        } else {
            ("", "")
        };

        let context = json!({
            "id": id,
            "action": action,
            "data": data,
        });

        if let Some(path) = self.find_template_path(module, action) {
            let base = self.endpoints_path.to_string_lossy().to_string();
            let rel = path
                .strip_prefix(&self.endpoints_path)
                .map_err(|e| e.to_string())?;
            let rel = rel.to_string_lossy().replace('\\', "/");
            let rendered = Engine::new(&base)
                .render_fragment(&rel, &context)
                .map_err(|e| e.to_string())?;
            return Ok(Value::String(rendered));
        }

        if let Some(path) = self.find_text_path(module, action) {
            let content = fs::read_to_string(&path)
                .map_err(|e| format!("Failed to read endpoint file {}: {}", path.display(), e))?;
            return Ok(Value::String(content));
        }

        if let Some(path) = self.find_json_path(module, action) {
            let content = fs::read_to_string(&path)
                .map_err(|e| format!("Failed to read endpoint file {}: {}", path.display(), e))?;
            let value: Value = serde_json::from_str(&content)
                .map_err(|e| format!("Invalid endpoint JSON in {}: {}", path.display(), e))?;
            return Ok(value);
        }

        Err(format!("Endpoint not found for path: {}", path))
    }

    fn find_template_path(&self, module: &str, action: &str) -> Option<PathBuf> {
        self.find_path(module, action, "hrml")
    }

    fn find_text_path(&self, module: &str, action: &str) -> Option<PathBuf> {
        self.find_path(module, action, "html")
            .or_else(|| self.find_path(module, action, "txt"))
    }

    fn find_json_path(&self, module: &str, action: &str) -> Option<PathBuf> {
        self.find_path(module, action, "json")
    }

    fn find_path(&self, module: &str, action: &str, ext: &str) -> Option<PathBuf> {
        let api_root = self.endpoints_path.join("api");
        let candidates = endpoint_candidates(&api_root, module, action, ext);
        candidates.into_iter().find(|candidate| candidate.exists())
    }
}

fn endpoint_candidates(api_root: &Path, module: &str, action: &str, ext: &str) -> Vec<PathBuf> {
    let mut out = Vec::new();

    if !action.is_empty() {
        out.push(api_root.join(module).join(format!("{}.{}", action, ext)));
        out.push(api_root.join(format!("{}_{}.{}", module, action, ext)));
    }

    out.push(api_root.join(format!("{}.{}", module, ext)));
    out
}