Lines
77.27 %
Functions
29.73 %
Branches
100 %
//! Macros for the `actionable` API framework.
#![forbid(unsafe_code)]
#![warn(
clippy::cargo,
missing_docs,
clippy::pedantic,
future_incompatible,
rust_2018_idioms
)]
#![cfg_attr(doc, deny(rustdoc::all))]
use proc_macro::TokenStream;
use proc_macro_error::{abort, emit_error, proc_macro_error};
use syn::{parse::Parse, parse_macro_input, DeriveInput};
mod action;
mod actionable;
mod dispatcher;
/// Derives the `actionable::Action` trait.
///
/// This trait can be customizd using the `action` attribute in these ways:
/// * Crate name override: `#[action(actionable = "someothername")]`. If you
/// find yourself needing to import `actionable` as another name, this setting
/// will replace all mentions of `actionable` with the identifier specified.
#[proc_macro_error]
#[proc_macro_derive(Action, attributes(action))]
pub fn action_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match action::derive(&input) {
Ok(tokens) => tokens.into(),
Err(err) => {
emit_error!(input.ident, err.to_string());
TokenStream::default()
}
/// Derives a set of traits that can be used to implement a permissions-driven
/// API. There are options that can be customized with the `#[actionable]`
/// attribute at the enum level:
/// * Crate name override: `#[actionable(actionable = "someothername")]`. If you
/// ## The Dispatcher Trait
/// The first trait that is generated is named `<EnumName>Dispatcher`. For
/// example, if the enum's name is `Request`, the generated trait name will be
/// `RequestDispatcher`. This trait has no methods for you to implement. It
/// defines several associated types:
/// * `Output`: The `Ok` side of the `Result`.
/// * `Error`: The `Err` side of the `Result`. Must implement
/// `From<actionable::PermissionDenied>`.
/// * For each variant in the enum, another Trait named `<VariantName>Handler`.
/// For example, if the enum variant was `Request::AddUser`, the trait will be
/// `AddUserHandler`. Each of these traits must be implemented by any type
/// implementing `<EnumName>Dispatcher`.
/// The dispatcher trait has a method available for you to use to dispatch
/// requests: `async fn dispatch(&self, permissions: &Permissions, request:
/// <EnumName>) -> Result<Self::Output, Self::Error>`.
/// ## The Handler Traits
/// For each variant in the enum, a trait will be generated named
/// `<VariantName>Handler`. Using the same example above, the enum variant
/// `Request::AddUser` would generate the trait `AddUserHandler`. These traits
/// are implemented using the
/// [`async-trait`](https://crates.io/crate/async-trait) trait.
/// Each variant must have a protection method assigned using the
/// `#[actionable]` attribute. There are three protection methods:
/// ### No Protection: `#[actionable(protection = "none")]`
/// A handler with no protection has one method:
/// ```rust
/// # type Output = ();
/// # type Error = ();
/// # use actionable::{Permissions, async_trait};
/// #[async_trait]
/// trait Handler {
/// type Dispatcher;
/// async fn handle(
/// dispatcher: &Self::Dispatcher,
/// permissions: &Permissions,
/// /* each field on this variant is passed
/// as a parameter to this method */
/// ) -> Result<Output, Error>;
/// }
/// ```
/// Actionable does not do any checks before invoking this handler.
/// ### Simple Protection: `#[actionable(protection = "simple")]`
/// A handler with simple protection exposes methods and types to allow
/// specifying an `actionable::ResourceName` and an `Action` for this handler:
/// # use actionable::{Permissions, ResourceName, async_trait};
/// type Action;
/// fn resource_name<'a>(
/// by reference as a parameter to this method */
/// ) -> ResourceName<'a>;
/// fn action() -> Self::Action;
/// async fn handle_protected(
/// When the handler is invoked, it first checks `permissions` to ensure that
/// `action()` is allowed to be performed on `resource_name()`. If it is not
/// allowed, an `actionable::PermissionDenied` error will be returned. If it is
/// allowed, `handle_protected()` will be executed.
/// ### Custom Protection: `#[actionable(protection = "custom")]`
/// A handler with custom protection has two methods, one to verify permissions
/// and one to execute the protected code:
/// async fn verify_permissions(
/// by refrence as a parameter to this method */
/// ) -> Result<(), Error>;
/// /* each field on this variant is passed as a parameter
/// to this method */
/// Actionable will first call `verify_permissions()`. If you return `Ok(())`,
/// your `handle_protected()` method is invoked.
/// ## Why should you use the built-in protection modes?
/// Actionable attempts to make permission handling easy to understand and
/// implement while making it difficult to forget implementing permission
/// handling. This is only effective if you use the protection levels.
/// Because Actionable includes `permissions` in every call to
/// `handle[_protected]()`, technically you could use a protection level of
/// `none` and implement permission handling within the `handle()` function.
/// While it would work, you shouldn't do this.
/// Actionable encourages placing information about permission handling in the
/// definition of the enum. By using `simple` and `custom` protection
/// strategies, consumers of your API will be able to see at the enum level what
/// APIs check permissions. When trying to understand what permissions are being
/// used, this is critical.
/// By placing your permission handling code in locations that follow a
/// repeatable patern, you're helping anyone who is reading the code separate
/// what logic is related to permission handling and what logic is related to
/// the API implementation.
/// ## What protection mode should you use?
/// * If your handler is operating on a single resource and performing a single
/// action, use the `simple` protection mode.
/// * If your handler needs to check permissions but it's more complicated than
/// the first scenario, use the `custom` protection mode.
/// * If you aren't enforcing permissions inside of this handler, use the `none`
/// protection mode.
#[proc_macro_derive(Actionable, attributes(actionable))]
pub fn actionable_derive(input: TokenStream) -> TokenStream {
match actionable::derive(&input) {
/// Derives the `Dispatcher` trait.
/// This trait requires the `input` parameter to be specified. The full list of
/// parameters that can be customized are:
/// * `input` Type: `#[dispatcher(input = "EnumName")]`. The enum name here
/// needs to have had [`Actionable`](actionable_derive) derived on it.
/// The `input` type must be in scope, as do the derived traits generated by
/// deriving `Actionable`.
#[proc_macro_derive(Dispatcher, attributes(dispatcher))]
pub fn dispatcher_derive(input: TokenStream) -> TokenStream {
match dispatcher::derive(&input, false) {
/// Derives the `AsyncDispatcher` trait.
#[proc_macro_derive(AsyncDispatcher, attributes(dispatcher))]
pub fn async_dispatcher_derive(input: TokenStream) -> TokenStream {
match dispatcher::derive(&input, true) {
fn actionable(actionable: Option<syn::Path>, span: proc_macro2::Span) -> syn::Path {
actionable.unwrap_or_else(|| {
let mut segments = syn::punctuated::Punctuated::new();
segments.push_value(syn::PathSegment {
ident: syn::Ident::new("actionable", span),
arguments: syn::PathArguments::None,
});
syn::Path {
leading_colon: None,
segments,
})
#[derive(Debug, thiserror::Error)]
pub(crate) enum Error {
#[error("darling error: {0}")]
Darling(#[from] darling::Error),
#[error("syn error: {0}")]
Syn(#[from] syn::Error),
#[derive(Debug, Clone)]
enum ActionableArg {
Actionable(syn::Path),
Async,
impl Parse for ActionableArg {
fn parse(input: &'_ syn::parse::ParseBuffer<'_>) -> syn::Result<Self> {
let ident: syn::Ident = input.parse()?;
if ident == "actionable" {
let _: syn::Token![=] = input.parse()?;
Ok(Self::Actionable(input.parse()?))
} else if ident == "async" {
Ok(Self::Async)
} else {
abort!(ident, "expected `actionable` or `async`")
#[derive(Clone, Debug, Default)]
struct ActionableArgs {
actionable: Option<syn::Path>,
asynchronous: bool,
impl Parse for ActionableArgs {
let content;
let _ = syn::parenthesized!(content in input);
let args: syn::punctuated::Punctuated<ActionableArg, syn::Token![,]> =
content.parse_terminated(ActionableArg::parse)?;
let mut result = Self::default();
for arg in args {
match arg {
ActionableArg::Actionable(path) => result.actionable = Some(path),
ActionableArg::Async => result.asynchronous = true,
Ok(result)