Skip to content

Commit 3dfedf1

Browse files
authored
Generate environment variables doc from code (#8493)
## Summary Resolves #8417 I've just begun learning procedural macros, so this PR is more of a proof of concept. It's still a work in progress, and I welcome any assistance or feedback.
1 parent 545a55f commit 3dfedf1

File tree

13 files changed

+491
-135
lines changed

13 files changed

+491
-135
lines changed

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ CHANGELOG.md
33
PREVIEW-CHANGELOG.md
44
docs/reference/cli.md
55
docs/reference/settings.md
6+
docs/configuration/environment.md
67
ecosystem/home-assistant-core/LICENSE.md
78
docs/guides/integration/gitlab.md

Cargo.lock

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-dev/src/generate_all.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
33
use anyhow::Result;
44

5-
use crate::{generate_cli_reference, generate_json_schema, generate_options_reference};
5+
use crate::{
6+
generate_cli_reference, generate_env_vars_reference, generate_json_schema,
7+
generate_options_reference,
8+
};
69

710
#[derive(clap::Args)]
811
pub(crate) struct Args {
@@ -27,5 +30,6 @@ pub(crate) fn main(args: &Args) -> Result<()> {
2730
generate_json_schema::main(&generate_json_schema::Args { mode: args.mode })?;
2831
generate_options_reference::main(&generate_options_reference::Args { mode: args.mode })?;
2932
generate_cli_reference::main(&generate_cli_reference::Args { mode: args.mode })?;
33+
generate_env_vars_reference::main(&generate_env_vars_reference::Args { mode: args.mode })?;
3034
Ok(())
3135
}

crates/uv-dev/src/generate_cli_reference.rs

-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ const SHOW_HIDDEN_COMMANDS: &[&str] = &["generate-shell-completion"];
3131

3232
#[derive(clap::Args)]
3333
pub(crate) struct Args {
34-
/// Write the generated output to stdout (rather than to `settings.md`).
3534
#[arg(long, default_value_t, value_enum)]
3635
pub(crate) mode: Mode,
3736
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//! Generate the environment variables reference from `uv_static::EnvVars`.
2+
3+
use anyhow::bail;
4+
use pretty_assertions::StrComparison;
5+
use std::path::PathBuf;
6+
7+
use uv_static::EnvVars;
8+
9+
use crate::generate_all::Mode;
10+
use crate::ROOT_DIR;
11+
12+
#[derive(clap::Args)]
13+
pub(crate) struct Args {
14+
#[arg(long, default_value_t, value_enum)]
15+
pub(crate) mode: Mode,
16+
}
17+
18+
pub(crate) fn main(args: &Args) -> anyhow::Result<()> {
19+
let reference_string = generate();
20+
let filename = "environment.md";
21+
let reference_path = PathBuf::from(ROOT_DIR)
22+
.join("docs")
23+
.join("configuration")
24+
.join(filename);
25+
26+
match args.mode {
27+
Mode::DryRun => {
28+
anstream::println!("{reference_string}");
29+
}
30+
Mode::Check => match fs_err::read_to_string(reference_path) {
31+
Ok(current) => {
32+
if current == reference_string {
33+
anstream::println!("Up-to-date: {filename}");
34+
} else {
35+
let comparison = StrComparison::new(&current, &reference_string);
36+
bail!("{filename} changed, please run `cargo dev generate-env-vars-reference`:\n{comparison}");
37+
}
38+
}
39+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
40+
bail!("{filename} not found, please run `cargo dev generate-env-vars-reference`");
41+
}
42+
Err(err) => {
43+
bail!(
44+
"{filename} changed, please run `cargo dev generate-env-vars-reference`:\n{err}"
45+
);
46+
}
47+
},
48+
Mode::Write => match fs_err::read_to_string(&reference_path) {
49+
Ok(current) => {
50+
if current == reference_string {
51+
anstream::println!("Up-to-date: {filename}");
52+
} else {
53+
anstream::println!("Updating: {filename}");
54+
fs_err::write(reference_path, reference_string.as_bytes())?;
55+
}
56+
}
57+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
58+
anstream::println!("Updating: {filename}");
59+
fs_err::write(reference_path, reference_string.as_bytes())?;
60+
}
61+
Err(err) => {
62+
bail!("{filename} changed, please run `cargo dev generate-env-vars-reference`:\n{err}");
63+
}
64+
},
65+
}
66+
67+
Ok(())
68+
}
69+
70+
fn generate() -> String {
71+
let mut output = String::new();
72+
73+
output.push_str("# Environment variables\n\n");
74+
output.push_str("uv respects the following environment variables:\n\n");
75+
76+
for (var, doc) in EnvVars::metadata() {
77+
// Remove empty lines and ddd two spaces to the beginning from the second line.
78+
let doc = doc
79+
.lines()
80+
.enumerate()
81+
.filter(|(_, line)| !line.trim().is_empty())
82+
.map(|(i, line)| {
83+
if i == 0 {
84+
line.to_string()
85+
} else {
86+
format!(" {line}")
87+
}
88+
})
89+
.collect::<Vec<_>>()
90+
.join("\n");
91+
output.push_str(&format!("- `{var}`: {doc}\n"));
92+
}
93+
94+
output
95+
}
96+
97+
#[cfg(test)]
98+
mod tests;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use std::env;
2+
3+
use anyhow::Result;
4+
5+
use uv_static::EnvVars;
6+
7+
use crate::generate_all::Mode;
8+
9+
use super::{main, Args};
10+
11+
#[test]
12+
fn test_generate_env_vars_reference() -> Result<()> {
13+
let mode = if env::var(EnvVars::UV_UPDATE_SCHEMA).as_deref() == Ok("1") {
14+
Mode::Write
15+
} else {
16+
Mode::Check
17+
};
18+
main(&Args { mode })
19+
}

crates/uv-dev/src/generate_json_schema.rs

-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ struct CombinedOptions {
2727

2828
#[derive(clap::Args)]
2929
pub(crate) struct Args {
30-
/// Write the generated output to stdout (rather than to `uv.schema.json`).
3130
#[arg(long, default_value_t, value_enum)]
3231
pub(crate) mode: Mode,
3332
}

crates/uv-dev/src/generate_options_reference.rs

-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ struct CombinedOptions {
3434

3535
#[derive(clap::Args)]
3636
pub(crate) struct Args {
37-
/// Write the generated output to stdout (rather than to `settings.md`).
3837
#[arg(long, default_value_t, value_enum)]
3938
pub(crate) mode: Mode,
4039
}

crates/uv-dev/src/main.rs

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use crate::clear_compile::ClearCompileArgs;
2020
use crate::compile::CompileArgs;
2121
use crate::generate_all::Args as GenerateAllArgs;
2222
use crate::generate_cli_reference::Args as GenerateCliReferenceArgs;
23+
use crate::generate_env_vars_reference::Args as GenerateEnvVarsReferenceArgs;
2324
use crate::generate_json_schema::Args as GenerateJsonSchemaArgs;
2425
use crate::generate_options_reference::Args as GenerateOptionsReferenceArgs;
2526
#[cfg(feature = "render")]
@@ -31,6 +32,7 @@ mod clear_compile;
3132
mod compile;
3233
mod generate_all;
3334
mod generate_cli_reference;
35+
mod generate_env_vars_reference;
3436
mod generate_json_schema;
3537
mod generate_options_reference;
3638
mod render_benchmarks;
@@ -54,6 +56,8 @@ enum Cli {
5456
GenerateOptionsReference(GenerateOptionsReferenceArgs),
5557
/// Generate the CLI reference for the documentation.
5658
GenerateCliReference(GenerateCliReferenceArgs),
59+
/// Generate the environment variables reference for the documentation.
60+
GenerateEnvVarsReference(GenerateEnvVarsReferenceArgs),
5761
#[cfg(feature = "render")]
5862
/// Render the benchmarks.
5963
RenderBenchmarks(RenderBenchmarksArgs),
@@ -70,6 +74,7 @@ async fn run() -> Result<()> {
7074
Cli::GenerateJSONSchema(args) => generate_json_schema::main(&args)?,
7175
Cli::GenerateOptionsReference(args) => generate_options_reference::main(&args)?,
7276
Cli::GenerateCliReference(args) => generate_cli_reference::main(&args)?,
77+
Cli::GenerateEnvVarsReference(args) => generate_env_vars_reference::main(&args)?,
7378
#[cfg(feature = "render")]
7479
Cli::RenderBenchmarks(args) => render_benchmarks::render_benchmarks(&args)?,
7580
}

crates/uv-macros/src/lib.rs

+90-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ mod options_metadata;
22

33
use proc_macro::TokenStream;
44
use quote::quote;
5-
use syn::{parse_macro_input, DeriveInput};
5+
use syn::{parse_macro_input, Attribute, DeriveInput, ImplItem, ItemImpl, LitStr};
66

77
#[proc_macro_derive(OptionsMetadata, attributes(option, doc, option_group))]
88
pub fn derive_options_metadata(input: TokenStream) -> TokenStream {
@@ -49,3 +49,92 @@ fn impl_combine(ast: &DeriveInput) -> TokenStream {
4949
};
5050
gen.into()
5151
}
52+
53+
fn get_doc_comment(attrs: &[Attribute]) -> String {
54+
attrs
55+
.iter()
56+
.filter_map(|attr| {
57+
if attr.path().is_ident("doc") {
58+
if let syn::Meta::NameValue(meta) = &attr.meta {
59+
if let syn::Expr::Lit(expr) = &meta.value {
60+
if let syn::Lit::Str(str) = &expr.lit {
61+
return Some(str.value().trim().to_string());
62+
}
63+
}
64+
}
65+
}
66+
None
67+
})
68+
.collect::<Vec<_>>()
69+
.join("\n")
70+
}
71+
72+
fn get_env_var_pattern_from_attr(attrs: &[Attribute]) -> Option<String> {
73+
attrs
74+
.iter()
75+
.find(|attr| attr.path().is_ident("attr_env_var_pattern"))
76+
.and_then(|attr| attr.parse_args::<LitStr>().ok())
77+
.map(|lit_str| lit_str.value())
78+
}
79+
80+
fn is_hidden(attrs: &[Attribute]) -> bool {
81+
attrs.iter().any(|attr| attr.path().is_ident("attr_hidden"))
82+
}
83+
84+
/// This attribute is used to generate environment variables metadata for [`uv_static::EnvVars`].
85+
#[proc_macro_attribute]
86+
pub fn attribute_env_vars_metadata(_attr: TokenStream, input: TokenStream) -> TokenStream {
87+
let ast = parse_macro_input!(input as ItemImpl);
88+
89+
let constants: Vec<_> = ast
90+
.items
91+
.iter()
92+
.filter_map(|item| match item {
93+
ImplItem::Const(item) if !is_hidden(&item.attrs) => {
94+
let name = item.ident.to_string();
95+
let doc = get_doc_comment(&item.attrs);
96+
Some((name, doc))
97+
}
98+
ImplItem::Fn(item) if !is_hidden(&item.attrs) => {
99+
// Extract the environment variable patterns.
100+
if let Some(pattern) = get_env_var_pattern_from_attr(&item.attrs) {
101+
let doc = get_doc_comment(&item.attrs);
102+
Some((pattern, doc))
103+
} else {
104+
None // Skip if pattern extraction fails.
105+
}
106+
}
107+
_ => None,
108+
})
109+
.collect();
110+
111+
let struct_name = &ast.self_ty;
112+
let pairs = constants.iter().map(|(name, doc)| {
113+
quote! {
114+
(#name, #doc)
115+
}
116+
});
117+
118+
let expanded = quote! {
119+
#ast
120+
121+
impl #struct_name {
122+
/// Returns a list of pairs of env var and their documentation defined in this impl block.
123+
pub fn metadata<'a>() -> &'a [(&'static str, &'static str)] {
124+
&[#(#pairs),*]
125+
}
126+
}
127+
};
128+
129+
expanded.into()
130+
}
131+
132+
#[proc_macro_attribute]
133+
pub fn attr_hidden(_attr: TokenStream, item: TokenStream) -> TokenStream {
134+
item
135+
}
136+
137+
#[proc_macro_attribute]
138+
pub fn attr_env_var_pattern(_attr: TokenStream, item: TokenStream) -> TokenStream {
139+
item
140+
}

crates/uv-static/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ doctest = false
1616
workspace = true
1717

1818
[dependencies]
19+
uv-macros = { workspace = true }

0 commit comments

Comments
 (0)