use serde::{Deserialize, Serialize};
use shikumi::{ConfigTier, TieredConfig};
pub const TIER_ENV: &str = "BLUE_TIER";
pub const CONFIG_ENV: &str = "BLUE_CONFIG";
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct BlueConfig {
pub solver_max_steps: usize,
pub max_expr_depth: usize,
}
impl Default for BlueConfig {
fn default() -> Self {
<Self as TieredConfig>::prescribed_default()
}
}
impl TieredConfig for BlueConfig {
fn bare() -> Self {
Self {
solver_max_steps: 0,
max_expr_depth: 0,
}
}
fn prescribed_default() -> Self {
Self {
solver_max_steps: blue_lang_pkg::DEFAULT_MAX_STEPS,
max_expr_depth: blue_lang_syntax::MAX_EXPR_DEPTH,
}
}
}
#[must_use]
pub fn resolve() -> BlueConfig {
BlueConfig::resolve_tier(tier_from_env())
}
fn tier_from_env() -> ConfigTier {
if std::env::var_os(TIER_ENV).is_some() {
return ConfigTier::from_env(TIER_ENV);
}
match std::env::var_os(CONFIG_ENV) {
Some(raw) => {
let path = std::path::PathBuf::from(raw);
if path.is_file() {
ConfigTier::Custom(path)
} else {
ConfigTier::Default
}
}
None => ConfigTier::Default,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_surface_is_exactly_two_knobs() {
let BlueConfig {
solver_max_steps: _,
max_expr_depth: _,
} = BlueConfig::prescribed_default();
}
#[test]
fn bare_is_zero_opinion() {
let b = BlueConfig::bare();
assert_eq!(b.solver_max_steps, 0);
assert_eq!(b.max_expr_depth, 0);
}
#[test]
fn prescribed_default_is_the_constants_themselves() {
let d = BlueConfig::prescribed_default();
assert_eq!(d.solver_max_steps, blue_lang_pkg::DEFAULT_MAX_STEPS);
assert_eq!(d.max_expr_depth, blue_lang_syntax::MAX_EXPR_DEPTH);
assert_eq!(d.solver_max_steps, 100_000, "the shipped value, pinned");
assert_eq!(d.max_expr_depth, 256, "the shipped value, pinned");
}
#[test]
fn bare_and_default_differ() {
assert_ne!(BlueConfig::bare(), BlueConfig::prescribed_default());
}
#[test]
fn default_trait_delegates_to_prescribed() {
assert_eq!(BlueConfig::default(), BlueConfig::prescribed_default());
}
#[test]
fn resolve_tier_dispatches_through_shikumi() {
assert_eq!(
BlueConfig::resolve_tier(ConfigTier::Bare),
BlueConfig::bare()
);
assert_eq!(
BlueConfig::resolve_tier(ConfigTier::Default),
BlueConfig::prescribed_default()
);
}
#[test]
fn the_progressive_fold_resolves_the_prescribed_tier() {
let resolved = BlueConfig::resolve_progressive();
assert_eq!(*resolved.value(), BlueConfig::prescribed_default());
}
#[test]
fn every_field_is_emitted_by_the_module_trio() {
let json =
serde_json::to_value(BlueConfig::prescribed_default()).expect("BlueConfig serializes");
let keys: Vec<String> = json
.as_object()
.expect("BlueConfig is a struct, so a JSON object")
.keys()
.cloned()
.collect();
assert_eq!(keys.len(), 2, "two knobs, per this module's docs");
let flake =
std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../../flake.nix"))
.expect("the workspace flake is two directories up from this crate");
for key in keys {
assert!(
flake.contains(&key),
"flake.nix must emit the `{key}` key in its shikumiDefaults — \
otherwise the deployed YAML sets nothing and blue silently \
runs on defaults"
);
}
}
#[test]
fn blue_config_env_matches_the_module_trio() {
assert_eq!(CONFIG_ENV, "BLUE_CONFIG");
assert_eq!(TIER_ENV, "BLUE_TIER");
}
#[test]
fn blue_dot_b_lowers_to_a_shape_shikumi_accepts() {
let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../../blue.b"))
.expect("blue.b sits at the workspace root");
let forms = blue_lang_syntax::parse_program(&src).expect("blue.b must parse");
assert_eq!(forms.len(), 1, "shikumi reads the FIRST form and only it");
let blue_lang_syntax::Sexp::List(items) = &forms[0] else {
panic!("a config form must be a list, got {:?}", forms[0]);
};
let start = match items.first() {
Some(blue_lang_syntax::Sexp::Atom(blue_lang_syntax::Atom::Symbol(s))) => {
assert_eq!(s, blue_lang_syntax::LOWERED_MAP);
1
}
other => panic!("expected a head symbol, got {other:?}"),
};
let rest = &items[start..];
assert!(!rest.is_empty(), "an empty body maps to an empty dict");
assert_eq!(rest.len() % 2, 0, "kwargs pair up");
let keys: Vec<&str> = rest
.iter()
.step_by(2)
.map(|s| match s {
blue_lang_syntax::Sexp::Atom(blue_lang_syntax::Atom::Keyword(k)) => k.as_str(),
other => panic!(
"every key must be a KEYWORD — a string key is `Atom::Str` and \
shikumi's kwargs test rejects it. Got {other:?}"
),
})
.collect();
assert_eq!(keys, vec!["solver_max_steps", "max_expr_depth"]);
let ints: Vec<i64> = rest
.iter()
.skip(1)
.step_by(2)
.map(|s| match s {
blue_lang_syntax::Sexp::Atom(blue_lang_syntax::Atom::Int(n)) => *n,
other => panic!("a bound must be an integer, got {other:?}"),
})
.collect();
let d = BlueConfig::prescribed_default();
assert_eq!(
ints,
vec![d.solver_max_steps as i64, d.max_expr_depth as i64],
"blue.b must state the shipped defaults"
);
}
#[test]
fn a_missing_config_file_falls_through_to_the_prescribed_tier() {
let missing = std::path::PathBuf::from("/nonexistent/blue/blue.yaml");
assert!(!missing.is_file(), "precondition");
let tier = if missing.is_file() {
ConfigTier::Custom(missing)
} else {
ConfigTier::Default
};
assert_eq!(
BlueConfig::resolve_tier(tier),
BlueConfig::prescribed_default()
);
}
}