use std::collections::HashSet;
use std::fs;
use std::process::Command;
include!("src/engine/data_tree_builder.rs");
const CALENDARS_DIR: &str = "../data/definitions";
const RESOURCES_DIR: &str = "../data/resources";
const CALENDAR_IDS_FILE: &str = "src/generated/calendar_ids.rs";
const LOCALE_IDS_FILE: &str = "src/generated/locale_ids.rs";
fn generate_constants() {
println!("cargo:info=Generating constants from JSON files...");
let mut calendar_infos = Vec::new();
collect_calendar_infos_recursively(CALENDARS_DIR, &mut calendar_infos);
let mut locales = HashSet::new();
collect_locale_codes_recursively(RESOURCES_DIR, &mut locales);
let mut calendar_ids: Vec<String> = calendar_infos.iter().map(|info| info.id.clone()).collect();
calendar_ids.sort();
let mut locales: Vec<String> = locales.into_iter().collect();
locales.sort();
let calendar_tree = build_calendar_tree(&calendar_infos);
let locale_tree = build_locale_tree(&locales);
let calendar_constants = calendar_ids
.iter()
.map(|id| format!(" \"{}\",", id))
.collect::<Vec<_>>()
.join("\n");
let locale_constants = locales
.iter()
.map(|locale| format!(" \"{}\",", locale))
.collect::<Vec<_>>()
.join("\n");
let calendar_tree_constant = generate_calendar_tree_constant(&calendar_tree);
let locale_tree_constant = generate_locale_tree_constant(&locale_tree);
let mut calendar_content = String::new();
calendar_content.push_str("//! Auto-generated calendar constants - Do not modify manually\n");
calendar_content.push_str("//! This file is generated by build.rs\n\n");
calendar_content.push_str("/// List of all calendar IDs\n");
calendar_content.push_str("///\n");
calendar_content.push_str("/// This constant contains all calendar IDs\n");
calendar_content.push_str("/// in the romcal project, sorted alphabetically.\n");
calendar_content.push_str("pub const CALENDAR_IDS: &[&str] = &[\n");
calendar_content.push_str(&calendar_constants);
calendar_content.push_str("\n\n];\n\n");
calendar_content.push_str("/// Calendar tree structure as JSON\n");
calendar_content.push_str("///\n");
calendar_content.push_str("/// This constant contains the hierarchical structure\n");
calendar_content.push_str("/// of all calendars in the romcal project as a JSON string.\n");
calendar_content.push_str("pub const CALENDAR_TREE_JSON: &str = r#\"");
calendar_content.push_str(&calendar_tree_constant);
calendar_content.push_str("\"#;\n");
fs::write(CALENDAR_IDS_FILE, calendar_content).expect("Failed to write calendar_ids.rs");
format_generated_file(CALENDAR_IDS_FILE);
let mut locale_content = String::new();
locale_content.push_str("//! Auto-generated locale constants - Do not modify manually\n");
locale_content.push_str("//! This file is generated by build.rs\n\n");
locale_content.push_str("/// List of all locale codes\n");
locale_content.push_str("///\n");
locale_content.push_str("/// This constant contains all locale codes\n");
locale_content.push_str("/// in the romcal project, sorted alphabetically.\n");
locale_content.push_str("pub const LOCALE_CODES: &[&str] = &[\n");
locale_content.push_str(&locale_constants);
locale_content.push_str("\n\n];\n\n");
locale_content.push_str("/// Locale tree structure as JSON\n");
locale_content.push_str("///\n");
locale_content.push_str("/// This constant contains the hierarchical structure\n");
locale_content.push_str("/// of all locales in the romcal project as a JSON string.\n");
locale_content
.push_str("/// The tree follows BCP 47 structure with base languages as parents\n");
locale_content.push_str("/// and specific locales (e.g., en-gb) as children.\n");
locale_content.push_str("pub const LOCALE_TREE_JSON: &str = r#\"");
locale_content.push_str(&locale_tree_constant);
locale_content.push_str("\"#;\n");
fs::write(LOCALE_IDS_FILE, locale_content).expect("Failed to write locale_ids.rs");
format_generated_file(LOCALE_IDS_FILE);
println!(
"cargo:info=Constants generated successfully: {} calendars, {} locales",
calendar_ids.len(),
locales.len()
);
}
fn collect_calendar_infos_recursively(dir: &str, infos: &mut Vec<CalendarInfo>) {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(dir_str) = path.to_str() {
collect_calendar_infos_recursively(dir_str, infos);
}
} else if let Some(name) = path.file_name().and_then(|n| n.to_str())
&& name.ends_with(".json")
&& let Ok(content) = fs::read_to_string(&path)
&& let Ok(json) = serde_json::from_str::<serde_json::Value>(&content)
&& let Some(id) = json.get("id").and_then(|v| v.as_str())
{
let mut parent_calendar_ids = json
.get("parent_calendar_ids")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_string())
.collect::<Vec<String>>()
})
.unwrap_or_default();
if id != "general_roman" {
parent_calendar_ids.insert(0, "general_roman".to_string());
}
infos.push(CalendarInfo {
id: id.to_string(),
parent_calendar_ids,
});
}
}
}
}
fn collect_locale_codes_recursively(dir: &str, locales: &mut HashSet<String>) {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(dir_str) = path.to_str() {
collect_locale_codes_recursively(dir_str, locales);
}
} else if let Some(name) = path.file_name().and_then(|n| n.to_str())
&& name.ends_with(".json")
&& let Ok(content) = fs::read_to_string(&path)
&& let Ok(json) = serde_json::from_str::<serde_json::Value>(&content)
&& let Some(locale) = json.get("locale").and_then(|v| v.as_str())
{
locales.insert(locale.to_string());
}
}
}
}
fn format_generated_file(file_path: &str) {
println!("cargo:info=Formatting generated file with rustfmt...");
let output = Command::new("rustfmt").arg(file_path).output();
match output {
Ok(output) => {
if output.status.success() {
println!("cargo:info=File formatted with rustfmt");
} else {
println!(
"cargo:warning=Warning: rustfmt failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}
Err(_) => {
println!("cargo:warning=Warning: rustfmt not found, skipping formatting");
}
}
}
#[cfg(feature = "bundled-data")]
fn generate_bundled_data() {
use std::env;
use std::path::Path;
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("bundled_data.rs");
let mut content = String::new();
content.push_str("// Auto-generated bundled data - Do not modify manually\n");
content
.push_str("// This file is generated by build.rs when bundled-data feature is enabled\n\n");
let mut definition_const_names = Vec::new();
let mut locale_info: Vec<(String, Vec<String>)> = Vec::new();
content.push_str("/// Embedded calendar definitions as JSON strings\n");
content.push_str("pub mod definitions {\n");
let calendars_path = Path::new(CALENDARS_DIR);
if calendars_path.exists() {
generate_definitions_module(
&mut content,
calendars_path,
" ",
&mut definition_const_names,
);
}
content.push_str("}\n\n");
content.push_str("/// Embedded locale resources as JSON strings\n");
content.push_str("pub mod resources {\n");
let resources_path = Path::new(RESOURCES_DIR);
if resources_path.exists() {
generate_resources_module(&mut content, resources_path, " ", &mut locale_info);
}
content.push_str("}\n\n");
content.push_str("/// Get all bundled calendar definition JSON strings\n");
content.push_str("pub fn get_all_definition_jsons() -> Vec<&'static str> {\n");
content.push_str(" vec![\n");
for name in &definition_const_names {
content.push_str(&format!(" definitions::{},\n", name));
}
content.push_str(" ]\n");
content.push_str("}\n\n");
content.push_str("/// Get all bundled resource JSON strings grouped by locale\n");
content
.push_str("pub fn get_all_resource_jsons() -> Vec<(&'static str, Vec<&'static str>)> {\n");
content.push_str(" vec![\n");
for (locale, file_names) in &locale_info {
let module_name = locale.replace('-', "_");
content.push_str(&format!(" (\"{}\", vec![\n", locale));
for name in file_names {
content.push_str(&format!(
" resources::{}::{},\n",
module_name, name
));
}
content.push_str(" ]),\n");
}
content.push_str(" ]\n");
content.push_str("}\n");
fs::write(&dest_path, content).expect("Failed to write bundled_data.rs");
format_generated_file(dest_path.to_str().unwrap());
println!(
"cargo:info=Bundled data generated successfully ({} definitions, {} locales)",
definition_const_names.len(),
locale_info.len()
);
}
#[cfg(feature = "bundled-data")]
fn generate_definitions_module(
content: &mut String,
dir: &std::path::Path,
indent: &str,
const_names: &mut Vec<String>,
) {
let mut json_files = Vec::new();
collect_json_files_recursively(dir, &mut json_files);
json_files.sort();
for file_path in json_files {
if let Ok(json_content) = fs::read_to_string(&file_path)
&& let Ok(json) = serde_json::from_str::<serde_json::Value>(&json_content)
&& let Some(id) = json.get("id").and_then(|v| v.as_str())
{
let const_name = id.to_uppercase().replace(['-', '.'], "_");
let rel_path_str = file_path.to_string_lossy().replace('\\', "/");
content.push_str(&format!("{}/// Calendar definition: {}\n", indent, id));
content.push_str(&format!(
"{}pub const {}: &str = include_str!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/{}\"));\n",
indent, const_name, rel_path_str
));
const_names.push(const_name);
}
}
}
#[cfg(feature = "bundled-data")]
fn collect_json_files_recursively(dir: &std::path::Path, files: &mut Vec<std::path::PathBuf>) {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_json_files_recursively(&path, files);
} else if path.extension().is_some_and(|ext| ext == "json") {
files.push(path);
}
}
}
}
#[cfg(feature = "bundled-data")]
fn generate_resources_module(
content: &mut String,
dir: &std::path::Path,
indent: &str,
locale_info: &mut Vec<(String, Vec<String>)>,
) {
if let Ok(entries) = fs::read_dir(dir) {
let mut entries: Vec<_> = entries.flatten().collect();
entries.sort_by_key(|e| e.path());
for entry in entries {
let path = entry.path();
if path.is_dir()
&& let Some(locale_name) = path.file_name().and_then(|n| n.to_str())
{
let module_name = locale_name.replace('-', "_");
content.push_str(&format!(
"{}/// Resources for locale: {}\n",
indent, locale_name
));
content.push_str(&format!("{}pub mod {} {{\n", indent, module_name));
let mut file_const_names = Vec::new();
if let Ok(files) = fs::read_dir(&path) {
let mut files: Vec<_> = files.flatten().collect();
files.sort_by_key(|e| e.path());
for file in files {
let file_path = file.path();
if file_path.extension().is_some_and(|ext| ext == "json")
&& let Some(file_name) = file_path.file_stem().and_then(|n| n.to_str())
{
let const_name = file_name.to_uppercase().replace(['-', '.'], "_");
let rel_path_str = file_path.to_string_lossy().replace('\\', "/");
content.push_str(&format!(
"{} pub const {}: &str = include_str!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/{}\"));\n",
indent, const_name, rel_path_str
));
file_const_names.push(const_name);
}
}
}
content.push_str(&format!("{}}}\n", indent));
locale_info.push((locale_name.to_string(), file_const_names));
}
}
}
}
fn main() {
if std::path::Path::new(CALENDARS_DIR).exists() && std::path::Path::new(RESOURCES_DIR).exists()
{
generate_constants();
#[cfg(feature = "bundled-data")]
generate_bundled_data();
println!("cargo:rerun-if-changed={}", CALENDARS_DIR);
println!("cargo:rerun-if-changed={}", RESOURCES_DIR);
}
}