use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::Path;
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::{Attribute, Fields, Type};
fn main() {
println!("cargo:rerun-if-changed=src/config");
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let out_dir = env::var("OUT_DIR").unwrap();
let config_dir = Path::new(&manifest_dir).join("src/config");
let template_output_path = Path::new(&out_dir).join("config.template.toml");
if let Some(parent) = template_output_path.parent()
&& !parent.exists()
{
fs::create_dir_all(parent).expect("Failed to create template directory");
}
match generate_config_template(&config_dir, &template_output_path) {
Ok(_) => println!("Configuration template generated successfully"),
Err(e) => {
eprintln!("Failed to generate configuration template: {e}");
std::process::exit(1);
}
}
}
fn generate_config_template(
config_dir: &Path,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let structs = parse_all_config_modules(config_dir)?;
let mut template_content = String::new();
template_content.push_str("# Actrix 辅助服务配置文件模板\n");
template_content.push_str("# 此文件由 config crate 的 build.rs 自动生成\n");
template_content.push_str("# 请根据实际部署环境修改相应的配置值\n\n");
if let Some(actrix_config) = structs.get("ActrixConfig") {
generate_struct_template(&mut template_content, actrix_config, &structs, "", 0)?;
}
fs::write(output_path, template_content)?;
Ok(())
}
fn parse_all_config_modules(
config_dir: &Path,
) -> Result<HashMap<String, syn::ItemStruct>, Box<dyn std::error::Error>> {
let mut structs = HashMap::new();
let mod_path = config_dir.join("mod.rs");
if mod_path.exists() {
parse_file_structs(&mod_path, &mut structs)?;
}
for entry in fs::read_dir(config_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().is_some_and(|ext| ext == "rs") {
if path.file_name().is_some_and(|name| name != "mod.rs") {
parse_file_structs(&path, &mut structs)?;
}
} else if path.is_dir() {
parse_submodule_dir(&path, &mut structs)?;
}
}
Ok(structs)
}
fn parse_file_structs(
path: &Path,
structs: &mut HashMap<String, syn::ItemStruct>,
) -> Result<(), Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)?;
if let Ok(syntax_tree) = syn::parse_str::<syn::File>(&content) {
for item in syntax_tree.items {
if let syn::Item::Struct(struct_item) = item {
structs.insert(struct_item.ident.to_string(), struct_item);
}
}
}
Ok(())
}
fn parse_submodule_dir(
dir: &Path,
structs: &mut HashMap<String, syn::ItemStruct>,
) -> Result<(), Box<dyn std::error::Error>> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().is_some_and(|ext| ext == "rs") {
parse_file_structs(&path, structs)?;
}
}
Ok(())
}
fn generate_struct_template(
template: &mut String,
struct_item: &syn::ItemStruct,
structs: &HashMap<String, syn::ItemStruct>,
section_prefix: &str,
_indent_level: usize,
) -> Result<(), Box<dyn std::error::Error>> {
if let Fields::Named(fields) = &struct_item.fields {
for field in &fields.named {
let field_name = field.ident.as_ref().unwrap().to_string();
add_doc_comments(template, &field.attrs);
match &field.ty {
Type::Path(type_path) if is_option_type(type_path) => {
let inner_type = extract_option_inner_type(type_path);
if structs.contains_key(&inner_type) {
template.push_str(&format!("# [{section_prefix}{field_name}]\n"));
if let Some(inner_struct) = structs.get(&inner_type) {
generate_optional_struct_fields(
template,
inner_struct,
structs,
&format!("{section_prefix}{field_name}."),
)?;
}
} else {
let default_value =
get_default_value_for_field_with_context(&field_name, section_prefix);
template.push_str(&format!("# {field_name} = {default_value}\n"));
}
}
Type::Path(type_path) if structs.contains_key(&type_path_to_string(type_path)) => {
let struct_type = type_path_to_string(type_path);
template.push_str(&format!("\n[{section_prefix}{field_name}]\n"));
if let Some(inner_struct) = structs.get(&struct_type) {
generate_struct_template(
template,
inner_struct,
structs,
&format!("{section_prefix}{field_name}."),
0,
)?;
}
}
_ => {
let default_value =
get_default_value_for_field_with_context(&field_name, section_prefix);
template.push_str(&format!("{field_name} = {default_value}\n"));
}
}
template.push('\n');
}
}
Ok(())
}
fn generate_optional_struct_fields(
template: &mut String,
struct_item: &syn::ItemStruct,
structs: &HashMap<String, syn::ItemStruct>,
section_prefix: &str,
) -> Result<(), Box<dyn std::error::Error>> {
if let Fields::Named(fields) = &struct_item.fields {
for field in &fields.named {
let field_name = field.ident.as_ref().unwrap().to_string();
add_doc_comments_commented(template, &field.attrs);
match &field.ty {
Type::Path(type_path) if structs.contains_key(&type_path_to_string(type_path)) => {
let struct_type = type_path_to_string(type_path);
template.push_str(&format!("# [{section_prefix}{field_name}]\n"));
if let Some(inner_struct) = structs.get(&struct_type) {
generate_optional_struct_fields(
template,
inner_struct,
structs,
&format!("{section_prefix}{field_name}."),
)?;
}
}
_ => {
let default_value =
get_default_value_for_field_with_context(&field_name, section_prefix);
template.push_str(&format!("# {field_name} = {default_value}\n"));
}
}
}
}
Ok(())
}
fn add_doc_comments(template: &mut String, attrs: &[Attribute]) {
for attr in attrs {
if attr.path().is_ident("doc")
&& let Ok(doc_string) = attr.value()
{
let doc_content = doc_string.to_token_stream().to_string();
let cleaned = doc_content.trim_matches('"').trim();
if !cleaned.is_empty() {
template.push_str(&format!("#{cleaned}\n"));
}
}
}
}
fn add_doc_comments_commented(template: &mut String, attrs: &[Attribute]) {
for attr in attrs {
if attr.path().is_ident("doc")
&& let Ok(doc_string) = attr.value()
{
let doc_content = doc_string.to_token_stream().to_string();
let cleaned = doc_content.trim_matches('"').trim();
if !cleaned.is_empty() {
template.push_str(&format!("# #{cleaned}\n"));
}
}
}
}
fn is_option_type(type_path: &syn::TypePath) -> bool {
if let Some(segment) = type_path.path.segments.first() {
segment.ident == "Option"
} else {
false
}
}
fn extract_option_inner_type(type_path: &syn::TypePath) -> String {
if let Some(segment) = type_path.path.segments.first()
&& let syn::PathArguments::AngleBracketed(args) = &segment.arguments
&& let Some(syn::GenericArgument::Type(inner_type)) = args.args.first()
{
return type_to_string(inner_type);
}
"Unknown".to_string()
}
fn type_path_to_string(type_path: &syn::TypePath) -> String {
type_path.path.segments.last().unwrap().ident.to_string()
}
fn type_to_string(ty: &Type) -> String {
match ty {
Type::Path(type_path) => type_path_to_string(type_path),
_ => "Unknown".to_string(),
}
}
fn get_default_value_for_field_with_context(field_name: &str, section_prefix: &str) -> String {
match field_name {
"enable" => "15".to_string(),
"name" => "\"actrix-default\"".to_string(),
"env" => "\"dev\"".to_string(),
"user" => "\"actrix\"".to_string(),
"group" => "\"actrix\"".to_string(),
"pid" => "\"logs/actrix.pid\"".to_string(),
"location_tag" => "\"default-location\"".to_string(),
"sqlite_path" => "\"database\"".to_string(),
"actrix_shared_key" => "\"CHANGE_ME_32_CHAR_RANDOM_KEY_HERE\"".to_string(),
"filter" => "\"digest\"".to_string(),
"sink" => "\"file:///var/log/actrix/actrix.log\"".to_string(),
"service_name" => "\"actrix\"".to_string(),
"endpoint" => {
if section_prefix.contains("admin") {
"\"http://localhost:50051\"".to_string()
} else {
"\"http://127.0.0.1:4317\"".to_string()
}
}
"domain_name" => "\"localhost\"".to_string(),
"advertised_ip" => "\"127.0.0.1\"".to_string(),
"ip" => "\"0.0.0.0\"".to_string(),
"port" => {
if section_prefix.contains("ice") {
"3478".to_string() } else if section_prefix.contains("https") {
"8443".to_string() } else {
"8080".to_string() }
}
"advertised_port" => "3478".to_string(),
"cert" => "\"certificates/server.crt\"".to_string(),
"key" => "\"certificates/server.key\"".to_string(),
"relay_port_range" => "\"49152-65535\"".to_string(),
"realm" => "\"actrix.local\"".to_string(),
"path" => {
if section_prefix.contains("services.signer.storage.sqlite") {
"\"ks.db\"".to_string()
} else {
"\"\"".to_string()
}
}
"node_id" => "\"\"".to_string(),
"secret" => "\"\"".to_string(),
"shared_secret" => {
"\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"".to_string()
}
"node_name" => "\"actrix-node\"".to_string(),
_ => "\"\"".to_string(),
}
}
#[allow(dead_code)]
fn get_default_value_for_field(field_name: &str) -> String {
get_default_value_for_field_with_context(field_name, "")
}
trait AttributeValue {
fn value(&self) -> Result<TokenStream, syn::Error>;
}
impl AttributeValue for Attribute {
fn value(&self) -> Result<TokenStream, syn::Error> {
self.parse_args()
}
}