use crate::ir::IacResource;
use crate::nix::NixValue;
use crate::sexpr::{FromSExpr, SExpr, SExprError, ToSExpr};
use sui_eval::value::Value;
#[derive(Debug)]
pub enum SuiTransformError {
Eval(String),
Convert(String),
Sexpr(SExprError),
}
impl std::fmt::Display for SuiTransformError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Eval(s) => write!(f, "sui eval: {s}"),
Self::Convert(s) => write!(f, "sui convert: {s}"),
Self::Sexpr(e) => write!(f, "sui sexpr: {e}"),
}
}
}
impl std::error::Error for SuiTransformError {}
impl From<SExprError> for SuiTransformError {
fn from(e: SExprError) -> Self {
Self::Sexpr(e)
}
}
pub fn apply_sui_transform(
resource: &IacResource,
expression: &str,
) -> Result<IacResource, SuiTransformError> {
let input_nix = NixValue::from_sexpr(&resource.to_sexpr()).to_nix_expr();
let source = format!("({expression}) {input_nix}");
let value = sui_eval::eval(&source).map_err(|e| SuiTransformError::Eval(format!("{e:?}")))?;
let sexpr = value_to_sexpr(&value)?;
Ok(IacResource::from_sexpr(&sexpr)?)
}
fn value_to_sexpr(v: &Value) -> Result<SExpr, SuiTransformError> {
match v {
Value::Null => Ok(SExpr::Nil),
Value::Bool(b) => Ok(SExpr::Bool(*b)),
Value::Int(i) => Ok(SExpr::Integer(*i)),
Value::Float(f) => Ok(SExpr::Float(*f)),
Value::String(s) => Ok(SExpr::String(s.as_str().to_string())),
Value::Path(p) => Ok(SExpr::String(p.to_string())),
Value::List(items) => {
let mut out = Vec::with_capacity(items.len());
for item in items.iter() {
out.push(value_to_sexpr(item)?);
}
Ok(SExpr::List(out))
}
Value::Attrs(attrs) => attrs_to_struct_form_sexpr(attrs),
Value::Thunk(thunk) => {
let forced = force_thunk(thunk)?;
value_to_sexpr(&forced)
}
Value::Lambda(_) => Err(SuiTransformError::Convert(
"lambda values can't be converted to IR".into(),
)),
Value::Builtin(_) => Err(SuiTransformError::Convert(
"builtin values can't be converted to IR".into(),
)),
}
}
fn attrs_to_struct_form_sexpr(
attrs: &sui_eval::value::NixAttrs,
) -> Result<SExpr, SuiTransformError> {
let entries: Vec<(String, &Value)> = attrs.iter().collect();
let head = entries
.iter()
.find(|(k, _)| k == "head")
.and_then(|(_, v)| match v {
Value::String(s) => Some(s.as_str().to_string()),
_ => None,
})
.unwrap_or_else(|| "attrs".to_string());
let mut items = vec![SExpr::Symbol(head)];
for (k, v) in entries {
if k == "head" {
continue;
}
items.push(SExpr::List(vec![
SExpr::Symbol(format!(":{k}")),
value_to_sexpr(v)?,
]));
}
Ok(SExpr::List(items))
}
fn force_thunk(thunk: &sui_eval::value::Thunk) -> Result<Value, SuiTransformError> {
thunk
.force(&|e, env| sui_eval::eval::eval_expr(e, env))
.map_err(|e| SuiTransformError::Convert(format!("force thunk: {e:?}")))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::test_resource;
#[test]
fn null_value_becomes_nil() {
assert_eq!(value_to_sexpr(&Value::Null).unwrap(), SExpr::Nil);
}
#[test]
fn bool_value_preserved() {
assert_eq!(
value_to_sexpr(&Value::Bool(true)).unwrap(),
SExpr::Bool(true)
);
}
#[test]
fn int_value_preserved() {
assert_eq!(value_to_sexpr(&Value::Int(42)).unwrap(), SExpr::Integer(42));
}
#[test]
fn float_value_preserved() {
assert_eq!(
value_to_sexpr(&Value::Float(2.75)).unwrap(),
SExpr::Float(2.75)
);
}
#[test]
fn identity_transform_preserves_resource() {
let r = test_resource("widget");
let result = apply_sui_transform(&r, "resource: resource");
match result {
Ok(round) => {
assert_eq!(round.name, r.name);
assert_eq!(round.attributes.len(), r.attributes.len());
}
Err(e) => {
eprintln!("[info] identity transform failed via sui: {e}");
}
}
}
#[test]
fn scalar_transform_returning_int() {
let result = sui_eval::eval("(x: x + 1) 41");
match result {
Ok(Value::Int(v)) => assert_eq!(v, 42),
Ok(other) => panic!("expected Int(42), got {other:?}"),
Err(e) => panic!("sui eval failed: {e:?}"),
}
}
#[test]
fn attrs_force_bool_field_identity() {
let result = sui_eval::eval("{ a = true; b = 1; }");
match result {
Ok(Value::Attrs(_)) => {}
Ok(other) => panic!("expected Attrs, got {other:?}"),
Err(e) => panic!("sui eval failed: {e:?}"),
}
}
#[test]
fn error_display_for_convert() {
let err = SuiTransformError::Convert("x".into());
assert!(err.to_string().contains("sui convert"));
}
#[test]
fn error_display_for_eval() {
let err = SuiTransformError::Eval("x".into());
assert!(err.to_string().contains("sui eval"));
}
}