use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
pub struct CxConfig {
pub package: PackageConfig,
pub dependencies: Option<HashMap<String, Dependency>>,
pub build: Option<BuildConfig>,
pub scripts: Option<ScriptsConfig>,
pub test: Option<TestConfig>,
pub workspace: Option<WorkspaceConfig>,
pub arduino: Option<ArduinoConfig>,
#[serde(skip)]
pub profiles: HashMap<String, Profile>,
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct Profile {
pub base: Option<String>,
pub target: Option<String>,
pub compiler: Option<String>,
pub flags: Option<Vec<String>>,
pub libs: Option<Vec<String>>,
pub bin: Option<String>,
}
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
pub struct ArduinoConfig {
pub board: Option<String>,
pub port: Option<String>,
pub flags: Option<Vec<String>>,
}
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
pub struct WorkspaceConfig {
pub members: Vec<String>,
}
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
pub struct TestConfig {
pub framework: Option<String>,
pub source_dir: Option<String>,
pub single_binary: Option<bool>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(untagged)]
pub enum Dependency {
Simple(String),
Complex {
git: Option<String>,
pkg: Option<String>,
branch: Option<String>,
tag: Option<String>,
rev: Option<String>,
build: Option<String>,
output: Option<String>,
},
}
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
pub struct PackageConfig {
pub name: String,
#[allow(dead_code)]
pub version: String,
#[serde(default = "default_edition")]
pub edition: String,
}
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
pub struct BuildConfig {
pub compiler: Option<String>,
pub bin: Option<String>,
pub flags: Option<Vec<String>>,
pub cflags: Option<Vec<String>>,
pub libs: Option<Vec<String>>,
pub ldflags: Option<Vec<String>>,
pub sources: Option<Vec<String>>,
pub pch: Option<String>,
pub subsystem: Option<String>,
}
impl BuildConfig {
pub fn get_flags(&self) -> Option<&Vec<String>> {
self.flags.as_ref().or(self.cflags.as_ref())
}
pub fn uses_deprecated_cflags(&self) -> bool {
self.cflags.is_some() && self.flags.is_none()
}
}
fn default_edition() -> String {
"c++23".to_string()
}
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
pub struct ScriptsConfig {
pub pre_build: Option<String>,
pub post_build: Option<String>,
}
pub fn create_ephemeral_config(
name: &str,
bin_name: &str,
compiler: &str,
has_cpp: bool,
) -> CxConfig {
CxConfig {
package: PackageConfig {
name: name.to_string(),
version: "0.0.0".to_string(),
edition: if has_cpp {
"c++23".to_string()
} else {
"c23".to_string()
},
},
build: Some(BuildConfig {
compiler: Some(compiler.to_string()),
bin: Some(bin_name.to_string()),
flags: None,
cflags: None,
libs: None,
ldflags: None,
sources: Some(vec![name.to_string()]),
pch: None,
subsystem: None,
}),
dependencies: None,
scripts: None,
test: None,
workspace: None,
arduino: None,
profiles: HashMap::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_config_get_flags_prefers_flags() {
let config = BuildConfig {
flags: Some(vec!["-O2".to_string()]),
cflags: Some(vec!["-O0".to_string()]),
..Default::default()
};
let flags = config.get_flags().unwrap();
assert_eq!(flags[0], "-O2");
}
#[test]
fn test_build_config_get_flags_falls_back_to_cflags() {
let config = BuildConfig {
flags: None,
cflags: Some(vec!["-Wall".to_string()]),
..Default::default()
};
let flags = config.get_flags().unwrap();
assert_eq!(flags[0], "-Wall");
}
#[test]
fn test_build_config_uses_deprecated_cflags() {
let deprecated = BuildConfig {
cflags: Some(vec![]),
flags: None,
..Default::default()
};
let modern = BuildConfig {
flags: Some(vec![]),
cflags: None,
..Default::default()
};
assert!(deprecated.uses_deprecated_cflags());
assert!(!modern.uses_deprecated_cflags());
}
#[test]
fn test_create_ephemeral_config_cpp() {
let config = create_ephemeral_config("main.cpp", "app", "g++", true);
assert_eq!(config.package.name, "main.cpp");
assert_eq!(config.package.edition, "c++23");
assert!(config.build.is_some());
let build = config.build.unwrap();
assert_eq!(build.compiler, Some("g++".to_string()));
assert_eq!(build.bin, Some("app".to_string()));
}
#[test]
fn test_create_ephemeral_config_c() {
let config = create_ephemeral_config("main.c", "prog", "gcc", false);
assert_eq!(config.package.edition, "c23");
}
#[test]
fn test_parse_minimal_config() {
let toml_str = r#"
[package]
name = "test"
version = "1.0.0"
"#;
let config: CxConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.package.name, "test");
assert_eq!(config.package.version, "1.0.0");
assert_eq!(config.package.edition, "c++23"); }
#[test]
fn test_parse_full_config() {
let toml_str = r#"
[package]
name = "myapp"
version = "2.0.0"
edition = "c++20"
[build]
compiler = "clang++"
bin = "myapp"
flags = ["-O3", "-Wall"]
libs = ["pthread"]
"#;
let config: CxConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.package.edition, "c++20");
let build = config.build.unwrap();
assert_eq!(build.compiler, Some("clang++".to_string()));
assert_eq!(
build.flags,
Some(vec!["-O3".to_string(), "-Wall".to_string()])
);
}
#[test]
fn test_dependency_simple() {
let toml_str = r#"
[package]
name = "test"
version = "1.0.0"
[dependencies]
glfw = "https://github.com/glfw/glfw"
"#;
let config: CxConfig = toml::from_str(toml_str).unwrap();
let deps = config.dependencies.unwrap();
match &deps["glfw"] {
Dependency::Simple(url) => assert!(url.contains("github.com")),
_ => panic!("Expected Simple dependency"),
}
}
#[test]
fn test_dependency_complex() {
let toml_str = r#"
[package]
name = "test"
version = "1.0.0"
[dependencies]
sdl2 = { git = "https://github.com/libsdl-org/SDL", tag = "release-2.30.0" }
"#;
let config: CxConfig = toml::from_str(toml_str).unwrap();
let deps = config.dependencies.unwrap();
match &deps["sdl2"] {
Dependency::Complex { git, tag, .. } => {
assert!(git.as_ref().unwrap().contains("SDL"));
assert_eq!(tag.as_ref().unwrap(), "release-2.30.0");
}
_ => panic!("Expected Complex dependency"),
}
}
}