use crate::domain::artifact::value_objects::{ArtifactCoordinates, Scope};
use crate::domain::maven::value_objects::LifecyclePhase;
use crate::domain::shared::value_objects::{FilePath, JavaVersion, Version};
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MavenProject {
coordinates: ArtifactCoordinates,
name: Option<String>,
description: Option<String>,
url: Option<String>,
base_directory: FilePath,
source_directory: FilePath,
test_source_directory: FilePath,
output_directory: FilePath,
test_output_directory: FilePath,
java_version: JavaVersion,
dependencies: Vec<MavenDependency>,
dependency_management: Vec<MavenDependency>,
plugins: Vec<MavenPlugin>,
plugin_management: Vec<MavenPlugin>,
properties: HashMap<String, String>,
parent: Option<ParentReference>,
modules: Vec<String>,
packaging: PackagingType,
}
impl MavenProject {
pub fn new(
coordinates: ArtifactCoordinates,
base_directory: impl Into<PathBuf>,
) -> Result<Self> {
let base_path = base_directory.into();
if !coordinates.is_valid() {
return Err(anyhow!("Invalid artifact coordinates"));
}
Ok(Self {
coordinates,
name: None,
description: None,
url: None,
base_directory: FilePath::new(base_path.clone()),
source_directory: FilePath::new(base_path.join("src/main/java")),
test_source_directory: FilePath::new(base_path.join("src/test/java")),
output_directory: FilePath::new(base_path.join("target/classes")),
test_output_directory: FilePath::new(base_path.join("target/test-classes")),
java_version: JavaVersion::new(17, 0, 0), dependencies: Vec::new(),
dependency_management: Vec::new(),
plugins: Vec::new(),
plugin_management: Vec::new(),
properties: HashMap::new(),
parent: None,
modules: Vec::new(),
packaging: PackagingType::Jar,
})
}
pub fn coordinates(&self) -> &ArtifactCoordinates {
&self.coordinates
}
pub fn version(&self) -> Version {
Version::new(self.coordinates.version())
}
pub fn with_metadata(
mut self,
name: Option<String>,
description: Option<String>,
url: Option<String>,
) -> Self {
self.name = name;
self.description = description;
self.url = url;
self
}
pub fn with_java_version(mut self, version: JavaVersion) -> Self {
self.java_version = version;
self
}
pub fn with_packaging(mut self, packaging: PackagingType) -> Self {
self.packaging = packaging;
self
}
pub fn add_dependency(&mut self, dependency: MavenDependency) -> Result<()> {
if self
.dependencies
.iter()
.any(|d| d.coordinates == dependency.coordinates)
{
return Err(anyhow!(
"Dependency {} already exists",
dependency.coordinates.gav()
));
}
self.dependencies.push(dependency);
Ok(())
}
pub fn add_plugin(&mut self, plugin: MavenPlugin) -> Result<()> {
if self
.plugins
.iter()
.any(|p| p.coordinates == plugin.coordinates)
{
return Err(anyhow!(
"Plugin {} already exists",
plugin.coordinates.gav()
));
}
self.plugins.push(plugin);
Ok(())
}
pub fn set_property(&mut self, key: String, value: String) {
self.properties.insert(key, value);
}
pub fn get_property(&self, key: &str) -> Option<&String> {
self.properties.get(key)
}
pub fn add_module(&mut self, module: String) -> Result<()> {
if self.modules.contains(&module) {
return Err(anyhow!("Module {module} already exists"));
}
self.modules.push(module);
Ok(())
}
pub fn dependencies_for_scope(&self, scope: Scope) -> Vec<&MavenDependency> {
self.dependencies
.iter()
.filter(|d| d.scope == scope)
.collect()
}
pub fn compile_dependencies(&self) -> Vec<&MavenDependency> {
self.dependencies
.iter()
.filter(|d| matches!(d.scope, Scope::Compile | Scope::Provided | Scope::System))
.collect()
}
pub fn test_dependencies(&self) -> Vec<&MavenDependency> {
self.dependencies
.iter()
.filter(|d| d.scope == Scope::Test)
.collect()
}
pub fn is_multi_module(&self) -> bool {
!self.modules.is_empty()
}
pub fn validate(&self) -> Result<()> {
if !self.coordinates.is_valid() {
return Err(anyhow!("Invalid project coordinates"));
}
if self.is_multi_module() && self.packaging != PackagingType::Pom {
return Err(anyhow!(
"Multi-module projects must have POM packaging, found {:?}",
self.packaging
));
}
let module_set: std::collections::HashSet<_> = self.modules.iter().collect();
if module_set.len() != self.modules.len() {
return Err(anyhow!("Duplicate modules detected"));
}
Ok(())
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn base_directory(&self) -> &FilePath {
&self.base_directory
}
pub fn source_directory(&self) -> &FilePath {
&self.source_directory
}
pub fn output_directory(&self) -> &FilePath {
&self.output_directory
}
pub fn java_version(&self) -> &JavaVersion {
&self.java_version
}
pub fn dependencies(&self) -> &[MavenDependency] {
&self.dependencies
}
pub fn plugins(&self) -> &[MavenPlugin] {
&self.plugins
}
pub fn modules(&self) -> &[String] {
&self.modules
}
pub fn packaging(&self) -> &PackagingType {
&self.packaging
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MavenDependency {
coordinates: ArtifactCoordinates,
scope: Scope,
optional: bool,
exclusions: Vec<ArtifactCoordinates>,
}
impl MavenDependency {
pub fn new(coordinates: ArtifactCoordinates, scope: Scope) -> Self {
Self {
coordinates,
scope,
optional: false,
exclusions: Vec::new(),
}
}
pub fn with_optional(mut self, optional: bool) -> Self {
self.optional = optional;
self
}
pub fn add_exclusion(&mut self, exclusion: ArtifactCoordinates) {
self.exclusions.push(exclusion);
}
pub fn coordinates(&self) -> &ArtifactCoordinates {
&self.coordinates
}
pub fn scope(&self) -> Scope {
self.scope
}
pub fn is_optional(&self) -> bool {
self.optional
}
pub fn exclusions(&self) -> &[ArtifactCoordinates] {
&self.exclusions
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MavenPlugin {
coordinates: ArtifactCoordinates,
executions: Vec<PluginExecution>,
configuration: HashMap<String, String>,
}
impl MavenPlugin {
pub fn new(coordinates: ArtifactCoordinates) -> Self {
Self {
coordinates,
executions: Vec::new(),
configuration: HashMap::new(),
}
}
pub fn add_execution(&mut self, execution: PluginExecution) {
self.executions.push(execution);
}
pub fn set_configuration(&mut self, key: String, value: String) {
self.configuration.insert(key, value);
}
pub fn coordinates(&self) -> &ArtifactCoordinates {
&self.coordinates
}
pub fn executions(&self) -> &[PluginExecution] {
&self.executions
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PluginExecution {
id: String,
pub phase: Option<LifecyclePhase>,
pub goals: Vec<String>,
}
impl PluginExecution {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
phase: None,
goals: Vec::new(),
}
}
pub fn with_phase(mut self, phase: LifecyclePhase) -> Self {
self.phase = Some(phase);
self
}
pub fn add_goal(&mut self, goal: String) {
self.goals.push(goal);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ParentReference {
coordinates: ArtifactCoordinates,
relative_path: Option<String>,
}
impl ParentReference {
pub fn new(coordinates: ArtifactCoordinates) -> Self {
Self {
coordinates,
relative_path: Some("../pom.xml".to_string()),
}
}
pub fn with_relative_path(mut self, path: String) -> Self {
self.relative_path = Some(path);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PackagingType {
Jar,
War,
Ear,
Pom,
MavenPlugin,
}
impl PackagingType {
pub fn as_str(&self) -> &str {
match self {
PackagingType::Jar => "jar",
PackagingType::War => "war",
PackagingType::Ear => "ear",
PackagingType::Pom => "pom",
PackagingType::MavenPlugin => "maven-plugin",
}
}
}
impl FromStr for PackagingType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"jar" => Ok(PackagingType::Jar),
"war" => Ok(PackagingType::War),
"ear" => Ok(PackagingType::Ear),
"pom" => Ok(PackagingType::Pom),
"maven-plugin" => Ok(PackagingType::MavenPlugin),
_ => Err(format!("Invalid packaging type: {}", s)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_maven_project_creation() {
let coords = ArtifactCoordinates::new("com.example", "my-app", "1.0.0").unwrap();
let project = MavenProject::new(coords, "/tmp/project").unwrap();
assert_eq!(project.coordinates().group_id(), "com.example");
assert_eq!(project.coordinates().artifact_id(), "my-app");
assert_eq!(project.version().as_str(), "1.0.0");
}
#[test]
fn test_add_dependency() {
let coords = ArtifactCoordinates::new("com.example", "my-app", "1.0.0").unwrap();
let mut project = MavenProject::new(coords, "/tmp/project").unwrap();
let dep_coords = ArtifactCoordinates::new("org.junit", "junit", "4.13").unwrap();
let dep = MavenDependency::new(dep_coords, Scope::Test);
assert!(project.add_dependency(dep).is_ok());
assert_eq!(project.dependencies().len(), 1);
}
#[test]
fn test_duplicate_dependency_rejected() {
let coords = ArtifactCoordinates::new("com.example", "my-app", "1.0.0").unwrap();
let mut project = MavenProject::new(coords, "/tmp/project").unwrap();
let dep_coords = ArtifactCoordinates::new("org.junit", "junit", "4.13").unwrap();
let dep1 = MavenDependency::new(dep_coords.clone(), Scope::Test);
let dep2 = MavenDependency::new(dep_coords, Scope::Test);
assert!(project.add_dependency(dep1).is_ok());
assert!(project.add_dependency(dep2).is_err());
}
#[test]
fn test_multi_module_validation() {
let coords = ArtifactCoordinates::new("com.example", "parent", "1.0.0").unwrap();
let mut project = MavenProject::new(coords, "/tmp/project")
.unwrap()
.with_packaging(PackagingType::Pom);
project.add_module("module1".to_string()).unwrap();
project.add_module("module2".to_string()).unwrap();
assert!(project.validate().is_ok());
assert!(project.is_multi_module());
}
#[test]
fn test_multi_module_must_be_pom() {
let coords = ArtifactCoordinates::new("com.example", "parent", "1.0.0").unwrap();
let mut project = MavenProject::new(coords, "/tmp/project").unwrap();
project.add_module("module1".to_string()).unwrap();
assert!(project.validate().is_err());
}
#[test]
fn test_dependencies_by_scope() {
let coords = ArtifactCoordinates::new("com.example", "my-app", "1.0.0").unwrap();
let mut project = MavenProject::new(coords, "/tmp/project").unwrap();
let compile_dep = MavenDependency::new(
ArtifactCoordinates::new("com.google.guava", "guava", "32.0").unwrap(),
Scope::Compile,
);
let test_dep = MavenDependency::new(
ArtifactCoordinates::new("org.junit", "junit", "4.13").unwrap(),
Scope::Test,
);
project.add_dependency(compile_dep).unwrap();
project.add_dependency(test_dep).unwrap();
assert_eq!(project.compile_dependencies().len(), 1);
assert_eq!(project.test_dependencies().len(), 1);
}
}