#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DockerIgnoreParseError {
EmptyPattern,
UnknownScope,
}
impl fmt::Display for DockerIgnoreParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyPattern => formatter.write_str("Docker ignore pattern cannot be empty"),
Self::UnknownScope => formatter.write_str("unknown Docker ignore scope"),
}
}
}
impl Error for DockerIgnoreParseError {}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum DockerIgnoreNegation {
Normal,
Negated,
}
impl DockerIgnoreNegation {
#[must_use]
pub const fn is_negated(self) -> bool {
matches!(self, Self::Negated)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum DockerIgnoreScope {
Blank,
Comment,
Pattern,
Directory,
}
impl DockerIgnoreScope {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Blank => "blank",
Self::Comment => "comment",
Self::Pattern => "pattern",
Self::Directory => "directory",
}
}
}
impl fmt::Display for DockerIgnoreScope {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for DockerIgnoreScope {
type Err = DockerIgnoreParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"blank" => Ok(Self::Blank),
"comment" => Ok(Self::Comment),
"pattern" => Ok(Self::Pattern),
"directory" | "dir" => Ok(Self::Directory),
_ => Err(DockerIgnoreParseError::UnknownScope),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DockerIgnorePattern(String);
impl DockerIgnorePattern {
pub fn new(value: impl AsRef<str>) -> Result<Self, DockerIgnoreParseError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
Err(DockerIgnoreParseError::EmptyPattern)
} else {
Ok(Self(trimmed.to_string()))
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for DockerIgnorePattern {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for DockerIgnorePattern {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DockerIgnoreRule {
original: String,
pattern: Option<DockerIgnorePattern>,
negation: DockerIgnoreNegation,
scope: DockerIgnoreScope,
}
impl DockerIgnoreRule {
pub fn parse(value: impl AsRef<str>) -> Result<Self, DockerIgnoreParseError> {
let original = value.as_ref().to_string();
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
return Ok(Self {
original,
pattern: None,
negation: DockerIgnoreNegation::Normal,
scope: DockerIgnoreScope::Blank,
});
}
if trimmed.starts_with('#') {
return Ok(Self {
original,
pattern: None,
negation: DockerIgnoreNegation::Normal,
scope: DockerIgnoreScope::Comment,
});
}
let (negation, pattern_text) = trimmed
.strip_prefix('!')
.map_or((DockerIgnoreNegation::Normal, trimmed), |rest| {
(DockerIgnoreNegation::Negated, rest)
});
let pattern = DockerIgnorePattern::new(pattern_text)?;
let scope = if pattern.as_str().ends_with('/') {
DockerIgnoreScope::Directory
} else {
DockerIgnoreScope::Pattern
};
Ok(Self {
original,
pattern: Some(pattern),
negation,
scope,
})
}
#[must_use]
pub fn original(&self) -> &str {
&self.original
}
#[must_use]
pub const fn pattern(&self) -> Option<&DockerIgnorePattern> {
self.pattern.as_ref()
}
#[must_use]
pub const fn negation(&self) -> DockerIgnoreNegation {
self.negation
}
#[must_use]
pub const fn scope(&self) -> DockerIgnoreScope {
self.scope
}
#[must_use]
pub const fn is_comment(&self) -> bool {
matches!(self.scope, DockerIgnoreScope::Comment)
}
#[must_use]
pub const fn is_blank(&self) -> bool {
matches!(self.scope, DockerIgnoreScope::Blank)
}
#[must_use]
pub const fn is_directory_only(&self) -> bool {
matches!(self.scope, DockerIgnoreScope::Directory)
}
}
#[cfg(test)]
mod tests {
use super::{DockerIgnoreNegation, DockerIgnoreRule, DockerIgnoreScope};
#[test]
fn classifies_dockerignore_lines() -> Result<(), Box<dyn std::error::Error>> {
let rule = DockerIgnoreRule::parse("!target/")?;
let comment = DockerIgnoreRule::parse("# generated files")?;
assert_eq!(rule.negation(), DockerIgnoreNegation::Negated);
assert_eq!(rule.scope(), DockerIgnoreScope::Directory);
assert!(rule.is_directory_only());
assert!(comment.is_comment());
Ok(())
}
}