Skip to content

Commit

Permalink
Autogenerate possible values for enums in reference documentation (#5137
Browse files Browse the repository at this point in the history
)

## Summary

For example:

![Screenshot 2024-07-16 at 7 44
10 PM](https://github.com/user-attachments/assets/73ce16ba-eb0e-43c4-a741-65a54637452f)

Closes #5129.
  • Loading branch information
charliermarsh committed Jul 17, 2024
1 parent 3e93255 commit a191f84
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 62 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

13 changes: 12 additions & 1 deletion crates/uv-dev/src/generate_options_reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,18 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S
output.push_str("\n\n");
output.push_str(&format!("**Default value**: `{}`\n", field.default));
output.push('\n');
output.push_str(&format!("**Type**: `{}`\n", field.value_type));
if let Some(possible_values) = field
.possible_values
.as_ref()
.filter(|values| !values.is_empty())
{
output.push_str("**Possible values**:\n\n");
for value in possible_values {
output.push_str(format!("- {value}\n").as_str());
}
} else {
output.push_str(&format!("**Type**: `{}`\n", field.value_type));
}
output.push('\n');
output.push_str("**Example usage**:\n\n");
output.push_str(&format_tab(
Expand Down
73 changes: 71 additions & 2 deletions crates/uv-macros/src/options_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use syn::meta::ParseNestedMeta;
use syn::spanned::Spanned;
use syn::{
AngleBracketedGenericArguments, Attribute, Data, DataStruct, DeriveInput, ExprLit, Field,
Fields, Lit, LitStr, Meta, Path, PathArguments, PathSegment, Type, TypePath,
Fields, GenericArgument, Lit, LitStr, Meta, Path, PathArguments, PathSegment, Type, TypePath,
};
use textwrap::dedent;

Expand Down Expand Up @@ -194,6 +194,7 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
value_type,
example,
scope,
possible_values,
} = parse_field_attributes(attr)?;
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());

Expand Down Expand Up @@ -224,6 +225,25 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
quote!(None)
};

let possible_values = if possible_values == Some(true) {
let inner_type = get_inner_type_if_option(&field.ty).unwrap_or(&field.ty);
let inner_type = quote!(#inner_type);
quote!(
Some(
<#inner_type as clap::ValueEnum>::value_variants()
.iter()
.filter_map(clap::ValueEnum::to_possible_value)
.map(|value| uv_options_metadata::PossibleValue {
name: value.get_name().to_string(),
help: value.get_help().map(ToString::to_string),
})
.collect()
)
)
} else {
quote!(None)
};

Ok(quote_spanned!(
ident.span() => {
visit.record_field(#kebab_name, uv_options_metadata::OptionField{
Expand All @@ -232,7 +252,8 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
value_type: &#value_type,
example: &#example,
scope: #scope,
deprecated: #deprecated
deprecated: #deprecated,
possible_values: #possible_values,
})
}
))
Expand All @@ -244,13 +265,15 @@ struct FieldAttributes {
value_type: String,
example: String,
scope: Option<String>,
possible_values: Option<bool>,
}

fn parse_field_attributes(attribute: &Attribute) -> syn::Result<FieldAttributes> {
let mut default = None;
let mut value_type = None;
let mut example = None;
let mut scope = None;
let mut possible_values = None;

attribute.parse_nested_meta(|meta| {
if meta.path.is_ident("default") {
Expand All @@ -262,6 +285,8 @@ fn parse_field_attributes(attribute: &Attribute) -> syn::Result<FieldAttributes>
} else if meta.path.is_ident("example") {
let example_text = get_string_literal(&meta, "value_type", "option")?.value();
example = Some(dedent(&example_text).trim_matches('\n').to_string());
} else if meta.path.is_ident("possible_values") {
possible_values = get_bool_literal(&meta, "possible_values", "option")?;
} else {
return Err(syn::Error::new(
meta.path.span(),
Expand Down Expand Up @@ -292,6 +317,7 @@ fn parse_field_attributes(attribute: &Attribute) -> syn::Result<FieldAttributes>
value_type,
example,
scope,
possible_values,
})
}

Expand All @@ -318,6 +344,23 @@ fn parse_deprecated_attribute(attribute: &Attribute) -> syn::Result<DeprecatedAt
Ok(deprecated)
}

fn get_inner_type_if_option(ty: &Type) -> Option<&Type> {
if let Type::Path(type_path) = ty {
if type_path.path.segments.len() == 1 && type_path.path.segments[0].ident == "Option" {
if let PathArguments::AngleBracketed(angle_bracketed_args) =
&type_path.path.segments[0].arguments
{
if angle_bracketed_args.args.len() == 1 {
if let GenericArgument::Type(inner_type) = &angle_bracketed_args.args[0] {
return Some(inner_type);
}
}
}
}
}
None
}

fn get_string_literal(
meta: &ParseNestedMeta,
meta_name: &str,
Expand Down Expand Up @@ -351,6 +394,32 @@ fn get_string_literal(
}
}

fn get_bool_literal(
meta: &ParseNestedMeta,
meta_name: &str,
attribute_name: &str,
) -> syn::Result<Option<bool>> {
let expr: syn::Expr = meta.value()?.parse()?;

let mut value = &expr;
while let syn::Expr::Group(e) = value {
value = &e.expr;
}

if let syn::Expr::Lit(ExprLit {
lit: Lit::Bool(lit),
..
}) = value
{
Ok(Some(lit.value))
} else {
Err(syn::Error::new(
expr.span(),
format!("expected {attribute_name} attribute to be a boolean: `{meta_name} = true`"),
))
}
}

#[derive(Default, Debug)]
struct DeprecatedAttribute {
since: Option<String>,
Expand Down
51 changes: 45 additions & 6 deletions crates/uv-options-metadata/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ impl OptionSet {
/// example: "",
/// scope: None,
/// deprecated: None,
/// possible_values: None
/// });
/// }
/// }
Expand All @@ -141,7 +142,8 @@ impl OptionSet {
/// value_type: "bool",
/// example: "",
/// scope: None,
/// deprecated: None
/// deprecated: None,
/// possible_values: None
/// });
///
/// visit.record_set("format", Nested::metadata());
Expand All @@ -158,7 +160,8 @@ impl OptionSet {
/// value_type: "bool",
/// example: "",
/// scope: None,
/// deprecated: None
/// deprecated: None,
/// possible_values: None
/// });
/// }
/// }
Expand Down Expand Up @@ -190,7 +193,8 @@ impl OptionSet {
/// value_type: "bool",
/// example: "",
/// scope: None,
/// deprecated: None
/// deprecated: None,
/// possible_values: None
/// };
///
/// impl OptionsMetadata for WithOptions {
Expand All @@ -213,7 +217,8 @@ impl OptionSet {
/// value_type: "bool",
/// example: "",
/// scope: None,
/// deprecated: None
/// deprecated: None,
/// possible_values: None
/// };
///
/// struct Root;
Expand All @@ -226,7 +231,8 @@ impl OptionSet {
/// value_type: "bool",
/// example: "",
/// scope: None,
/// deprecated: None
/// deprecated: None,
/// possible_values: None
/// });
///
/// visit.record_set("format", Nested::metadata());
Expand Down Expand Up @@ -388,6 +394,7 @@ pub struct OptionField {
pub scope: Option<&'static str>,
pub example: &'static str,
pub deprecated: Option<Deprecated>,
pub possible_values: Option<Vec<PossibleValue>>,
}

#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
Expand All @@ -400,8 +407,22 @@ impl Display for OptionField {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{}", self.doc)?;
writeln!(f)?;

writeln!(f, "Default value: {}", self.default)?;
writeln!(f, "Type: {}", self.value_type)?;

if let Some(possible_values) = self
.possible_values
.as_ref()
.filter(|values| !values.is_empty())
{
writeln!(f, "Possible values:")?;
writeln!(f)?;
for value in possible_values {
writeln!(f, "- {value}")?;
}
} else {
writeln!(f, "Type: {}", self.value_type)?;
}

if let Some(deprecated) = &self.deprecated {
write!(f, "Deprecated")?;
Expand All @@ -420,3 +441,21 @@ impl Display for OptionField {
writeln!(f, "Example usage:\n```toml\n{}\n```", self.example)
}
}

/// A possible value for an enum, similar to Clap's `PossibleValue` type (but without a dependency
/// on Clap).
#[derive(Debug, Eq, PartialEq, Clone, Serialize)]
pub struct PossibleValue {
pub name: String,
pub help: Option<String>,
}

impl Display for PossibleValue {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "`\"{}\"`", self.name)?;
if let Some(help) = &self.help {
write!(f, ": {help}")?;
}
Ok(())
}
}
11 changes: 6 additions & 5 deletions crates/uv-settings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,19 @@ workspace = true

[dependencies]
distribution-types = { workspace = true, features = ["schemars"] }
install-wheel-rs = { workspace = true, features = ["schemars"] }
install-wheel-rs = { workspace = true, features = ["schemars", "clap"] }
pep508_rs = { workspace = true }
pypi-types = { workspace = true }
uv-configuration = { workspace = true, features = ["schemars"] }
uv-configuration = { workspace = true, features = ["schemars", "clap"] }
uv-fs = { workspace = true }
uv-macros = { workspace = true }
uv-normalize = { workspace = true, features = ["schemars"] }
uv-options-metadata = { workspace = true }
uv-python = { workspace = true, features = ["schemars"] }
uv-resolver = { workspace = true, features = ["schemars"] }
uv-python = { workspace = true, features = ["schemars", "clap"] }
uv-resolver = { workspace = true, features = ["schemars", "clap"] }
uv-warnings = { workspace = true }

clap = { workspace = true }
dirs-sys = { workspace = true }
fs-err = { workspace = true }
schemars = { workspace = true, optional = true }
Expand All @@ -35,4 +36,4 @@ toml = { workspace = true }
tracing = { workspace = true }

[package.metadata.cargo-shear]
ignored = ["uv-options-metadata"]
ignored = ["uv-options-metadata", "clap"]
Loading

0 comments on commit a191f84

Please sign in to comment.