use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use tatara_lisp::DeriveTataraDomain;
use crate::SpecError;
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
#[tatara(keyword = "defoption-type")]
pub struct OptionTypeSpec {
pub name: String,
#[serde(rename = "mergeStrategy")]
pub merge_strategy: MergeStrategy,
#[serde(rename = "checkKind")]
pub check_kind: TypeCheckKind,
#[serde(default, rename = "elementType")]
pub element_type: Option<String>,
#[serde(default, rename = "memberTypes")]
pub member_types: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergeStrategy {
LastWins,
Concatenate,
SubmoduleMerge,
AttrsetMerge,
Disjoint,
Custom,
AnyLastWins,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum TypeCheckKind {
Bool,
Int,
Str,
Path,
Null,
IntBetween,
Any,
Enum,
ListOf,
AttrsOf,
Submodule,
OneOf,
NullOr,
Attrs,
Package,
FunctionTo,
}
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
#[tatara(keyword = "defpriority")]
pub struct PriorityRank {
pub name: String,
pub level: u32,
pub origin: PriorityOrigin,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum PriorityOrigin {
MkDefault,
Normal,
MkOverride,
MkForce,
MkOptionDefault,
}
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
#[tatara(keyword = "defmodule-eval-algorithm")]
pub struct ModuleEvalAlgorithm {
pub name: String,
pub phases: Vec<ModulePhase>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ModulePhase {
pub kind: ModulePhaseKind,
#[serde(default)]
pub bind: Option<String>,
#[serde(default)]
pub from: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModulePhaseKind {
CollectModules,
PartitionOptionsAndConfig,
BuildOptionTree,
GroupDefinitions,
ResolveConditionals,
ResolvePriorities,
MergePerOption,
TypeCheck,
EvaluateRecursive,
EmitConfig,
}
pub type NixValue = serde_json::Value;
#[derive(Debug, Clone, Default)]
pub struct Module {
pub imports: Vec<String>,
pub options: HashMap<String, OptionDecl>,
pub config: Vec<Definition>,
}
#[derive(Debug, Clone)]
pub struct OptionDecl {
pub type_name: String,
pub default: Option<NixValue>,
pub description: String,
pub submodule: Option<Box<Module>>,
}
impl Default for OptionDecl {
fn default() -> Self {
Self {
type_name: "any".into(),
default: None,
description: String::new(),
submodule: None,
}
}
}
#[derive(Debug, Clone)]
pub struct Definition {
pub path: String,
pub value: NixValue,
pub priority: u32,
pub cond: Option<bool>,
}
pub type Config = HashMap<String, NixValue>;
pub fn eval_modules(
modules: &[Module],
option_types: &[OptionTypeSpec],
) -> Result<Config, SpecError> {
let mut option_tree: HashMap<String, OptionDecl> = HashMap::new();
for module in modules {
for (path, decl) in &module.options {
option_tree.entry(path.clone()).or_insert_with(|| decl.clone());
}
}
let mut by_path: HashMap<String, Vec<Definition>> = HashMap::new();
for module in modules {
for def in &module.config {
if def.cond == Some(false) {
continue;
}
by_path.entry(def.path.clone()).or_default().push(def.clone());
}
}
for path in by_path.keys() {
if !option_tree.contains_key(path) {
return Err(SpecError::Interp {
phase: "unknown-option".into(),
message: format!(
"definition for `{path}` but no module declares this option",
),
});
}
}
let type_by_name: HashMap<&str, &OptionTypeSpec> =
option_types.iter().map(|t| (t.name.as_str(), t)).collect();
let mut config: Config = HashMap::new();
for (path, decl) in &option_tree {
let type_spec = type_by_name.get(decl.type_name.as_str()).ok_or_else(|| {
SpecError::Interp {
phase: "unknown-type".into(),
message: format!(
"option `{path}` declares type `{}` but no \
OptionTypeSpec with that name is in the registry",
decl.type_name,
),
}
})?;
let mut defs = by_path.remove(path).unwrap_or_default();
if defs.is_empty() {
if let Some(default) = &decl.default {
config.insert(path.clone(), default.clone());
}
continue;
}
defs.sort_by_key(|d| d.priority);
let top_priority = defs[0].priority;
let winners: Vec<&Definition> =
defs.iter().filter(|d| d.priority == top_priority).collect();
for d in &winners {
check_value(type_spec, &d.value, &d.path)?;
}
let merged = if type_spec.merge_strategy == MergeStrategy::SubmoduleMerge {
merge_submodule(decl, &winners, path, option_types)?
} else {
merge_definitions(type_spec, &winners, path)?
};
config.insert(path.clone(), merged);
}
Ok(config)
}
fn check_value(
type_spec: &OptionTypeSpec,
value: &NixValue,
path: &str,
) -> Result<(), SpecError> {
let ok = match type_spec.check_kind {
TypeCheckKind::Bool => value.is_boolean(),
TypeCheckKind::Int => value.is_i64() || value.is_u64(),
TypeCheckKind::Str => value.is_string(),
TypeCheckKind::Path => value.is_string(),
TypeCheckKind::Null => value.is_null(),
TypeCheckKind::Any => true,
TypeCheckKind::Attrs => value.is_object(),
TypeCheckKind::ListOf => value.is_array(),
TypeCheckKind::AttrsOf => value.is_object(),
TypeCheckKind::NullOr => true, TypeCheckKind::Submodule => value.is_object(),
TypeCheckKind::IntBetween | TypeCheckKind::Enum
| TypeCheckKind::OneOf | TypeCheckKind::Package
| TypeCheckKind::FunctionTo => true,
};
if !ok {
return Err(SpecError::Interp {
phase: "type-check".into(),
message: format!(
"option `{path}` declared `{}` but value is `{}`: {}",
type_spec.name,
value_type(value),
truncate_value(value),
),
});
}
Ok(())
}
fn value_type(v: &NixValue) -> &'static str {
if v.is_boolean() { "bool" }
else if v.is_i64() || v.is_u64() { "int" }
else if v.is_f64() { "float" }
else if v.is_string() { "str" }
else if v.is_array() { "list" }
else if v.is_object() { "attrs" }
else if v.is_null() { "null" }
else { "unknown" }
}
fn truncate_value(v: &NixValue) -> String {
let s = v.to_string();
if s.len() <= 60 { s } else { format!("{}…", &s[..60]) }
}
fn merge_definitions(
type_spec: &OptionTypeSpec,
winners: &[&Definition],
path: &str,
) -> Result<NixValue, SpecError> {
match type_spec.merge_strategy {
MergeStrategy::LastWins | MergeStrategy::AnyLastWins => {
if winners.len() > 1 {
let first = &winners[0].value;
let all_equal = winners.iter().all(|d| &d.value == first);
if !all_equal {
return Err(SpecError::Interp {
phase: "merge-conflict".into(),
message: format!(
"option `{path}` has {} distinct top-priority \
definitions; LastWins requires either one \
definition at the top or all equal",
winners.len(),
),
});
}
}
Ok(winners[0].value.clone())
}
MergeStrategy::Concatenate => {
let mut acc: Vec<NixValue> = Vec::new();
for w in winners {
let Some(arr) = w.value.as_array() else {
return Err(SpecError::Interp {
phase: "type-check".into(),
message: format!(
"option `{path}` is Concatenate (listOf) \
but a definition value is not a list",
),
});
};
for item in arr {
acc.push(item.clone());
}
}
Ok(NixValue::Array(acc))
}
MergeStrategy::AttrsetMerge => {
let mut acc = serde_json::Map::new();
for w in winners {
let Some(obj) = w.value.as_object() else {
return Err(SpecError::Interp {
phase: "type-check".into(),
message: format!(
"option `{path}` is AttrsetMerge but a \
definition value is not an attrset",
),
});
};
for (k, v) in obj {
acc.insert(k.clone(), v.clone());
}
}
Ok(NixValue::Object(acc))
}
MergeStrategy::Disjoint => {
if winners.len() > 1 {
return Err(SpecError::Interp {
phase: "merge-conflict".into(),
message: format!(
"option `{path}` is Disjoint (oneOf) but has \
{} top-priority definitions",
winners.len(),
),
});
}
Ok(winners[0].value.clone())
}
MergeStrategy::SubmoduleMerge => {
let mut synth_options: HashMap<String, OptionDecl> = HashMap::new();
let _ = (&mut synth_options, winners, path);
Err(SpecError::Interp {
phase: "submodule-misuse".into(),
message: format!(
"option `{path}` is submodule-typed but \
merge_definitions was called without the \
submodule context — eval_modules must dispatch \
submodule merges directly via merge_submodule",
),
})
}
MergeStrategy::Custom => {
Err(SpecError::Interp {
phase: "merge-unimplemented".into(),
message: format!(
"option `{path}` uses Custom merge strategy — \
M2.x will land the named-merge-function registry",
),
})
}
}
}
fn merge_submodule(
decl: &OptionDecl,
winners: &[&Definition],
path: &str,
option_types: &[OptionTypeSpec],
) -> Result<NixValue, SpecError> {
let Some(sub_template) = decl.submodule.as_deref() else {
return Err(SpecError::Interp {
phase: "submodule-missing-spec".into(),
message: format!(
"option `{path}` is submodule-typed but its OptionDecl \
has no `submodule` field set — declare the nested \
Module schema",
),
});
};
let mut synthetic: Vec<Module> = Vec::new();
for w in winners {
let Some(obj) = w.value.as_object() else {
return Err(SpecError::Interp {
phase: "type-check".into(),
message: format!(
"submodule option `{path}` definition must be an \
attrset, got `{}`",
value_type(&w.value),
),
});
};
let mut sub_defs: Vec<Definition> = Vec::new();
for (key, value) in obj {
sub_defs.push(Definition {
path: key.clone(),
value: value.clone(),
priority: w.priority,
cond: None,
});
}
synthetic.push(Module {
imports: Vec::new(),
options: sub_template.options.clone(),
config: sub_defs,
});
}
if !sub_template.config.is_empty() {
synthetic.push((**decl.submodule.as_ref().unwrap()).clone());
}
let sub_config = eval_modules(&synthetic, option_types)?;
let mut obj = serde_json::Map::new();
for (k, v) in sub_config {
obj.insert(k, v);
}
Ok(NixValue::Object(obj))
}
pub fn flatten_imports<F>(
roots: &[Module],
mut resolver: F,
) -> Result<Vec<Module>, SpecError>
where
F: FnMut(&str) -> Option<Module>,
{
let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut in_path: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut out: Vec<Module> = Vec::new();
fn visit<F>(
module: &Module,
name: Option<&str>,
resolver: &mut F,
seen: &mut std::collections::BTreeSet<String>,
in_path: &mut std::collections::BTreeSet<String>,
out: &mut Vec<Module>,
) -> Result<(), SpecError>
where F: FnMut(&str) -> Option<Module>,
{
for import_name in &module.imports {
if in_path.contains(import_name) {
return Err(SpecError::Interp {
phase: "imports-cycle".into(),
message: format!(
"import cycle detected involving `{import_name}`",
),
});
}
if !seen.insert(import_name.clone()) {
continue;
}
in_path.insert(import_name.clone());
let imported = resolver(import_name).ok_or_else(|| {
SpecError::Load(format!("import `{import_name}` not found"))
})?;
visit(&imported, Some(import_name), resolver, seen, in_path, out)?;
in_path.remove(import_name);
}
let _ = name;
out.push(module.clone());
Ok(())
}
for root in roots {
visit(root, None, &mut resolver, &mut seen, &mut in_path, &mut out)?;
}
Ok(out)
}
pub fn apply(
_algo: &ModuleEvalAlgorithm,
_args: EvalModulesArgs,
) -> Result<String, SpecError> {
Err(SpecError::Interp {
phase: "module-eval".into(),
message: "pipeline-driven apply() awaits the sui-eval Value \
bridge (M2.1). The M2.0 minimal interpreter is in \
eval_modules() — call that directly with typed \
Module values".into(),
})
}
pub struct EvalModulesArgs {
pub initial_modules: Vec<String>,
pub scratchpad: HashMap<String, String>,
}
impl EvalModulesArgs {
#[must_use]
pub fn new(initial_modules: Vec<String>) -> Self {
Self { initial_modules, scratchpad: HashMap::new() }
}
}
pub const CANONICAL_MODULE_SYSTEM_LISP: &str =
include_str!("../specs/module_system.lisp");
pub fn load_canonical() -> Result<CanonicalSpecs, SpecError> {
let algos = crate::loader::load_all::<ModuleEvalAlgorithm>(
CANONICAL_MODULE_SYSTEM_LISP,
)?;
let types = crate::loader::load_all::<OptionTypeSpec>(
CANONICAL_MODULE_SYSTEM_LISP,
)?;
let priorities = crate::loader::load_all::<PriorityRank>(
CANONICAL_MODULE_SYSTEM_LISP,
)?;
Ok(CanonicalSpecs { algos, types, priorities })
}
pub struct CanonicalSpecs {
pub algos: Vec<ModuleEvalAlgorithm>,
pub types: Vec<OptionTypeSpec>,
pub priorities: Vec<PriorityRank>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_specs_parse() {
let specs = load_canonical().expect("canonical specs must compile");
assert!(
specs.algos.iter().any(|a| a.name == "cppnix-module-eval"),
"canonical specs must contain `cppnix-module-eval` algorithm",
);
}
#[test]
fn algorithm_has_expected_phases() {
let specs = load_canonical().unwrap();
let cppnix = specs
.algos
.iter()
.find(|a| a.name == "cppnix-module-eval")
.expect("cppnix algorithm must exist");
let kinds: Vec<ModulePhaseKind> =
cppnix.phases.iter().map(|p| p.kind).collect();
for required in [
ModulePhaseKind::CollectModules,
ModulePhaseKind::ResolvePriorities,
ModulePhaseKind::MergePerOption,
ModulePhaseKind::TypeCheck,
ModulePhaseKind::EmitConfig,
] {
assert!(
kinds.contains(&required),
"cppnix-module-eval missing required phase: {required:?}",
);
}
}
#[test]
fn canonical_option_types_cover_core() {
let specs = load_canonical().unwrap();
let names: std::collections::HashSet<&str> =
specs.types.iter().map(|t| t.name.as_str()).collect();
for required in ["bool", "int", "str", "path", "listOf", "attrsOf", "submodule"] {
assert!(
names.contains(required),
"canonical option-type registry missing `{required}`",
);
}
}
#[test]
fn canonical_priorities_cover_core() {
let specs = load_canonical().unwrap();
let names: std::collections::HashSet<&str> =
specs.priorities.iter().map(|p| p.name.as_str()).collect();
for required in ["mkDefault", "normal", "mkForce", "mkOptionDefault"] {
assert!(
names.contains(required),
"canonical priority lattice missing `{required}`",
);
}
}
#[test]
fn pipeline_apply_still_typed_not_yet() {
let algo = ModuleEvalAlgorithm {
name: "test".into(),
phases: vec![],
};
let err = apply(&algo, EvalModulesArgs::new(vec![]))
.expect_err("pipeline apply must return error until M2.1");
match err {
SpecError::Interp { phase, message } => {
assert_eq!(phase, "module-eval");
assert!(message.contains("M2.1") || message.contains("eval_modules"));
}
_ => panic!("expected SpecError::Interp, got {err:?}"),
}
}
fn registry() -> Vec<OptionTypeSpec> {
load_canonical().unwrap().types
}
fn opt(type_name: &str) -> OptionDecl {
OptionDecl {
type_name: type_name.into(),
..Default::default()
}
}
fn opt_with_default(type_name: &str, default: NixValue) -> OptionDecl {
OptionDecl {
type_name: type_name.into(),
default: Some(default),
..Default::default()
}
}
fn opt_submodule(template: Module) -> OptionDecl {
OptionDecl {
type_name: "submodule".into(),
submodule: Some(Box::new(template)),
..Default::default()
}
}
fn def(path: &str, value: NixValue) -> Definition {
Definition { path: path.into(), value, priority: 100, cond: None }
}
fn def_if(path: &str, value: NixValue, cond: bool) -> Definition {
Definition { path: path.into(), value, priority: 100, cond: Some(cond) }
}
#[test]
fn eval_modules_trivial_bool_passes_through() {
let mut module = Module::default();
module.options.insert("foo".into(), opt("bool"));
module.config.push(def("foo", NixValue::Bool(true)));
let config = eval_modules(&[module], ®istry()).unwrap();
assert_eq!(config.get("foo"), Some(&NixValue::Bool(true)));
}
#[test]
fn eval_modules_default_when_no_definition() {
let mut module = Module::default();
module.options.insert(
"foo".into(),
opt_with_default("bool", NixValue::Bool(false)),
);
let config = eval_modules(&[module], ®istry()).unwrap();
assert_eq!(config.get("foo"), Some(&NixValue::Bool(false)));
}
#[test]
fn eval_modules_listof_concatenates() {
let mut m1 = Module::default();
m1.options.insert("xs".into(), opt("listOf"));
m1.config.push(def("xs", serde_json::json!([1, 2])));
let mut m2 = Module::default();
m2.config.push(def("xs", serde_json::json!([3, 4])));
let config = eval_modules(&[m1, m2], ®istry()).unwrap();
assert_eq!(config.get("xs"), Some(&serde_json::json!([1, 2, 3, 4])));
}
#[test]
fn eval_modules_attrsof_deep_merges() {
let mut m1 = Module::default();
m1.options.insert("attrs".into(), opt("attrsOf"));
m1.config.push(def("attrs", serde_json::json!({"a": 1})));
let mut m2 = Module::default();
m2.config.push(def("attrs", serde_json::json!({"b": 2})));
let config = eval_modules(&[m1, m2], ®istry()).unwrap();
assert_eq!(config.get("attrs"), Some(&serde_json::json!({"a": 1, "b": 2})));
}
#[test]
fn eval_modules_higher_priority_wins() {
let mut module = Module::default();
module.options.insert("foo".into(), opt("int"));
module.config.push(Definition {
path: "foo".into(),
value: NixValue::from(7),
priority: 1000, cond: None,
});
module.config.push(Definition {
path: "foo".into(),
value: NixValue::from(99),
priority: 50, cond: None,
});
let config = eval_modules(&[module], ®istry()).unwrap();
assert_eq!(config.get("foo"), Some(&NixValue::from(99)));
}
#[test]
fn eval_modules_type_check_rejects_wrong_type() {
let mut module = Module::default();
module.options.insert("foo".into(), opt("bool"));
module.config.push(def("foo", NixValue::from(42))); let err = eval_modules(&[module], ®istry()).unwrap_err();
match err {
SpecError::Interp { phase, message } => {
assert_eq!(phase, "type-check");
assert!(message.contains("foo"));
assert!(message.contains("bool"));
}
_ => panic!("expected type-check error"),
}
}
#[test]
fn eval_modules_rejects_unknown_option() {
let module = Module {
imports: vec![],
options: HashMap::new(),
config: vec![def("undeclared.path", NixValue::Bool(true))],
};
let err = eval_modules(&[module], ®istry()).unwrap_err();
match err {
SpecError::Interp { phase, message } => {
assert_eq!(phase, "unknown-option");
assert!(message.contains("undeclared.path"));
}
_ => panic!("expected unknown-option error"),
}
}
#[test]
fn eval_modules_rejects_unknown_type_name() {
let mut module = Module::default();
module.options.insert("foo".into(), opt("nonexistent-type"));
let err = eval_modules(&[module], ®istry()).unwrap_err();
match err {
SpecError::Interp { phase, message } => {
assert_eq!(phase, "unknown-type");
assert!(message.contains("nonexistent-type"));
}
_ => panic!("expected unknown-type error"),
}
}
#[test]
fn eval_modules_lastwins_tie_at_top_priority_with_identical_value_passes() {
let mut m1 = Module::default();
m1.options.insert("foo".into(), opt("str"));
m1.config.push(def("foo", NixValue::from("hello")));
let mut m2 = Module::default();
m2.config.push(def("foo", NixValue::from("hello")));
let config = eval_modules(&[m1, m2], ®istry()).unwrap();
assert_eq!(config.get("foo"), Some(&NixValue::from("hello")));
}
#[test]
fn eval_modules_lastwins_tie_at_top_priority_distinct_errors() {
let mut m1 = Module::default();
m1.options.insert("foo".into(), opt("str"));
m1.config.push(def("foo", NixValue::from("a")));
let mut m2 = Module::default();
m2.config.push(def("foo", NixValue::from("b")));
let err = eval_modules(&[m1, m2], ®istry()).unwrap_err();
match err {
SpecError::Interp { phase, .. } => assert_eq!(phase, "merge-conflict"),
_ => panic!("expected merge-conflict"),
}
}
#[test]
fn mkif_false_drops_definition() {
let mut module = Module::default();
module.options.insert(
"foo".into(),
opt_with_default("bool", NixValue::Bool(false)),
);
module.config.push(def_if("foo", NixValue::Bool(true), false));
let config = eval_modules(&[module], ®istry()).unwrap();
assert_eq!(config.get("foo"), Some(&NixValue::Bool(false)));
}
#[test]
fn mkif_true_keeps_definition() {
let mut module = Module::default();
module.options.insert(
"foo".into(),
opt_with_default("bool", NixValue::Bool(false)),
);
module.config.push(def_if("foo", NixValue::Bool(true), true));
let config = eval_modules(&[module], ®istry()).unwrap();
assert_eq!(config.get("foo"), Some(&NixValue::Bool(true)));
}
#[test]
fn mkif_filters_one_def_in_a_list_of_definitions() {
let mut module = Module::default();
module.options.insert("xs".into(), opt("listOf"));
module.config.push(def("xs", serde_json::json!([1, 2])));
module.config.push(def_if("xs", serde_json::json!([99]), false));
let config = eval_modules(&[module], ®istry()).unwrap();
assert_eq!(config.get("xs"), Some(&serde_json::json!([1, 2])));
}
#[test]
fn submodule_evaluates_nested_options() {
let mut sub_template = Module::default();
sub_template.options.insert(
"enable".into(),
opt_with_default("bool", NixValue::Bool(false)),
);
sub_template.options.insert(
"port".into(),
opt_with_default("int", NixValue::from(80)),
);
let mut outer = Module::default();
outer.options.insert("foo".into(), opt_submodule(sub_template));
outer.config.push(def(
"foo",
serde_json::json!({"enable": true, "port": 8080}),
));
let config = eval_modules(&[outer], ®istry()).unwrap();
let foo = config.get("foo").expect("foo must resolve");
assert_eq!(foo.get("enable"), Some(&NixValue::Bool(true)));
assert_eq!(foo.get("port"), Some(&NixValue::from(8080)));
}
#[test]
fn submodule_picks_up_inner_defaults_when_unset() {
let mut sub_template = Module::default();
sub_template.options.insert(
"enable".into(),
opt_with_default("bool", NixValue::Bool(false)),
);
sub_template.options.insert(
"port".into(),
opt_with_default("int", NixValue::from(80)),
);
let mut outer = Module::default();
outer.options.insert("foo".into(), opt_submodule(sub_template));
outer.config.push(def("foo", serde_json::json!({"enable": true})));
let config = eval_modules(&[outer], ®istry()).unwrap();
let foo = config.get("foo").unwrap();
assert_eq!(foo.get("enable"), Some(&NixValue::Bool(true)));
assert_eq!(foo.get("port"), Some(&NixValue::from(80)));
}
#[test]
fn submodule_type_checks_inner_values() {
let mut sub_template = Module::default();
sub_template.options.insert("enable".into(), opt("bool"));
let mut outer = Module::default();
outer.options.insert("foo".into(), opt_submodule(sub_template));
outer.config.push(def(
"foo",
serde_json::json!({"enable": 42}),
));
let err = eval_modules(&[outer], ®istry()).unwrap_err();
match err {
SpecError::Interp { phase, .. } => assert_eq!(phase, "type-check"),
_ => panic!("expected type-check error"),
}
}
#[test]
fn flatten_imports_resolves_one_level() {
let inner = Module {
imports: vec![],
options: {
let mut h = HashMap::new();
h.insert("nested".into(), opt("str"));
h
},
config: vec![],
};
let root = Module {
imports: vec!["inner".into()],
options: HashMap::new(),
config: vec![def("nested", NixValue::from("from-root"))],
};
let inner_for_resolve = inner.clone();
let flat = flatten_imports(&[root], move |name| {
if name == "inner" { Some(inner_for_resolve.clone()) } else { None }
})
.unwrap();
assert_eq!(flat.len(), 2);
assert!(flat[0].options.contains_key("nested"));
assert!(!flat[0].config.iter().any(|d| d.path == "nested"));
assert!(flat[1].config.iter().any(|d| d.path == "nested"));
let config = eval_modules(&flat, ®istry()).unwrap();
assert_eq!(config.get("nested"), Some(&NixValue::from("from-root")));
}
#[test]
fn flatten_imports_dedups_repeated() {
let common = Module::default();
let root_a = Module {
imports: vec!["common".into()],
options: HashMap::new(),
config: vec![],
};
let root_b = Module {
imports: vec!["common".into()],
options: HashMap::new(),
config: vec![],
};
let common_for_resolve = common.clone();
let flat = flatten_imports(&[root_a, root_b], move |name| {
if name == "common" { Some(common_for_resolve.clone()) } else { None }
})
.unwrap();
assert_eq!(flat.len(), 3);
}
#[test]
fn flatten_imports_detects_cycle() {
let a = Module { imports: vec!["b".into()], ..Default::default() };
let b = Module { imports: vec!["a".into()], ..Default::default() };
let a_for_resolve = a.clone();
let b_for_resolve = b.clone();
let err = flatten_imports(&[a.clone()], move |name| match name {
"a" => Some(a_for_resolve.clone()),
"b" => Some(b_for_resolve.clone()),
_ => None,
})
.unwrap_err();
match err {
SpecError::Interp { phase, .. } => assert_eq!(phase, "imports-cycle"),
_ => panic!("expected imports-cycle"),
}
}
#[test]
fn flatten_imports_unknown_name_errors() {
let root = Module {
imports: vec!["nope".into()],
options: HashMap::new(),
config: vec![],
};
let err = flatten_imports(&[root], |_| None).unwrap_err();
match err {
SpecError::Load(msg) => assert!(msg.contains("nope")),
_ => panic!("expected Load error for missing import"),
}
}
#[test]
fn priority_ordering_matches_cppnix_convention() {
let specs = load_canonical().unwrap();
let level = |n: &str| -> u32 {
specs.priorities.iter()
.find(|p| p.name == n)
.expect(n)
.level
};
assert!(level("mkForce") < level("normal"));
assert!(level("normal") < level("mkDefault"));
assert!(level("mkDefault") < level("mkOptionDefault"));
}
}