use ::path_matchers::PathMatcher;
use path_slash::PathExt;
use std::path::{Path, PathBuf};
pub mod path_matchers {
pub use ::path_matchers::*;
}
pub struct ChangeDetection;
impl ChangeDetection {
pub fn path<P>(path: P) -> ChangeDetectionBuilder
where
P: AsRef<Path>,
{
ChangeDetectionBuilder::default().path(path)
}
pub fn path_include<P, F>(path: P, filter: F) -> ChangeDetectionBuilder
where
P: AsRef<Path>,
F: PathMatcher + 'static,
{
ChangeDetectionBuilder::default().path_include(path, filter)
}
pub fn path_exclude<P, F>(path: P, filter: F) -> ChangeDetectionBuilder
where
P: AsRef<Path>,
F: PathMatcher + 'static,
{
ChangeDetectionBuilder::default().path_exclude(path, filter)
}
pub fn path_filter<P, F1, F2>(path: P, include: F1, exclude: F2) -> ChangeDetectionBuilder
where
P: AsRef<Path>,
F1: PathMatcher + 'static,
F2: PathMatcher + 'static,
{
ChangeDetectionBuilder::default().path_filter(path, include, exclude)
}
pub fn include<F>(filter: F) -> ChangeDetectionBuilder
where
F: PathMatcher + 'static,
{
ChangeDetectionBuilder::default().include(filter)
}
pub fn exclude<F>(filter: F) -> ChangeDetectionBuilder
where
F: PathMatcher + 'static,
{
ChangeDetectionBuilder::default().exclude(filter)
}
pub fn filter<F1, F2>(include: F1, exclude: F2) -> ChangeDetectionBuilder
where
F1: PathMatcher + 'static,
F2: PathMatcher + 'static,
{
ChangeDetectionBuilder::default()
.include(include)
.exclude(exclude)
}
}
#[derive(Default)]
pub struct ChangeDetectionBuilder {
include: Option<Box<dyn PathMatcher>>,
exclude: Option<Box<dyn PathMatcher>>,
paths: Vec<ChangeDetectionPath>,
}
impl ChangeDetectionBuilder {
pub fn path<P>(mut self, path: P) -> ChangeDetectionBuilder
where
P: Into<ChangeDetectionPath>,
{
self.paths.push(path.into());
self
}
pub fn path_include<P, F>(mut self, path: P, filter: F) -> ChangeDetectionBuilder
where
P: AsRef<Path>,
F: PathMatcher + 'static,
{
self.paths.push(ChangeDetectionPath::PathInclude(
path.as_ref().into(),
Box::new(filter),
));
self
}
pub fn path_exclude<P, F>(mut self, path: P, filter: F) -> ChangeDetectionBuilder
where
P: AsRef<Path>,
F: PathMatcher + 'static,
{
self.paths.push(ChangeDetectionPath::PathExclude(
path.as_ref().into(),
Box::new(filter),
));
self
}
pub fn path_filter<P, F1, F2>(
mut self,
path: P,
include: F1,
exclude: F2,
) -> ChangeDetectionBuilder
where
P: AsRef<Path>,
F1: PathMatcher + 'static,
F2: PathMatcher + 'static,
{
self.paths.push(ChangeDetectionPath::PathIncludeExclude {
path: path.as_ref().into(),
include: Box::new(include),
exclude: Box::new(exclude),
});
self
}
fn include<F>(mut self, filter: F) -> ChangeDetectionBuilder
where
F: PathMatcher + 'static,
{
self.include = Some(Box::new(filter));
self
}
fn exclude<F>(mut self, filter: F) -> ChangeDetectionBuilder
where
F: PathMatcher + 'static,
{
self.exclude = Some(Box::new(filter));
self
}
pub fn generate(self) {
self.generate_extended(print_change_detection_instruction)
}
fn generate_extended<F>(self, mut f: F)
where
F: FnMut(&Path),
{
for path in &self.paths {
path.generate(&self, &mut f);
}
}
fn filter_include_exclude(&self, path: &Path) -> bool {
self.include
.as_ref()
.map_or(true, |filter| filter.matches(path))
&& self
.exclude
.as_ref()
.map_or(true, |filter| !filter.matches(path))
}
}
pub enum ChangeDetectionPath {
Path(PathBuf),
PathInclude(PathBuf, Box<dyn PathMatcher>),
PathExclude(PathBuf, Box<dyn PathMatcher>),
PathIncludeExclude {
path: PathBuf,
include: Box<dyn PathMatcher>,
exclude: Box<dyn PathMatcher>,
},
}
fn print_change_detection_instruction(path: &Path) {
println!(
"cargo:rerun-if-changed={}",
path.to_slash().expect("can't convert path to utf-8 string")
);
}
impl ChangeDetectionPath {
fn collect(&self, builder: &ChangeDetectionBuilder) -> std::io::Result<Vec<PathBuf>> {
let filter_fn: Box<dyn Fn(&_) -> bool> =
Box::new(|path: &std::path::Path| builder.filter_include_exclude(path));
let (path, filter): (&PathBuf, Box<dyn Fn(&_) -> bool>) = match self {
ChangeDetectionPath::Path(path) => (path, filter_fn),
ChangeDetectionPath::PathInclude(path, include_filter) => (
path,
Box::new(move |p: &Path| filter_fn(p.as_ref()) && include_filter.matches(p)),
),
ChangeDetectionPath::PathExclude(path, exclude_filter) => (
path,
Box::new(move |p: &Path| filter_fn(p.as_ref()) && !exclude_filter.matches(p)),
),
ChangeDetectionPath::PathIncludeExclude {
path,
include,
exclude,
} => (
path,
Box::new(move |p: &Path| {
filter_fn(p.as_ref()) && include.matches(p) && !exclude.matches(p)
}),
),
};
collect_resources(path, &filter)
}
fn generate<F>(&self, builder: &ChangeDetectionBuilder, printer: &mut F)
where
F: FnMut(&Path),
{
for path in self.collect(builder).expect("error collecting resources") {
printer(path.as_ref());
}
}
}
impl<T> From<T> for ChangeDetectionPath
where
T: AsRef<Path>,
{
fn from(path: T) -> Self {
ChangeDetectionPath::Path(path.as_ref().into())
}
}
fn collect_resources(path: &Path, filter: &dyn PathMatcher) -> std::io::Result<Vec<PathBuf>> {
let mut result = vec![];
if filter.matches(path.as_ref()) {
result.push(path.into());
}
if !path.is_dir() {
return Ok(result);
}
for entry in std::fs::read_dir(&path)? {
let entry = entry?;
let path = entry.path();
let nested = collect_resources(path.as_ref(), filter)?;
result.extend(nested);
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::{ChangeDetection, ChangeDetectionBuilder};
use std::path::{Path, PathBuf};
fn assert_change_detection(builder: ChangeDetectionBuilder, expected: &[&str]) {
let mut result: Vec<PathBuf> = vec![];
let r = &mut result;
builder.generate_extended(move |path| r.push(path.into()));
let mut expected = expected
.iter()
.map(|s| PathBuf::from(s))
.collect::<Vec<_>>();
expected.sort();
result.sort();
assert_eq!(result, expected);
}
#[test]
fn single_file() {
assert_change_detection(ChangeDetection::path("src/lib.rs"), &["src/lib.rs"]);
}
#[test]
fn single_path() {
assert_change_detection(ChangeDetection::path("src"), &["src", "src/lib.rs"]);
}
#[test]
fn fixture_01() {
assert_change_detection(
ChangeDetection::path("fixtures-01"),
&[
"fixtures-01",
"fixtures-01/a",
"fixtures-01/ab",
"fixtures-01/b",
"fixtures-01/bc",
"fixtures-01/c",
"fixtures-01/cd",
],
);
}
#[test]
fn fixture_01_global_include() {
assert_change_detection(
ChangeDetection::include(|path: &Path| {
path.file_name()
.map(|filename| filename.to_str().unwrap().ends_with("b"))
.unwrap_or(false)
})
.path("fixtures-01"),
&["fixtures-01/ab", "fixtures-01/b"],
);
}
#[test]
fn fixture_01_global_exclude() {
assert_change_detection(
ChangeDetection::exclude(|path: &Path| {
path.file_name()
.map(|filename| filename.to_str().unwrap().ends_with("b"))
.unwrap_or(false)
})
.path("fixtures-01"),
&[
"fixtures-01",
"fixtures-01/a",
"fixtures-01/bc",
"fixtures-01/c",
"fixtures-01/cd",
],
);
}
#[test]
fn fixture_01_global_filter() {
assert_change_detection(
ChangeDetection::filter(
|path: &Path| {
path.file_name()
.map(|filename| filename.to_str().unwrap().ends_with("b"))
.unwrap_or(false)
},
|path: &Path| {
path.file_name()
.map(|filename| filename.to_str().unwrap().starts_with("a"))
.unwrap_or(false)
},
)
.path("fixtures-01"),
&["fixtures-01/b"],
);
}
#[test]
fn fixture_02() {
assert_change_detection(
ChangeDetection::path("fixtures-02"),
&[
"fixtures-02",
"fixtures-02/abc",
"fixtures-02/def",
"fixtures-02/ghk",
],
);
}
#[test]
fn fixture_03() {
assert_change_detection(
ChangeDetection::path("fixtures-03"),
&[
"fixtures-03",
"fixtures-03/hello",
"fixtures-03/hello.c",
"fixtures-03/hello.js",
],
);
}
#[test]
fn all_fixtures() {
assert_change_detection(
ChangeDetection::path("fixtures-01")
.path("fixtures-02")
.path("fixtures-03"),
&[
"fixtures-01",
"fixtures-01/a",
"fixtures-01/ab",
"fixtures-01/b",
"fixtures-01/bc",
"fixtures-01/c",
"fixtures-01/cd",
"fixtures-02",
"fixtures-02/abc",
"fixtures-02/def",
"fixtures-02/ghk",
"fixtures-03",
"fixtures-03/hello",
"fixtures-03/hello.c",
"fixtures-03/hello.js",
],
);
}
#[test]
#[cfg(feature = "glob")]
fn path_matchers() {
use path_matchers::glob;
assert_change_detection(
ChangeDetection::include(glob("**/a*").unwrap())
.path("fixtures-01")
.path("fixtures-02")
.path("fixtures-03"),
&["fixtures-01/a", "fixtures-01/ab", "fixtures-02/abc"],
);
}
#[test]
fn npm_example() {
assert_change_detection(
ChangeDetection::path_exclude("fixtures-04", |path: &Path| {
path.to_str() == Some("fixtures-04") || !path.is_dir()
}),
&[
"fixtures-04/dist",
"fixtures-04/dist/imgs",
"fixtures-04/src",
"fixtures-04/src/imgs",
],
);
}
}