diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 919bb486..ff214420 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -28,10 +28,19 @@ use self::proxy::ProxyConfig; use crate::{err, error::Error, utils::sys, Result}; /// all the config options for conduwuit -#[config_example_generator] -#[derive(Clone, Debug, Deserialize)] #[allow(clippy::struct_excessive_bools)] #[allow(rustdoc::broken_intra_doc_links, rustdoc::bare_urls)] +#[derive(Clone, Debug, Deserialize)] +#[config_example_generator( + filename = "conduwuit-example.toml", + section = "global", + undocumented = "# This item is undocumented. Please contribute documentation for it.", + header = "### Conduwuit Configuration\n###\n### THIS FILE IS GENERATED. YOUR CHANGES WILL BE OVERWRITTEN!\n### \ + You should rename this file before configuring your server. Changes\n### to documentation and defaults \ + can be contributed in sourcecode at\n### src/core/config/mod.rs. This file is generated when \ + building.\n###\n", + ignore = "catchall well_known tls" +)] pub struct Config { /// The server_name is the pretty name of this server. It is used as a /// suffix for user and room ids. Examples: matrix.org, conduit.rs @@ -71,6 +80,7 @@ pub struct Config { #[serde(default = "default_port")] port: ListeningPort, + // external structure; separate section pub tls: Option, /// Uncomment unix_socket_path to listen on a UNIX socket at the specified @@ -458,15 +468,18 @@ pub struct Config { #[serde(default = "true_fn")] pub allow_unstable_room_versions: bool, + /// default: 10 #[serde(default = "default_default_room_version")] pub default_room_version: RoomVersionId, + // external structure; separate section #[serde(default)] pub well_known: WellKnownConfig, #[serde(default)] pub allow_jaeger: bool, + /// default: "info" #[serde(default = "default_jaeger_filter")] pub jaeger_filter: String, @@ -478,12 +491,38 @@ pub struct Config { #[serde(default)] pub tracing_flame: bool, + /// default: "info" #[serde(default = "default_tracing_flame_filter")] pub tracing_flame_filter: String, + /// default: "./tracing.folded" #[serde(default = "default_tracing_flame_output_path")] pub tracing_flame_output_path: String, + /// Examples: + /// - No proxy (default): + /// proxy ="none" + /// + /// - For global proxy, create the section at the bottom of this file: + /// [global.proxy] + /// global = { url = "socks5h://localhost:9050" } + /// + /// - To proxy some domains: + /// [global.proxy] + /// [[global.proxy.by_domain]] + /// url = "socks5h://localhost:9050" + /// include = ["*.onion", "matrix.myspecial.onion"] + /// exclude = ["*.myspecial.onion"] + /// + /// Include vs. Exclude: + /// - If include is an empty list, it is assumed to be `["*"]`. + /// - If a domain matches both the exclude and include list, the proxy will + /// only be used if it was included because of a more specific rule than + /// it was excluded. In the above example, the proxy would be used for + /// `ordinary.onion`, `matrix.myspecial.onion`, but not + /// `hello.myspecial.onion`. + /// + /// default: "none" #[serde(default)] pub proxy: ProxyConfig, @@ -1278,6 +1317,7 @@ pub struct Config { } #[derive(Clone, Debug, Deserialize)] +#[config_example_generator(filename = "conduwuit-example.toml", section = "global.tls")] pub struct TlsConfig { pub certs: String, pub key: String, @@ -1287,6 +1327,7 @@ pub struct TlsConfig { } #[derive(Clone, Debug, Deserialize, Default)] +#[config_example_generator(filename = "conduwuit-example.toml", section = "global.well_known")] pub struct WellKnownConfig { pub client: Option, pub server: Option, diff --git a/src/macros/config.rs b/src/macros/config.rs index 3c93bd08..f8616352 100644 --- a/src/macros/config.rs +++ b/src/macros/config.rs @@ -1,18 +1,21 @@ -use std::{fmt::Write as _, fs::File, io::Write as _}; +use std::{ + collections::{HashMap, HashSet}, + fmt::Write as _, + fs::OpenOptions, + io::Write as _, +}; use proc_macro::TokenStream; use proc_macro2::Span; use quote::ToTokens; use syn::{ - parse::Parser, punctuated::Punctuated, Error, Expr, ExprLit, Field, Fields, FieldsNamed, ItemStruct, Lit, Meta, - MetaList, MetaNameValue, Type, TypePath, + parse::Parser, punctuated::Punctuated, spanned::Spanned, Error, Expr, ExprLit, Field, Fields, FieldsNamed, + ItemStruct, Lit, Meta, MetaList, MetaNameValue, Type, TypePath, }; use crate::{utils::is_cargo_build, Result}; const UNDOCUMENTED: &str = "# This item is undocumented. Please contribute documentation for it."; -const HEADER: &str = "## Conduwuit Configuration\n##\n## THIS FILE IS GENERATED. Changes to documentation and \ - defaults must\n## be made within the code found at src/core/config/\n"; #[allow(clippy::needless_pass_by_value)] pub(super) fn example_generator(input: ItemStruct, args: &[Meta]) -> Result { @@ -25,11 +28,41 @@ pub(super) fn example_generator(input: ItemStruct, args: &[Meta]) -> Result Result<()> { - let mut file = File::create("conduwuit-example.toml") +fn generate_example(input: &ItemStruct, args: &[Meta]) -> Result<()> { + let settings = get_settings(args); + + let filename = settings + .get("filename") + .ok_or_else(|| Error::new(args[0].span(), "missing required 'filename' attribute argument"))?; + + let undocumented = settings + .get("undocumented") + .map_or(UNDOCUMENTED, String::as_str); + + let ignore: HashSet<&str> = settings + .get("ignore") + .map_or("", String::as_str) + .split(' ') + .collect(); + + let section = settings + .get("section") + .ok_or_else(|| Error::new(args[0].span(), "missing required 'section' attribute argument"))?; + + let mut file = OpenOptions::new() + .write(true) + .create(section == "global") + .truncate(section == "global") + .append(section != "global") + .open(filename) .map_err(|e| Error::new(Span::call_site(), format!("Failed to open config file for generation: {e}")))?; - file.write_all(HEADER.as_bytes()) + if let Some(header) = settings.get("header") { + file.write_all(header.as_bytes()) + .expect("written to config file"); + } + + file.write_fmt(format_args!("\n[{section}]\n")) .expect("written to config file"); if let Fields::Named(FieldsNamed { @@ -42,12 +75,16 @@ fn generate_example(input: &ItemStruct, _args: &[Meta]) -> Result<()> { continue; }; + if ignore.contains(ident.to_string().as_str()) { + continue; + } + let Some(type_name) = get_type_name(field) else { continue; }; let doc = get_doc_comment(field) - .unwrap_or_else(|| UNDOCUMENTED.into()) + .unwrap_or_else(|| undocumented.into()) .trim_end() .to_owned(); @@ -75,9 +112,47 @@ fn generate_example(input: &ItemStruct, _args: &[Meta]) -> Result<()> { } } + if let Some(footer) = settings.get("footer") { + file.write_all(footer.as_bytes()) + .expect("written to config file"); + } + Ok(()) } +fn get_settings(args: &[Meta]) -> HashMap { + let mut map = HashMap::new(); + for arg in args { + let Meta::NameValue(MetaNameValue { + path, + value, + .. + }) = arg + else { + continue; + }; + + let Expr::Lit( + ExprLit { + lit: Lit::Str(str), + .. + }, + .., + ) = value + else { + continue; + }; + + let Some(key) = path.segments.iter().next().map(|s| s.ident.clone()) else { + continue; + }; + + map.insert(key.to_string(), str.value()); + } + + map +} + fn get_default(field: &Field) -> Option { for attr in &field.attrs { let Meta::List(MetaList {