Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: String based permission nodes #632

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open

Conversation

Flemmli97
Copy link
Contributor

@Flemmli97 Flemmli97 commented Nov 19, 2024

What this PR does 📖

Rewrites the community permission system to use string based permissions instead of enums. String based permission have the form some.permission and have the advantage that they easily expandable and also allows hierarchy checking.

When granting/revoking some it will grant all permissions under the same namespace e.g. some.permission1 and some.permission2.

The builtin permissions are still in an enum to allow ease of usage. The API to check a permission accepts anything that is representable as a String.

Also adds has_community_permission and has_community_channel_permission to the API

@Flemmli97 Flemmli97 marked this pull request as draft November 19, 2024 15:58
@Flemmli97 Flemmli97 marked this pull request as ready for review November 22, 2024 16:17
Copy link
Contributor

@dariusc93 dariusc93 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain how this has benefits over just using enums, given that the permissions themselves are fixed, having them as strings altogether seems a bit redundant. Internally when is serialized to a string, it can represent as this node-style and it could be easily deserialize to an enum.

Comment on lines 1 to 116
/// }
///
/// assert_eq!(SomeEnum::SomeValue1.to_string(), "prefix.some_value1");
/// assert_eq!(SomeEnum::SomeValue2.to_string(), "some_value2");
/// ```
#[proc_macro_derive(EnumFormatting, attributes(permission))]
pub fn permission_node(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
if let Data::Enum(enum_data) = &input.data {
let mut variants = vec![];
for variant in &enum_data.variants {
let mut format = None;
if let Some(attr) = variant
.attrs
.iter()
.find(|attr| attr.path().is_ident("permission"))
{
if let syn::Meta::List(l) = &attr.meta {
if let Err(err) = l.parse_nested_meta(|meta| {
if meta.path.is_ident("formatting") {
let exp: syn::Expr = meta.value()?.parse()?;
if let syn::Expr::Lit(v) = exp {
if let syn::Lit::Str(s) = v.lit {
format = Some(s.value());
}
}
return Ok(());
}
Err(meta.error("Unrecognized attribute"))
}) {
panic!("{:?}", err);
}
}
}
let format = format.unwrap_or("{}".into());

let variant_name = &variant.ident;

variants.push(quote! {
#name::#variant_name => {
let mut snake = String::new();
for (i, ch) in stringify!(#variant_name).char_indices() {
if i > 0 && ch.is_uppercase() {
snake.push('_');
}
snake.push(ch.to_ascii_lowercase());
}
format!(#format, snake)
},
});
}

let expanded = quote! {
impl std::fmt::Display for #name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let result = match self {
#(#variants)*
};
f.write_str(&format!("{}", result))
}
}
};

TokenStream::from(expanded)
} else {
panic!("Only Enums are supported");
}
}

/// Implements #values() for enum values that returns all defined values in the enum
/// E.g.
/// ```
/// use enum_macro::EnumValues;
/// #[derive(EnumValues, PartialEq, Debug)]
/// pub enum SomeEnum {
/// SomeValue1,
/// SomeValue2
/// }
///
/// assert_eq!([SomeEnum::SomeValue1, SomeEnum::SomeValue2], SomeEnum::values());
/// ```
#[proc_macro_derive(EnumValues)]
pub fn enum_values(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
if let Data::Enum(enum_data) = input.data {
let variants: Vec<_> = enum_data.variants.into_iter().map(|v| v.ident).collect();
let expanded = quote! {
impl #name {
pub fn values() -> &'static[#name] {
&[ #(#name::#variants),* ]
}
}
};
TokenStream::from(expanded)
} else {
panic!("Only Enums are supported");
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldnt it be better to just use serde to rename the enum than to use a macro?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serde can only to something like upper/lowercase etc. they dont have anything like formatting sadly

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use #[serde(rename="some.permission.node")] for each enum variant. Ie

#[derive(Serialize, Deserialize)]
enum PermissionNode {
    #[serde(rename="add.identity")]
    AddIdentity,
    #[serde(rename="remove.identity")]
    RemoveIdentity
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes but with that its a bit decoupled from the actual enum name which i didnt like

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats only an example though. It could be named anything, however serde would handle that though when it comes to deserializing it back to the enum.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i meant that serde does not give access to the enum name inside renaming. so if you e.g. rename the enum itself AddIdentity you also need to update the serialized name. its a bit nitpicky but initially i wanted the enums purely for sanity checks within warp and allow arbitrary strings as permissions

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case you would want to serialize it manually if you dont want to update the rename attr macro, although this shouldnt exactly be a problem to rename it either (though to take caution against breaking changes too).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can change it back to serde if you think its better. i personally think manually defining the string everytime is a bit cumbersome

@Flemmli97
Copy link
Contributor Author

Could you explain how this has benefits over just using enums, given that the permissions themselves are fixed, having them as strings altogether seems a bit redundant. Internally when is serialized to a string, it can represent as this node-style and it could be easily deserialize to an enum.

initially it was so you could set (and check) arbitrary permission as otherwise they would be hardcoded. It would allow other devs etc. to make use of this system too and defining their own permissions. though i agree that the way its currently written still doesnt allow that

@Flemmli97
Copy link
Contributor Author

Flemmli97 commented Nov 29, 2024

hmm not sure why running cargo clippy -- -D warnings locally does not throw these linter errors...

@dariusc93
Copy link
Contributor

dariusc93 commented Nov 30, 2024

Test failing due to invalid field (should be permissions) and invalid type.

EDIT: Looks like the event originally had permission so im not sure if its intended to provide an array or a single permission?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants