use crate::feature::{FeatureMatrix, FeatureSet};
use anyhow::{Result, anyhow};
use figment::{
Error, Figment, Metadata, Profile, Provider,
value::{Dict, Map},
};
use getset::{Getters, MutGetters, Setters};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Getters, MutGetters, Serialize, Setters)]
#[getset(get = "pub(crate)")]
pub(crate) struct Config {
#[getset(get_mut = "pub(crate)")]
channel: Vec<Channel>,
#[getset(skip)]
#[serde(rename = "skip-package", default)]
skip_package: Option<bool>,
}
impl Config {
pub(crate) fn from<T: Provider>(provider: T) -> Result<Self> {
Ok(Figment::from(provider).extract()?)
}
pub(crate) fn seed(&self, channel: &str) -> Result<Option<FeatureSet>> {
if let Some(seed) = self
.get_channel(channel)
.or_else(|_| self.get_default())?
.seed()
{
Ok(Some(seed.clone()))
} else {
Ok(self.get_default()?.seed().clone())
}
}
pub(crate) fn always_include(&self, channel: &str) -> Result<FeatureSet> {
if let Some(always_include) = self
.get_channel(channel)
.or_else(|_| self.get_default())?
.always_include()
{
Ok(always_include.clone())
} else {
Ok(self
.get_default()?
.always_include()
.clone()
.unwrap_or_default())
}
}
pub(crate) fn always_deny(&self, channel: &str) -> Result<FeatureSet> {
if let Some(always_deny) = self
.get_channel(channel)
.or_else(|_| self.get_default())?
.always_deny()
{
Ok(always_deny.clone())
} else {
Ok(self
.get_default()?
.always_deny()
.clone()
.unwrap_or_default())
}
}
pub(crate) fn skip(&self, channel: &str) -> Result<FeatureMatrix> {
if let Some(skip) = self
.get_channel(channel)
.or_else(|_| self.get_default())?
.skip()
{
Ok(skip.clone())
} else {
Ok(self.get_default()?.skip().clone().unwrap_or_default())
}
}
pub(crate) fn mutually_exclusive(&self, channel: &str) -> Result<FeatureMatrix> {
if let Some(mutually_exclusive) = self
.get_channel(channel)
.or_else(|_| self.get_default())?
.mutually_exclusive()
{
Ok(mutually_exclusive.clone())
} else {
Ok(self
.get_default()?
.mutually_exclusive()
.clone()
.unwrap_or_default())
}
}
pub(crate) fn include_hidden(&self, channel: &str) -> Result<bool> {
if let Some(include_hidden) = self
.get_channel(channel)
.or_else(|_| self.get_default())?
.include_hidden()
{
Ok(*include_hidden)
} else {
Ok(self.get_default()?.include_hidden().unwrap_or_default())
}
}
pub(crate) fn include_all_optional(&self, channel: &str) -> Result<bool> {
if let Some(include_all_optional) = self
.get_channel(channel)
.or_else(|_| self.get_default())?
.include_all_optional()
{
Ok(*include_all_optional)
} else {
Ok(self
.get_default()?
.include_all_optional()
.unwrap_or_default())
}
}
pub(crate) fn include_optional(&self, channel: &str) -> Result<FeatureSet> {
if let Some(include_optional) = self
.get_channel(channel)
.or_else(|_| self.get_default())?
.include_optional()
{
Ok(include_optional.clone())
} else {
Ok(self
.get_default()?
.include_optional()
.clone()
.unwrap_or_default())
}
}
pub(crate) fn skip_package(&self) -> bool {
self.skip_package.unwrap_or(false)
}
fn get_default(&self) -> Result<&'_ Channel> {
self.get_channel("default")
}
fn get_channel(&self, channel: &str) -> Result<&'_ Channel> {
self.channel
.iter()
.find(|c| c.name() == channel)
.ok_or_else(|| anyhow!(format!("channel '{channel}' not defined")))
}
}
impl Default for Config {
fn default() -> Self {
Self {
channel: vec![Channel {
name: "default".to_string(),
..Default::default()
}],
skip_package: None,
}
}
}
impl Provider for Config {
fn metadata(&self) -> Metadata {
Metadata::named("config")
}
fn data(&self) -> Result<Map<Profile, Dict>, Error> {
figment::providers::Serialized::defaults(self).data()
}
}
#[derive(Clone, Debug, Default, Deserialize, Getters, Serialize)]
#[getset(get = "pub(crate)")]
pub(crate) struct Channel {
name: String,
seed: Option<FeatureSet>,
always_include: Option<FeatureSet>,
always_deny: Option<FeatureSet>,
skip: Option<FeatureMatrix>,
mutually_exclusive: Option<FeatureMatrix>,
include_hidden: Option<bool>,
include_all_optional: Option<bool>,
include_optional: Option<FeatureSet>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::feature::Feature;
use figment::providers::{Format, Json};
fn make_config(json: &str) -> Config {
let figment = Figment::from(Config::default()).merge(Figment::from(Json::string(json)));
Config::from(figment).unwrap()
}
fn feature_set(features: &[&str]) -> FeatureSet {
features.iter().map(|&s| Feature::from(s)).collect()
}
#[test]
fn mutually_exclusive_returns_empty_when_not_set() {
let config = Config::from(Figment::from(Config::default())).unwrap();
let result = config.mutually_exclusive("default").unwrap();
assert!(result.is_empty());
}
#[test]
fn mutually_exclusive_returns_configured_groups() {
let config = make_config(
r#"{
"channel": [{
"name": "default",
"mutually_exclusive": [["feat-a", "feat-b"], ["feat-x", "feat-y"]]
}]
}"#,
);
let result = config.mutually_exclusive("default").unwrap();
assert_eq!(result.len(), 2);
assert!(result.contains(&feature_set(&["feat-a", "feat-b"])));
assert!(result.contains(&feature_set(&["feat-x", "feat-y"])));
}
#[test]
fn mutually_exclusive_falls_back_to_default_channel() {
let config = make_config(
r#"{
"channel": [
{
"name": "default",
"mutually_exclusive": [["feat-a", "feat-b"]]
},
{
"name": "nightly"
}
]
}"#,
);
let result = config.mutually_exclusive("nightly").unwrap();
assert_eq!(result.len(), 1);
assert!(result.contains(&feature_set(&["feat-a", "feat-b"])));
}
#[test]
fn mutually_exclusive_named_channel_overrides_default() {
let config = make_config(
r#"{
"channel": [
{
"name": "default",
"mutually_exclusive": [["feat-a", "feat-b"]]
},
{
"name": "nightly",
"mutually_exclusive": [["feat-x", "feat-y"]]
}
]
}"#,
);
let result = config.mutually_exclusive("nightly").unwrap();
assert_eq!(result.len(), 1);
assert!(result.contains(&feature_set(&["feat-x", "feat-y"])));
assert!(!result.contains(&feature_set(&["feat-a", "feat-b"])));
}
#[test]
fn seed_returns_none_when_not_set() {
let config = Config::from(Figment::from(Config::default())).unwrap();
assert!(config.seed("default").unwrap().is_none());
}
#[test]
fn seed_returns_value_when_set() {
let config = make_config(r#"{"channel": [{"name": "default", "seed": ["x", "y"]}]}"#);
let result = config.seed("default").unwrap();
assert_eq!(result, Some(feature_set(&["x", "y"])));
}
#[test]
fn always_include_returns_empty_when_not_set() {
let config = Config::from(Figment::from(Config::default())).unwrap();
assert!(config.always_include("default").unwrap().is_empty());
}
#[test]
fn always_include_returns_configured_features() {
let config = make_config(
r#"{"channel": [{"name": "default", "always_include": ["feat-a", "feat-b"]}]}"#,
);
let result = config.always_include("default").unwrap();
assert_eq!(result, feature_set(&["feat-a", "feat-b"]));
}
#[test]
fn always_include_falls_back_to_default_value_for_existing_channel() {
let config = make_config(
r#"{"channel": [{"name": "default", "always_include": ["feat-a"]}, {"name": "nightly"}]}"#,
);
let result = config.always_include("nightly").unwrap();
assert_eq!(result, feature_set(&["feat-a"]));
}
#[test]
fn always_deny_returns_empty_when_not_set() {
let config = Config::from(Figment::from(Config::default())).unwrap();
assert!(config.always_deny("default").unwrap().is_empty());
}
#[test]
fn always_deny_returns_configured_features() {
let config =
make_config(r#"{"channel": [{"name": "default", "always_deny": ["bad-feat"]}]}"#);
let result = config.always_deny("default").unwrap();
assert_eq!(result, feature_set(&["bad-feat"]));
}
#[test]
fn skip_returns_empty_when_not_set() {
let config = Config::from(Figment::from(Config::default())).unwrap();
assert!(config.skip("default").unwrap().is_empty());
}
#[test]
fn skip_returns_configured_sets() {
let config =
make_config(r#"{"channel": [{"name": "default", "skip": [["feat-a", "feat-b"]]}]}"#);
let result = config.skip("default").unwrap();
assert_eq!(result.len(), 1);
assert!(result.contains(&feature_set(&["feat-a", "feat-b"])));
}
#[test]
fn include_hidden_defaults_to_false() {
let config = Config::from(Figment::from(Config::default())).unwrap();
assert!(!config.include_hidden("default").unwrap());
}
#[test]
fn include_hidden_returns_true_when_set() {
let config = make_config(r#"{"channel": [{"name": "default", "include_hidden": true}]}"#);
assert!(config.include_hidden("default").unwrap());
}
#[test]
fn include_all_optional_defaults_to_false() {
let config = Config::from(Figment::from(Config::default())).unwrap();
assert!(!config.include_all_optional("default").unwrap());
}
#[test]
fn include_all_optional_returns_true_when_set() {
let config =
make_config(r#"{"channel": [{"name": "default", "include_all_optional": true}]}"#);
assert!(config.include_all_optional("default").unwrap());
}
#[test]
fn include_optional_returns_empty_when_not_set() {
let config = Config::from(Figment::from(Config::default())).unwrap();
assert!(config.include_optional("default").unwrap().is_empty());
}
#[test]
fn include_optional_returns_configured_features() {
let config =
make_config(r#"{"channel": [{"name": "default", "include_optional": ["dep-a"]}]}"#);
let result = config.include_optional("default").unwrap();
assert_eq!(result, feature_set(&["dep-a"]));
}
#[test]
fn all_methods_fall_back_when_channel_not_found() {
let config = Config::from(Figment::from(Config::default())).unwrap();
assert!(config.seed("no-such").unwrap().is_none());
assert!(config.always_include("no-such").unwrap().is_empty());
assert!(config.always_deny("no-such").unwrap().is_empty());
assert!(config.skip("no-such").unwrap().is_empty());
assert!(config.mutually_exclusive("no-such").unwrap().is_empty());
assert!(!config.include_hidden("no-such").unwrap());
assert!(!config.include_all_optional("no-such").unwrap());
assert!(config.include_optional("no-such").unwrap().is_empty());
}
#[test]
fn get_channel_errors_when_default_missing() {
let config = make_config(r#"{"channel": [{"name": "custom"}]}"#);
assert!(config.seed("nonexistent").is_err());
}
#[test]
fn skip_package_defaults_to_false() {
let config = Config::from(Figment::from(Config::default())).unwrap();
assert!(!config.skip_package());
}
#[test]
fn skip_package_returns_true_when_set() {
let config = make_config(r#"{"skip-package": true, "channel": [{"name": "default"}]}"#);
assert!(config.skip_package());
}
}