add config generator controls via attribute metadatas

Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
Jason Volk 2024-10-22 22:16:59 +00:00 committed by strawberry
parent 367d153380
commit 5cb0a5f676
2 changed files with 127 additions and 11 deletions

View file

@ -28,10 +28,19 @@ use self::proxy::ProxyConfig;
use crate::{err, error::Error, utils::sys, Result}; use crate::{err, error::Error, utils::sys, Result};
/// all the config options for conduwuit /// all the config options for conduwuit
#[config_example_generator]
#[derive(Clone, Debug, Deserialize)]
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
#[allow(rustdoc::broken_intra_doc_links, rustdoc::bare_urls)] #[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 { pub struct Config {
/// The server_name is the pretty name of this server. It is used as a /// 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 /// suffix for user and room ids. Examples: matrix.org, conduit.rs
@ -71,6 +80,7 @@ pub struct Config {
#[serde(default = "default_port")] #[serde(default = "default_port")]
port: ListeningPort, port: ListeningPort,
// external structure; separate section
pub tls: Option<TlsConfig>, pub tls: Option<TlsConfig>,
/// Uncomment unix_socket_path to listen on a UNIX socket at the specified /// Uncomment unix_socket_path to listen on a UNIX socket at the specified
@ -458,15 +468,18 @@ pub struct Config {
#[serde(default = "true_fn")] #[serde(default = "true_fn")]
pub allow_unstable_room_versions: bool, pub allow_unstable_room_versions: bool,
/// default: 10
#[serde(default = "default_default_room_version")] #[serde(default = "default_default_room_version")]
pub default_room_version: RoomVersionId, pub default_room_version: RoomVersionId,
// external structure; separate section
#[serde(default)] #[serde(default)]
pub well_known: WellKnownConfig, pub well_known: WellKnownConfig,
#[serde(default)] #[serde(default)]
pub allow_jaeger: bool, pub allow_jaeger: bool,
/// default: "info"
#[serde(default = "default_jaeger_filter")] #[serde(default = "default_jaeger_filter")]
pub jaeger_filter: String, pub jaeger_filter: String,
@ -478,12 +491,38 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub tracing_flame: bool, pub tracing_flame: bool,
/// default: "info"
#[serde(default = "default_tracing_flame_filter")] #[serde(default = "default_tracing_flame_filter")]
pub tracing_flame_filter: String, pub tracing_flame_filter: String,
/// default: "./tracing.folded"
#[serde(default = "default_tracing_flame_output_path")] #[serde(default = "default_tracing_flame_output_path")]
pub tracing_flame_output_path: String, 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)] #[serde(default)]
pub proxy: ProxyConfig, pub proxy: ProxyConfig,
@ -1278,6 +1317,7 @@ pub struct Config {
} }
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.tls")]
pub struct TlsConfig { pub struct TlsConfig {
pub certs: String, pub certs: String,
pub key: String, pub key: String,
@ -1287,6 +1327,7 @@ pub struct TlsConfig {
} }
#[derive(Clone, Debug, Deserialize, Default)] #[derive(Clone, Debug, Deserialize, Default)]
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.well_known")]
pub struct WellKnownConfig { pub struct WellKnownConfig {
pub client: Option<Url>, pub client: Option<Url>,
pub server: Option<OwnedServerName>, pub server: Option<OwnedServerName>,

View file

@ -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_macro::TokenStream;
use proc_macro2::Span; use proc_macro2::Span;
use quote::ToTokens; use quote::ToTokens;
use syn::{ use syn::{
parse::Parser, punctuated::Punctuated, Error, Expr, ExprLit, Field, Fields, FieldsNamed, ItemStruct, Lit, Meta, parse::Parser, punctuated::Punctuated, spanned::Spanned, Error, Expr, ExprLit, Field, Fields, FieldsNamed,
MetaList, MetaNameValue, Type, TypePath, ItemStruct, Lit, Meta, MetaList, MetaNameValue, Type, TypePath,
}; };
use crate::{utils::is_cargo_build, Result}; use crate::{utils::is_cargo_build, Result};
const UNDOCUMENTED: &str = "# This item is undocumented. Please contribute documentation for it."; 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)] #[allow(clippy::needless_pass_by_value)]
pub(super) fn example_generator(input: ItemStruct, args: &[Meta]) -> Result<TokenStream> { pub(super) fn example_generator(input: ItemStruct, args: &[Meta]) -> Result<TokenStream> {
@ -25,11 +28,41 @@ pub(super) fn example_generator(input: ItemStruct, args: &[Meta]) -> Result<Toke
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
#[allow(unused_variables)] #[allow(unused_variables)]
fn generate_example(input: &ItemStruct, _args: &[Meta]) -> Result<()> { fn generate_example(input: &ItemStruct, args: &[Meta]) -> Result<()> {
let mut file = File::create("conduwuit-example.toml") 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}")))?; .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"); .expect("written to config file");
if let Fields::Named(FieldsNamed { if let Fields::Named(FieldsNamed {
@ -42,12 +75,16 @@ fn generate_example(input: &ItemStruct, _args: &[Meta]) -> Result<()> {
continue; continue;
}; };
if ignore.contains(ident.to_string().as_str()) {
continue;
}
let Some(type_name) = get_type_name(field) else { let Some(type_name) = get_type_name(field) else {
continue; continue;
}; };
let doc = get_doc_comment(field) let doc = get_doc_comment(field)
.unwrap_or_else(|| UNDOCUMENTED.into()) .unwrap_or_else(|| undocumented.into())
.trim_end() .trim_end()
.to_owned(); .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(()) Ok(())
} }
fn get_settings(args: &[Meta]) -> HashMap<String, String> {
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<String> { fn get_default(field: &Field) -> Option<String> {
for attr in &field.attrs { for attr in &field.attrs {
let Meta::List(MetaList { let Meta::List(MetaList {