use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DependencyScope {
Compile,
Provided,
Runtime,
Test,
System,
Import,
}
impl DependencyScope {
pub fn as_str(&self) -> &'static str {
match self {
DependencyScope::Compile => "compile",
DependencyScope::Provided => "provided",
DependencyScope::Runtime => "runtime",
DependencyScope::Test => "test",
DependencyScope::System => "system",
DependencyScope::Import => "import",
}
}
pub fn in_compile_classpath(&self) -> bool {
matches!(
self,
DependencyScope::Compile | DependencyScope::Provided | DependencyScope::System
)
}
pub fn in_runtime_classpath(&self) -> bool {
matches!(self, DependencyScope::Compile | DependencyScope::Runtime)
}
pub fn in_test_classpath(&self) -> bool {
!matches!(self, DependencyScope::Import)
}
}
impl FromStr for DependencyScope {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"compile" => Ok(DependencyScope::Compile),
"provided" => Ok(DependencyScope::Provided),
"runtime" => Ok(DependencyScope::Runtime),
"test" => Ok(DependencyScope::Test),
"system" => Ok(DependencyScope::System),
"import" => Ok(DependencyScope::Import),
_ => Err(format!("Invalid dependency scope: {}", s)),
}
}
}
#[derive(Debug, Clone)]
pub struct ResolvedDependency {
pub group_id: String,
pub artifact_id: String,
pub version: String,
pub scope: DependencyScope,
pub file: Option<PathBuf>,
pub optional: bool,
pub classifier: Option<String>,
pub artifact_type: String,
pub dependencies: Vec<ResolvedDependency>,
}
impl ResolvedDependency {
pub fn new(
group_id: impl Into<String>,
artifact_id: impl Into<String>,
version: impl Into<String>,
) -> Self {
Self {
group_id: group_id.into(),
artifact_id: artifact_id.into(),
version: version.into(),
scope: DependencyScope::Compile,
file: None,
optional: false,
classifier: None,
artifact_type: "jar".to_string(),
dependencies: Vec::new(),
}
}
pub fn with_scope(mut self, scope: DependencyScope) -> Self {
self.scope = scope;
self
}
pub fn with_file(mut self, file: PathBuf) -> Self {
self.file = Some(file);
self
}
pub fn with_classifier(mut self, classifier: impl Into<String>) -> Self {
self.classifier = Some(classifier.into());
self
}
pub fn gav(&self) -> String {
format!("{}:{}:{}", self.group_id, self.artifact_id, self.version)
}
pub fn full_coordinate(&self) -> String {
if let Some(ref classifier) = self.classifier {
format!(
"{}:{}:{}:{}",
self.group_id, self.artifact_id, self.version, classifier
)
} else {
self.gav()
}
}
}
#[derive(Debug, Default)]
pub struct DependencyContext {
dependencies: Vec<ResolvedDependency>,
dependency_index: HashMap<String, usize>,
exclusions: HashSet<String>,
scope_filter: Option<HashSet<DependencyScope>>,
}
impl DependencyContext {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, dep: ResolvedDependency) {
let gav = dep.gav();
if !self.dependency_index.contains_key(&gav) {
let index = self.dependencies.len();
self.dependencies.push(dep);
self.dependency_index.insert(gav, index);
}
}
pub fn add_exclusion(&mut self, pattern: impl Into<String>) {
self.exclusions.insert(pattern.into());
}
pub fn is_excluded(&self, group_id: &str, artifact_id: &str) -> bool {
let key = format!("{group_id}:{artifact_id}");
self.exclusions.contains(&key)
|| self.exclusions.contains(&format!("{group_id}:*"))
|| self.exclusions.contains("*:*")
}
pub fn with_scope_filter(mut self, scopes: HashSet<DependencyScope>) -> Self {
self.scope_filter = Some(scopes);
self
}
pub fn all(&self) -> &[ResolvedDependency] {
&self.dependencies
}
pub fn compile_classpath(&self) -> Vec<&ResolvedDependency> {
self.dependencies
.iter()
.filter(|d| d.scope.in_compile_classpath())
.collect()
}
pub fn runtime_classpath(&self) -> Vec<&ResolvedDependency> {
self.dependencies
.iter()
.filter(|d| d.scope.in_runtime_classpath())
.collect()
}
pub fn test_classpath(&self) -> Vec<&ResolvedDependency> {
self.dependencies
.iter()
.filter(|d| d.scope.in_test_classpath())
.collect()
}
pub fn classpath_paths(&self, include_test: bool) -> Vec<PathBuf> {
let deps = if include_test {
self.test_classpath()
} else {
self.compile_classpath()
};
deps.iter().filter_map(|d| d.file.clone()).collect()
}
pub fn classpath_string(&self, include_test: bool) -> String {
let paths = self.classpath_paths(include_test);
let separator = if cfg!(windows) { ";" } else { ":" };
paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<_>>()
.join(separator)
}
pub fn get(&self, gav: &str) -> Option<&ResolvedDependency> {
self.dependency_index
.get(gav)
.map(|&i| &self.dependencies[i])
}
pub fn contains(&self, gav: &str) -> bool {
self.dependency_index.contains_key(gav)
}
pub fn len(&self) -> usize {
self.dependencies.len()
}
pub fn is_empty(&self) -> bool {
self.dependencies.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dependency_scope() {
assert_eq!(
DependencyScope::from_str("compile"),
Ok(DependencyScope::Compile)
);
assert_eq!(DependencyScope::from_str("TEST"), Ok(DependencyScope::Test));
assert_eq!(DependencyScope::Compile.as_str(), "compile");
}
#[test]
fn test_scope_classpath_inclusion() {
assert!(DependencyScope::Compile.in_compile_classpath());
assert!(DependencyScope::Compile.in_runtime_classpath());
assert!(DependencyScope::Compile.in_test_classpath());
assert!(DependencyScope::Provided.in_compile_classpath());
assert!(!DependencyScope::Provided.in_runtime_classpath());
assert!(!DependencyScope::Test.in_compile_classpath());
assert!(!DependencyScope::Test.in_runtime_classpath());
assert!(DependencyScope::Test.in_test_classpath());
}
#[test]
fn test_resolved_dependency() {
let dep = ResolvedDependency::new("com.example", "lib", "1.0.0")
.with_scope(DependencyScope::Test)
.with_file(PathBuf::from("/repo/lib-1.0.0.jar"));
assert_eq!(dep.gav(), "com.example:lib:1.0.0");
assert_eq!(dep.scope, DependencyScope::Test);
assert!(dep.file.is_some());
}
#[test]
fn test_dependency_context() {
let mut ctx = DependencyContext::new();
ctx.add(
ResolvedDependency::new("g", "a", "1.0")
.with_scope(DependencyScope::Compile)
.with_file(PathBuf::from("/a.jar")),
);
ctx.add(
ResolvedDependency::new("g", "b", "1.0")
.with_scope(DependencyScope::Test)
.with_file(PathBuf::from("/b.jar")),
);
assert_eq!(ctx.len(), 2);
assert_eq!(ctx.compile_classpath().len(), 1);
assert_eq!(ctx.test_classpath().len(), 2);
}
#[test]
fn test_exclusions() {
let mut ctx = DependencyContext::new();
ctx.add_exclusion("com.example:excluded");
assert!(ctx.is_excluded("com.example", "excluded"));
assert!(!ctx.is_excluded("com.example", "included"));
}
#[test]
fn test_classpath_string() {
let mut ctx = DependencyContext::new();
ctx.add(ResolvedDependency::new("g", "a", "1.0").with_file(PathBuf::from("/a.jar")));
ctx.add(ResolvedDependency::new("g", "b", "1.0").with_file(PathBuf::from("/b.jar")));
let cp = ctx.classpath_string(false);
assert!(cp.contains("/a.jar"));
assert!(cp.contains("/b.jar"));
}
}