use std::process::{Command, Stdio};
use crate::ir::IacResource;
use crate::sexpr::{FromSExpr, SExpr, SExprError, ToSExpr};
#[derive(Debug)]
pub enum NixTransformError {
EvaluatorUnavailable(String),
EvaluatorFailed(String),
DecodeError(String),
Sexpr(SExprError),
}
impl std::fmt::Display for NixTransformError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EvaluatorUnavailable(s) => write!(f, "nix evaluator unavailable: {s}"),
Self::EvaluatorFailed(s) => write!(f, "nix evaluator failed: {s}"),
Self::DecodeError(s) => write!(f, "nix transform decode: {s}"),
Self::Sexpr(e) => write!(f, "nix transform sexpr: {e}"),
}
}
}
impl std::error::Error for NixTransformError {}
impl From<SExprError> for NixTransformError {
fn from(e: SExprError) -> Self {
Self::Sexpr(e)
}
}
#[derive(Debug, Clone)]
pub enum NixEvaluator {
Default,
Custom(String),
}
impl NixEvaluator {
fn binary(&self) -> String {
match self {
Self::Default => {
std::env::var("NIX_EVALUATOR").unwrap_or_else(|_| "nix-instantiate".to_string())
}
Self::Custom(s) => s.clone(),
}
}
}
impl Default for NixEvaluator {
fn default() -> Self {
Self::Default
}
}
pub fn apply_nix_transform(
resource: &IacResource,
expression: &str,
evaluator: &NixEvaluator,
) -> Result<IacResource, NixTransformError> {
let input_nix = resource_to_nix_text(resource);
let full_expr = format!("builtins.toJSON (({expression}) {input_nix})");
let binary = evaluator.binary();
let out = Command::new(&binary)
.args(["--eval", "--expr", &full_expr])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.map_err(|e| NixTransformError::EvaluatorUnavailable(e.to_string()))?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
return Err(NixTransformError::EvaluatorFailed(stderr));
}
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
let inner_json: String = serde_json::from_str(&stdout)
.map_err(|e| NixTransformError::DecodeError(format!("outer json: {e}")))?;
let value: serde_json::Value = serde_json::from_str(&inner_json)
.map_err(|e| NixTransformError::DecodeError(format!("inner json: {e}")))?;
let sexpr = json_to_sexpr(&value)?;
Ok(IacResource::from_sexpr(&sexpr)?)
}
fn resource_to_nix_text(resource: &IacResource) -> String {
use crate::nix::NixValue;
let nv = NixValue::from_sexpr(&resource.to_sexpr());
nv.to_nix_expr()
}
fn json_to_sexpr(v: &serde_json::Value) -> Result<SExpr, NixTransformError> {
use serde_json::Value as J;
match v {
J::Null => Ok(SExpr::Nil),
J::Bool(b) => Ok(SExpr::Bool(*b)),
J::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(SExpr::Integer(i))
} else if let Some(f) = n.as_f64() {
Ok(SExpr::Float(f))
} else {
Err(NixTransformError::DecodeError(format!(
"non-representable number: {n}"
)))
}
}
J::String(s) => Ok(SExpr::String(s.clone())),
J::Array(items) => {
let mut out = Vec::with_capacity(items.len());
for item in items {
out.push(json_to_sexpr(item)?);
}
Ok(SExpr::List(out))
}
J::Object(map) => {
let head_name = match map.get("head") {
Some(J::String(s)) => s.clone(),
_ => {
return Err(NixTransformError::DecodeError(
"object missing `head` key — can't reconstruct struct-form".into(),
));
}
};
let mut out = vec![SExpr::Symbol(head_name)];
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
for k in keys {
if k == "head" {
continue;
}
let v = json_to_sexpr(&map[k])?;
out.push(SExpr::List(vec![SExpr::Symbol(format!(":{k}")), v]));
}
Ok(SExpr::List(out))
}
}
}
#[must_use]
pub fn evaluator_available(evaluator: &NixEvaluator) -> bool {
Command::new(evaluator.binary())
.arg("--version")
.output()
.is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::test_resource;
#[test]
fn json_null_becomes_nil() {
let v: serde_json::Value = serde_json::from_str("null").unwrap();
assert_eq!(json_to_sexpr(&v).unwrap(), SExpr::Nil);
}
#[test]
fn json_bool_and_int_preserved() {
assert_eq!(
json_to_sexpr(&serde_json::from_str("true").unwrap()).unwrap(),
SExpr::Bool(true)
);
assert_eq!(
json_to_sexpr(&serde_json::from_str("42").unwrap()).unwrap(),
SExpr::Integer(42)
);
}
#[test]
fn json_object_without_head_rejected() {
let v: serde_json::Value = serde_json::from_str(r#"{"a":1}"#).unwrap();
let err = json_to_sexpr(&v).unwrap_err();
assert!(matches!(err, NixTransformError::DecodeError(_)));
}
#[test]
fn resource_to_nix_text_is_valid_shape() {
let r = test_resource("widget");
let nix = resource_to_nix_text(&r);
assert!(nix.starts_with("{ "));
assert!(nix.contains("head = \"resource\""));
assert!(nix.contains("name"));
}
#[test]
fn evaluator_available_returns_false_for_bogus_binary() {
let ev = NixEvaluator::Custom("definitely-not-a-real-binary-xyzzy".into());
assert!(!evaluator_available(&ev));
}
#[test]
fn identity_transform_preserves_the_resource() {
let ev = NixEvaluator::Default;
if !evaluator_available(&ev) {
eprintln!("[skip] nix evaluator not available");
return;
}
let r = test_resource("widget");
let result = apply_nix_transform(&r, "resource: resource", &ev);
match result {
Ok(round) => {
assert_eq!(round.name, r.name);
assert_eq!(round.attributes.len(), r.attributes.len());
}
Err(e) => {
eprintln!("[skip] transform failed (environment issue): {e}");
}
}
}
#[test]
fn nix_transform_can_mutate_description() {
let ev = NixEvaluator::Default;
if !evaluator_available(&ev) {
eprintln!("[skip] nix evaluator not available");
return;
}
let r = test_resource("widget");
let expr = r#"resource: resource // { description = "transformed"; }"#;
match apply_nix_transform(&r, expr, &ev) {
Ok(round) => {
assert_eq!(round.description, "transformed");
assert_eq!(round.name, r.name); }
Err(e) => {
eprintln!("[skip] transform failed (environment issue): {e}");
}
}
}
}