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 docker: Docker,
pub build_info: BuildInfo,
pub dependencies: BTreeMap<String, String>,
pub test_dependencies: BTreeMap<String, String>,
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>>,
}
#[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),
}
#[derive(Debug, Deserialize)]
struct RawDescriptor {
application: Option<Application>,
library: Option<Library>,
workspace: Option<Workspace>,
#[serde(default)]
java: Java,
#[serde(default)]
docker: Docker,
#[serde(rename = "build-info", default)]
build_info: BuildInfo,
#[serde(default)]
dependencies: BTreeMap<String, String>,
#[serde(rename = "test-dependencies", default)]
test_dependencies: BTreeMap<String, String>,
#[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,
}
#[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 = "mainClass")]
pub main_class: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct Library {
pub name: String,
pub version: String,
}
#[derive(Debug, Deserialize)]
pub struct Workspace {
pub members: Vec<String>,
}
#[derive(Debug, Deserialize, Default)]
pub struct Java {
#[serde(rename = "sourceCompatibility")]
pub source_compatibility: Option<String>,
}
impl Java {
pub fn effective(&self) -> &str {
self.source_compatibility.as_deref().unwrap_or("21")
}
}
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)
}
}
#[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)
}
}
#[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, Clone)]
pub struct RepositoryEntry {
pub name: String,
pub url: String,
}
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 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",
}
}
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,
}
}
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,
}
}
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) {
(Some(a), None, None) => DescriptorKind::Application(a),
(None, Some(l), None) => DescriptorKind::Library(l),
(None, None, Some(w)) => DescriptorKind::Workspace(w),
(None, None, None) => bail!(
"Curie.toml must contain one of [application], [library], or [workspace]"
),
_ => bail!(
"Curie.toml must contain only one of [application], [library], or [workspace]"
),
};
let mut docker = parsed.docker;
docker.section_present = docker_section_present;
let descriptor = Descriptor {
kind,
java: parsed.java,
test: parsed.test,
kotlin: parsed.kotlin,
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(),
};
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"
);
}
Ok(descriptor)
}
pub fn docker_enabled(project_root: &Path, desc: &Descriptor) -> bool {
desc.docker.section_present || project_root.join("Dockerfile").exists()
}
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(
"both [application] and [library] require a `name` field.".to_string(),
);
}
if message.contains("missing field") && message.contains("version") {
return Some(
"both [application] and [library] 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]]
name = "Nexus"
url = "https://example.com/m2"
"#;
let d = load_str(toml).unwrap();
assert_eq!(d.java.effective(), "17");
assert_eq!(d.repositories.len(), 1);
}
#[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);
}
}