use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use anyhow::Result;
use crate::maven::dependency_context::{DependencyContext, DependencyScope, ResolvedDependency};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ResolutionScope {
Compile,
CompilePlusRuntime,
Runtime,
Test,
}
impl ResolutionScope {
pub fn included_scopes(&self) -> HashSet<DependencyScope> {
match self {
ResolutionScope::Compile => {
[DependencyScope::Compile, DependencyScope::Provided, DependencyScope::System]
.into_iter().collect()
}
ResolutionScope::CompilePlusRuntime => {
[DependencyScope::Compile, DependencyScope::Provided,
DependencyScope::System, DependencyScope::Runtime]
.into_iter().collect()
}
ResolutionScope::Runtime => {
[DependencyScope::Compile, DependencyScope::Runtime]
.into_iter().collect()
}
ResolutionScope::Test => {
[DependencyScope::Compile, DependencyScope::Provided,
DependencyScope::System, DependencyScope::Runtime, DependencyScope::Test]
.into_iter().collect()
}
}
}
}
#[derive(Debug, Clone)]
pub struct DependencyResolutionRequest {
pub project_group_id: String,
pub project_artifact_id: String,
pub project_version: String,
pub dependencies: Vec<DependencySpec>,
pub managed_dependencies: HashMap<String, String>,
pub scope: ResolutionScope,
pub exclusions: HashSet<String>,
pub local_repository: PathBuf,
pub remote_repositories: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct DependencySpec {
pub group_id: String,
pub artifact_id: String,
pub version: Option<String>,
pub scope: DependencyScope,
pub optional: bool,
pub classifier: Option<String>,
pub artifact_type: String,
pub exclusions: Vec<String>,
}
impl DependencySpec {
pub fn new(group_id: impl Into<String>, artifact_id: impl Into<String>) -> Self {
Self {
group_id: group_id.into(),
artifact_id: artifact_id.into(),
version: None,
scope: DependencyScope::Compile,
optional: false,
classifier: None,
artifact_type: "jar".to_string(),
exclusions: Vec::new(),
}
}
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
pub fn with_scope(mut self, scope: DependencyScope) -> Self {
self.scope = scope;
self
}
pub fn key(&self) -> String {
format!("{}:{}", self.group_id, self.artifact_id)
}
}
impl DependencyResolutionRequest {
pub fn new(
group_id: impl Into<String>,
artifact_id: impl Into<String>,
version: impl Into<String>,
) -> Self {
Self {
project_group_id: group_id.into(),
project_artifact_id: artifact_id.into(),
project_version: version.into(),
dependencies: Vec::new(),
managed_dependencies: HashMap::new(),
scope: ResolutionScope::Compile,
exclusions: HashSet::new(),
local_repository: PathBuf::from(
std::env::var("HOME").unwrap_or_else(|_| ".".to_string())
).join(".m2/repository"),
remote_repositories: vec!["https://repo.maven.apache.org/maven2".to_string()],
}
}
pub fn with_scope(mut self, scope: ResolutionScope) -> Self {
self.scope = scope;
self
}
pub fn add_dependency(&mut self, dep: DependencySpec) {
self.dependencies.push(dep);
}
pub fn add_managed_dependency(&mut self, key: impl Into<String>, version: impl Into<String>) {
self.managed_dependencies.insert(key.into(), version.into());
}
}
#[derive(Debug)]
pub struct DependencyResolutionResult {
pub context: DependencyContext,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
impl DependencyResolutionResult {
pub fn new(context: DependencyContext) -> Self {
Self {
context,
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn is_success(&self) -> bool {
self.errors.is_empty()
}
pub fn add_error(&mut self, error: impl Into<String>) {
self.errors.push(error.into());
}
pub fn add_warning(&mut self, warning: impl Into<String>) {
self.warnings.push(warning.into());
}
}
pub struct ProjectDependenciesResolver {
local_repository: PathBuf,
}
impl ProjectDependenciesResolver {
pub fn new(local_repository: PathBuf) -> Self {
Self { local_repository }
}
pub fn resolve(&self, request: &DependencyResolutionRequest) -> Result<DependencyResolutionResult> {
let context = DependencyContext::new();
let mut result = DependencyResolutionResult::new(context);
let included_scopes = request.scope.included_scopes();
for dep_spec in &request.dependencies {
if !included_scopes.contains(&dep_spec.scope) {
continue;
}
if request.exclusions.contains(&dep_spec.key()) {
continue;
}
let version = dep_spec.version.clone()
.or_else(|| request.managed_dependencies.get(&dep_spec.key()).cloned())
.unwrap_or_else(|| "LATEST".to_string());
let artifact_path = self.find_artifact(
&dep_spec.group_id,
&dep_spec.artifact_id,
&version,
dep_spec.classifier.as_deref(),
&dep_spec.artifact_type,
);
let mut resolved = ResolvedDependency::new(
&dep_spec.group_id,
&dep_spec.artifact_id,
&version,
).with_scope(dep_spec.scope);
if let Some(path) = artifact_path {
resolved = resolved.with_file(path);
} else {
result.add_warning(format!(
"Artifact not found in local repository: {}:{}:{}",
dep_spec.group_id, dep_spec.artifact_id, version
));
}
if let Some(ref classifier) = dep_spec.classifier {
resolved = resolved.with_classifier(classifier);
}
result.context.add(resolved);
}
Ok(result)
}
fn find_artifact(
&self,
group_id: &str,
artifact_id: &str,
version: &str,
classifier: Option<&str>,
artifact_type: &str,
) -> Option<PathBuf> {
let group_path = group_id.replace('.', "/");
let filename = if let Some(classifier) = classifier {
format!("{artifact_id}-{version}-{classifier}.{artifact_type}")
} else {
format!("{artifact_id}-{version}.{artifact_type}")
};
let path = self.local_repository
.join(&group_path)
.join(artifact_id)
.join(version)
.join(&filename);
if path.exists() {
Some(path)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolution_scope_included_scopes() {
let compile_scopes = ResolutionScope::Compile.included_scopes();
assert!(compile_scopes.contains(&DependencyScope::Compile));
assert!(compile_scopes.contains(&DependencyScope::Provided));
assert!(!compile_scopes.contains(&DependencyScope::Test));
let test_scopes = ResolutionScope::Test.included_scopes();
assert!(test_scopes.contains(&DependencyScope::Compile));
assert!(test_scopes.contains(&DependencyScope::Test));
}
#[test]
fn test_dependency_spec() {
let spec = DependencySpec::new("com.example", "lib")
.with_version("1.0.0")
.with_scope(DependencyScope::Test);
assert_eq!(spec.key(), "com.example:lib");
assert_eq!(spec.version, Some("1.0.0".to_string()));
assert_eq!(spec.scope, DependencyScope::Test);
}
#[test]
fn test_dependency_resolution_request() {
let mut request = DependencyResolutionRequest::new("com.example", "app", "1.0.0")
.with_scope(ResolutionScope::Test);
request.add_dependency(DependencySpec::new("junit", "junit").with_version("4.13.2"));
request.add_managed_dependency("org.slf4j:slf4j-api", "2.0.0");
assert_eq!(request.dependencies.len(), 1);
assert!(request.managed_dependencies.contains_key("org.slf4j:slf4j-api"));
}
#[test]
fn test_project_dependencies_resolver() {
let resolver = ProjectDependenciesResolver::new(PathBuf::from("/tmp/repo"));
let request = DependencyResolutionRequest::new("com.example", "app", "1.0.0");
let result = resolver.resolve(&request).unwrap();
assert!(result.is_success());
}
}