use globset::{Glob, GlobSet, GlobSetBuilder};
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error(
"unknown convention `{0}`; valid values: `snake_case`, `CamelCase`, `camelCase`, `SCREAMING_SNAKE_CASE`, `kebab-case`"
)]
pub struct UnknownConvention(pub String);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Convention {
SnakeCase,
CamelCase,
LowerCamelCase,
ScreamingSnakeCase,
KebabCase,
}
impl Convention {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::SnakeCase => "snake_case",
Self::CamelCase => "CamelCase",
Self::LowerCamelCase => "camelCase",
Self::ScreamingSnakeCase => "SCREAMING_SNAKE_CASE",
Self::KebabCase => "kebab-case",
}
}
#[must_use]
pub fn is_valid(&self, stem: &str) -> bool {
let Some(first) = stem.chars().next() else {
return false;
};
match self {
Self::SnakeCase => {
first.is_ascii_lowercase()
&& stem
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
&& !stem.contains("__")
&& !stem.ends_with('_')
}
Self::CamelCase => {
first.is_ascii_uppercase() && stem.chars().all(|c| c.is_ascii_alphanumeric())
}
Self::LowerCamelCase => {
first.is_ascii_lowercase() && stem.chars().all(|c| c.is_ascii_alphanumeric())
}
Self::ScreamingSnakeCase => {
first.is_ascii_uppercase()
&& stem
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
&& !stem.contains("__")
&& !stem.ends_with('_')
}
Self::KebabCase => {
first.is_ascii_lowercase()
&& stem
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
&& !stem.contains("--")
&& !stem.ends_with('-')
}
}
}
}
impl FromStr for Convention {
type Err = UnknownConvention;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"snake_case" => Ok(Self::SnakeCase),
"CamelCase" | "PascalCase" => Ok(Self::CamelCase),
"camelCase" => Ok(Self::LowerCamelCase),
"SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnakeCase),
"kebab-case" => Ok(Self::KebabCase),
other => Err(UnknownConvention(other.to_owned())),
}
}
}
impl fmt::Display for Convention {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct Matcher {
include: Option<GlobSet>,
exclude: Option<GlobSet>,
}
impl Matcher {
pub fn new(include: &[String], exclude: &[String]) -> Result<Self, globset::Error> {
let build_set = |patterns: &[String]| -> Result<Option<GlobSet>, globset::Error> {
if patterns.is_empty() {
return Ok(None);
}
let mut builder = GlobSetBuilder::new();
for p in patterns {
builder.add(Glob::new(p)?);
}
Ok(Some(builder.build()?))
};
Ok(Self {
include: build_set(include)?,
exclude: build_set(exclude)?,
})
}
#[must_use]
pub fn is_match(&self, filename: &str) -> bool {
if let Some(ref exc) = self.exclude {
if exc.is_match(filename) {
return false;
}
}
self.include
.as_ref()
.is_none_or(|inc| inc.is_match(filename))
}
}