use std::path::{Path, PathBuf};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use v_utils_macros::{Settings, SettingsNested};
#[derive(Clone, Debug, Default, Deserialize, JsonSchema, Serialize, SettingsNested)]
#[allow(unused)]
struct Logging {
#[serde(default)]
level: String,
#[serde(default)]
file: Option<String>,
#[serde(default)]
tags: Vec<String>,
}
#[derive(Clone, Debug, Default, JsonSchema, Settings, v_utils_macros::MyConfigPrimitives)]
#[allow(unused)]
struct ModuleConfig {
#[serde(default)]
host: String,
#[serde(default)]
port: u16,
#[settings(flatten)]
#[serde(default)]
logging: Logging,
}
fn emit_module() -> (PathBuf, String) {
let tmp = tempfile::tempdir().unwrap();
unsafe {
std::env::set_var("XDG_CONFIG_HOME", tmp.path());
}
let path = ModuleConfig::write_module().expect("JsonSchema is derived, so this must succeed");
let contents = std::fs::read_to_string(&path).unwrap();
std::mem::forget(tmp);
(path, contents)
}
#[test]
fn emits_expected_option_types() {
let (path, m) = emit_module();
assert_eq!(path.extension().and_then(|e| e.to_str()), Some("nix"));
assert!(m.contains("{ lib, ... }:"), "module must be a lib-taking function:\n{m}");
assert!(m.contains("options ="), "module must declare an options set:\n{m}");
assert!(m.contains("host = lib.mkOption { type = lib.types.str;"), "host should be a str option:\n{m}");
assert!(m.contains("port = lib.mkOption { type = lib.types.int;"), "port (u16) should map to types.int:\n{m}");
assert!(m.contains("logging = lib.mkOption { type = lib.types.submodule"), "logging should be a submodule:\n{m}");
assert!(m.contains("lib.types.nullOr lib.types.str"), "optional file should be nullOr str:\n{m}");
assert!(m.contains("default = null;"), "optional field should carry `default = null;`:\n{m}");
assert!(m.contains("lib.types.listOf lib.types.str"), "tags should be listOf str:\n{m}");
}
fn nix_available() -> bool {
std::process::Command::new("nix")
.args(["eval", "--impure", "--expr", "(import <nixpkgs> {}).lib.types.int.name"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn eval_check(module_path: &Path, config_body: &str) -> bool {
let expr = format!(
r#"let pkgs = import <nixpkgs> {{}}; cfg = (pkgs.lib.evalModules {{ modules = [ {module} ({{ ... }}: {{ {body} }}) ]; }}).config; in builtins.deepSeq cfg true"#,
module = module_path.display(),
body = config_body,
);
std::process::Command::new("nix")
.args(["eval", "--impure", "--expr", &expr])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[test]
fn nix_evalmodules_typechecks_config() {
if !nix_available() {
eprintln!("skipping nix evalModules test: nix/<nixpkgs> unavailable");
return;
}
let (path, _) = emit_module();
assert!(
eval_check(&path, r#"host = "localhost"; port = 8080; logging = { level = "info"; tags = [ "a" ]; };"#),
"a correctly-typed config should pass evalModules"
);
assert!(
!eval_check(&path, r#"host = "localhost"; port = "not-an-int"; logging = { level = "info"; tags = [ ]; };"#),
"a string `port` should fail evalModules type checking"
);
}