use crate::errors::RulesError;
use globset::{Glob, GlobSet, GlobSetBuilder};
use guppy::graph::{PackageGraph, PackageMetadata, PackageSet, Workspace};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DeterminatorRules {
#[serde(default = "default_true", rename = "use-default-rules")]
use_default_rules: bool,
#[serde(default, rename = "path-rule")]
pub path_rules: Vec<PathRule>,
#[serde(default, rename = "package-rule")]
pub package_rules: Vec<PackageRule>,
}
impl Default for DeterminatorRules {
fn default() -> Self {
Self {
use_default_rules: true,
path_rules: vec![],
package_rules: vec![],
}
}
}
#[inline]
fn default_true() -> bool {
true
}
macro_rules! doc_comment {
($doc:expr, $($t:tt)*) => (
#[doc = $doc]
$($t)*
);
}
impl DeterminatorRules {
pub fn parse(s: &str) -> Result<Self, toml::de::Error> {
toml::from_str(s)
}
doc_comment! {
concat!("\
Contains the default rules in a TOML file format.
The default rules included with this copy of the determinator are:
```toml
", include_str!("../default-rules.toml"), "\
```
The latest version of the default rules is available
[on GitHub](https://github.com/guppy-rs/guppy/blob/main/tools/determinator/default-rules.toml).
"),
pub const DEFAULT_RULES_TOML: &'static str = include_str!("../default-rules.toml");
}
pub fn default_rules() -> &'static DeterminatorRules {
static DEFAULT_RULES: Lazy<DeterminatorRules> = Lazy::new(|| {
DeterminatorRules::parse(DeterminatorRules::DEFAULT_RULES_TOML)
.expect("default rules should parse")
});
&DEFAULT_RULES
}
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct PathRule {
pub globs: Vec<String>,
#[serde(with = "mark_changed_impl")]
pub mark_changed: DeterminatorMarkChanged,
#[serde(default)]
pub post_rule: DeterminatorPostRule,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
#[derive(Default)]
pub enum DeterminatorPostRule {
#[default]
Skip,
SkipRules,
Fallthrough,
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct PackageRule {
pub on_affected: Vec<String>,
#[serde(with = "mark_changed_impl")]
pub mark_changed: DeterminatorMarkChanged,
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", untagged)]
pub enum DeterminatorMarkChanged {
Packages(Vec<String>),
All,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum PathMatch {
RuleMatchedAll,
RuleMatched(RuleIndex),
AncestorMatched,
NoMatches,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum RuleIndex {
CustomPath(usize),
DefaultPath(usize),
Package(usize),
}
impl fmt::Display for RuleIndex {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
RuleIndex::CustomPath(index) => write!(f, "custom path rule {}", index),
RuleIndex::DefaultPath(index) => write!(f, "default path rule {}", index),
RuleIndex::Package(index) => write!(f, "package rule {}", index),
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct RulesImpl<'g> {
pub(crate) path_rules: Vec<PathRuleImpl<'g>>,
pub(crate) package_rules: Vec<PackageRuleImpl<'g>>,
}
impl<'g> RulesImpl<'g> {
pub(crate) fn new(
graph: &'g PackageGraph,
options: &DeterminatorRules,
) -> Result<Self, RulesError> {
let workspace = graph.workspace();
let custom_path_rules = options
.path_rules
.iter()
.enumerate()
.map(|(idx, rule)| (RuleIndex::CustomPath(idx), rule));
let default_path_rules = if options.use_default_rules {
let default_rules = DeterminatorRules::default_rules();
default_rules.path_rules.as_slice()
} else {
&[]
};
let default_path_rules = default_path_rules
.iter()
.enumerate()
.map(|(idx, rule)| (RuleIndex::DefaultPath(idx), rule));
let path_rules = custom_path_rules
.chain(default_path_rules)
.map(
|(
rule_index,
PathRule {
globs,
mark_changed,
post_rule,
},
)| {
let mut builder = GlobSetBuilder::new();
for glob in globs {
let glob = Glob::new(glob)
.map_err(|err| RulesError::glob_parse(rule_index, err))?;
builder.add(glob);
}
let glob_set = builder
.build()
.map_err(|err| RulesError::glob_parse(rule_index, err))?;
let mark_changed = MarkChangedImpl::new(&workspace, mark_changed)
.map_err(|err| RulesError::resolve_ref(rule_index, err))?;
Ok(PathRuleImpl {
rule_index,
glob_set,
mark_changed,
post_rule: *post_rule,
})
},
)
.collect::<Result<Vec<_>, _>>()?;
let package_rules = options
.package_rules
.iter()
.enumerate()
.map(
|(
rule_index,
PackageRule {
on_affected,
mark_changed,
},
)| {
let rule_index = RuleIndex::Package(rule_index);
let on_affected = graph
.resolve_workspace_names(on_affected)
.map_err(|err| RulesError::resolve_ref(rule_index, err))?;
let mark_changed = MarkChangedImpl::new(&workspace, mark_changed)
.map_err(|err| RulesError::resolve_ref(rule_index, err))?;
Ok(PackageRuleImpl {
on_affected,
mark_changed,
})
},
)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self {
path_rules,
package_rules,
})
}
}
#[derive(Clone, Debug)]
pub(crate) struct PathRuleImpl<'g> {
pub(crate) rule_index: RuleIndex,
pub(crate) glob_set: GlobSet,
pub(crate) mark_changed: MarkChangedImpl<'g>,
pub(crate) post_rule: DeterminatorPostRule,
}
#[derive(Clone, Debug)]
pub(crate) struct PackageRuleImpl<'g> {
pub(crate) on_affected: PackageSet<'g>,
pub(crate) mark_changed: MarkChangedImpl<'g>,
}
#[derive(Clone, Debug)]
pub(crate) enum MarkChangedImpl<'g> {
All,
Packages(Vec<PackageMetadata<'g>>),
}
impl<'g> MarkChangedImpl<'g> {
fn new(
workspace: &Workspace<'g>,
mark_changed: &DeterminatorMarkChanged,
) -> Result<Self, guppy::Error> {
match mark_changed {
DeterminatorMarkChanged::Packages(names) => Ok(MarkChangedImpl::Packages(
workspace.members_by_names(names)?,
)),
DeterminatorMarkChanged::All => Ok(MarkChangedImpl::All),
}
}
}
mod mark_changed_impl {
use super::*;
use serde::{de::Error, Deserializer, Serializer};
pub fn serialize<S>(
mark_changed: &DeterminatorMarkChanged,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match mark_changed {
DeterminatorMarkChanged::Packages(names) => names.serialize(serializer),
DeterminatorMarkChanged::All => "all".serialize(serializer),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<DeterminatorMarkChanged, D::Error>
where
D: Deserializer<'de>,
{
let d = MarkChangedDeserialized::deserialize(deserializer)?;
match d {
MarkChangedDeserialized::String(s) => match s.as_str() {
"all" => Ok(DeterminatorMarkChanged::All),
other => Err(D::Error::custom(format!(
"unknown string for mark-changed: {}",
other,
))),
},
MarkChangedDeserialized::VecString(strings) => {
Ok(DeterminatorMarkChanged::Packages(strings))
}
}
}
#[derive(Deserialize)]
#[serde(untagged)]
enum MarkChangedDeserialized {
String(String),
VecString(Vec<String>),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse() {
let s = r#"[[path-rule]]
globs = ["all/*"]
mark-changed = "all"
post-rule = "fallthrough"
[[path-rule]]
globs = ["all/1/2/*"]
mark-changed = ["c"]
post-rule = "skip-rules"
[[path-rule]]
globs = ["none/**/test", "foo/bar"]
mark-changed = []
[[package-rule]]
on-affected = ["foo"]
mark-changed = ["wat"]
[[package-rule]]
on-affected = ["test1"]
mark-changed = "all"
"#;
let expected = DeterminatorRules {
use_default_rules: true,
path_rules: vec![
PathRule {
globs: vec!["all/*".to_owned()],
mark_changed: DeterminatorMarkChanged::All,
post_rule: DeterminatorPostRule::Fallthrough,
},
PathRule {
globs: vec!["all/1/2/*".to_owned()],
mark_changed: DeterminatorMarkChanged::Packages(vec!["c".to_owned()]),
post_rule: DeterminatorPostRule::SkipRules,
},
PathRule {
globs: vec!["none/**/test".to_owned(), "foo/bar".to_owned()],
mark_changed: DeterminatorMarkChanged::Packages(vec![]),
post_rule: DeterminatorPostRule::Skip,
},
],
package_rules: vec![
PackageRule {
on_affected: vec!["foo".to_string()],
mark_changed: DeterminatorMarkChanged::Packages(vec!["wat".to_string()]),
},
PackageRule {
on_affected: vec!["test1".to_string()],
mark_changed: DeterminatorMarkChanged::All,
},
],
};
assert_eq!(
DeterminatorRules::parse(s),
Ok(expected),
"parse() result matches"
);
}
#[test]
fn parse_empty() {
let expected = DeterminatorRules::default();
assert_eq!(
DeterminatorRules::parse(""),
Ok(expected),
"parse_empty() returns default"
);
}
#[test]
fn parse_bad() {
let bads = &[
r#"[[foo]]
bar = "baz"
"#,
r#"[foo]
bar = "baz"
"#,
r#"[[path-rule]]
globs = ["a/b"]
mark-changed = []
foo = "bar"
"#,
r#"[[path-rule]]
globs = "x"
mark-changed = []
"#,
r#"[[path-rule]]
globs = [123, "a/b"]
mark-changed = []
"#,
r#"[[path-rule]]
"#,
r#"[[path-rule]]
mark-changed = "all"
"#,
r#"[[path-rule]]
globs = ["a/b"]
"#,
r#"[[path-rule]]
globs = ["a/b"]
mark-changed = "foo"
"#,
r#"[[path-rule]]
globs = ["a/b"]
mark-changed = 123
"#,
r#"[[path-rule]]
globs = ["a/b"]
mark-changed = [123, "abc"]
"#,
r#"[[path-rule]]
globs = ["a/b"]
mark-changed = []
post-rule = "abc"
"#,
r#"[[path-rule]]
globs = ["a/b"]
mark-changed = "all"
post-rule = []
"#,
r#"[[package-rule]]
on-affected = ["foo"]
mark-changed = []
foo = "bar"
"#,
r#"[[package-rule]]
on-affected = "foo"
mark-changed = []
"#,
r#"[[package-rule]]
on-affected = ["foo", 123]
mark-changed = []
"#,
r#"[[package-rule]]
on-affected = ["foo"]
mark-changed = 123
"#,
r#"[[package-rule]]
on-affected = ["foo", 123]
mark-changed = ["bar", 456]
"#,
r#"[[package-rule]]
on-affected = ["foo"]
mark-changed = "bar"
"#,
r#"[[package-rule]]
mark-changed = "all"
"#,
r#"[[package-rule]]
on-affected = ["foo"]
"#,
];
for &bad in bads {
let res = DeterminatorRules::parse(bad);
if res.is_ok() {
panic!(
"parsing should have failed but succeeded:\n\
input = {}\n\
output: {:?}\n",
bad, res
);
}
}
}
}