Skip to content

Commit

Permalink
Generate environment variables doc from code (#8493)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
j178 authored Nov 3, 2024
1 parent 545a55f commit 3dfedf1
Show file tree
Hide file tree
Showing 13 changed files with 491 additions and 135 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ CHANGELOG.md
PREVIEW-CHANGELOG.md
docs/reference/cli.md
docs/reference/settings.md
docs/configuration/environment.md
ecosystem/home-assistant-core/LICENSE.md
docs/guides/integration/gitlab.md
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion crates/uv-dev/src/generate_all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

use anyhow::Result;

use crate::{generate_cli_reference, generate_json_schema, generate_options_reference};
use crate::{
generate_cli_reference, generate_env_vars_reference, generate_json_schema,
generate_options_reference,
};

#[derive(clap::Args)]
pub(crate) struct Args {
Expand All @@ -27,5 +30,6 @@ pub(crate) fn main(args: &Args) -> Result<()> {
generate_json_schema::main(&generate_json_schema::Args { mode: args.mode })?;
generate_options_reference::main(&generate_options_reference::Args { mode: args.mode })?;
generate_cli_reference::main(&generate_cli_reference::Args { mode: args.mode })?;
generate_env_vars_reference::main(&generate_env_vars_reference::Args { mode: args.mode })?;
Ok(())
}
1 change: 0 additions & 1 deletion crates/uv-dev/src/generate_cli_reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ const SHOW_HIDDEN_COMMANDS: &[&str] = &["generate-shell-completion"];

#[derive(clap::Args)]
pub(crate) struct Args {
/// Write the generated output to stdout (rather than to `settings.md`).
#[arg(long, default_value_t, value_enum)]
pub(crate) mode: Mode,
}
Expand Down
98 changes: 98 additions & 0 deletions crates/uv-dev/src/generate_env_vars_reference.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//! Generate the environment variables reference from `uv_static::EnvVars`.

use anyhow::bail;
use pretty_assertions::StrComparison;
use std::path::PathBuf;

use uv_static::EnvVars;

use crate::generate_all::Mode;
use crate::ROOT_DIR;

#[derive(clap::Args)]
pub(crate) struct Args {
#[arg(long, default_value_t, value_enum)]
pub(crate) mode: Mode,
}

pub(crate) fn main(args: &Args) -> anyhow::Result<()> {
let reference_string = generate();
let filename = "environment.md";
let reference_path = PathBuf::from(ROOT_DIR)
.join("docs")
.join("configuration")
.join(filename);

match args.mode {
Mode::DryRun => {
anstream::println!("{reference_string}");
}
Mode::Check => match fs_err::read_to_string(reference_path) {
Ok(current) => {
if current == reference_string {
anstream::println!("Up-to-date: {filename}");
} else {
let comparison = StrComparison::new(&current, &reference_string);
bail!("{filename} changed, please run `cargo dev generate-env-vars-reference`:\n{comparison}");
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
bail!("{filename} not found, please run `cargo dev generate-env-vars-reference`");
}
Err(err) => {
bail!(
"{filename} changed, please run `cargo dev generate-env-vars-reference`:\n{err}"
);
}
},
Mode::Write => match fs_err::read_to_string(&reference_path) {
Ok(current) => {
if current == reference_string {
anstream::println!("Up-to-date: {filename}");
} else {
anstream::println!("Updating: {filename}");
fs_err::write(reference_path, reference_string.as_bytes())?;
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
anstream::println!("Updating: {filename}");
fs_err::write(reference_path, reference_string.as_bytes())?;
}
Err(err) => {
bail!("{filename} changed, please run `cargo dev generate-env-vars-reference`:\n{err}");
}
},
}

Ok(())
}

fn generate() -> String {
let mut output = String::new();

output.push_str("# Environment variables\n\n");
output.push_str("uv respects the following environment variables:\n\n");

for (var, doc) in EnvVars::metadata() {
// Remove empty lines and ddd two spaces to the beginning from the second line.
let doc = doc
.lines()
.enumerate()
.filter(|(_, line)| !line.trim().is_empty())
.map(|(i, line)| {
if i == 0 {
line.to_string()
} else {
format!(" {line}")
}
})
.collect::<Vec<_>>()
.join("\n");
output.push_str(&format!("- `{var}`: {doc}\n"));
}

output
}

#[cfg(test)]
mod tests;
19 changes: 19 additions & 0 deletions crates/uv-dev/src/generate_env_vars_reference/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use std::env;

use anyhow::Result;

use uv_static::EnvVars;

use crate::generate_all::Mode;

use super::{main, Args};

#[test]
fn test_generate_env_vars_reference() -> Result<()> {
let mode = if env::var(EnvVars::UV_UPDATE_SCHEMA).as_deref() == Ok("1") {
Mode::Write
} else {
Mode::Check
};
main(&Args { mode })
}
1 change: 0 additions & 1 deletion crates/uv-dev/src/generate_json_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ struct CombinedOptions {

#[derive(clap::Args)]
pub(crate) struct Args {
/// Write the generated output to stdout (rather than to `uv.schema.json`).
#[arg(long, default_value_t, value_enum)]
pub(crate) mode: Mode,
}
Expand Down
1 change: 0 additions & 1 deletion crates/uv-dev/src/generate_options_reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ struct CombinedOptions {

#[derive(clap::Args)]
pub(crate) struct Args {
/// Write the generated output to stdout (rather than to `settings.md`).
#[arg(long, default_value_t, value_enum)]
pub(crate) mode: Mode,
}
Expand Down
5 changes: 5 additions & 0 deletions crates/uv-dev/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::clear_compile::ClearCompileArgs;
use crate::compile::CompileArgs;
use crate::generate_all::Args as GenerateAllArgs;
use crate::generate_cli_reference::Args as GenerateCliReferenceArgs;
use crate::generate_env_vars_reference::Args as GenerateEnvVarsReferenceArgs;
use crate::generate_json_schema::Args as GenerateJsonSchemaArgs;
use crate::generate_options_reference::Args as GenerateOptionsReferenceArgs;
#[cfg(feature = "render")]
Expand All @@ -31,6 +32,7 @@ mod clear_compile;
mod compile;
mod generate_all;
mod generate_cli_reference;
mod generate_env_vars_reference;
mod generate_json_schema;
mod generate_options_reference;
mod render_benchmarks;
Expand All @@ -54,6 +56,8 @@ enum Cli {
GenerateOptionsReference(GenerateOptionsReferenceArgs),
/// Generate the CLI reference for the documentation.
GenerateCliReference(GenerateCliReferenceArgs),
/// Generate the environment variables reference for the documentation.
GenerateEnvVarsReference(GenerateEnvVarsReferenceArgs),
#[cfg(feature = "render")]
/// Render the benchmarks.
RenderBenchmarks(RenderBenchmarksArgs),
Expand All @@ -70,6 +74,7 @@ async fn run() -> Result<()> {
Cli::GenerateJSONSchema(args) => generate_json_schema::main(&args)?,
Cli::GenerateOptionsReference(args) => generate_options_reference::main(&args)?,
Cli::GenerateCliReference(args) => generate_cli_reference::main(&args)?,
Cli::GenerateEnvVarsReference(args) => generate_env_vars_reference::main(&args)?,
#[cfg(feature = "render")]
Cli::RenderBenchmarks(args) => render_benchmarks::render_benchmarks(&args)?,
}
Expand Down
91 changes: 90 additions & 1 deletion crates/uv-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ mod options_metadata;

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
use syn::{parse_macro_input, Attribute, DeriveInput, ImplItem, ItemImpl, LitStr};

#[proc_macro_derive(OptionsMetadata, attributes(option, doc, option_group))]
pub fn derive_options_metadata(input: TokenStream) -> TokenStream {
Expand Down Expand Up @@ -49,3 +49,92 @@ fn impl_combine(ast: &DeriveInput) -> TokenStream {
};
gen.into()
}

fn get_doc_comment(attrs: &[Attribute]) -> String {
attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("doc") {
if let syn::Meta::NameValue(meta) = &attr.meta {
if let syn::Expr::Lit(expr) = &meta.value {
if let syn::Lit::Str(str) = &expr.lit {
return Some(str.value().trim().to_string());
}
}
}
}
None
})
.collect::<Vec<_>>()
.join("\n")
}

fn get_env_var_pattern_from_attr(attrs: &[Attribute]) -> Option<String> {
attrs
.iter()
.find(|attr| attr.path().is_ident("attr_env_var_pattern"))
.and_then(|attr| attr.parse_args::<LitStr>().ok())
.map(|lit_str| lit_str.value())
}

fn is_hidden(attrs: &[Attribute]) -> bool {
attrs.iter().any(|attr| attr.path().is_ident("attr_hidden"))
}

/// This attribute is used to generate environment variables metadata for [`uv_static::EnvVars`].
#[proc_macro_attribute]
pub fn attribute_env_vars_metadata(_attr: TokenStream, input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as ItemImpl);

let constants: Vec<_> = ast
.items
.iter()
.filter_map(|item| match item {
ImplItem::Const(item) if !is_hidden(&item.attrs) => {
let name = item.ident.to_string();
let doc = get_doc_comment(&item.attrs);
Some((name, doc))
}
ImplItem::Fn(item) if !is_hidden(&item.attrs) => {
// Extract the environment variable patterns.
if let Some(pattern) = get_env_var_pattern_from_attr(&item.attrs) {
let doc = get_doc_comment(&item.attrs);
Some((pattern, doc))
} else {
None // Skip if pattern extraction fails.
}
}
_ => None,
})
.collect();

let struct_name = &ast.self_ty;
let pairs = constants.iter().map(|(name, doc)| {
quote! {
(#name, #doc)
}
});

let expanded = quote! {
#ast

impl #struct_name {
/// Returns a list of pairs of env var and their documentation defined in this impl block.
pub fn metadata<'a>() -> &'a [(&'static str, &'static str)] {
&[#(#pairs),*]
}
}
};

expanded.into()
}

#[proc_macro_attribute]
pub fn attr_hidden(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}

#[proc_macro_attribute]
pub fn attr_env_var_pattern(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
1 change: 1 addition & 0 deletions crates/uv-static/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ doctest = false
workspace = true

[dependencies]
uv-macros = { workspace = true }
Loading

0 comments on commit 3dfedf1

Please sign in to comment.