use crate::spec::EnvSpec;
use serde::{Deserialize, Serialize};
use tatara_lisp::Sexp;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum CompileError {
#[error("program declares no `(defenv …)` form")]
MissingDefenv,
#[error("program declares more than one `(defenv …)` — found {0}")]
MultipleDefenvs(usize),
#[error("`(defenv …)` form failed to compile: {0}")]
BadDefenv(String),
#[error("resource `({head} …)`: {message}")]
ResourceCompile { head: String, message: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Resource {
pub keyword: String,
pub value: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Env {
pub spec: EnvSpec,
pub resources: Vec<Resource>,
}
impl Env {
#[must_use]
pub fn resources_by_keyword(&self, kw: &str) -> Vec<&Resource> {
self.resources.iter().filter(|r| r.keyword == kw).collect()
}
#[must_use]
pub fn keywords(&self) -> Vec<&str> {
let mut seen = Vec::new();
for r in &self.resources {
if !seen.iter().any(|k: &&str| **k == *r.keyword) {
seen.push(r.keyword.as_str());
}
}
seen
}
}
pub fn compile_into_env(forms: &[Sexp]) -> Result<Env, CompileError> {
let mut spec: Option<EnvSpec> = None;
let mut resources = Vec::new();
let mut defenv_count = 0;
for form in forms {
let Some(list) = form.as_list() else {
continue;
};
let Some(head) = list.first().and_then(Sexp::as_symbol) else {
continue;
};
if head == "defenv" {
defenv_count += 1;
if defenv_count > 1 {
continue;
}
let handler = tatara_lisp::domain::lookup(head).ok_or_else(|| {
CompileError::BadDefenv(
"EnvSpec not registered — call tatara_env::register() before compiling"
.into(),
)
})?;
let json = (handler.compile)(&list[1..])
.map_err(|e| CompileError::BadDefenv(format!("{e}")))?;
spec = Some(serde_json::from_value(json).map_err(|e| {
CompileError::BadDefenv(format!("EnvSpec deserialize: {e}"))
})?);
continue;
}
if let Some(handler) = tatara_lisp::domain::lookup(head) {
let value = (handler.compile)(&list[1..]).map_err(|e| {
CompileError::ResourceCompile {
head: head.to_string(),
message: format!("{e}"),
}
})?;
resources.push(Resource {
keyword: head.to_string(),
value,
});
}
}
if defenv_count > 1 {
return Err(CompileError::MultipleDefenvs(defenv_count));
}
let spec = spec.ok_or(CompileError::MissingDefenv)?;
Ok(Env { spec, resources })
}