mirror of
https://github.com/nushell/nushell.git
synced 2025-06-01 23:55:50 +02:00
# Description After discussing with @sholderbach the cumbersome usage of `nu_protocol::Value` in Rust, I created a derive macro to simplify it. I’ve added a new crate called `nu-derive-value`, which includes two macros, `IntoValue` and `FromValue`. These are re-exported in `nu-protocol` and should be encouraged to be used via that re-export. The macros ensure that all types can easily convert from and into `Value`. For example, as a plugin author, you can define your plugin configuration using a Rust struct and easily convert it using `FromValue`. This makes plugin configuration less of a hassle. I introduced the `IntoValue` trait for a standardized approach to converting values into `Value` (and a fallible variant `TryIntoValue`). This trait could potentially replace existing `into_value` methods. Along with this, I've implemented `FromValue` for several standard types and refined other implementations to use blanket implementations where applicable. I made these design choices with input from @devyn. There are more improvements possible, but this is a solid start and the PR is already quite substantial. # User-Facing Changes For `nu-protocol` users, these changes simplify the handling of `Value`s. There are no changes for end-users of nushell itself. # Tests + Formatting Documenting the macros itself is not really possible, as they cannot really reference any other types since they are the root of the dependency graph. The standard library has the same problem ([std::Debug](https://doc.rust-lang.org/stable/std/fmt/derive.Debug.html)). However I documented the `FromValue` and `IntoValue` traits completely. For testing, I made of use `proc-macro2` in the derive macro code. This would allow testing the generated source code. Instead I just tested that the derived functionality is correct. This is done in `nu_protocol::value::test_derive`, as a consumer of `nu-derive-value` needs to do the testing of the macro usage. I think that these tests should provide a stable baseline so that users can be sure that the impl works. # After Submitting With these macros available, we can probably use them in some examples for plugins to showcase the use of them.
117 lines
4.3 KiB
Rust
117 lines
4.3 KiB
Rust
use convert_case::Case;
|
|
use syn::{spanned::Spanned, Attribute, Fields, LitStr};
|
|
|
|
use crate::{error::DeriveError, HELPER_ATTRIBUTE};
|
|
|
|
#[derive(Debug)]
|
|
pub struct ContainerAttributes {
|
|
pub rename_all: Case,
|
|
}
|
|
|
|
impl Default for ContainerAttributes {
|
|
fn default() -> Self {
|
|
Self {
|
|
rename_all: Case::Snake,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ContainerAttributes {
|
|
pub fn parse_attrs<'a, M>(
|
|
iter: impl Iterator<Item = &'a Attribute>,
|
|
) -> Result<Self, DeriveError<M>> {
|
|
let mut container_attrs = ContainerAttributes::default();
|
|
for attr in filter(iter) {
|
|
// This is a container to allow returning derive errors inside the parse_nested_meta fn.
|
|
let mut err = Ok(());
|
|
|
|
attr.parse_nested_meta(|meta| {
|
|
let ident = meta.path.require_ident()?;
|
|
match ident.to_string().as_str() {
|
|
"rename_all" => {
|
|
// The matched case are all useful variants from `convert_case` with aliases
|
|
// that `serde` uses.
|
|
let case: LitStr = meta.value()?.parse()?;
|
|
let case = match case.value().as_str() {
|
|
"UPPER CASE" | "UPPER WITH SPACES CASE" => Case::Upper,
|
|
"lower case" | "lower with spaces case" => Case::Lower,
|
|
"Title Case" => Case::Title,
|
|
"camelCase" => Case::Camel,
|
|
"PascalCase" | "UpperCamelCase" => Case::Pascal,
|
|
"snake_case" => Case::Snake,
|
|
"UPPER_SNAKE_CASE" | "SCREAMING_SNAKE_CASE" => Case::UpperSnake,
|
|
"kebab-case" => Case::Kebab,
|
|
"COBOL-CASE" | "UPPER-KEBAB-CASE" | "SCREAMING-KEBAB-CASE" => {
|
|
Case::Cobol
|
|
}
|
|
"Train-Case" => Case::Train,
|
|
"flatcase" | "lowercase" => Case::Flat,
|
|
"UPPERFLATCASE" | "UPPERCASE" => Case::UpperFlat,
|
|
// Although very funny, we don't support `Case::{Toggle, Alternating}`,
|
|
// as we see no real benefit.
|
|
c => {
|
|
err = Err(DeriveError::InvalidAttributeValue {
|
|
value_span: case.span(),
|
|
value: Box::new(c.to_string()),
|
|
});
|
|
return Ok(()); // We stored the err in `err`.
|
|
}
|
|
};
|
|
container_attrs.rename_all = case;
|
|
}
|
|
ident => {
|
|
err = Err(DeriveError::UnexpectedAttribute {
|
|
meta_span: ident.span(),
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
})
|
|
.map_err(DeriveError::Syn)?;
|
|
|
|
err?; // Shortcircuit here if `err` is holding some error.
|
|
}
|
|
|
|
Ok(container_attrs)
|
|
}
|
|
}
|
|
|
|
pub fn filter<'a>(
|
|
iter: impl Iterator<Item = &'a Attribute>,
|
|
) -> impl Iterator<Item = &'a Attribute> {
|
|
iter.filter(|attr| attr.path().is_ident(HELPER_ATTRIBUTE))
|
|
}
|
|
|
|
// The deny functions are built to easily deny the use of the helper attribute if used incorrectly.
|
|
// As the usage of it gets more complex, these functions might be discarded or replaced.
|
|
|
|
/// Deny any attribute that uses the helper attribute.
|
|
pub fn deny<M>(attrs: &[Attribute]) -> Result<(), DeriveError<M>> {
|
|
match filter(attrs.iter()).next() {
|
|
Some(attr) => Err(DeriveError::InvalidAttributePosition {
|
|
attribute_span: attr.span(),
|
|
}),
|
|
None => Ok(()),
|
|
}
|
|
}
|
|
|
|
/// Deny any attributes that uses the helper attribute on any field.
|
|
pub fn deny_fields<M>(fields: &Fields) -> Result<(), DeriveError<M>> {
|
|
match fields {
|
|
Fields::Named(fields) => {
|
|
for field in fields.named.iter() {
|
|
deny(&field.attrs)?;
|
|
}
|
|
}
|
|
Fields::Unnamed(fields) => {
|
|
for field in fields.unnamed.iter() {
|
|
deny(&field.attrs)?;
|
|
}
|
|
}
|
|
Fields::Unit => (),
|
|
}
|
|
|
|
Ok(())
|
|
}
|