use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct Configuration {
pub name: String,
pub description: Option<String>,
pub can_be_consumed: bool,
pub can_be_resolved: bool,
pub extends_from: Vec<String>,
pub dependencies: Vec<ConfigurationDependency>,
}
#[derive(Debug, Clone)]
pub struct ConfigurationDependency {
pub notation: String,
pub group: Option<String>,
pub name: Option<String>,
pub version: Option<String>,
pub is_project: bool,
pub project_path: Option<String>,
}
impl Configuration {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: None,
can_be_consumed: true,
can_be_resolved: true,
extends_from: Vec::new(),
dependencies: Vec::new(),
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn extends_from(mut self, config: impl Into<String>) -> Self {
self.extends_from.push(config.into());
self
}
pub fn consumable(mut self, value: bool) -> Self {
self.can_be_consumed = value;
self
}
pub fn resolvable(mut self, value: bool) -> Self {
self.can_be_resolved = value;
self
}
pub fn add_dependency(&mut self, dep: ConfigurationDependency) {
self.dependencies.push(dep);
}
}
impl ConfigurationDependency {
pub fn from_notation(notation: impl Into<String>) -> Self {
let notation = notation.into();
if notation.starts_with("project(") || notation.starts_with(":") {
let project_path = notation
.trim_start_matches("project(")
.trim_end_matches(')')
.trim_matches('\'')
.trim_matches('"')
.to_string();
return Self {
notation: notation.clone(),
group: None,
name: None,
version: None,
is_project: true,
project_path: Some(project_path),
};
}
let parts: Vec<&str> = notation.split(':').collect();
let (group, name, version) = match parts.len() {
3 => (Some(parts[0].to_string()), Some(parts[1].to_string()), Some(parts[2].to_string())),
2 => (Some(parts[0].to_string()), Some(parts[1].to_string()), None),
1 => (None, Some(parts[0].to_string()), None),
_ => (None, None, None),
};
Self {
notation,
group,
name,
version,
is_project: false,
project_path: None,
}
}
pub fn project(path: impl Into<String>) -> Self {
let path = path.into();
Self {
notation: format!("project('{path}')"),
group: None,
name: None,
version: None,
is_project: true,
project_path: Some(path),
}
}
}
#[derive(Debug, Default)]
pub struct ConfigurationContainer {
configurations: HashMap<String, Configuration>,
}
impl ConfigurationContainer {
pub fn new() -> Self {
Self::default()
}
pub fn with_java_defaults() -> Self {
let mut container = Self::new();
container.add(Configuration::new("api")
.with_description("API dependencies for this library")
.consumable(true)
.resolvable(false));
container.add(Configuration::new("implementation")
.with_description("Implementation only dependencies")
.extends_from("api")
.consumable(false)
.resolvable(false));
container.add(Configuration::new("compileClasspath")
.with_description("Compile classpath for source set 'main'")
.extends_from("implementation")
.consumable(false)
.resolvable(true));
container.add(Configuration::new("runtimeClasspath")
.with_description("Runtime classpath for source set 'main'")
.extends_from("implementation")
.consumable(false)
.resolvable(true));
container.add(Configuration::new("testImplementation")
.with_description("Implementation only dependencies for tests")
.extends_from("implementation")
.consumable(false)
.resolvable(false));
container.add(Configuration::new("testCompileClasspath")
.with_description("Compile classpath for source set 'test'")
.extends_from("testImplementation")
.extends_from("compileClasspath")
.consumable(false)
.resolvable(true));
container.add(Configuration::new("testRuntimeClasspath")
.with_description("Runtime classpath for source set 'test'")
.extends_from("testImplementation")
.extends_from("runtimeClasspath")
.consumable(false)
.resolvable(true));
container.add(Configuration::new("compileOnly")
.with_description("Compile only dependencies")
.consumable(false)
.resolvable(false));
container.add(Configuration::new("runtimeOnly")
.with_description("Runtime only dependencies")
.consumable(false)
.resolvable(false));
container.add(Configuration::new("annotationProcessor")
.with_description("Annotation processors and their dependencies")
.consumable(false)
.resolvable(true));
container
}
pub fn add(&mut self, config: Configuration) {
self.configurations.insert(config.name.clone(), config);
}
pub fn get(&self, name: &str) -> Option<&Configuration> {
self.configurations.get(name)
}
pub fn get_mut(&mut self, name: &str) -> Option<&mut Configuration> {
self.configurations.get_mut(name)
}
pub fn has(&self, name: &str) -> bool {
self.configurations.contains_key(name)
}
pub fn names(&self) -> Vec<String> {
self.configurations.keys().cloned().collect()
}
pub fn resolve_dependencies(&self, name: &str) -> Vec<ConfigurationDependency> {
let mut deps = Vec::new();
let mut visited = std::collections::HashSet::new();
self.collect_dependencies(name, &mut deps, &mut visited);
deps
}
fn collect_dependencies(
&self,
name: &str,
deps: &mut Vec<ConfigurationDependency>,
visited: &mut std::collections::HashSet<String>,
) {
if visited.contains(name) {
return;
}
visited.insert(name.to_string());
if let Some(config) = self.configurations.get(name) {
for extended in &config.extends_from {
self.collect_dependencies(extended, deps, visited);
}
deps.extend(config.dependencies.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_configuration_creation() {
let config = Configuration::new("implementation")
.with_description("Implementation dependencies")
.consumable(false)
.resolvable(false);
assert_eq!(config.name, "implementation");
assert!(!config.can_be_consumed);
assert!(!config.can_be_resolved);
}
#[test]
fn test_configuration_extends() {
let config = Configuration::new("testImplementation")
.extends_from("implementation");
assert_eq!(config.extends_from, vec!["implementation".to_string()]);
}
#[test]
fn test_dependency_from_notation() {
let dep = ConfigurationDependency::from_notation("org.junit:junit:4.13.2");
assert_eq!(dep.group, Some("org.junit".to_string()));
assert_eq!(dep.name, Some("junit".to_string()));
assert_eq!(dep.version, Some("4.13.2".to_string()));
assert!(!dep.is_project);
}
#[test]
fn test_project_dependency() {
let dep = ConfigurationDependency::project(":core");
assert!(dep.is_project);
assert_eq!(dep.project_path, Some(":core".to_string()));
}
#[test]
fn test_configuration_container_java_defaults() {
let container = ConfigurationContainer::with_java_defaults();
assert!(container.has("implementation"));
assert!(container.has("testImplementation"));
assert!(container.has("compileClasspath"));
assert!(container.has("runtimeClasspath"));
assert!(container.has("api"));
}
#[test]
fn test_resolve_dependencies() {
let mut container = ConfigurationContainer::new();
let mut api = Configuration::new("api");
api.add_dependency(ConfigurationDependency::from_notation("com.google:guava:31.0"));
container.add(api);
let mut impl_config = Configuration::new("implementation").extends_from("api");
impl_config.add_dependency(ConfigurationDependency::from_notation("org.slf4j:slf4j-api:2.0.0"));
container.add(impl_config);
let deps = container.resolve_dependencies("implementation");
assert_eq!(deps.len(), 2);
assert!(deps.iter().any(|d| d.notation.contains("guava")));
assert!(deps.iter().any(|d| d.notation.contains("slf4j")));
}
}