use std::collections::HashMap;
use std::rc::Rc;
use super::*;
use sui_spec::module_system::{
self, Definition, Module, NixValue, OptionDecl,
};
pub(crate) fn register(sui_ext: &mut NixAttrs) {
register_builtin(sui_ext, "evalModules", |args| {
eval_modules_builtin(&args[0])
});
}
fn eval_modules_builtin(modules_arg: &Value) -> Result<Value, EvalError> {
let forced = crate::eval::force_value(modules_arg)?;
let list = match forced {
Value::List(l) => l,
other => {
return Err(EvalError::type_error(format!(
"builtins.sui.evalModules: expected a list of module attrsets, got {}",
other.type_name(),
)));
}
};
let mut modules: Vec<Module> = Vec::with_capacity(list.len());
for (i, m) in list.iter().enumerate() {
modules.push(parse_module(m, i)?);
}
let registry = module_system::load_canonical()
.map_err(|e| EvalError::type_error(format!(
"builtins.sui.evalModules: registry load: {e:?}",
)))?
.types;
let config = module_system::eval_modules(&modules, ®istry)
.map_err(|e| EvalError::type_error(format!(
"builtins.sui.evalModules: {e:?}",
)))?;
Ok(config_to_value(config))
}
fn parse_module(value: &Value, idx: usize) -> Result<Module, EvalError> {
let forced = crate::eval::force_value(value)?;
let attrs = match forced {
Value::Attrs(a) => a,
other => {
return Err(EvalError::type_error(format!(
"builtins.sui.evalModules: module[{idx}] must be an attrset, got {}",
other.type_name(),
)));
}
};
let mut module = Module::default();
if let Some(opts) = attrs.get("options") {
let forced_opts = crate::eval::force_value(opts)?;
let opts_attrs = match forced_opts {
Value::Attrs(a) => a,
other => {
return Err(EvalError::type_error(format!(
"builtins.sui.evalModules: module[{idx}].options must be an attrset, got {}",
other.type_name(),
)));
}
};
for (path, decl_val) in opts_attrs.iter() {
module
.options
.insert(path.to_string(), parse_option_decl(decl_val, &path)?);
}
}
if let Some(cfg) = attrs.get("config") {
let forced_cfg = crate::eval::force_value(cfg)?;
let cfg_attrs = match forced_cfg {
Value::Attrs(a) => a,
other => {
return Err(EvalError::type_error(format!(
"builtins.sui.evalModules: module[{idx}].config must be an attrset, got {}",
other.type_name(),
)));
}
};
for (path, value) in cfg_attrs.iter() {
module.config.push(Definition {
path: path.to_string(),
value: crate::eval::force_value(value)?.to_json(),
priority: 100, cond: None,
});
}
}
if let Some(imports) = attrs.get("imports") {
let forced = crate::eval::force_value(imports)?;
match forced {
Value::List(l) => {
for item in l.iter() {
let forced_item = crate::eval::force_value(item)?;
if !matches!(forced_item, Value::String(_)) {
return Err(EvalError::type_error(format!(
"builtins.sui.evalModules: module[{idx}].imports \
items must be strings (M3.1); attrset/function imports land in M3.2",
)));
}
}
}
_ => {
return Err(EvalError::type_error(format!(
"builtins.sui.evalModules: module[{idx}].imports must be a list",
)));
}
}
}
Ok(module)
}
fn parse_option_decl(decl_val: &Value, path: &str) -> Result<OptionDecl, EvalError> {
let forced = crate::eval::force_value(decl_val)?;
let attrs = match forced {
Value::Attrs(a) => a,
other => {
return Err(EvalError::type_error(format!(
"builtins.sui.evalModules: option `{path}` declaration must be an \
attrset, got {}",
other.type_name(),
)));
}
};
let type_name = match attrs.get("type") {
Some(v) => parse_type_field(v, path)?,
None => {
return Err(EvalError::type_error(format!(
"builtins.sui.evalModules: option `{path}` missing required `type` field",
)));
}
};
let default = match attrs.get("default") {
Some(v) => Some(crate::eval::force_value(v)?.to_json()),
None => None,
};
let description = match attrs.get("description") {
Some(v) => match crate::eval::force_value(v)? {
Value::String(s) => s.chars.to_string(),
_ => String::new(),
},
None => String::new(),
};
Ok(OptionDecl {
type_name,
default,
description,
submodule: None,
})
}
fn parse_type_field(v: &Value, path: &str) -> Result<String, EvalError> {
let forced = crate::eval::force_value(v)?;
match forced {
Value::String(s) => Ok(s.chars.to_string()),
Value::Attrs(attrs) => match attrs.get("name") {
Some(name_val) => match crate::eval::force_value(name_val)? {
Value::String(s) => Ok(s.chars.to_string()),
other => Err(EvalError::type_error(format!(
"builtins.sui.evalModules: option `{path}`.type is a typed \
object but its `name` field is {}, expected string",
other.type_name(),
))),
},
None => Err(EvalError::type_error(format!(
"builtins.sui.evalModules: option `{path}`.type is an attrset \
but has no `name` field — typed-object types (M3.2) must \
carry their type name in `name`",
))),
},
other => Err(EvalError::type_error(format!(
"builtins.sui.evalModules: option `{path}`.type must be a string \
(e.g. \"bool\") or a typed object with a `name` field; got {}",
other.type_name(),
))),
}
}
fn config_to_value(config: HashMap<String, NixValue>) -> Value {
let mut attrs = NixAttrs::new();
for (k, v) in config {
attrs.insert(k, json_to_value(&v));
}
Value::Attrs(Rc::new(attrs))
}
#[cfg(test)]
mod tests {
use super::*;
fn module_list(modules: Vec<Value>) -> Value {
Value::List(Rc::new(NixList::new(modules)))
}
fn attrs_of(pairs: &[(&str, Value)]) -> Value {
let mut a = NixAttrs::new();
for (k, v) in pairs {
a.insert(k.to_string(), v.clone());
}
Value::Attrs(Rc::new(a))
}
#[test]
fn trivial_bool_evaluates_through_the_bridge() {
let opt_decl = attrs_of(&[("type", Value::string("bool"))]);
let options = attrs_of(&[("enable", opt_decl)]);
let config = attrs_of(&[("enable", Value::Bool(true))]);
let module = attrs_of(&[("options", options), ("config", config)]);
let result = eval_modules_builtin(&module_list(vec![module])).unwrap();
let attrs = match result {
Value::Attrs(a) => a,
_ => panic!("expected attrs result"),
};
match attrs.get("enable") {
Some(Value::Bool(b)) => assert!(*b),
other => panic!("expected enable=true, got {other:?}"),
}
}
#[test]
fn default_surfaces_when_undefined() {
let opt_decl = attrs_of(&[
("type", Value::string("int")),
("default", Value::Int(80)),
]);
let options = attrs_of(&[("port", opt_decl)]);
let module = attrs_of(&[("options", options)]);
let result = eval_modules_builtin(&module_list(vec![module])).unwrap();
let attrs = match result {
Value::Attrs(a) => a,
_ => panic!("expected attrs"),
};
match attrs.get("port") {
Some(Value::Int(n)) => assert_eq!(*n, 80),
other => panic!("expected port=80, got {other:?}"),
}
}
#[test]
fn rejects_non_list_arg() {
let bogus = Value::Bool(true);
let err = eval_modules_builtin(&bogus).unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("list of module attrsets"));
}
#[test]
fn rejects_module_with_non_attrset() {
let result = eval_modules_builtin(&module_list(vec![Value::Int(42)]));
assert!(result.is_err());
}
#[test]
fn rejects_option_with_missing_type() {
let opt_decl = attrs_of(&[]); let options = attrs_of(&[("foo", opt_decl)]);
let module = attrs_of(&[("options", options)]);
let err = eval_modules_builtin(&module_list(vec![module])).unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("missing required `type`"));
}
#[test]
fn m32_accepts_typed_object_with_name_field() {
let typed_obj = attrs_of(&[
("name", Value::string("bool")),
("check", Value::string("<fake-fn>")), ]);
let opt_decl = attrs_of(&[("type", typed_obj)]);
let options = attrs_of(&[("enable", opt_decl)]);
let config = attrs_of(&[("enable", Value::Bool(true))]);
let module = attrs_of(&[("options", options), ("config", config)]);
let result = eval_modules_builtin(&module_list(vec![module])).unwrap();
let attrs = match result {
Value::Attrs(a) => a,
_ => panic!("expected attrs"),
};
match attrs.get("enable") {
Some(Value::Bool(b)) => assert!(*b),
other => panic!("expected enable=true, got {other:?}"),
}
}
#[test]
fn m32_rejects_typed_object_without_name_field() {
let typed_obj = attrs_of(&[("check", Value::string("<fn>"))]);
let opt_decl = attrs_of(&[("type", typed_obj)]);
let options = attrs_of(&[("foo", opt_decl)]);
let module = attrs_of(&[("options", options)]);
let err = eval_modules_builtin(&module_list(vec![module])).unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("no `name` field"));
}
#[test]
fn m32_rejects_typed_object_with_non_string_name() {
let typed_obj = attrs_of(&[("name", Value::Int(42))]);
let opt_decl = attrs_of(&[("type", typed_obj)]);
let options = attrs_of(&[("foo", opt_decl)]);
let module = attrs_of(&[("options", options)]);
let err = eval_modules_builtin(&module_list(vec![module])).unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("`name` field is"));
}
#[test]
fn type_mismatch_surfaces_through_bridge() {
let opt_decl = attrs_of(&[("type", Value::string("bool"))]);
let options = attrs_of(&[("enable", opt_decl)]);
let config = attrs_of(&[("enable", Value::Int(42))]);
let module = attrs_of(&[("options", options), ("config", config)]);
let err = eval_modules_builtin(&module_list(vec![module])).unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("type-check") || msg.contains("bool"));
}
#[test]
fn list_of_concatenates_across_modules() {
let opt_decl = attrs_of(&[("type", Value::string("listOf"))]);
let options = attrs_of(&[("xs", opt_decl)]);
let cfg1 = attrs_of(&[("xs", Value::list(vec![Value::Int(1), Value::Int(2)]))]);
let mod1 = attrs_of(&[("options", options), ("config", cfg1)]);
let cfg2 = attrs_of(&[("xs", Value::list(vec![Value::Int(3), Value::Int(4)]))]);
let mod2 = attrs_of(&[("config", cfg2)]);
let result = eval_modules_builtin(&module_list(vec![mod1, mod2])).unwrap();
let attrs = match result {
Value::Attrs(a) => a,
_ => panic!("expected attrs"),
};
let list_val = attrs.get("xs").expect("xs must resolve");
let items = match list_val {
Value::List(l) => l,
_ => panic!("expected list, got {list_val:?}"),
};
assert_eq!(items.len(), 4);
}
}