use globset::{Glob, GlobSet, GlobSetBuilder};
use crate::format::OutputFormat;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ProcessorOptions {
pub(crate) output_format: OutputFormat,
pub(crate) ignore_extensions: Vec<String>,
pub(crate) ignore_globs: GlobSet,
pub(crate) ignore_glob_strings: Vec<String>,
pub(crate) include_dotfiles: bool,
pub(crate) max_files: Option<usize>,
pub(crate) max_output_bytes: Option<usize>,
pub(crate) root_label: String,
}
impl Default for ProcessorOptions {
fn default() -> Self {
Self {
output_format: OutputFormat::default(),
ignore_extensions: Vec::new(),
ignore_globs: GlobSet::empty(),
ignore_glob_strings: Vec::new(),
include_dotfiles: false,
max_files: None,
max_output_bytes: None,
root_label: String::from("."),
}
}
}
impl ProcessorOptions {
#[must_use]
pub fn builder() -> ProcessorOptionsBuilder {
ProcessorOptionsBuilder::default()
}
#[must_use]
pub const fn output_format(&self) -> OutputFormat {
self.output_format
}
#[must_use]
pub fn ignore_extensions(&self) -> &[String] {
&self.ignore_extensions
}
#[must_use]
pub fn ignore_glob_strings(&self) -> &[String] {
&self.ignore_glob_strings
}
#[must_use]
pub const fn include_dotfiles(&self) -> bool {
self.include_dotfiles
}
#[must_use]
pub const fn max_files(&self) -> Option<usize> {
self.max_files
}
#[must_use]
pub const fn max_output_bytes(&self) -> Option<usize> {
self.max_output_bytes
}
#[must_use]
pub fn root_label(&self) -> &str {
&self.root_label
}
}
fn compile_globs(patterns: &[String]) -> super::Result<GlobSet> {
if patterns.is_empty() {
return Ok(GlobSet::empty());
}
let mut builder = GlobSetBuilder::new();
for pattern in patterns {
let _ = builder.add(Glob::new(pattern)?);
}
Ok(builder.build()?)
}
#[derive(Debug, Default)]
pub struct ProcessorOptionsBuilder {
output_format: OutputFormat,
ignore_extensions: Vec<String>,
ignore_glob_strings: Vec<String>,
include_dotfiles: bool,
max_files: Option<usize>,
max_output_bytes: Option<usize>,
root_label: Option<String>,
}
impl ProcessorOptionsBuilder {
#[must_use]
pub const fn output_format(mut self, format: OutputFormat) -> Self {
self.output_format = format;
self
}
#[must_use]
pub fn ignore_extensions(mut self, exts: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.ignore_extensions = exts.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn ignore_globs(mut self, patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.ignore_glob_strings = patterns.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub const fn include_dotfiles(mut self, include: bool) -> Self {
self.include_dotfiles = include;
self
}
#[must_use]
pub const fn max_files(mut self, max: usize) -> Self {
self.max_files = Some(max);
self
}
#[must_use]
pub const fn max_output_bytes(mut self, max: usize) -> Self {
self.max_output_bytes = Some(max);
self
}
#[must_use]
pub fn root_label(mut self, label: impl Into<String>) -> Self {
self.root_label = Some(label.into());
self
}
pub fn build(self) -> super::Result<ProcessorOptions> {
let ignore_globs = compile_globs(&self.ignore_glob_strings)?;
Ok(ProcessorOptions {
output_format: self.output_format,
ignore_extensions: self.ignore_extensions,
ignore_globs,
ignore_glob_strings: self.ignore_glob_strings,
include_dotfiles: self.include_dotfiles,
max_files: self.max_files,
max_output_bytes: self.max_output_bytes,
root_label: self.root_label.unwrap_or_else(|| String::from(".")),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_options() {
let opts = ProcessorOptions::default();
assert_eq!(opts.output_format(), OutputFormat::Markdown);
assert!(!opts.include_dotfiles());
assert!(opts.max_files().is_none());
assert!(opts.max_output_bytes().is_none());
assert!(opts.ignore_extensions().is_empty());
assert_eq!(opts.root_label(), ".");
}
#[test]
fn builder_round_trip() {
let opts = ProcessorOptions::builder()
.output_format(OutputFormat::Tree)
.ignore_extensions(["lock", "min.js"])
.ignore_globs(["**/target/**"])
.include_dotfiles(true)
.max_files(100)
.root_label("my-project")
.build()
.unwrap();
assert_eq!(opts.output_format(), OutputFormat::Tree);
assert_eq!(opts.ignore_extensions(), &["lock", "min.js"]);
assert!(opts.include_dotfiles());
assert_eq!(opts.max_files(), Some(100));
assert_eq!(opts.root_label(), "my-project");
}
#[test]
fn builder_accepts_slices_and_arrays() {
let opts = ProcessorOptions::builder()
.ignore_extensions(["lock", "log"])
.ignore_globs(["**/target/**"])
.build()
.unwrap();
assert_eq!(opts.ignore_extensions(), &["lock", "log"]);
let exts = vec!["lock".to_string(), "log".to_string()];
let opts = ProcessorOptions::builder()
.ignore_extensions(exts)
.build()
.unwrap();
assert_eq!(opts.ignore_extensions(), &["lock", "log"]);
let source = ["lock", "log"];
let opts = ProcessorOptions::builder()
.ignore_extensions(source.iter().copied())
.build()
.unwrap();
assert_eq!(opts.ignore_extensions(), &["lock", "log"]);
}
#[test]
fn glob_strings_round_trip() {
let opts = ProcessorOptions::builder()
.ignore_globs(["**/target/**", "dist/**"])
.build()
.unwrap();
assert_eq!(opts.ignore_glob_strings(), &["**/target/**", "dist/**"]);
}
#[test]
fn invalid_glob_rejected() {
let result = ProcessorOptionsBuilder::default()
.ignore_globs(["[invalid"])
.build();
assert!(result.is_err());
}
#[test]
fn output_format_serde() {
let json = serde_json::to_string(&OutputFormat::Tree).unwrap();
assert_eq!(json, r#""tree""#);
let parsed: OutputFormat = serde_json::from_str(r#""markdown""#).unwrap();
assert_eq!(parsed, OutputFormat::Markdown);
}
#[test]
fn builder_no_globs_is_infallible() {
let opts = ProcessorOptions::builder()
.output_format(OutputFormat::Markdown)
.build()
.unwrap();
assert_eq!(opts.output_format(), OutputFormat::Markdown);
}
}