1use std::collections::HashMap;
45
46use serde::{Deserialize, Serialize};
47use tatara_lisp::DeriveTataraDomain;
48
49use crate::SpecError;
50
51#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
56#[tatara(keyword = "defoption-type")]
57pub struct OptionTypeSpec {
58 pub name: String,
60 #[serde(rename = "mergeStrategy")]
63 pub merge_strategy: MergeStrategy,
64 #[serde(rename = "checkKind")]
66 pub check_kind: TypeCheckKind,
67 #[serde(default, rename = "elementType")]
70 pub element_type: Option<String>,
71 #[serde(default, rename = "memberTypes")]
73 pub member_types: Vec<String>,
74}
75
76#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
83pub enum MergeStrategy {
84 LastWins,
88 Concatenate,
91 SubmoduleMerge,
94 AttrsetMerge,
97 Disjoint,
100 Custom,
104 AnyLastWins,
107}
108
109#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
111pub enum TypeCheckKind {
112 Bool,
113 Int,
114 Str,
115 Path,
116 Null,
117 IntBetween,
119 Any,
121 Enum,
123 ListOf,
125 AttrsOf,
127 Submodule,
129 OneOf,
131 NullOr,
133 Attrs,
135 Package,
137 FunctionTo,
139}
140
141#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
148#[tatara(keyword = "defpriority")]
149pub struct PriorityRank {
150 pub name: String,
151 pub level: u32,
152 pub origin: PriorityOrigin,
153}
154
155#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
157pub enum PriorityOrigin {
158 MkDefault,
160 Normal,
162 MkOverride,
164 MkForce,
166 MkOptionDefault,
169}
170
171#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
178#[tatara(keyword = "defmodule-eval-algorithm")]
179pub struct ModuleEvalAlgorithm {
180 pub name: String,
181 pub phases: Vec<ModulePhase>,
182}
183
184#[derive(Serialize, Deserialize, Debug, Clone)]
188pub struct ModulePhase {
189 pub kind: ModulePhaseKind,
190 #[serde(default)]
191 pub bind: Option<String>,
192 #[serde(default)]
193 pub from: Option<String>,
194}
195
196#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
200pub enum ModulePhaseKind {
201 CollectModules,
205 PartitionOptionsAndConfig,
208 BuildOptionTree,
211 GroupDefinitions,
215 ResolveConditionals,
217 ResolvePriorities,
219 MergePerOption,
223 TypeCheck,
226 EvaluateRecursive,
229 EmitConfig,
231}
232
233pub type NixValue = serde_json::Value;
241
242#[derive(Debug, Clone, Default)]
245pub struct Module {
246 pub imports: Vec<String>,
249 pub options: HashMap<String, OptionDecl>,
252 pub config: Vec<Definition>,
255}
256
257#[derive(Debug, Clone)]
259pub struct OptionDecl {
260 pub type_name: String,
263 pub default: Option<NixValue>,
265 pub description: String,
267 pub submodule: Option<Box<Module>>,
272}
273
274impl Default for OptionDecl {
275 fn default() -> Self {
276 Self {
277 type_name: "any".into(),
278 default: None,
279 description: String::new(),
280 submodule: None,
281 }
282 }
283}
284
285#[derive(Debug, Clone)]
289pub struct Definition {
290 pub path: String,
292 pub value: NixValue,
294 pub priority: u32,
296 pub cond: Option<bool>,
304}
305
306pub type Config = HashMap<String, NixValue>;
308
309pub fn eval_modules(
330 modules: &[Module],
331 option_types: &[OptionTypeSpec],
332) -> Result<Config, SpecError> {
333 let mut option_tree: HashMap<String, OptionDecl> = HashMap::new();
339 for module in modules {
340 for (path, decl) in &module.options {
341 option_tree.entry(path.clone()).or_insert_with(|| decl.clone());
342 }
343 }
344
345 let mut by_path: HashMap<String, Vec<Definition>> = HashMap::new();
350 for module in modules {
351 for def in &module.config {
352 if def.cond == Some(false) {
353 continue;
354 }
355 by_path.entry(def.path.clone()).or_default().push(def.clone());
356 }
357 }
358
359 for path in by_path.keys() {
363 if !option_tree.contains_key(path) {
364 return Err(SpecError::Interp {
365 phase: "unknown-option".into(),
366 message: format!(
367 "definition for `{path}` but no module declares this option",
368 ),
369 });
370 }
371 }
372
373 let type_by_name: HashMap<&str, &OptionTypeSpec> =
374 option_types.iter().map(|t| (t.name.as_str(), t)).collect();
375
376 let mut config: Config = HashMap::new();
377
378 for (path, decl) in &option_tree {
379 let type_spec = type_by_name.get(decl.type_name.as_str()).ok_or_else(|| {
381 SpecError::Interp {
382 phase: "unknown-type".into(),
383 message: format!(
384 "option `{path}` declares type `{}` but no \
385 OptionTypeSpec with that name is in the registry",
386 decl.type_name,
387 ),
388 }
389 })?;
390
391 let mut defs = by_path.remove(path).unwrap_or_default();
397 if defs.is_empty() {
398 if let Some(default) = &decl.default {
400 config.insert(path.clone(), default.clone());
401 }
402 continue;
403 }
404 defs.sort_by_key(|d| d.priority);
405 let top_priority = defs[0].priority;
406 let winners: Vec<&Definition> =
407 defs.iter().filter(|d| d.priority == top_priority).collect();
408
409 for d in &winners {
411 check_value(type_spec, &d.value, &d.path)?;
412 }
413
414 let merged = if type_spec.merge_strategy == MergeStrategy::SubmoduleMerge {
419 merge_submodule(decl, &winners, path, option_types)?
420 } else {
421 merge_definitions(type_spec, &winners, path)?
422 };
423
424 config.insert(path.clone(), merged);
426 }
427
428 Ok(config)
429}
430
431fn check_value(
434 type_spec: &OptionTypeSpec,
435 value: &NixValue,
436 path: &str,
437) -> Result<(), SpecError> {
438 let ok = match type_spec.check_kind {
439 TypeCheckKind::Bool => value.is_boolean(),
440 TypeCheckKind::Int => value.is_i64() || value.is_u64(),
441 TypeCheckKind::Str => value.is_string(),
442 TypeCheckKind::Path => value.is_string(),
443 TypeCheckKind::Null => value.is_null(),
444 TypeCheckKind::Any => true,
445 TypeCheckKind::Attrs => value.is_object(),
446 TypeCheckKind::ListOf => value.is_array(),
447 TypeCheckKind::AttrsOf => value.is_object(),
448 TypeCheckKind::NullOr => true, TypeCheckKind::Submodule => value.is_object(),
450 TypeCheckKind::IntBetween | TypeCheckKind::Enum
452 | TypeCheckKind::OneOf | TypeCheckKind::Package
453 | TypeCheckKind::FunctionTo => true,
454 };
455 if !ok {
456 return Err(SpecError::Interp {
457 phase: "type-check".into(),
458 message: format!(
459 "option `{path}` declared `{}` but value is `{}`: {}",
460 type_spec.name,
461 value_type(value),
462 truncate_value(value),
463 ),
464 });
465 }
466 Ok(())
467}
468
469fn value_type(v: &NixValue) -> &'static str {
470 if v.is_boolean() { "bool" }
471 else if v.is_i64() || v.is_u64() { "int" }
472 else if v.is_f64() { "float" }
473 else if v.is_string() { "str" }
474 else if v.is_array() { "list" }
475 else if v.is_object() { "attrs" }
476 else if v.is_null() { "null" }
477 else { "unknown" }
478}
479
480fn truncate_value(v: &NixValue) -> String {
481 let s = v.to_string();
482 if s.len() <= 60 { s } else { format!("{}…", &s[..60]) }
483}
484
485fn merge_definitions(
488 type_spec: &OptionTypeSpec,
489 winners: &[&Definition],
490 path: &str,
491) -> Result<NixValue, SpecError> {
492 match type_spec.merge_strategy {
493 MergeStrategy::LastWins | MergeStrategy::AnyLastWins => {
494 if winners.len() > 1 {
498 let first = &winners[0].value;
499 let all_equal = winners.iter().all(|d| &d.value == first);
500 if !all_equal {
501 return Err(SpecError::Interp {
502 phase: "merge-conflict".into(),
503 message: format!(
504 "option `{path}` has {} distinct top-priority \
505 definitions; LastWins requires either one \
506 definition at the top or all equal",
507 winners.len(),
508 ),
509 });
510 }
511 }
512 Ok(winners[0].value.clone())
513 }
514 MergeStrategy::Concatenate => {
515 let mut acc: Vec<NixValue> = Vec::new();
519 for w in winners {
520 let Some(arr) = w.value.as_array() else {
521 return Err(SpecError::Interp {
522 phase: "type-check".into(),
523 message: format!(
524 "option `{path}` is Concatenate (listOf) \
525 but a definition value is not a list",
526 ),
527 });
528 };
529 for item in arr {
530 acc.push(item.clone());
531 }
532 }
533 Ok(NixValue::Array(acc))
534 }
535 MergeStrategy::AttrsetMerge => {
536 let mut acc = serde_json::Map::new();
538 for w in winners {
539 let Some(obj) = w.value.as_object() else {
540 return Err(SpecError::Interp {
541 phase: "type-check".into(),
542 message: format!(
543 "option `{path}` is AttrsetMerge but a \
544 definition value is not an attrset",
545 ),
546 });
547 };
548 for (k, v) in obj {
549 acc.insert(k.clone(), v.clone());
550 }
551 }
552 Ok(NixValue::Object(acc))
553 }
554 MergeStrategy::Disjoint => {
555 if winners.len() > 1 {
556 return Err(SpecError::Interp {
557 phase: "merge-conflict".into(),
558 message: format!(
559 "option `{path}` is Disjoint (oneOf) but has \
560 {} top-priority definitions",
561 winners.len(),
562 ),
563 });
564 }
565 Ok(winners[0].value.clone())
566 }
567 MergeStrategy::SubmoduleMerge => {
568 let mut synth_options: HashMap<String, OptionDecl> = HashMap::new();
580 let _ = (&mut synth_options, winners, path);
591 Err(SpecError::Interp {
592 phase: "submodule-misuse".into(),
593 message: format!(
594 "option `{path}` is submodule-typed but \
595 merge_definitions was called without the \
596 submodule context — eval_modules must dispatch \
597 submodule merges directly via merge_submodule",
598 ),
599 })
600 }
601 MergeStrategy::Custom => {
602 Err(SpecError::Interp {
603 phase: "merge-unimplemented".into(),
604 message: format!(
605 "option `{path}` uses Custom merge strategy — \
606 M2.x will land the named-merge-function registry",
607 ),
608 })
609 }
610 }
611}
612
613fn merge_submodule(
619 decl: &OptionDecl,
620 winners: &[&Definition],
621 path: &str,
622 option_types: &[OptionTypeSpec],
623) -> Result<NixValue, SpecError> {
624 let Some(sub_template) = decl.submodule.as_deref() else {
625 return Err(SpecError::Interp {
626 phase: "submodule-missing-spec".into(),
627 message: format!(
628 "option `{path}` is submodule-typed but its OptionDecl \
629 has no `submodule` field set — declare the nested \
630 Module schema",
631 ),
632 });
633 };
634 let mut synthetic: Vec<Module> = Vec::new();
638 for w in winners {
639 let Some(obj) = w.value.as_object() else {
640 return Err(SpecError::Interp {
641 phase: "type-check".into(),
642 message: format!(
643 "submodule option `{path}` definition must be an \
644 attrset, got `{}`",
645 value_type(&w.value),
646 ),
647 });
648 };
649 let mut sub_defs: Vec<Definition> = Vec::new();
650 for (key, value) in obj {
651 sub_defs.push(Definition {
652 path: key.clone(),
653 value: value.clone(),
654 priority: w.priority,
655 cond: None,
656 });
657 }
658 synthetic.push(Module {
659 imports: Vec::new(),
660 options: sub_template.options.clone(),
661 config: sub_defs,
662 });
663 }
664 if !sub_template.config.is_empty() {
667 synthetic.push((**decl.submodule.as_ref().unwrap()).clone());
668 }
669 let sub_config = eval_modules(&synthetic, option_types)?;
670 let mut obj = serde_json::Map::new();
672 for (k, v) in sub_config {
673 obj.insert(k, v);
674 }
675 Ok(NixValue::Object(obj))
676}
677
678pub fn flatten_imports<F>(
694 roots: &[Module],
695 mut resolver: F,
696) -> Result<Vec<Module>, SpecError>
697where
698 F: FnMut(&str) -> Option<Module>,
699{
700 let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
701 let mut in_path: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
702 let mut out: Vec<Module> = Vec::new();
703
704 fn visit<F>(
705 module: &Module,
706 name: Option<&str>,
707 resolver: &mut F,
708 seen: &mut std::collections::BTreeSet<String>,
709 in_path: &mut std::collections::BTreeSet<String>,
710 out: &mut Vec<Module>,
711 ) -> Result<(), SpecError>
712 where F: FnMut(&str) -> Option<Module>,
713 {
714 for import_name in &module.imports {
721 if in_path.contains(import_name) {
722 return Err(SpecError::Interp {
723 phase: "imports-cycle".into(),
724 message: format!(
725 "import cycle detected involving `{import_name}`",
726 ),
727 });
728 }
729 if !seen.insert(import_name.clone()) {
730 continue;
731 }
732 in_path.insert(import_name.clone());
733 let imported = resolver(import_name).ok_or_else(|| {
734 SpecError::Load(format!("import `{import_name}` not found"))
735 })?;
736 visit(&imported, Some(import_name), resolver, seen, in_path, out)?;
737 in_path.remove(import_name);
738 }
739 let _ = name;
743 out.push(module.clone());
744 Ok(())
745 }
746
747 for root in roots {
748 visit(root, None, &mut resolver, &mut seen, &mut in_path, &mut out)?;
749 }
750 Ok(out)
751}
752
753pub fn apply(
765 _algo: &ModuleEvalAlgorithm,
766 _args: EvalModulesArgs,
767) -> Result<String, SpecError> {
768 Err(SpecError::Interp {
769 phase: "module-eval".into(),
770 message: "pipeline-driven apply() awaits the sui-eval Value \
771 bridge (M2.1). The M2.0 minimal interpreter is in \
772 eval_modules() — call that directly with typed \
773 Module values".into(),
774 })
775}
776
777pub struct EvalModulesArgs {
781 pub initial_modules: Vec<String>,
782 pub scratchpad: HashMap<String, String>,
783}
784
785impl EvalModulesArgs {
786 #[must_use]
787 pub fn new(initial_modules: Vec<String>) -> Self {
788 Self { initial_modules, scratchpad: HashMap::new() }
789 }
790}
791
792pub const CANONICAL_MODULE_SYSTEM_LISP: &str =
795 include_str!("../specs/module_system.lisp");
796
797pub fn load_canonical() -> Result<CanonicalSpecs, SpecError> {
806 let algos = crate::loader::load_all::<ModuleEvalAlgorithm>(
807 CANONICAL_MODULE_SYSTEM_LISP,
808 )?;
809 let types = crate::loader::load_all::<OptionTypeSpec>(
810 CANONICAL_MODULE_SYSTEM_LISP,
811 )?;
812 let priorities = crate::loader::load_all::<PriorityRank>(
813 CANONICAL_MODULE_SYSTEM_LISP,
814 )?;
815 Ok(CanonicalSpecs { algos, types, priorities })
816}
817
818pub struct CanonicalSpecs {
820 pub algos: Vec<ModuleEvalAlgorithm>,
821 pub types: Vec<OptionTypeSpec>,
822 pub priorities: Vec<PriorityRank>,
823}
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828
829 #[test]
830 fn canonical_specs_parse() {
831 let specs = load_canonical().expect("canonical specs must compile");
832 assert!(
837 specs.algos.iter().any(|a| a.name == "cppnix-module-eval"),
838 "canonical specs must contain `cppnix-module-eval` algorithm",
839 );
840 }
841
842 #[test]
843 fn algorithm_has_expected_phases() {
844 let specs = load_canonical().unwrap();
845 let cppnix = specs
846 .algos
847 .iter()
848 .find(|a| a.name == "cppnix-module-eval")
849 .expect("cppnix algorithm must exist");
850 let kinds: Vec<ModulePhaseKind> =
851 cppnix.phases.iter().map(|p| p.kind).collect();
852 for required in [
853 ModulePhaseKind::CollectModules,
854 ModulePhaseKind::ResolvePriorities,
855 ModulePhaseKind::MergePerOption,
856 ModulePhaseKind::TypeCheck,
857 ModulePhaseKind::EmitConfig,
858 ] {
859 assert!(
860 kinds.contains(&required),
861 "cppnix-module-eval missing required phase: {required:?}",
862 );
863 }
864 }
865
866 #[test]
867 fn canonical_option_types_cover_core() {
868 let specs = load_canonical().unwrap();
869 let names: std::collections::HashSet<&str> =
870 specs.types.iter().map(|t| t.name.as_str()).collect();
871 for required in ["bool", "int", "str", "path", "listOf", "attrsOf", "submodule"] {
875 assert!(
876 names.contains(required),
877 "canonical option-type registry missing `{required}`",
878 );
879 }
880 }
881
882 #[test]
883 fn canonical_priorities_cover_core() {
884 let specs = load_canonical().unwrap();
885 let names: std::collections::HashSet<&str> =
886 specs.priorities.iter().map(|p| p.name.as_str()).collect();
887 for required in ["mkDefault", "normal", "mkForce", "mkOptionDefault"] {
888 assert!(
889 names.contains(required),
890 "canonical priority lattice missing `{required}`",
891 );
892 }
893 }
894
895 #[test]
896 fn pipeline_apply_still_typed_not_yet() {
897 let algo = ModuleEvalAlgorithm {
901 name: "test".into(),
902 phases: vec![],
903 };
904 let err = apply(&algo, EvalModulesArgs::new(vec![]))
905 .expect_err("pipeline apply must return error until M2.1");
906 match err {
907 SpecError::Interp { phase, message } => {
908 assert_eq!(phase, "module-eval");
909 assert!(message.contains("M2.1") || message.contains("eval_modules"));
910 }
911 _ => panic!("expected SpecError::Interp, got {err:?}"),
912 }
913 }
914
915 fn registry() -> Vec<OptionTypeSpec> {
918 load_canonical().unwrap().types
919 }
920
921 fn opt(type_name: &str) -> OptionDecl {
922 OptionDecl {
923 type_name: type_name.into(),
924 ..Default::default()
925 }
926 }
927
928 fn opt_with_default(type_name: &str, default: NixValue) -> OptionDecl {
929 OptionDecl {
930 type_name: type_name.into(),
931 default: Some(default),
932 ..Default::default()
933 }
934 }
935
936 fn opt_submodule(template: Module) -> OptionDecl {
937 OptionDecl {
938 type_name: "submodule".into(),
939 submodule: Some(Box::new(template)),
940 ..Default::default()
941 }
942 }
943
944 fn def(path: &str, value: NixValue) -> Definition {
945 Definition { path: path.into(), value, priority: 100, cond: None }
946 }
947
948 fn def_if(path: &str, value: NixValue, cond: bool) -> Definition {
949 Definition { path: path.into(), value, priority: 100, cond: Some(cond) }
950 }
951
952 #[test]
953 fn eval_modules_trivial_bool_passes_through() {
954 let mut module = Module::default();
955 module.options.insert("foo".into(), opt("bool"));
956 module.config.push(def("foo", NixValue::Bool(true)));
957 let config = eval_modules(&[module], ®istry()).unwrap();
958 assert_eq!(config.get("foo"), Some(&NixValue::Bool(true)));
959 }
960
961 #[test]
962 fn eval_modules_default_when_no_definition() {
963 let mut module = Module::default();
964 module.options.insert(
965 "foo".into(),
966 opt_with_default("bool", NixValue::Bool(false)),
967 );
968 let config = eval_modules(&[module], ®istry()).unwrap();
969 assert_eq!(config.get("foo"), Some(&NixValue::Bool(false)));
970 }
971
972 #[test]
973 fn eval_modules_listof_concatenates() {
974 let mut m1 = Module::default();
975 m1.options.insert("xs".into(), opt("listOf"));
976 m1.config.push(def("xs", serde_json::json!([1, 2])));
977 let mut m2 = Module::default();
978 m2.config.push(def("xs", serde_json::json!([3, 4])));
979 let config = eval_modules(&[m1, m2], ®istry()).unwrap();
980 assert_eq!(config.get("xs"), Some(&serde_json::json!([1, 2, 3, 4])));
981 }
982
983 #[test]
984 fn eval_modules_attrsof_deep_merges() {
985 let mut m1 = Module::default();
986 m1.options.insert("attrs".into(), opt("attrsOf"));
987 m1.config.push(def("attrs", serde_json::json!({"a": 1})));
988 let mut m2 = Module::default();
989 m2.config.push(def("attrs", serde_json::json!({"b": 2})));
990 let config = eval_modules(&[m1, m2], ®istry()).unwrap();
991 assert_eq!(config.get("attrs"), Some(&serde_json::json!({"a": 1, "b": 2})));
992 }
993
994 #[test]
995 fn eval_modules_higher_priority_wins() {
996 let mut module = Module::default();
997 module.options.insert("foo".into(), opt("int"));
998 module.config.push(Definition {
999 path: "foo".into(),
1000 value: NixValue::from(7),
1001 priority: 1000, cond: None,
1003 });
1004 module.config.push(Definition {
1005 path: "foo".into(),
1006 value: NixValue::from(99),
1007 priority: 50, cond: None,
1009 });
1010 let config = eval_modules(&[module], ®istry()).unwrap();
1011 assert_eq!(config.get("foo"), Some(&NixValue::from(99)));
1012 }
1013
1014 #[test]
1015 fn eval_modules_type_check_rejects_wrong_type() {
1016 let mut module = Module::default();
1017 module.options.insert("foo".into(), opt("bool"));
1018 module.config.push(def("foo", NixValue::from(42))); let err = eval_modules(&[module], ®istry()).unwrap_err();
1020 match err {
1021 SpecError::Interp { phase, message } => {
1022 assert_eq!(phase, "type-check");
1023 assert!(message.contains("foo"));
1024 assert!(message.contains("bool"));
1025 }
1026 _ => panic!("expected type-check error"),
1027 }
1028 }
1029
1030 #[test]
1031 fn eval_modules_rejects_unknown_option() {
1032 let module = Module {
1033 imports: vec![],
1034 options: HashMap::new(),
1035 config: vec![def("undeclared.path", NixValue::Bool(true))],
1036 };
1037 let err = eval_modules(&[module], ®istry()).unwrap_err();
1038 match err {
1039 SpecError::Interp { phase, message } => {
1040 assert_eq!(phase, "unknown-option");
1041 assert!(message.contains("undeclared.path"));
1042 }
1043 _ => panic!("expected unknown-option error"),
1044 }
1045 }
1046
1047 #[test]
1048 fn eval_modules_rejects_unknown_type_name() {
1049 let mut module = Module::default();
1050 module.options.insert("foo".into(), opt("nonexistent-type"));
1051 let err = eval_modules(&[module], ®istry()).unwrap_err();
1052 match err {
1053 SpecError::Interp { phase, message } => {
1054 assert_eq!(phase, "unknown-type");
1055 assert!(message.contains("nonexistent-type"));
1056 }
1057 _ => panic!("expected unknown-type error"),
1058 }
1059 }
1060
1061 #[test]
1062 fn eval_modules_lastwins_tie_at_top_priority_with_identical_value_passes() {
1063 let mut m1 = Module::default();
1064 m1.options.insert("foo".into(), opt("str"));
1065 m1.config.push(def("foo", NixValue::from("hello")));
1066 let mut m2 = Module::default();
1067 m2.config.push(def("foo", NixValue::from("hello")));
1068 let config = eval_modules(&[m1, m2], ®istry()).unwrap();
1069 assert_eq!(config.get("foo"), Some(&NixValue::from("hello")));
1070 }
1071
1072 #[test]
1073 fn eval_modules_lastwins_tie_at_top_priority_distinct_errors() {
1074 let mut m1 = Module::default();
1075 m1.options.insert("foo".into(), opt("str"));
1076 m1.config.push(def("foo", NixValue::from("a")));
1077 let mut m2 = Module::default();
1078 m2.config.push(def("foo", NixValue::from("b")));
1079 let err = eval_modules(&[m1, m2], ®istry()).unwrap_err();
1080 match err {
1081 SpecError::Interp { phase, .. } => assert_eq!(phase, "merge-conflict"),
1082 _ => panic!("expected merge-conflict"),
1083 }
1084 }
1085
1086 #[test]
1089 fn mkif_false_drops_definition() {
1090 let mut module = Module::default();
1091 module.options.insert(
1092 "foo".into(),
1093 opt_with_default("bool", NixValue::Bool(false)),
1094 );
1095 module.config.push(def_if("foo", NixValue::Bool(true), false));
1096 let config = eval_modules(&[module], ®istry()).unwrap();
1097 assert_eq!(config.get("foo"), Some(&NixValue::Bool(false)));
1099 }
1100
1101 #[test]
1102 fn mkif_true_keeps_definition() {
1103 let mut module = Module::default();
1104 module.options.insert(
1105 "foo".into(),
1106 opt_with_default("bool", NixValue::Bool(false)),
1107 );
1108 module.config.push(def_if("foo", NixValue::Bool(true), true));
1109 let config = eval_modules(&[module], ®istry()).unwrap();
1110 assert_eq!(config.get("foo"), Some(&NixValue::Bool(true)));
1111 }
1112
1113 #[test]
1114 fn mkif_filters_one_def_in_a_list_of_definitions() {
1115 let mut module = Module::default();
1118 module.options.insert("xs".into(), opt("listOf"));
1119 module.config.push(def("xs", serde_json::json!([1, 2])));
1120 module.config.push(def_if("xs", serde_json::json!([99]), false));
1121 let config = eval_modules(&[module], ®istry()).unwrap();
1122 assert_eq!(config.get("xs"), Some(&serde_json::json!([1, 2])));
1124 }
1125
1126 #[test]
1127 fn submodule_evaluates_nested_options() {
1128 let mut sub_template = Module::default();
1132 sub_template.options.insert(
1133 "enable".into(),
1134 opt_with_default("bool", NixValue::Bool(false)),
1135 );
1136 sub_template.options.insert(
1137 "port".into(),
1138 opt_with_default("int", NixValue::from(80)),
1139 );
1140
1141 let mut outer = Module::default();
1143 outer.options.insert("foo".into(), opt_submodule(sub_template));
1144 outer.config.push(def(
1145 "foo",
1146 serde_json::json!({"enable": true, "port": 8080}),
1147 ));
1148
1149 let config = eval_modules(&[outer], ®istry()).unwrap();
1150 let foo = config.get("foo").expect("foo must resolve");
1151 assert_eq!(foo.get("enable"), Some(&NixValue::Bool(true)));
1152 assert_eq!(foo.get("port"), Some(&NixValue::from(8080)));
1153 }
1154
1155 #[test]
1156 fn submodule_picks_up_inner_defaults_when_unset() {
1157 let mut sub_template = Module::default();
1158 sub_template.options.insert(
1159 "enable".into(),
1160 opt_with_default("bool", NixValue::Bool(false)),
1161 );
1162 sub_template.options.insert(
1163 "port".into(),
1164 opt_with_default("int", NixValue::from(80)),
1165 );
1166
1167 let mut outer = Module::default();
1168 outer.options.insert("foo".into(), opt_submodule(sub_template));
1169 outer.config.push(def("foo", serde_json::json!({"enable": true})));
1171
1172 let config = eval_modules(&[outer], ®istry()).unwrap();
1173 let foo = config.get("foo").unwrap();
1174 assert_eq!(foo.get("enable"), Some(&NixValue::Bool(true)));
1175 assert_eq!(foo.get("port"), Some(&NixValue::from(80)));
1176 }
1177
1178 #[test]
1179 fn submodule_type_checks_inner_values() {
1180 let mut sub_template = Module::default();
1181 sub_template.options.insert("enable".into(), opt("bool"));
1182
1183 let mut outer = Module::default();
1184 outer.options.insert("foo".into(), opt_submodule(sub_template));
1185 outer.config.push(def(
1188 "foo",
1189 serde_json::json!({"enable": 42}),
1190 ));
1191
1192 let err = eval_modules(&[outer], ®istry()).unwrap_err();
1193 match err {
1194 SpecError::Interp { phase, .. } => assert_eq!(phase, "type-check"),
1195 _ => panic!("expected type-check error"),
1196 }
1197 }
1198
1199 #[test]
1200 fn flatten_imports_resolves_one_level() {
1201 let inner = Module {
1202 imports: vec![],
1203 options: {
1204 let mut h = HashMap::new();
1205 h.insert("nested".into(), opt("str"));
1206 h
1207 },
1208 config: vec![],
1209 };
1210 let root = Module {
1211 imports: vec!["inner".into()],
1212 options: HashMap::new(),
1213 config: vec![def("nested", NixValue::from("from-root"))],
1214 };
1215 let inner_for_resolve = inner.clone();
1216 let flat = flatten_imports(&[root], move |name| {
1217 if name == "inner" { Some(inner_for_resolve.clone()) } else { None }
1218 })
1219 .unwrap();
1220 assert_eq!(flat.len(), 2);
1222 assert!(flat[0].options.contains_key("nested"));
1223 assert!(!flat[0].config.iter().any(|d| d.path == "nested"));
1224 assert!(flat[1].config.iter().any(|d| d.path == "nested"));
1225 let config = eval_modules(&flat, ®istry()).unwrap();
1227 assert_eq!(config.get("nested"), Some(&NixValue::from("from-root")));
1228 }
1229
1230 #[test]
1231 fn flatten_imports_dedups_repeated() {
1232 let common = Module::default();
1234 let root_a = Module {
1235 imports: vec!["common".into()],
1236 options: HashMap::new(),
1237 config: vec![],
1238 };
1239 let root_b = Module {
1240 imports: vec!["common".into()],
1241 options: HashMap::new(),
1242 config: vec![],
1243 };
1244 let common_for_resolve = common.clone();
1245 let flat = flatten_imports(&[root_a, root_b], move |name| {
1246 if name == "common" { Some(common_for_resolve.clone()) } else { None }
1247 })
1248 .unwrap();
1249 assert_eq!(flat.len(), 3);
1252 }
1253
1254 #[test]
1255 fn flatten_imports_detects_cycle() {
1256 let a = Module { imports: vec!["b".into()], ..Default::default() };
1258 let b = Module { imports: vec!["a".into()], ..Default::default() };
1259 let a_for_resolve = a.clone();
1260 let b_for_resolve = b.clone();
1261 let err = flatten_imports(&[a.clone()], move |name| match name {
1262 "a" => Some(a_for_resolve.clone()),
1263 "b" => Some(b_for_resolve.clone()),
1264 _ => None,
1265 })
1266 .unwrap_err();
1267 match err {
1268 SpecError::Interp { phase, .. } => assert_eq!(phase, "imports-cycle"),
1269 _ => panic!("expected imports-cycle"),
1270 }
1271 }
1272
1273 #[test]
1274 fn flatten_imports_unknown_name_errors() {
1275 let root = Module {
1276 imports: vec!["nope".into()],
1277 options: HashMap::new(),
1278 config: vec![],
1279 };
1280 let err = flatten_imports(&[root], |_| None).unwrap_err();
1281 match err {
1282 SpecError::Load(msg) => assert!(msg.contains("nope")),
1283 _ => panic!("expected Load error for missing import"),
1284 }
1285 }
1286
1287 #[test]
1288 fn priority_ordering_matches_cppnix_convention() {
1289 let specs = load_canonical().unwrap();
1290 let level = |n: &str| -> u32 {
1291 specs.priorities.iter()
1292 .find(|p| p.name == n)
1293 .expect(n)
1294 .level
1295 };
1296 assert!(level("mkForce") < level("normal"));
1298 assert!(level("normal") < level("mkDefault"));
1299 assert!(level("mkDefault") < level("mkOptionDefault"));
1300 }
1301}