use anyhow::{bail, Context, Result};
use serde::Deserialize;
use std::collections::BTreeMap;
use std::path::Path;
#[derive(Debug)]
pub struct Descriptor {
pub kind: DescriptorKind,
pub java: Java,
pub test: Test,
pub kotlin: Kotlin,
pub groovy: Groovy,
pub spock: Spock,
pub native_image: NativeImage,
pub docker: Docker,
pub build_info: BuildInfo,
pub dependencies: BTreeMap<String, DependencyValue>,
pub test_dependencies: BTreeMap<String, DependencyValue>,
pub repositories: Vec<RepositoryEntry>,
pub bom_imports: BTreeMap<String, String>,
pub test_bom_imports: BTreeMap<String, String>,
pub inherited_bom_imports: BTreeMap<String, String>,
pub inherited_test_bom_imports: BTreeMap<String, String>,
pub workspace_dependencies: BTreeMap<String, WorkspaceDep>,
pub annotation_processors: BTreeMap<String, AnnotationProcessor>,
pub test_annotation_processors: BTreeMap<String, AnnotationProcessor>,
pub inherited_annotation_processors: BTreeMap<String, AnnotationProcessor>,
pub inherited_test_annotation_processors: BTreeMap<String, AnnotationProcessor>,
pub annotation_processor_options: BTreeMap<String, BTreeMap<String, String>>,
pub test_annotation_processor_options: BTreeMap<String, BTreeMap<String, String>>,
pub inherited_annotation_processor_options: BTreeMap<String, BTreeMap<String, String>>,
pub inherited_test_annotation_processor_options: BTreeMap<String, BTreeMap<String, String>>,
pub publish: PublishConfig,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum AnnotationProcessor {
Version(String),
Detailed(AnnotationProcessorDetailed),
}
#[derive(Debug, Deserialize, Clone)]
pub struct AnnotationProcessorDetailed {
pub version: String,
#[serde(default, rename = "on-compile-classpath")]
pub on_compile_classpath: bool,
}
impl AnnotationProcessor {
pub fn version(&self) -> &str {
match self {
AnnotationProcessor::Version(v) => v,
AnnotationProcessor::Detailed(d) => &d.version,
}
}
pub fn on_compile_classpath(&self) -> bool {
match self {
AnnotationProcessor::Version(_) => false,
AnnotationProcessor::Detailed(d) => d.on_compile_classpath,
}
}
}
#[derive(Debug)]
pub enum DescriptorKind {
Application(Application),
Library(Library),
Workspace(Workspace),
Bom(Bom),
}
#[derive(Debug, Deserialize)]
struct RawDescriptor {
application: Option<Application>,
library: Option<Library>,
workspace: Option<Workspace>,
bom: Option<Bom>,
#[serde(default)]
java: Java,
#[serde(default)]
docker: Docker,
#[serde(rename = "build-info", default)]
build_info: BuildInfo,
#[serde(default)]
dependencies: BTreeMap<String, DependencyValue>,
#[serde(rename = "test-dependencies", default)]
test_dependencies: BTreeMap<String, DependencyValue>,
#[serde(default)]
repositories: Vec<RepositoryEntry>,
#[serde(rename = "bom-imports", default)]
bom_imports: BTreeMap<String, String>,
#[serde(rename = "test-bom-imports", default)]
test_bom_imports: BTreeMap<String, String>,
#[serde(rename = "workspace-dependencies", default)]
workspace_dependencies: BTreeMap<String, WorkspaceDep>,
#[serde(rename = "annotation-processors", default)]
annotation_processors: BTreeMap<String, AnnotationProcessor>,
#[serde(rename = "test-annotation-processors", default)]
test_annotation_processors: BTreeMap<String, AnnotationProcessor>,
#[serde(rename = "annotation-processor-options", default)]
annotation_processor_options: BTreeMap<String, BTreeMap<String, String>>,
#[serde(rename = "test-annotation-processor-options", default)]
test_annotation_processor_options: BTreeMap<String, BTreeMap<String, String>>,
#[serde(default)]
test: Test,
#[serde(default)]
kotlin: Kotlin,
#[serde(default)]
groovy: Groovy,
#[serde(default)]
spock: Spock,
#[serde(rename = "native-image", default)]
native_image: NativeImage,
#[serde(default)]
publish: PublishConfig,
}
#[derive(Debug, Deserialize, Clone)]
pub struct WorkspaceDep {
pub path: String,
#[serde(default)]
#[allow(dead_code)]
pub version: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct Application {
pub name: String,
pub version: String,
#[serde(rename = "groupId", default)]
pub group_id: Option<String>,
#[serde(rename = "mainClass")]
pub main_class: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct Library {
pub name: String,
pub version: String,
#[serde(rename = "groupId", default)]
pub group_id: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct Workspace {
pub members: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct Bom {
pub name: String,
pub version: String,
#[serde(rename = "groupId", default)]
pub group_id: Option<String>,
}
#[derive(Debug, Deserialize, Default)]
pub struct Java {
#[serde(rename = "sourceCompatibility")]
pub source_compatibility: Option<String>,
#[serde(rename = "enablePreview")]
pub enable_preview: Option<bool>,
}
impl Java {
pub fn effective(&self) -> &str {
self.source_compatibility.as_deref().unwrap_or("21")
}
pub fn preview_enabled(&self) -> bool {
self.enable_preview.unwrap_or(false)
}
}
pub const DEFAULT_JUNIT_PLATFORM_VERSION: &str = "6.0.3";
pub const DEFAULT_KOTLIN_VERSION: &str = "2.1.21";
#[derive(Debug, Deserialize, Default, Clone)]
pub struct Test {
#[serde(rename = "junitPlatformVersion", default)]
pub junit_platform_version: Option<String>,
}
impl Test {
pub fn junit_platform_version(&self) -> &str {
self.junit_platform_version
.as_deref()
.unwrap_or(DEFAULT_JUNIT_PLATFORM_VERSION)
}
pub fn junit_platform_version_is_user_set(&self) -> bool {
self.junit_platform_version.is_some()
}
}
#[derive(Debug, Deserialize, Default, Clone)]
pub struct Kotlin {
#[serde(default)]
pub version: Option<String>,
}
impl Kotlin {
pub fn version(&self) -> &str {
self.version.as_deref().unwrap_or(DEFAULT_KOTLIN_VERSION)
}
}
pub const DEFAULT_GROOVY_VERSION: &str = "5.0.6";
#[derive(Debug, Deserialize, Default, Clone)]
pub struct NativeImage {
#[serde(rename = "outputName", default)]
pub output_name: Option<String>,
#[serde(rename = "configDir", default)]
pub config_dir: Option<String>,
#[serde(rename = "extraArgs", default)]
pub extra_args: Vec<String>,
#[serde(skip)]
pub section_present: bool,
}
impl NativeImage {
pub fn resolved_output_name<'a>(&'a self, app_name: &'a str) -> &'a str {
self.output_name.as_deref().unwrap_or(app_name)
}
}
#[derive(Debug, Deserialize, Default, Clone)]
pub struct Groovy {
#[serde(default)]
pub version: Option<String>,
}
impl Groovy {
pub fn version(&self) -> &str {
self.version.as_deref().unwrap_or(DEFAULT_GROOVY_VERSION)
}
}
pub const DEFAULT_SPOCK_VERSION: &str = "2.4-groovy-5.0";
#[derive(Debug, Deserialize, Default, Clone)]
pub struct Spock {
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub enabled: Option<bool>,
#[serde(skip)]
pub section_present: bool,
}
impl Spock {
pub fn version(&self) -> &str {
self.version.as_deref().unwrap_or(DEFAULT_SPOCK_VERSION)
}
pub fn enabled(&self) -> bool {
self.enabled.unwrap_or(self.section_present)
}
}
#[derive(Debug, Deserialize)]
pub struct Docker {
#[serde(rename = "baseImage", default = "default_base_image")]
pub base_image: String,
#[serde(rename = "imageName")]
pub image_name: Option<String>,
#[serde(rename = "imageTag")]
pub image_tag: Option<String>,
#[serde(skip)]
pub section_present: bool,
}
fn default_base_image() -> String {
"eclipse-temurin:21-jre-alpine".to_string()
}
impl Default for Docker {
fn default() -> Self {
Docker {
base_image: default_base_image(),
image_name: None,
image_tag: None,
section_present: false,
}
}
}
#[derive(Debug, Deserialize)]
pub struct BuildInfo {
#[serde(default = "default_build_info_enabled")]
pub enabled: bool,
}
fn default_build_info_enabled() -> bool {
true
}
impl Default for BuildInfo {
fn default() -> Self {
BuildInfo { enabled: true }
}
}
#[derive(Debug, Deserialize, Default, Clone)]
pub struct PublishConfig {
pub repository: Option<String>,
pub url: Option<String>,
#[serde(default = "default_true")]
pub sign: bool,
#[serde(default = "default_true")]
pub javadoc: bool,
pub description: Option<String>,
pub homepage: Option<String>,
#[serde(default)]
pub licenses: Vec<String>,
#[serde(default)]
pub developers: Vec<Developer>,
pub scm: Option<Scm>,
}
#[derive(Debug, Deserialize, Default, Clone)]
pub struct Developer {
pub id: Option<String>,
pub name: Option<String>,
pub email: Option<String>,
}
#[derive(Debug, Deserialize, Default, Clone)]
pub struct Scm {
pub url: Option<String>,
pub connection: Option<String>,
#[serde(rename = "developerConnection")]
pub developer_connection: Option<String>,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Deserialize, Clone)]
pub struct RepositoryEntry {
pub id: String,
pub name: Option<String>,
pub url: String,
}
impl RepositoryEntry {
pub fn display_name(&self) -> &str {
self.name.as_deref().unwrap_or(&self.id)
}
}
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum DependencyValue {
Version(String),
Detailed(DependencyDetailed),
}
#[derive(Debug, Deserialize, Clone)]
pub struct DependencyDetailed {
pub version: String,
#[serde(default)]
pub repository: Option<String>,
}
impl DependencyValue {
pub fn version(&self) -> &str {
match self {
DependencyValue::Version(v) => v,
DependencyValue::Detailed(d) => &d.version,
}
}
pub fn repository(&self) -> Option<&str> {
match self {
DependencyValue::Version(_) => None,
DependencyValue::Detailed(d) => d.repository.as_deref(),
}
}
}
impl Descriptor {
pub fn is_library(&self) -> bool {
matches!(self.kind, DescriptorKind::Library(_))
}
pub fn is_workspace(&self) -> bool {
matches!(self.kind, DescriptorKind::Workspace(_))
}
pub fn is_bom(&self) -> bool {
matches!(self.kind, DescriptorKind::Bom(_))
}
pub fn application(&self) -> Option<&Application> {
match &self.kind {
DescriptorKind::Application(a) => Some(a),
_ => None,
}
}
pub fn workspace(&self) -> Option<&Workspace> {
match &self.kind {
DescriptorKind::Workspace(w) => Some(w),
_ => None,
}
}
pub fn kind_label(&self) -> &'static str {
match &self.kind {
DescriptorKind::Application(_) => "application",
DescriptorKind::Library(_) => "library",
DescriptorKind::Workspace(_) => "workspace",
DescriptorKind::Bom(_) => "bom",
}
}
pub fn project_name(&self) -> Option<&str> {
match &self.kind {
DescriptorKind::Application(a) => Some(&a.name),
DescriptorKind::Library(l) => Some(&l.name),
DescriptorKind::Workspace(_) => None,
DescriptorKind::Bom(b) => Some(&b.name),
}
}
pub fn group_id(&self) -> Option<&str> {
match &self.kind {
DescriptorKind::Application(a) => a.group_id.as_deref(),
DescriptorKind::Library(l) => l.group_id.as_deref(),
DescriptorKind::Workspace(_) => None,
DescriptorKind::Bom(b) => b.group_id.as_deref(),
}
}
pub fn project_version(&self) -> Option<&str> {
match &self.kind {
DescriptorKind::Application(a) => Some(&a.version),
DescriptorKind::Library(l) => Some(&l.version),
DescriptorKind::Workspace(_) => None,
DescriptorKind::Bom(b) => Some(&b.version),
}
}
pub fn buildable_name(&self) -> &str {
self.project_name()
.expect("buildable_name() called on a workspace descriptor")
}
pub fn buildable_version(&self) -> &str {
self.project_version()
.expect("buildable_version() called on a workspace descriptor")
}
pub fn image_name(&self) -> &str {
self.docker
.image_name
.as_deref()
.or_else(|| self.project_name())
.expect("image_name() called on a workspace descriptor")
}
pub fn image_tag(&self) -> &str {
self.docker
.image_tag
.as_deref()
.or_else(|| self.project_version())
.expect("image_tag() called on a workspace descriptor")
}
pub fn image_ref(&self) -> String {
format!("{}:{}", self.image_name(), self.image_tag())
}
pub fn prod_bom_gavs(&self) -> anyhow::Result<Vec<curie_deps::Gav>> {
let mut v: Vec<curie_deps::Gav> = self
.inherited_bom_imports
.iter()
.map(|(k, ver)| curie_deps::Gav::from_key_version(k, ver))
.collect::<anyhow::Result<_>>()
.context("invalid coordinate in workspace [bom-imports]")?;
let own: Vec<curie_deps::Gav> = self
.bom_imports
.iter()
.map(|(k, ver)| curie_deps::Gav::from_key_version(k, ver))
.collect::<anyhow::Result<_>>()
.context("invalid coordinate in [bom-imports]")?;
v.extend(own);
Ok(v)
}
pub fn test_bom_gavs(&self) -> anyhow::Result<Vec<curie_deps::Gav>> {
let mut v = self.prod_bom_gavs()?;
let inherited_test: Vec<curie_deps::Gav> = self
.inherited_test_bom_imports
.iter()
.map(|(k, ver)| curie_deps::Gav::from_key_version(k, ver))
.collect::<anyhow::Result<_>>()
.context("invalid coordinate in workspace [test-bom-imports]")?;
v.extend(inherited_test);
let own_test: Vec<curie_deps::Gav> = self
.test_bom_imports
.iter()
.map(|(k, ver)| curie_deps::Gav::from_key_version(k, ver))
.collect::<anyhow::Result<_>>()
.context("invalid coordinate in [test-bom-imports]")?;
v.extend(own_test);
Ok(v)
}
pub fn ap_pairs(&self) -> Vec<(&str, &str)> {
ap_pairs_merged(&self.inherited_annotation_processors, &self.annotation_processors)
}
pub fn test_ap_pairs(&self) -> Vec<(&str, &str)> {
ap_pairs_merged(
&self.inherited_test_annotation_processors,
&self.test_annotation_processors,
)
}
pub fn ap_on_compile_classpath_coords(&self) -> Vec<&str> {
let mut out: Vec<&str> = Vec::new();
for map in [&self.inherited_annotation_processors, &self.annotation_processors] {
for (k, v) in map {
if v.on_compile_classpath() {
out.push(k.as_str());
}
}
}
out
}
pub fn test_ap_on_compile_classpath_coords(&self) -> Vec<&str> {
let mut out = self.ap_on_compile_classpath_coords();
for map in [
&self.inherited_test_annotation_processors,
&self.test_annotation_processors,
] {
for (k, v) in map {
if v.on_compile_classpath() {
out.push(k.as_str());
}
}
}
out
}
pub fn flat_ap_options(&self) -> Vec<(String, String)> {
flatten_ap_options(
&self.inherited_annotation_processor_options,
&self.annotation_processor_options,
)
}
pub fn flat_test_ap_options(&self) -> Vec<(String, String)> {
let mut merged = self.flat_ap_options();
let test = flatten_ap_options(
&self.inherited_test_annotation_processor_options,
&self.test_annotation_processor_options,
);
for (k, v) in test {
if let Some(existing) = merged.iter_mut().find(|(ek, _)| ek == &k) {
existing.1 = v;
} else {
merged.push((k, v));
}
}
merged
}
}
fn ap_pairs_merged<'a>(
inherited: &'a BTreeMap<String, AnnotationProcessor>,
own: &'a BTreeMap<String, AnnotationProcessor>,
) -> Vec<(&'a str, &'a str)> {
let mut out: Vec<(&'a str, &'a str)> = Vec::with_capacity(inherited.len() + own.len());
for (k, v) in inherited {
if !own.contains_key(k) {
out.push((k.as_str(), v.version()));
}
}
for (k, v) in own {
out.push((k.as_str(), v.version()));
}
out
}
fn flatten_ap_options(
inherited: &BTreeMap<String, BTreeMap<String, String>>,
own: &BTreeMap<String, BTreeMap<String, String>>,
) -> Vec<(String, String)> {
let mut merged: BTreeMap<String, BTreeMap<String, String>> = inherited.clone();
for (prefix, inner) in own {
let dst = merged.entry(prefix.clone()).or_default();
for (k, v) in inner {
dst.insert(k.clone(), v.clone());
}
}
let mut out: Vec<(String, String)> = Vec::new();
for (prefix, inner) in &merged {
for (k, v) in inner {
out.push((format!("{}.{}", prefix, k), v.clone()));
}
}
out
}
pub fn load(project_root: &Path) -> Result<Descriptor> {
let path = project_root.join("Curie.toml");
if !path.exists() {
bail!(
"no Curie.toml found in {}",
project_root.display()
);
}
let content = std::fs::read_to_string(&path)
.with_context(|| format!("failed to read {}", path.display()))?;
let raw: toml::Value = toml::from_str(&content)
.map_err(|e| format_parse_error(e, &content, &path))?;
let table = raw.as_table();
let docker_section_present = table.map(|t| t.contains_key("docker")).unwrap_or(false);
let parsed: RawDescriptor = toml::from_str(&content)
.map_err(|e| format_parse_error(e, &content, &path))?;
let kind = match (parsed.application, parsed.library, parsed.workspace, parsed.bom) {
(Some(a), None, None, None) => DescriptorKind::Application(a),
(None, Some(l), None, None) => DescriptorKind::Library(l),
(None, None, Some(w), None) => DescriptorKind::Workspace(w),
(None, None, None, Some(b)) => DescriptorKind::Bom(b),
(None, None, None, None) => bail!(
"Curie.toml must contain one of [application], [library], [workspace], or [bom]"
),
_ => bail!(
"Curie.toml must contain only one of [application], [library], [workspace], or [bom]"
),
};
let mut docker = parsed.docker;
docker.section_present = docker_section_present;
let native_image_section_present = table.map(|t| t.contains_key("native-image")).unwrap_or(false);
let mut native_image = parsed.native_image;
native_image.section_present = native_image_section_present;
let spock_section_present = table.map(|t| t.contains_key("spock")).unwrap_or(false);
let mut spock = parsed.spock;
spock.section_present = spock_section_present;
let descriptor = Descriptor {
kind,
java: parsed.java,
test: parsed.test,
kotlin: parsed.kotlin,
groovy: parsed.groovy,
spock,
native_image,
docker,
build_info: parsed.build_info,
dependencies: parsed.dependencies,
test_dependencies: parsed.test_dependencies,
repositories: parsed.repositories,
bom_imports: parsed.bom_imports,
test_bom_imports: parsed.test_bom_imports,
inherited_bom_imports: BTreeMap::new(),
inherited_test_bom_imports: BTreeMap::new(),
workspace_dependencies: parsed.workspace_dependencies,
annotation_processors: parsed.annotation_processors,
test_annotation_processors: parsed.test_annotation_processors,
inherited_annotation_processors: BTreeMap::new(),
inherited_test_annotation_processors: BTreeMap::new(),
annotation_processor_options: parsed.annotation_processor_options,
test_annotation_processor_options: parsed.test_annotation_processor_options,
inherited_annotation_processor_options: BTreeMap::new(),
inherited_test_annotation_processor_options: BTreeMap::new(),
publish: parsed.publish,
};
if descriptor.is_workspace() {
if !descriptor.dependencies.is_empty() {
bail!("workspace Curie.toml must not declare [dependencies] — declare them in each member");
}
if !descriptor.test_dependencies.is_empty() {
bail!("workspace Curie.toml must not declare [test-dependencies] — declare them in each member");
}
if !descriptor.workspace_dependencies.is_empty() {
bail!("workspace Curie.toml must not declare [workspace-dependencies] — declare them on each member");
}
if docker_section_present {
bail!("workspace Curie.toml must not declare [docker] — declare it on each application member");
}
}
for (label, dep) in &descriptor.workspace_dependencies {
if dep.version.is_some() {
bail!(
"workspace-dependency \"{}\" must not declare a version — \
the depended-on member's own version is used. Remove the \
`version` key from [workspace-dependencies.{}].",
label, label,
);
}
if dep.path.trim().is_empty() {
bail!("workspace-dependency \"{}\" has an empty `path`", label);
}
}
if descriptor.is_library() && docker_section_present {
bail!(
"library projects do not support Docker: remove the [docker] section from Curie.toml"
);
}
if descriptor.is_library() && native_image_section_present {
bail!(
"library projects do not support native-image compilation: \
remove the [native-image] section from Curie.toml"
);
}
if descriptor.is_bom() {
validate_bom_restrictions(&descriptor, docker_section_present, native_image_section_present,
table.map(|t| t.contains_key("test")).unwrap_or(false),
table.map(|t| t.contains_key("test-dependencies")).unwrap_or(false),
table.map(|t| t.contains_key("test-bom-imports")).unwrap_or(false),
table.map(|t| t.contains_key("annotation-processors")).unwrap_or(false),
table.map(|t| t.contains_key("test-annotation-processors")).unwrap_or(false),
)?;
}
validate_dep_repo_refs(&descriptor)?;
Ok(descriptor)
}
#[allow(clippy::too_many_arguments)]
fn validate_bom_restrictions(
desc: &Descriptor,
docker_present: bool,
native_image_present: bool,
test_present: bool,
test_deps_present: bool,
test_bom_imports_present: bool,
annotation_processors_present: bool,
test_annotation_processors_present: bool,
) -> Result<()> {
if docker_present {
bail!("BOM projects do not support Docker: remove the [docker] section from Curie.toml");
}
if native_image_present {
bail!("BOM projects do not support native-image compilation: remove the [native-image] section from Curie.toml");
}
if test_present {
bail!("BOM projects must not declare a [test] section");
}
if test_deps_present {
bail!("BOM projects must not declare [test-dependencies]");
}
if test_bom_imports_present {
bail!("BOM projects must not declare [test-bom-imports]");
}
if annotation_processors_present {
bail!("BOM projects must not declare [annotation-processors]");
}
if test_annotation_processors_present {
bail!("BOM projects must not declare [test-annotation-processors]");
}
for (coord, dep) in &desc.dependencies {
if dep.version().is_empty() {
bail!(
"BOM dependency \"{}\" must have an explicit version; \
BOM-delegated versions (\"\") are not allowed in [bom] projects",
coord
);
}
}
Ok(())
}
pub fn validate_dep_repo_refs(desc: &Descriptor) -> Result<()> {
let known_ids: std::collections::HashSet<&str> =
desc.repositories.iter().map(|r| r.id.as_str()).collect();
for (coord, dep) in &desc.dependencies {
if let Some(repo_id) = dep.repository() {
if !known_ids.contains(repo_id) {
bail!(
"dependency \"{}\" references unknown repository \"{}\"; \
declare it with [[repositories]]",
coord, repo_id
);
}
}
}
for (coord, dep) in &desc.test_dependencies {
if let Some(repo_id) = dep.repository() {
if !known_ids.contains(repo_id) {
bail!(
"test-dependency \"{}\" references unknown repository \"{}\"; \
declare it with [[repositories]]",
coord, repo_id
);
}
}
}
Ok(())
}
pub fn docker_enabled(project_root: &Path, desc: &Descriptor) -> bool {
desc.docker.section_present || project_root.join("Dockerfile").exists()
}
pub fn native_image_enabled(desc: &Descriptor) -> bool {
desc.native_image.section_present
}
fn format_parse_error(err: toml::de::Error, _source: &str, path: &Path) -> anyhow::Error {
let file_name = path
.file_name()
.map(|f| f.to_string_lossy().into_owned())
.unwrap_or_else(|| path.to_string_lossy().into_owned());
let raw_display = err.to_string();
let contextual = if let Some(rest) = raw_display.strip_prefix("TOML parse error at ") {
let reformatted = rest
.replacen("line ", "", 1)
.replacen(", column ", ":", 1);
format!(
"failed to parse {}\n\n --> {}:{}",
path.display(),
file_name,
reformatted
)
} else {
format!("failed to parse {}\n\n{}", path.display(), raw_display)
};
let message = raw_display
.lines()
.rev()
.find(|l| !l.trim().is_empty())
.unwrap_or("")
.trim();
let hint = hint_for(message, &file_name);
let full = if let Some(h) = hint {
format!("{}\n\n hint: {}", contextual, h)
} else {
contextual
};
anyhow::anyhow!("{}", full)
}
fn hint_for(message: &str, _file_name: &str) -> Option<String> {
if message.contains("missing field") && message.contains("name") {
return Some(
"[application], [library], and [bom] all require a `name` field.".to_string(),
);
}
if message.contains("missing field") && message.contains("version") {
return Some(
"[application], [library], and [bom] all require a `version` field.".to_string(),
);
}
if message.contains("unknown field") {
return Some(
"check for typos in field names; see the README for all supported fields.".to_string(),
);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn load_str(content: &str) -> Result<Descriptor> {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("Curie.toml"), content).unwrap();
load(dir.path())
}
#[test]
fn parse_workspace_with_members() {
let toml = r#"
[workspace]
members = ["a", "b", "nested/c"]
"#;
let d = load_str(toml).unwrap();
assert!(d.is_workspace());
assert_eq!(d.kind_label(), "workspace");
let ws = d.workspace().expect("workspace section present");
assert_eq!(ws.members, vec!["a", "b", "nested/c"]);
assert_eq!(d.project_name(), None);
assert_eq!(d.project_version(), None);
}
#[test]
fn parse_application_still_works() {
let toml = r#"
[application]
name = "x"
version = "1.0"
"#;
let d = load_str(toml).unwrap();
assert!(!d.is_workspace());
assert_eq!(d.kind_label(), "application");
assert_eq!(d.project_name(), Some("x"));
assert_eq!(d.project_version(), Some("1.0"));
assert!(d.application().is_some());
}
#[test]
fn workspace_with_application_is_rejected() {
let toml = r#"
[workspace]
members = ["a"]
[application]
name = "x"
version = "1.0"
"#;
let err = load_str(toml).unwrap_err().to_string();
assert!(err.contains("only one"), "got: {err}");
}
#[test]
fn workspace_with_library_is_rejected() {
let toml = r#"
[workspace]
members = ["a"]
[library]
name = "x"
version = "1.0"
"#;
let err = load_str(toml).unwrap_err().to_string();
assert!(err.contains("only one"), "got: {err}");
}
#[test]
fn workspace_with_dependencies_is_rejected() {
let toml = r#"
[workspace]
members = ["a"]
[dependencies]
"com.example:foo" = "1.0"
"#;
let err = load_str(toml).unwrap_err().to_string();
assert!(err.contains("[dependencies]"), "got: {err}");
}
#[test]
fn workspace_with_docker_is_rejected() {
let toml = r#"
[workspace]
members = ["a"]
[docker]
"#;
let err = load_str(toml).unwrap_err().to_string();
assert!(err.contains("[docker]"), "got: {err}");
}
#[test]
fn workspace_allows_shared_java_and_repositories() {
let toml = r#"
[workspace]
members = ["a"]
[java]
sourceCompatibility = "17"
[[repositories]]
id = "nexus"
url = "https://example.com/m2"
"#;
let d = load_str(toml).unwrap();
assert_eq!(d.java.effective(), "17");
assert_eq!(d.repositories.len(), 1);
assert_eq!(d.repositories[0].id, "nexus");
}
#[test]
fn empty_descriptor_is_rejected() {
let err = load_str("").unwrap_err().to_string();
assert!(err.contains("must contain one of"), "got: {err}");
}
#[test]
fn build_info_enabled_by_default() {
let toml = r#"
[application]
name = "x"
version = "1.0"
"#;
let d = load_str(toml).unwrap();
assert!(d.build_info.enabled, "build-info must be enabled by default");
}
#[test]
fn build_info_can_be_disabled() {
let toml = r#"
[application]
name = "x"
version = "1.0"
[build-info]
enabled = false
"#;
let d = load_str(toml).unwrap();
assert!(!d.build_info.enabled);
}
#[test]
fn build_info_explicitly_enabled() {
let toml = r#"
[application]
name = "x"
version = "1.0"
[build-info]
enabled = true
"#;
let d = load_str(toml).unwrap();
assert!(d.build_info.enabled);
}
#[test]
fn parse_workspace_dependencies_path_only() {
let toml = r#"
[application]
name = "x"
version = "1.0"
mainClass = "X"
[workspace-dependencies]
core = { path = "../core" }
data = { path = "../sibling/data" }
"#;
let d = load_str(toml).unwrap();
let core = d.workspace_dependencies.get("core").unwrap();
assert_eq!(core.path, "../core");
assert!(core.version.is_none());
assert_eq!(d.workspace_dependencies.len(), 2);
}
#[test]
fn workspace_dependency_with_version_is_rejected() {
let toml = r#"
[application]
name = "x"
version = "1.0"
mainClass = "X"
[workspace-dependencies]
core = { path = "../core", version = "1.0" }
"#;
let err = load_str(toml).unwrap_err().to_string();
assert!(err.contains("must not declare a version"), "got: {err}");
assert!(err.contains("core"), "got: {err}");
}
#[test]
fn workspace_dependency_with_empty_path_is_rejected() {
let toml = r#"
[application]
name = "x"
version = "1.0"
mainClass = "X"
[workspace-dependencies]
core = { path = "" }
"#;
let err = load_str(toml).unwrap_err().to_string();
assert!(err.contains("empty `path`"), "got: {err}");
}
#[test]
fn workspace_root_with_workspace_dependencies_is_rejected() {
let toml = r#"
[workspace]
members = ["a"]
[workspace-dependencies]
core = { path = "../core" }
"#;
let err = load_str(toml).unwrap_err().to_string();
assert!(err.contains("[workspace-dependencies]"), "got: {err}");
}
#[test]
fn parse_annotation_processors_both_forms() {
let toml = r#"
[application]
name = "x"
version = "1.0"
mainClass = "X"
[annotation-processors]
"com.google.dagger:dagger-compiler" = "2.50"
"org.projectlombok:lombok" = { version = "1.18.30", on-compile-classpath = true }
"#;
let d = load_str(toml).unwrap();
let dagger = d.annotation_processors.get("com.google.dagger:dagger-compiler").unwrap();
assert_eq!(dagger.version(), "2.50");
assert!(!dagger.on_compile_classpath());
let lombok = d.annotation_processors.get("org.projectlombok:lombok").unwrap();
assert_eq!(lombok.version(), "1.18.30");
assert!(lombok.on_compile_classpath());
}
#[test]
fn ap_pairs_returns_inherited_then_own() {
let toml = r#"
[application]
name = "x"
version = "1.0"
mainClass = "X"
[annotation-processors]
"own:proc" = "2.0"
"#;
let mut d = load_str(toml).unwrap();
d.inherited_annotation_processors.insert(
"ws:proc".into(),
AnnotationProcessor::Version("1.0".into()),
);
let pairs = d.ap_pairs();
assert_eq!(
pairs,
vec![("ws:proc", "1.0"), ("own:proc", "2.0")],
"inherited entries should come first so own can override on collision",
);
}
#[test]
fn ap_pairs_own_overrides_inherited_on_same_coord() {
let toml = r#"
[application]
name = "x"
version = "1.0"
mainClass = "X"
[annotation-processors]
"shared:proc" = "2.0"
"#;
let mut d = load_str(toml).unwrap();
d.inherited_annotation_processors.insert(
"shared:proc".into(),
AnnotationProcessor::Version("1.0".into()),
);
let pairs = d.ap_pairs();
assert_eq!(pairs, vec![("shared:proc", "2.0")]);
}
#[test]
fn test_ap_pairs_uses_test_table_only() {
let toml = r#"
[application]
name = "x"
version = "1.0"
mainClass = "X"
[annotation-processors]
"prod:proc" = "1.0"
[test-annotation-processors]
"test:proc" = "2.0"
"#;
let d = load_str(toml).unwrap();
assert_eq!(d.ap_pairs(), vec![("prod:proc", "1.0")]);
assert_eq!(d.test_ap_pairs(), vec![("test:proc", "2.0")]);
}
#[test]
fn on_compile_classpath_coords_listed() {
let toml = r#"
[application]
name = "x"
version = "1.0"
mainClass = "X"
[annotation-processors]
"org.projectlombok:lombok" = { version = "1.18.30", on-compile-classpath = true }
"com.google.dagger:dagger-compiler" = "2.50"
"#;
let d = load_str(toml).unwrap();
let on_cp = d.ap_on_compile_classpath_coords();
assert_eq!(on_cp, vec!["org.projectlombok:lombok"]);
}
#[test]
fn parse_nested_ap_options_emits_dotted_flags() {
let toml = r#"
[application]
name = "x"
version = "1.0"
mainClass = "X"
[annotation-processor-options.dagger]
fastInit = "enabled"
formatGeneratedSource = "disabled"
[annotation-processor-options.mapstruct]
suppressGeneratorTimestamp = "true"
"#;
let d = load_str(toml).unwrap();
let flat = d.flat_ap_options();
assert_eq!(
flat,
vec![
("dagger.fastInit".to_string(), "enabled".to_string()),
("dagger.formatGeneratedSource".to_string(), "disabled".to_string()),
("mapstruct.suppressGeneratorTimestamp".to_string(), "true".to_string()),
],
);
}
#[test]
fn ap_options_inheritance_member_overrides_per_key() {
let toml = r#"
[application]
name = "x"
version = "1.0"
mainClass = "X"
[annotation-processor-options.dagger]
fastInit = "enabled"
"#;
let mut d = load_str(toml).unwrap();
let mut ws_dagger = BTreeMap::new();
ws_dagger.insert("fastInit".to_string(), "disabled".to_string());
ws_dagger.insert("formatGeneratedSource".to_string(), "disabled".to_string());
d.inherited_annotation_processor_options.insert("dagger".to_string(), ws_dagger);
let flat = d.flat_ap_options();
assert_eq!(
flat,
vec![
("dagger.fastInit".to_string(), "enabled".to_string()),
("dagger.formatGeneratedSource".to_string(), "disabled".to_string()),
],
);
}
#[test]
fn flat_test_ap_options_layers_test_on_top_of_prod() {
let toml = r#"
[application]
name = "x"
version = "1.0"
mainClass = "X"
[annotation-processor-options.dagger]
fastInit = "enabled"
[test-annotation-processor-options.dagger]
fastInit = "disabled"
"#;
let d = load_str(toml).unwrap();
assert_eq!(
d.flat_ap_options(),
vec![("dagger.fastInit".to_string(), "enabled".to_string())],
);
assert_eq!(
d.flat_test_ap_options(),
vec![("dagger.fastInit".to_string(), "disabled".to_string())],
);
}
#[test]
fn workspace_may_declare_test_and_kotlin_versions() {
let toml = r#"
[workspace]
members = ["a"]
[test]
junitPlatformVersion = "6.0.3"
[kotlin]
version = "2.1.21"
"#;
let d = load_str(toml).unwrap();
assert!(d.is_workspace());
assert_eq!(d.test.junit_platform_version(), "6.0.3");
assert_eq!(d.kotlin.version(), "2.1.21");
}
#[test]
fn test_and_kotlin_versions_inherit_from_workspace_when_omitted() {
let toml = r#"
[workspace]
members = ["member"]
[test]
junitPlatformVersion = "6.1.0"
[kotlin]
version = "2.2.0"
"#;
let dir = tempfile::tempdir().unwrap();
let ws_path = dir.path();
std::fs::write(ws_path.join("Curie.toml"), toml).unwrap();
std::fs::create_dir(ws_path.join("member")).unwrap();
let member_toml = r#"
[application]
name = "member"
version = "0.0.0"
mainClass = "M"
"#;
std::fs::write(ws_path.join("member").join("Curie.toml"), member_toml).unwrap();
let ws = crate::workspace::load(ws_path).unwrap();
let member_desc = &ws.members[0].descriptor;
assert_eq!(member_desc.test.junit_platform_version(), "6.1.0");
assert_eq!(member_desc.kotlin.version(), "2.2.0");
}
#[test]
fn member_version_overrides_workspace_version() {
let toml = r#"
[workspace]
members = ["m"]
[test]
junitPlatformVersion = "6.0.3"
[kotlin]
version = "2.1.21"
"#;
let dir = tempfile::tempdir().unwrap();
let ws_path = dir.path();
std::fs::write(ws_path.join("Curie.toml"), toml).unwrap();
std::fs::create_dir(ws_path.join("m")).unwrap();
let member_toml = r#"
[application]
name = "m"
version = "0.0.0"
mainClass = "M"
[test]
junitPlatformVersion = "6.5.0"
[kotlin]
version = "1.9.25"
"#;
std::fs::write(ws_path.join("m").join("Curie.toml"), member_toml).unwrap();
let ws = crate::workspace::load(ws_path).unwrap();
let m = &ws.members[0].descriptor;
assert_eq!(m.test.junit_platform_version(), "6.5.0");
assert_eq!(m.kotlin.version(), "1.9.25");
}
#[test]
fn tool_versions_fall_back_to_defaults_when_absent() {
let toml = r#"
[application]
name = "x"
version = "0.1"
mainClass = "X"
"#;
let d = load_str(toml).unwrap();
assert_eq!(d.test.junit_platform_version(), crate::descriptor::DEFAULT_JUNIT_PLATFORM_VERSION);
assert_eq!(d.kotlin.version(), crate::descriptor::DEFAULT_KOTLIN_VERSION);
}
#[test]
fn parse_dependency_shorthand_form() {
let toml = r#"
[application]
name = "x"
version = "1.0"
[dependencies]
"com.example:foo" = "1.2.3"
"#;
let d = load_str(toml).unwrap();
let v = d.dependencies.get("com.example:foo").unwrap();
assert_eq!(v.version(), "1.2.3");
assert_eq!(v.repository(), None);
}
#[test]
fn parse_dependency_detailed_form_without_repo() {
let toml = r#"
[application]
name = "x"
version = "1.0"
[dependencies]
"com.example:foo" = { version = "2.0.0" }
"#;
let d = load_str(toml).unwrap();
let v = d.dependencies.get("com.example:foo").unwrap();
assert_eq!(v.version(), "2.0.0");
assert_eq!(v.repository(), None);
}
#[test]
fn parse_dependency_detailed_form_with_repo() {
let toml = r#"
[application]
name = "x"
version = "1.0"
[[repositories]]
id = "my-repo"
url = "https://repo.example.com/m2"
[dependencies]
"com.example:bar" = { version = "3.0.0", repository = "my-repo" }
"#;
let d = load_str(toml).unwrap();
let v = d.dependencies.get("com.example:bar").unwrap();
assert_eq!(v.version(), "3.0.0");
assert_eq!(v.repository(), Some("my-repo"));
}
#[test]
fn dep_with_unknown_repo_id_is_rejected() {
let toml = r#"
[application]
name = "x"
version = "1.0"
[dependencies]
"com.example:foo" = { version = "1.0", repository = "does-not-exist" }
"#;
let err = load_str(toml).unwrap_err().to_string();
assert!(err.contains("does-not-exist"), "expected unknown-repo error, got: {err}");
assert!(err.contains("[[repositories]]"), "should hint about [[repositories]], got: {err}");
}
#[test]
fn dep_with_known_repo_id_is_accepted() {
let toml = r#"
[application]
name = "x"
version = "1.0"
[[repositories]]
id = "shibboleth"
url = "https://build.shibboleth.net/nexus/content/repositories/releases/"
[dependencies]
"net.shibboleth.oidc:oidc-common-crypto-api" = { version = "3.3.0", repository = "shibboleth" }
"#;
let d = load_str(toml).unwrap();
let v = d.dependencies.get("net.shibboleth.oidc:oidc-common-crypto-api").unwrap();
assert_eq!(v.version(), "3.3.0");
assert_eq!(v.repository(), Some("shibboleth"));
}
#[test]
fn test_dep_with_unknown_repo_id_is_rejected() {
let toml = r#"
[application]
name = "x"
version = "1.0"
[test-dependencies]
"com.example:foo" = { version = "1.0", repository = "ghost" }
"#;
let err = load_str(toml).unwrap_err().to_string();
assert!(err.contains("ghost"), "expected unknown-repo error, got: {err}");
}
#[test]
fn repository_entry_display_name_defaults_to_id() {
let toml = r#"
[application]
name = "x"
version = "1.0"
[[repositories]]
id = "shibboleth"
url = "https://example.com"
"#;
let d = load_str(toml).unwrap();
assert_eq!(d.repositories[0].display_name(), "shibboleth");
}
#[test]
fn repository_entry_display_name_uses_name_when_set() {
let toml = r#"
[application]
name = "x"
version = "1.0"
[[repositories]]
id = "shibboleth"
name = "Shibboleth Releases"
url = "https://example.com"
"#;
let d = load_str(toml).unwrap();
assert_eq!(d.repositories[0].id, "shibboleth");
assert_eq!(d.repositories[0].display_name(), "Shibboleth Releases");
}
#[test]
fn spock_section_absent_is_disabled() {
let toml = r#"
[application]
name = "x"
version = "0.1"
mainClass = "X"
"#;
let d = load_str(toml).unwrap();
assert!(!d.spock.enabled(), "absent [spock] must leave enabled = false");
}
#[test]
fn spock_section_present_but_enabled_false_is_disabled() {
let toml = r#"
[application]
name = "x"
version = "0.1"
mainClass = "X"
[spock]
enabled = false
"#;
let d = load_str(toml).unwrap();
assert!(!d.spock.enabled(), "explicit enabled=false must override section presence");
}
#[test]
fn spock_section_present_is_enabled() {
let toml = r#"
[application]
name = "x"
version = "0.1"
mainClass = "X"
[spock]
"#;
let d = load_str(toml).unwrap();
assert!(d.spock.enabled(), "[spock] present must set enabled = true");
assert_eq!(d.spock.version(), crate::descriptor::DEFAULT_SPOCK_VERSION);
}
#[test]
fn spock_version_can_be_set() {
let toml = r#"
[application]
name = "x"
version = "0.1"
mainClass = "X"
[spock]
version = "2.4-groovy-4.0"
"#;
let d = load_str(toml).unwrap();
assert!(d.spock.enabled());
assert_eq!(d.spock.version(), "2.4-groovy-4.0");
}
#[test]
fn groovy_version_defaults_to_constant() {
let toml = r#"
[application]
name = "x"
version = "0.1"
mainClass = "X"
"#;
let d = load_str(toml).unwrap();
assert_eq!(d.groovy.version(), crate::descriptor::DEFAULT_GROOVY_VERSION);
}
#[test]
fn groovy_version_can_be_set() {
let toml = r#"
[application]
name = "x"
version = "0.1"
mainClass = "X"
[groovy]
version = "3.0.22"
"#;
let d = load_str(toml).unwrap();
assert_eq!(d.groovy.version(), "3.0.22");
}
#[test]
fn enable_preview_defaults_to_false() {
let toml = r#"
[application]
name = "x"
version = "0.1"
mainClass = "X"
"#;
let d = load_str(toml).unwrap();
assert!(!d.java.preview_enabled(), "enablePreview must default to false");
assert!(d.java.enable_preview.is_none(), "absent key must stay None for inheritance");
}
#[test]
fn enable_preview_explicit_false_is_distinguished_from_absent() {
let toml = r#"
[application]
name = "x"
version = "0.1"
mainClass = "X"
[java]
enablePreview = false
"#;
let d = load_str(toml).unwrap();
assert!(!d.java.preview_enabled());
assert_eq!(d.java.enable_preview, Some(false), "explicit false must be Some(false), not None");
}
#[test]
fn enable_preview_can_be_set_true() {
let toml = r#"
[application]
name = "x"
version = "0.1"
mainClass = "X"
[java]
sourceCompatibility = "21"
enablePreview = true
"#;
let d = load_str(toml).unwrap();
assert!(d.java.preview_enabled());
assert_eq!(d.java.effective(), "21");
}
#[test]
fn native_image_absent_means_disabled() {
let toml = r#"
[application]
name = "x"
version = "0.1"
mainClass = "X"
"#;
let d = load_str(toml).unwrap();
assert!(!d.native_image.section_present);
assert!(!native_image_enabled(&d));
}
#[test]
fn native_image_section_present_enables_it() {
let toml = r#"
[application]
name = "x"
version = "0.1"
mainClass = "X"
[native-image]
"#;
let d = load_str(toml).unwrap();
assert!(d.native_image.section_present);
assert!(native_image_enabled(&d));
}
#[test]
fn native_image_output_name_defaults_to_app_name() {
let toml = r#"
[application]
name = "my-app"
version = "0.1"
mainClass = "X"
[native-image]
"#;
let d = load_str(toml).unwrap();
assert_eq!(d.native_image.resolved_output_name("my-app"), "my-app");
}
#[test]
fn native_image_output_name_override() {
let toml = r#"
[application]
name = "my-app"
version = "0.1"
mainClass = "X"
[native-image]
outputName = "my-binary"
"#;
let d = load_str(toml).unwrap();
assert_eq!(d.native_image.output_name.as_deref(), Some("my-binary"));
assert_eq!(d.native_image.resolved_output_name("my-app"), "my-binary");
}
#[test]
fn native_image_config_dir_parsed() {
let toml = r#"
[application]
name = "x"
version = "0.1"
mainClass = "X"
[native-image]
configDir = "src/main/resources/META-INF/native-image"
"#;
let d = load_str(toml).unwrap();
assert_eq!(
d.native_image.config_dir.as_deref(),
Some("src/main/resources/META-INF/native-image")
);
}
#[test]
fn native_image_extra_args_parsed() {
let toml = r#"
[application]
name = "x"
version = "0.1"
mainClass = "X"
[native-image]
extraArgs = ["--no-fallback", "-H:+ReportExceptionStackTraces"]
"#;
let d = load_str(toml).unwrap();
assert_eq!(
d.native_image.extra_args,
vec!["--no-fallback", "-H:+ReportExceptionStackTraces"]
);
}
#[test]
fn native_image_on_library_is_rejected() {
let toml = r#"
[library]
name = "x"
version = "0.1"
[native-image]
"#;
let err = load_str(toml).unwrap_err().to_string();
assert!(err.contains("library") && err.contains("native-image"), "got: {err}");
}
#[test]
fn parse_bom_section() {
let toml = r#"
[bom]
name = "my-platform"
version = "1.0.0"
groupId = "com.example"
"#;
let d = load_str(toml).unwrap();
assert!(d.is_bom(), "should be recognised as a BOM project");
assert_eq!(d.kind_label(), "bom");
assert_eq!(d.project_name(), Some("my-platform"));
assert_eq!(d.project_version(), Some("1.0.0"));
assert_eq!(d.group_id(), Some("com.example"));
}
#[test]
fn bom_with_docker_is_rejected() {
let toml = r#"
[bom]
name = "x"
version = "0.1"
[docker]
"#;
let err = load_str(toml).unwrap_err().to_string();
assert!(err.contains("BOM") && err.contains("docker"), "got: {err}");
}
#[test]
fn bom_with_test_dependencies_is_rejected() {
let toml = r#"
[bom]
name = "x"
version = "0.1"
[test-dependencies]
"com.example:foo" = "1.0"
"#;
let err = load_str(toml).unwrap_err().to_string();
assert!(err.contains("BOM") && err.contains("test-dependencies"), "got: {err}");
}
#[test]
fn bom_dep_without_explicit_version_is_rejected() {
let toml = r#"
[bom]
name = "x"
version = "0.1"
[dependencies]
"com.example:foo" = ""
"#;
let err = load_str(toml).unwrap_err().to_string();
assert!(err.contains("explicit version"), "got: {err}");
assert!(err.contains("com.example:foo"), "got: {err}");
}
#[test]
fn bom_with_two_sections_is_rejected() {
let toml = r#"
[bom]
name = "x"
version = "0.1"
[library]
name = "y"
version = "0.1"
"#;
let err = load_str(toml).unwrap_err().to_string();
assert!(err.contains("only one"), "got: {err}");
}
#[test]
fn bom_with_explicit_deps_is_accepted() {
let toml = r#"
[bom]
name = "my-platform"
version = "1.0.0"
groupId = "com.example"
[dependencies]
"com.google.guava:guava" = "33.0.0-jre"
[bom-imports]
"io.micronaut:micronaut-bom" = "4.3.2"
"#;
let d = load_str(toml).unwrap();
assert!(d.is_bom());
assert_eq!(d.dependencies.len(), 1);
assert_eq!(d.bom_imports.len(), 1);
}
}
#[cfg(test)]
pub(crate) fn fake_bom_desc(
group_id: Option<&str>,
name: &str,
version: &str,
dependencies: BTreeMap<String, DependencyValue>,
bom_imports: BTreeMap<String, String>,
publish: PublishConfig,
) -> Descriptor {
Descriptor {
kind: DescriptorKind::Bom(Bom {
name: name.to_string(),
version: version.to_string(),
group_id: group_id.map(String::from),
}),
java: Java::default(),
test: Test::default(),
kotlin: Kotlin::default(),
groovy: Groovy::default(),
spock: Spock::default(),
native_image: NativeImage::default(),
docker: Docker::default(),
build_info: BuildInfo::default(),
dependencies,
test_dependencies: BTreeMap::new(),
repositories: vec![],
bom_imports,
test_bom_imports: BTreeMap::new(),
inherited_bom_imports: BTreeMap::new(),
inherited_test_bom_imports: BTreeMap::new(),
workspace_dependencies: BTreeMap::new(),
annotation_processors: BTreeMap::new(),
test_annotation_processors: BTreeMap::new(),
inherited_annotation_processors: BTreeMap::new(),
inherited_test_annotation_processors: BTreeMap::new(),
annotation_processor_options: BTreeMap::new(),
test_annotation_processor_options: BTreeMap::new(),
inherited_annotation_processor_options: BTreeMap::new(),
inherited_test_annotation_processor_options: BTreeMap::new(),
publish,
}
}
#[cfg(test)]
pub(crate) fn fake_library_desc(
group_id: Option<&str>,
name: &str,
version: &str,
publish: PublishConfig,
) -> Descriptor {
use std::collections::BTreeMap;
Descriptor {
kind: DescriptorKind::Library(Library {
name: name.to_string(),
version: version.to_string(),
group_id: group_id.map(String::from),
}),
java: Java::default(),
test: Test::default(),
kotlin: Kotlin::default(),
groovy: Groovy::default(),
spock: Spock::default(),
native_image: NativeImage::default(),
docker: Docker::default(),
build_info: BuildInfo::default(),
dependencies: BTreeMap::new(),
test_dependencies: BTreeMap::new(),
repositories: vec![],
bom_imports: BTreeMap::new(),
test_bom_imports: BTreeMap::new(),
inherited_bom_imports: BTreeMap::new(),
inherited_test_bom_imports: BTreeMap::new(),
workspace_dependencies: BTreeMap::new(),
annotation_processors: BTreeMap::new(),
test_annotation_processors: BTreeMap::new(),
inherited_annotation_processors: BTreeMap::new(),
inherited_test_annotation_processors: BTreeMap::new(),
annotation_processor_options: BTreeMap::new(),
test_annotation_processor_options: BTreeMap::new(),
inherited_annotation_processor_options: BTreeMap::new(),
inherited_test_annotation_processor_options: BTreeMap::new(),
publish,
}
}