use crate::config::ForeignTypeConfig;
use crate::error::EvenframeError;
use crate::typesync::config::{
ArrayStyle, CollisionStrategy, FileNamingConvention, OutputConfig, OutputMode,
};
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct BuildConfig {
pub scan_path: PathBuf,
pub output_path: PathBuf,
pub apply_aliases: Vec<String>,
pub expand_macros: bool,
pub arktype: bool,
pub effect: bool,
pub macroforge: bool,
pub flatbuffers: bool,
pub protobuf: bool,
pub flatbuffers_namespace: Option<String>,
pub protobuf_package: Option<String>,
pub protobuf_import_validate: bool,
pub output: OutputConfig,
pub collision_strategy: CollisionStrategy,
pub foreign_types: BTreeMap<String, ForeignTypeConfig>,
pub output_rule_plugins: BTreeMap<String, crate::config::OutputRulePluginConfig>,
pub synthetic_item_plugins: BTreeMap<String, crate::config::SyntheticItemPluginConfig>,
}
impl Default for BuildConfig {
fn default() -> Self {
Self {
scan_path: PathBuf::from("."),
output_path: PathBuf::from("./src/generated/"),
apply_aliases: Vec::new(),
expand_macros: false,
arktype: true,
effect: false,
macroforge: false,
flatbuffers: false,
protobuf: false,
flatbuffers_namespace: None,
protobuf_package: None,
protobuf_import_validate: false,
output: OutputConfig::default(),
collision_strategy: CollisionStrategy::Error,
foreign_types: BTreeMap::new(),
output_rule_plugins: BTreeMap::new(),
synthetic_item_plugins: BTreeMap::new(),
}
}
}
impl BuildConfig {
pub fn new() -> Self {
Self::default()
}
pub fn from_toml() -> Result<Self, EvenframeError> {
let start_dir = env::var("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
Self::from_toml_search(&start_dir)
}
pub fn from_toml_path(path: impl AsRef<Path>) -> Result<Self, EvenframeError> {
let path = path.as_ref();
let content = fs::read_to_string(path)?;
Self::parse_toml(&content, path)
}
fn from_toml_search(start_dir: &Path) -> Result<Self, EvenframeError> {
let mut current = start_dir.to_path_buf();
loop {
let dotdir_config = current.join(".evenframe").join("config.toml");
if dotdir_config.exists() {
return Self::from_toml_path(&dotdir_config);
}
let config_path = current.join("evenframe.toml");
if config_path.exists() {
return Self::from_toml_path(&config_path);
}
if !current.pop() {
return Err(EvenframeError::ConfigNotFound {
search_start: start_dir.to_path_buf(),
});
}
}
}
fn parse_toml(content: &str, path: &Path) -> Result<Self, EvenframeError> {
let value: toml::Value =
toml::from_str(content).map_err(|e| EvenframeError::config_error(e.to_string()))?;
let mut config = Self::default();
if let Some(general) = value.get("general") {
let general_config: crate::config::GeneralConfig =
general.clone().try_into().map_err(|e| {
EvenframeError::config_error(format!("Failed to parse [general]: {e}"))
})?;
config.apply_aliases = general_config.apply_aliases;
config.expand_macros = general_config.expand_macros;
config.foreign_types = general_config.foreign_types;
config.output_rule_plugins = general_config.output_rule_plugins;
config.synthetic_item_plugins = general_config.synthetic_item_plugins;
}
let config_dir = path.parent().unwrap_or(Path::new("."));
let project_root = if config_dir.file_name().and_then(|n| n.to_str()) == Some(".evenframe")
{
config_dir.parent().unwrap_or(Path::new("."))
} else {
config_dir
};
if let Some(typesync) = value.get("typesync").and_then(|v| v.as_table()) {
if let Some(output) = typesync.get("output_path").and_then(|v| v.as_str()) {
config.output_path = project_root.join(output);
}
if let Some(v) = typesync.get("should_generate_arktype_types") {
config.arktype = v.as_bool().unwrap_or(false);
}
if let Some(v) = typesync.get("should_generate_effect_types") {
config.effect = v.as_bool().unwrap_or(false);
}
if let Some(v) = typesync.get("should_generate_macroforge_types") {
config.macroforge = v.as_bool().unwrap_or(false);
}
if let Some(v) = typesync.get("should_generate_flatbuffers_types") {
config.flatbuffers = v.as_bool().unwrap_or(false);
}
if let Some(v) = typesync.get("should_generate_protobuf_types") {
config.protobuf = v.as_bool().unwrap_or(false);
}
if let Some(ns) = typesync
.get("flatbuffers_namespace")
.and_then(|v| v.as_str())
{
config.flatbuffers_namespace = Some(ns.to_string());
}
if let Some(pkg) = typesync.get("protobuf_package").and_then(|v| v.as_str()) {
config.protobuf_package = Some(pkg.to_string());
}
if let Some(v) = typesync.get("protobuf_import_validate") {
config.protobuf_import_validate = v.as_bool().unwrap_or(false);
}
if let Some(strategy_str) = typesync.get("collision_strategy").and_then(|v| v.as_str())
{
config.collision_strategy = match strategy_str {
"auto_rename" => CollisionStrategy::AutoRename,
_ => CollisionStrategy::Error,
};
}
if let Some(output_table) = typesync.get("output").and_then(|v| v.as_table()) {
if let Some(mode_str) = output_table.get("mode").and_then(|v| v.as_str()) {
config.output.mode = match mode_str {
"per_file" => OutputMode::PerFile,
_ => OutputMode::Single,
};
}
if let Some(v) = output_table.get("barrel_file").and_then(|v| v.as_bool()) {
config.output.barrel_file = v;
}
if let Some(naming_str) = output_table.get("file_naming").and_then(|v| v.as_str()) {
config.output.file_naming = match naming_str {
"pascal" => FileNamingConvention::Pascal,
"snake" => FileNamingConvention::Snake,
"camel" => FileNamingConvention::Camel,
_ => FileNamingConvention::Kebab,
};
}
if let Some(style_str) = output_table.get("array_style").and_then(|v| v.as_str()) {
config.output.array_style = match style_str {
"generic" => ArrayStyle::Generic,
_ => ArrayStyle::Shorthand,
};
}
}
}
config.scan_path = project_root.to_path_buf();
Ok(config)
}
pub fn builder() -> BuildConfigBuilder {
BuildConfigBuilder::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct BuildConfigBuilder {
config: BuildConfig,
}
impl BuildConfigBuilder {
pub fn new() -> Self {
Self {
config: BuildConfig::default(),
}
}
pub fn scan_path(mut self, path: impl Into<PathBuf>) -> Self {
self.config.scan_path = path.into();
self
}
pub fn output_path(mut self, path: impl Into<PathBuf>) -> Self {
self.config.output_path = path.into();
self
}
pub fn apply_alias(mut self, alias: impl Into<String>) -> Self {
self.config.apply_aliases.push(alias.into());
self
}
pub fn apply_aliases(mut self, aliases: Vec<String>) -> Self {
self.config.apply_aliases = aliases;
self
}
pub fn expand_macros(mut self, enabled: bool) -> Self {
self.config.expand_macros = enabled;
self
}
pub fn enable_arktype(mut self) -> Self {
self.config.arktype = true;
self
}
pub fn disable_arktype(mut self) -> Self {
self.config.arktype = false;
self
}
pub fn enable_effect(mut self) -> Self {
self.config.effect = true;
self
}
pub fn disable_effect(mut self) -> Self {
self.config.effect = false;
self
}
pub fn enable_macroforge(mut self) -> Self {
self.config.macroforge = true;
self
}
pub fn disable_macroforge(mut self) -> Self {
self.config.macroforge = false;
self
}
pub fn enable_flatbuffers(mut self, namespace: Option<String>) -> Self {
self.config.flatbuffers = true;
self.config.flatbuffers_namespace = namespace;
self
}
pub fn disable_flatbuffers(mut self) -> Self {
self.config.flatbuffers = false;
self
}
pub fn enable_protobuf(mut self, package: Option<String>, import_validate: bool) -> Self {
self.config.protobuf = true;
self.config.protobuf_package = package;
self.config.protobuf_import_validate = import_validate;
self
}
pub fn disable_protobuf(mut self) -> Self {
self.config.protobuf = false;
self
}
pub fn output_mode(mut self, mode: OutputMode) -> Self {
self.config.output.mode = mode;
self
}
pub fn barrel_file(mut self, enabled: bool) -> Self {
self.config.output.barrel_file = enabled;
self
}
pub fn file_naming(mut self, naming: FileNamingConvention) -> Self {
self.config.output.file_naming = naming;
self
}
pub fn array_style(mut self, style: ArrayStyle) -> Self {
self.config.output.array_style = style;
self
}
pub fn foreign_types(mut self, foreign_types: BTreeMap<String, ForeignTypeConfig>) -> Self {
self.config.foreign_types = foreign_types;
self
}
pub fn enable_all(mut self) -> Self {
self.config.arktype = true;
self.config.effect = true;
self.config.macroforge = true;
self.config.flatbuffers = true;
self.config.protobuf = true;
self
}
pub fn disable_all(mut self) -> Self {
self.config.arktype = false;
self.config.effect = false;
self.config.macroforge = false;
self.config.flatbuffers = false;
self.config.protobuf = false;
self
}
pub fn build(self) -> BuildConfig {
self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = BuildConfig::default();
assert!(config.arktype);
assert!(!config.effect);
assert!(!config.macroforge);
assert!(!config.flatbuffers);
assert!(!config.protobuf);
}
#[test]
fn test_builder_enable_arktype() {
let config = BuildConfig::builder().enable_arktype().build();
assert!(config.arktype);
}
#[test]
fn test_builder_enable_all() {
let config = BuildConfig::builder().enable_all().build();
assert!(config.arktype);
assert!(config.effect);
assert!(config.macroforge);
assert!(config.flatbuffers);
assert!(config.protobuf);
}
#[test]
fn test_builder_custom_paths() {
let config = BuildConfig::builder()
.scan_path("/custom/scan")
.output_path("/custom/output")
.build();
assert_eq!(config.scan_path, PathBuf::from("/custom/scan"));
assert_eq!(config.output_path, PathBuf::from("/custom/output"));
}
#[test]
fn test_builder_apply_aliases() {
let config = BuildConfig::builder()
.apply_alias("MyMacro")
.apply_alias("OtherMacro")
.build();
assert_eq!(config.apply_aliases.len(), 2);
assert!(config.apply_aliases.contains(&"MyMacro".to_string()));
assert!(config.apply_aliases.contains(&"OtherMacro".to_string()));
}
#[test]
fn test_parse_toml_basic() {
let toml_content = r#"
[general]
apply_aliases = ["MyMacro"]
[typesync]
output_path = "./generated/"
should_generate_arktype_types = true
should_generate_effect_types = true
"#;
let config = BuildConfig::parse_toml(toml_content, Path::new("/test/evenframe.toml"))
.expect("Should parse successfully");
assert!(config.arktype);
assert!(config.effect);
assert_eq!(config.apply_aliases, vec!["MyMacro".to_string()]);
}
}