use std::path::PathBuf;
mod loader;
mod matching;
mod parser;
mod sources;
mod string_loader;
mod types;
pub use loader::{home_dir, load_file};
pub use parser::{parse_action_word, parse_rule};
pub use sources::{ConfigSourceInfo, enumerate_config_sources, find_project_config};
pub use string_loader::ConfigFormat;
pub use types::{ConfigDirective, Rule, RuleTarget};
use loader::{
apply_setting, build_weakening_suffix, detect_broad_allow, detect_dangerous_setting,
has_trust_setting, load_first_existing, load_project_config_if_trusted,
};
use matching::{format_rule_reason, matches_structured};
use std::path::Path;
use crate::condition::{MatchContext, evaluate_all};
use crate::error::RippyError;
use crate::pattern::Pattern;
use crate::verdict::{Decision, Verdict};
#[derive(Debug, Clone, Default)]
pub struct Config {
rules: Vec<Rule>,
after_rules: Vec<(Pattern, String)>,
pub default_action: Option<Decision>,
pub log_file: Option<std::path::PathBuf>,
pub log_full: bool,
pub tracking_db: Option<std::path::PathBuf>,
pub self_protect: bool,
pub trust_project_configs: bool,
aliases: Vec<(String, String)>,
pub cd_allowed_dirs: Vec<std::path::PathBuf>,
project_rules_range: Option<std::ops::Range<usize>>,
project_weakening_suffix: String,
pub active_package: Option<crate::packages::Package>,
}
impl Config {
pub fn load(cwd: &Path, env_config: Option<&Path>) -> Result<Self, RippyError> {
Self::load_with_home(cwd, env_config, home_dir())
}
pub fn load_with_home(
cwd: &Path,
env_config: Option<&Path>,
home: Option<PathBuf>,
) -> Result<Self, RippyError> {
let mut directives = crate::stdlib::stdlib_directives()?;
let package = resolve_package(home.as_ref(), cwd);
if let Some(pkg) = &package {
directives.extend(crate::packages::package_directives(pkg)?);
}
if let Some(home) = home {
load_first_existing(
&[
home.join(".rippy/config.toml"),
home.join(".rippy/config"),
home.join(".dippy/config"),
],
&mut directives,
)?;
}
directives.push(ConfigDirective::ProjectBoundary);
if let Some(project_config) = find_project_config(cwd) {
let trust_all = has_trust_setting(&directives);
load_project_config_if_trusted(&project_config, trust_all, &mut directives)?;
}
directives.push(ConfigDirective::ProjectBoundary);
if let Some(env_path) = env_config {
load_file(env_path, &mut directives)?;
}
let mut config = Self::from_directives(directives);
config.active_package = package;
Ok(config)
}
#[must_use]
pub fn empty() -> Self {
Self::default()
}
#[must_use]
pub fn weakening_suffix(&self) -> &str {
&self.project_weakening_suffix
}
#[must_use]
pub fn match_command(&self, command: &str, ctx: Option<&MatchContext>) -> Option<Verdict> {
self.match_rules(RuleTarget::Command, command, "matched rule", ctx)
}
#[must_use]
pub fn match_redirect(&self, path: &str, ctx: Option<&MatchContext>) -> Option<Verdict> {
self.match_rules(RuleTarget::Redirect, path, "redirect rule", ctx)
}
#[must_use]
pub fn match_mcp(&self, tool_name: &str) -> Option<Verdict> {
self.match_rules(RuleTarget::Mcp, tool_name, "MCP rule", None)
}
#[must_use]
pub fn match_file_read(&self, path: &str, ctx: Option<&MatchContext>) -> Option<Verdict> {
self.match_rules(RuleTarget::FileRead, path, "file-read rule", ctx)
}
#[must_use]
pub fn match_file_write(&self, path: &str, ctx: Option<&MatchContext>) -> Option<Verdict> {
self.match_rules(RuleTarget::FileWrite, path, "file-write rule", ctx)
}
#[must_use]
pub fn match_file_edit(&self, path: &str, ctx: Option<&MatchContext>) -> Option<Verdict> {
self.match_rules(RuleTarget::FileEdit, path, "file-edit rule", ctx)
}
#[must_use]
pub fn match_after(&self, command: &str) -> Option<String> {
let mut result = None;
for (pattern, message) in &self.after_rules {
if pattern.matches(command) {
result = Some(message.clone());
}
}
result
}
#[must_use]
pub fn resolve_alias<'a>(&'a self, command: &'a str) -> &'a str {
for (source, target) in &self.aliases {
if command == source
|| command
.strip_prefix(source.as_str())
.is_some_and(|rest| rest.starts_with('/'))
{
return target;
}
}
command
}
fn match_rules(
&self,
target: RuleTarget,
input: &str,
label: &str,
ctx: Option<&MatchContext>,
) -> Option<Verdict> {
let mut result = None;
let mut baseline_decision: Option<Decision> = None;
let project_range = self.project_rules_range.as_ref();
for (i, rule) in self.rules.iter().enumerate() {
if rule.target != target {
continue;
}
if !rule.pattern.matches(input) {
continue;
}
if rule.has_structured_fields() && !matches_structured(rule, input) {
continue;
}
if !rule.conditions.is_empty() {
match ctx {
Some(c) if evaluate_all(&rule.conditions, c) => {}
_ => continue,
}
}
let is_project_rule = project_range.is_some_and(|r| r.contains(&i));
if !is_project_rule {
baseline_decision = Some(rule.decision);
}
let mut reason = if is_project_rule
&& rule.decision == Decision::Allow
&& baseline_decision.is_some_and(|d| d != Decision::Allow)
{
let overridden = baseline_decision.map_or("ask", Decision::as_str);
format!(
"matched project rule (overrides {overridden}: {})",
rule.pattern.raw()
)
} else {
rule.message
.as_deref()
.map_or_else(|| format_rule_reason(rule, label), String::from)
};
if is_project_rule && rule.decision == Decision::Allow {
reason.push_str(&self.project_weakening_suffix);
}
result = Some(Verdict {
decision: rule.decision,
reason,
resolved_command: None,
});
}
result
}
pub fn from_directives(directives: Vec<ConfigDirective>) -> Self {
let mut config = Self {
self_protect: true,
..Self::default()
};
let mut in_project_section = false;
let mut project_start: Option<usize> = None;
let mut weakening_notes: Vec<String> = Vec::new();
for directive in directives {
match directive {
ConfigDirective::Rule(r) => {
if r.target == RuleTarget::After {
if let Some(msg) = &r.message {
config.after_rules.push((r.pattern, msg.clone()));
}
} else {
if in_project_section {
detect_broad_allow(&r, &mut weakening_notes);
}
config.rules.push(r);
}
}
ConfigDirective::Set { key, value } => {
if in_project_section {
detect_dangerous_setting(&key, &value, &mut weakening_notes);
}
apply_setting(&mut config, &key, &value);
}
ConfigDirective::Alias { source, target } => {
config.aliases.push((source, target));
}
ConfigDirective::ProjectBoundary => {
if in_project_section {
if let Some(start) = project_start {
config.project_rules_range = Some(start..config.rules.len());
}
in_project_section = false;
} else {
project_start = Some(config.rules.len());
in_project_section = true;
}
}
ConfigDirective::CdAllow(path) => {
config
.cd_allowed_dirs
.push(crate::handlers::normalize_path(&path));
}
}
}
if in_project_section && project_start.is_some() {
config.project_rules_range = project_start.map(|start| start..config.rules.len());
}
config.project_weakening_suffix = build_weakening_suffix(&weakening_notes);
config
}
}
fn resolve_package(home: Option<&PathBuf>, cwd: &Path) -> Option<crate::packages::Package> {
let mut package_name: Option<String> = None;
if let Some(home) = home {
for path in &[
home.join(".rippy/config.toml"),
home.join(".rippy/config"),
home.join(".dippy/config"),
] {
if path.is_file() {
package_name = loader::extract_package_setting(path);
break; }
}
}
if let Some(project_config) = find_project_config(cwd)
&& let Some(name) = loader::extract_package_setting(&project_config)
{
package_name = Some(name);
}
let name = package_name?;
match crate::packages::Package::resolve(&name, home.map(PathBuf::as_path)) {
Ok(pkg) => Some(pkg),
Err(e) => {
eprintln!("[rippy] {e}");
None
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
use crate::condition::Condition;
#[test]
fn last_match_wins() {
let config = Config::from_directives(vec![
ConfigDirective::Rule(
Rule::new(RuleTarget::Command, Decision::Deny, "rm").with_message("blocked"),
),
ConfigDirective::Rule(
Rule::new(RuleTarget::Command, Decision::Allow, "rm --help")
.with_message("help is fine"),
),
]);
let v = config.match_command("rm --help", None).unwrap();
assert_eq!(v.decision, Decision::Allow);
assert_eq!(v.reason, "help is fine");
}
#[test]
fn alias_resolution() {
let config = Config {
aliases: vec![("~/custom-git".into(), "git".into())],
..Config::default()
};
assert_eq!(config.resolve_alias("~/custom-git"), "git");
assert_eq!(config.resolve_alias("npm"), "npm");
}
#[test]
fn match_redirect_last_wins() {
let config = Config::from_directives(vec![
ConfigDirective::Rule(
Rule::new(RuleTarget::Redirect, Decision::Deny, "/etc/*")
.with_message("no writes to /etc"),
),
ConfigDirective::Rule(
Rule::new(RuleTarget::Redirect, Decision::Allow, "/etc/hosts")
.with_message("hosts ok"),
),
]);
let v = config.match_redirect("/etc/hosts", None).unwrap();
assert_eq!(v.decision, Decision::Allow);
}
#[test]
fn settings_extracted() {
let config = Config::from_directives(vec![
ConfigDirective::Set {
key: "default".into(),
value: "deny".into(),
},
ConfigDirective::Set {
key: "log".into(),
value: "~/.rippy/audit.log".into(),
},
ConfigDirective::Set {
key: "log-full".into(),
value: String::new(),
},
]);
assert_eq!(config.default_action, Some(Decision::Deny));
assert!(config.log_file.is_some());
assert!(config.log_full);
}
#[test]
fn match_mcp_rule() {
let config = Config::from_directives(vec![ConfigDirective::Rule(Rule::new(
RuleTarget::Mcp,
Decision::Deny,
"dangerous*",
))]);
let v = config.match_mcp("dangerous_tool").unwrap();
assert_eq!(v.decision, Decision::Deny);
assert!(config.match_mcp("safe_tool").is_none());
}
#[test]
fn match_after_rule() {
let config = Config::from_directives(vec![ConfigDirective::Rule(
Rule::new(RuleTarget::After, Decision::Allow, "git commit").with_message("committed!"),
)]);
assert_eq!(
config.match_after("git commit -m foo"),
Some("committed!".into())
);
assert!(config.match_after("ls").is_none());
}
#[test]
fn allow_uv_run_python_c() {
let config = Config::from_directives(vec![
ConfigDirective::Rule(
Rule::new(RuleTarget::Command, Decision::Deny, "python")
.with_message("Use uv run python"),
),
ConfigDirective::Rule(Rule::new(
RuleTarget::Command,
Decision::Allow,
"uv run python -c",
)),
]);
let v = config.match_command("python foo.py", None).unwrap();
assert_eq!(v.decision, Decision::Deny);
let v = config
.match_command("uv run python -c 'print(1)'", None)
.unwrap();
assert_eq!(v.decision, Decision::Allow);
}
#[test]
fn match_file_read_rules() {
let config = Config::from_directives(vec![
ConfigDirective::Rule(
Rule::new(RuleTarget::FileRead, Decision::Deny, "**/.env*").with_message("no env"),
),
ConfigDirective::Rule(Rule::new(RuleTarget::FileRead, Decision::Allow, "/tmp/**")),
]);
let v = config.match_file_read(".env.local", None).unwrap();
assert_eq!(v.decision, Decision::Deny);
assert_eq!(v.reason, "no env");
let v = config.match_file_read("/tmp/safe.txt", None).unwrap();
assert_eq!(v.decision, Decision::Allow);
assert!(config.match_file_read("main.rs", None).is_none());
}
#[test]
fn match_file_write_rules() {
let config = Config::from_directives(vec![ConfigDirective::Rule(
Rule::new(RuleTarget::FileWrite, Decision::Deny, "**/.rippy*")
.with_message("config protected"),
)]);
let v = config.match_file_write(".rippy.toml", None).unwrap();
assert_eq!(v.decision, Decision::Deny);
assert!(config.match_file_write("other.txt", None).is_none());
}
#[test]
fn match_file_edit_rules() {
let config = Config::from_directives(vec![ConfigDirective::Rule(
Rule::new(RuleTarget::FileEdit, Decision::Ask, "**/node_modules/**")
.with_message("vendor"),
)]);
let v = config
.match_file_edit("node_modules/pkg/index.js", None)
.unwrap();
assert_eq!(v.decision, Decision::Ask);
assert!(config.match_file_edit("src/main.rs", None).is_none());
}
#[test]
fn file_rules_last_match_wins() {
let config = Config::from_directives(vec![
ConfigDirective::Rule(Rule::new(RuleTarget::FileRead, Decision::Allow, "**")),
ConfigDirective::Rule(
Rule::new(RuleTarget::FileRead, Decision::Deny, "**/.env*").with_message("blocked"),
),
]);
let v = config.match_file_read(".env", None).unwrap();
assert_eq!(v.decision, Decision::Deny);
let v = config.match_file_read("main.rs", None).unwrap();
assert_eq!(v.decision, Decision::Allow);
}
#[test]
fn conditional_rule_skipped_when_condition_fails() {
let config = Config::from_directives(vec![ConfigDirective::Rule(
Rule::new(RuleTarget::Command, Decision::Deny, "echo *")
.with_message("blocked on main")
.with_conditions(vec![Condition::BranchEq("main".into())]),
)]);
let ctx = MatchContext {
branch: Some("develop"),
cwd: std::path::Path::new("/tmp"),
};
assert!(config.match_command("echo hello", Some(&ctx)).is_none());
}
#[test]
fn conditional_rule_applies_when_condition_passes() {
let config = Config::from_directives(vec![ConfigDirective::Rule(
Rule::new(RuleTarget::Command, Decision::Deny, "echo *")
.with_message("blocked on main")
.with_conditions(vec![Condition::BranchEq("main".into())]),
)]);
let ctx = MatchContext {
branch: Some("main"),
cwd: std::path::Path::new("/tmp"),
};
let v = config.match_command("echo hello", Some(&ctx)).unwrap();
assert_eq!(v.decision, Decision::Deny);
assert_eq!(v.reason, "blocked on main");
}
#[test]
fn conditional_rule_skipped_without_context() {
let config = Config::from_directives(vec![ConfigDirective::Rule(
Rule::new(RuleTarget::Command, Decision::Deny, "echo *")
.with_conditions(vec![Condition::BranchEq("main".into())]),
)]);
assert!(config.match_command("echo hello", None).is_none());
}
#[test]
fn structured_rule_in_config() {
let mut rule = Rule::new(RuleTarget::Command, Decision::Deny, "*");
rule.pattern = crate::pattern::Pattern::any();
rule.command = Some("git".into());
rule.subcommand = Some("push".into());
let config = Config::from_directives(vec![ConfigDirective::Rule(rule)]);
let v = config.match_command("git push origin main", None);
assert!(v.is_some());
assert_eq!(v.unwrap().decision, Decision::Deny);
assert!(config.match_command("git status", None).is_none());
}
#[test]
fn structured_rule_with_when_condition() {
let mut rule = Rule::new(RuleTarget::Command, Decision::Deny, "*");
rule.pattern = crate::pattern::Pattern::any();
rule.command = Some("git".into());
rule.subcommand = Some("push".into());
let rule = rule.with_conditions(vec![Condition::BranchEq("main".into())]);
let config = Config::from_directives(vec![ConfigDirective::Rule(rule)]);
let ctx_main = MatchContext {
branch: Some("main"),
cwd: std::path::Path::new("/tmp"),
};
let ctx_feat = MatchContext {
branch: Some("feature"),
cwd: std::path::Path::new("/tmp"),
};
assert!(
config
.match_command("git push origin", Some(&ctx_main))
.is_some()
);
assert!(
config
.match_command("git push origin", Some(&ctx_feat))
.is_none()
);
}
#[test]
fn project_rule_override_annotated() {
let directives = vec![
ConfigDirective::Rule(Rule::new(RuleTarget::Command, Decision::Deny, "rm -rf *")),
ConfigDirective::ProjectBoundary,
ConfigDirective::Rule(Rule::new(RuleTarget::Command, Decision::Allow, "rm -rf *")),
];
let config = Config::from_directives(directives);
let v = config.match_command("rm -rf /tmp", None).unwrap();
assert_eq!(v.decision, Decision::Allow);
assert!(
v.reason.contains("overrides deny"),
"reason should mention override, got: {}",
v.reason
);
}
#[test]
fn project_rule_no_override_not_annotated() {
let directives = vec![
ConfigDirective::ProjectBoundary,
ConfigDirective::Rule(Rule::new(RuleTarget::Command, Decision::Allow, "echo *")),
];
let config = Config::from_directives(directives);
let v = config.match_command("echo hello", None).unwrap();
assert_eq!(v.decision, Decision::Allow);
assert!(
!v.reason.contains("overrides"),
"no baseline deny → should not mention override, got: {}",
v.reason
);
}
#[test]
fn baseline_rule_not_annotated() {
let directives = vec![
ConfigDirective::Rule(Rule::new(RuleTarget::Command, Decision::Deny, "rm *")),
ConfigDirective::ProjectBoundary,
];
let config = Config::from_directives(directives);
let v = config.match_command("rm -rf /", None).unwrap();
assert_eq!(v.decision, Decision::Deny);
assert!(!v.reason.contains("overrides"));
}
#[test]
fn project_ask_overriding_deny_not_annotated() {
let directives = vec![
ConfigDirective::Rule(Rule::new(RuleTarget::Command, Decision::Deny, "rm *")),
ConfigDirective::ProjectBoundary,
ConfigDirective::Rule(Rule::new(RuleTarget::Command, Decision::Ask, "rm *")),
];
let config = Config::from_directives(directives);
let v = config.match_command("rm -rf /", None).unwrap();
assert_eq!(v.decision, Decision::Ask);
assert!(!v.reason.contains("overrides"));
}
#[test]
fn project_allow_overriding_ask_annotated() {
let directives = vec![
ConfigDirective::Rule(Rule::new(
RuleTarget::Command,
Decision::Ask,
"docker run *",
)),
ConfigDirective::ProjectBoundary,
ConfigDirective::Rule(Rule::new(
RuleTarget::Command,
Decision::Allow,
"docker run *",
)),
];
let config = Config::from_directives(directives);
let v = config.match_command("docker run nginx", None).unwrap();
assert_eq!(v.decision, Decision::Allow);
assert!(v.reason.contains("overrides ask"));
}
#[test]
fn project_rules_range_set_correctly() {
let directives = vec![
ConfigDirective::Rule(Rule::new(RuleTarget::Command, Decision::Deny, "a")),
ConfigDirective::ProjectBoundary,
ConfigDirective::Rule(Rule::new(RuleTarget::Command, Decision::Allow, "b")),
ConfigDirective::ProjectBoundary,
ConfigDirective::Rule(Rule::new(RuleTarget::Command, Decision::Allow, "c")),
];
let config = Config::from_directives(directives);
assert_eq!(config.project_rules_range, Some(1..2));
}
#[test]
fn env_override_allow_not_annotated_as_project() {
let directives = vec![
ConfigDirective::Rule(Rule::new(RuleTarget::Command, Decision::Deny, "rm *")),
ConfigDirective::ProjectBoundary,
ConfigDirective::ProjectBoundary,
ConfigDirective::Rule(Rule::new(RuleTarget::Command, Decision::Allow, "rm *")),
];
let config = Config::from_directives(directives);
let v = config.match_command("rm -rf /", None).unwrap();
assert_eq!(v.decision, Decision::Allow);
assert!(!v.reason.contains("overrides"));
}
#[test]
fn project_default_allow_detected() {
let directives = vec![
ConfigDirective::ProjectBoundary,
ConfigDirective::Set {
key: "default".to_string(),
value: "allow".to_string(),
},
ConfigDirective::ProjectBoundary,
];
let config = Config::from_directives(directives);
assert!(
config
.weakening_suffix()
.contains("default action to allow")
);
}
#[test]
fn project_self_protect_off_detected() {
let directives = vec![
ConfigDirective::ProjectBoundary,
ConfigDirective::Set {
key: "self-protect".to_string(),
value: "off".to_string(),
},
ConfigDirective::ProjectBoundary,
];
let config = Config::from_directives(directives);
assert!(config.weakening_suffix().contains("self-protection"));
}
#[test]
fn project_broad_allow_detected() {
let directives = vec![
ConfigDirective::ProjectBoundary,
ConfigDirective::Rule(Rule::new(RuleTarget::Command, Decision::Allow, "*")),
ConfigDirective::ProjectBoundary,
];
let config = Config::from_directives(directives);
assert!(config.weakening_suffix().contains("allows all commands"));
}
#[test]
fn project_deny_only_no_weakening_notes() {
let directives = vec![
ConfigDirective::ProjectBoundary,
ConfigDirective::Rule(Rule::new(RuleTarget::Command, Decision::Deny, "rm *")),
ConfigDirective::Set {
key: "default".to_string(),
value: "ask".to_string(),
},
ConfigDirective::ProjectBoundary,
];
let config = Config::from_directives(directives);
assert!(config.weakening_suffix().is_empty());
}
#[test]
fn weakening_notes_appended_to_project_allow_verdict() {
let directives = vec![
ConfigDirective::ProjectBoundary,
ConfigDirective::Set {
key: "default".to_string(),
value: "allow".to_string(),
},
ConfigDirective::Rule(Rule::new(RuleTarget::Command, Decision::Allow, "echo *")),
ConfigDirective::ProjectBoundary,
];
let config = Config::from_directives(directives);
let v = config.match_command("echo hello", None).unwrap();
assert_eq!(v.decision, Decision::Allow);
assert!(v.reason.contains("NOTE: project config"));
assert!(v.reason.contains("default action to allow"));
}
#[test]
fn package_setting_loads_develop_rules() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "[settings]\npackage = \"develop\"\n").unwrap();
let mut directives = Vec::new();
loader::load_file(&config_path, &mut directives).unwrap();
let has_package = directives
.iter()
.any(|d| matches!(d, ConfigDirective::Set { key, value } if key == "package" && value == "develop"));
assert!(has_package, "should emit package setting directive");
}
#[test]
fn package_loads_via_config_pipeline() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
std::fs::create_dir_all(home.join(".rippy")).unwrap();
std::fs::write(
home.join(".rippy/config.toml"),
"[settings]\npackage = \"develop\"\n",
)
.unwrap();
let config = Config::load_with_home(dir.path(), None, Some(home)).unwrap();
assert_eq!(
config.active_package,
Some(crate::packages::Package::Develop)
);
let v = config.match_command("cargo test", None);
assert!(v.is_some(), "develop package should match cargo test");
assert_eq!(v.unwrap().decision, Decision::Allow);
}
#[test]
fn project_package_overrides_global() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
std::fs::create_dir_all(home.join(".rippy")).unwrap();
std::fs::write(
home.join(".rippy/config.toml"),
"[settings]\npackage = \"develop\"\n",
)
.unwrap();
let project = dir.path().join("project");
std::fs::create_dir_all(&project).unwrap();
std::fs::write(
project.join(".rippy.toml"),
"[settings]\npackage = \"review\"\n",
)
.unwrap();
let config = Config::load_with_home(&project, None, Some(home)).unwrap();
assert_eq!(
config.active_package,
Some(crate::packages::Package::Review)
);
}
#[test]
fn no_package_setting_backward_compatible() {
let dir = tempfile::tempdir().unwrap();
let config = Config::load_with_home(dir.path(), None, None).unwrap();
assert_eq!(config.active_package, None);
}
#[test]
fn user_rules_override_package_rules() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
std::fs::create_dir_all(home.join(".rippy")).unwrap();
std::fs::write(
home.join(".rippy/config.toml"),
"[settings]\npackage = \"develop\"\n\n\
[[rules]]\naction = \"deny\"\ncommand = \"rm\"\nmessage = \"no rm\"\n",
)
.unwrap();
let config = Config::load_with_home(dir.path(), None, Some(home)).unwrap();
let v = config.match_command("rm foo", None);
assert!(v.is_some());
assert_eq!(v.unwrap().decision, Decision::Deny);
}
#[test]
fn line_based_config_package_setting() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
std::fs::create_dir_all(home.join(".rippy")).unwrap();
std::fs::write(home.join(".rippy/config"), "set package develop\n").unwrap();
let config = Config::load_with_home(dir.path(), None, Some(home)).unwrap();
assert_eq!(
config.active_package,
Some(crate::packages::Package::Develop)
);
}
#[test]
fn invalid_package_name_produces_none() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
std::fs::create_dir_all(home.join(".rippy")).unwrap();
std::fs::write(
home.join(".rippy/config.toml"),
"[settings]\npackage = \"yolo\"\n",
)
.unwrap();
let config = Config::load_with_home(dir.path(), None, Some(home)).unwrap();
assert_eq!(config.active_package, None);
}
#[test]
fn custom_package_loads_via_config_pipeline() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
std::fs::create_dir_all(home.join(".rippy/packages")).unwrap();
std::fs::write(
home.join(".rippy/packages/team.toml"),
r#"
[meta]
name = "team"
extends = "develop"
[[rules]]
action = "deny"
pattern = "npm publish"
message = "team policy"
"#,
)
.unwrap();
std::fs::create_dir_all(home.join(".rippy")).unwrap();
std::fs::write(
home.join(".rippy/config.toml"),
"[settings]\npackage = \"team\"\n",
)
.unwrap();
let config = Config::load_with_home(dir.path(), None, Some(home)).unwrap();
match &config.active_package {
Some(crate::packages::Package::Custom(c)) => assert_eq!(c.name, "team"),
other => panic!("expected Custom(team), got {other:?}"),
}
let v = config.match_command("cargo test", None);
assert!(v.is_some());
assert_eq!(v.unwrap().decision, Decision::Allow);
let v = config.match_command("npm publish", None);
assert!(v.is_some());
assert_eq!(v.unwrap().decision, Decision::Deny);
}
}