actionable_macros/lib.rs
1//! Macros for the `actionable` API framework.
2
3#![forbid(unsafe_code)]
4#![warn(
5 clippy::cargo,
6 missing_docs,
7 clippy::pedantic,
8 future_incompatible,
9 rust_2018_idioms
10)]
11#![cfg_attr(doc, deny(rustdoc::all))]
12
13use proc_macro::TokenStream;
14use proc_macro_error::{abort, emit_error, proc_macro_error};
15use syn::{parse::Parse, parse_macro_input, DeriveInput};
16
17mod action;
18mod actionable;
19mod dispatcher;
20
21/// Derives the `actionable::Action` trait.
22///
23/// This trait can be customizd using the `action` attribute in these ways:
24///
25/// * Crate name override: `#[action(actionable = "someothername")]`. If you
26/// find yourself needing to import `actionable` as another name, this setting
27/// will replace all mentions of `actionable` with the identifier specified.
28#[proc_macro_error]
29#[proc_macro_derive(Action, attributes(action))]
30pub fn action_derive(input: TokenStream) -> TokenStream {
31 let input = parse_macro_input!(input as DeriveInput);
32 match action::derive(&input) {
33 Ok(tokens) => tokens.into(),
34 Err(err) => {
35 emit_error!(input.ident, err.to_string());
36 TokenStream::default()
37 }
38 }
39}
40
41/// Derives a set of traits that can be used to implement a permissions-driven
42/// API. There are options that can be customized with the `#[actionable]`
43/// attribute at the enum level:
44///
45/// * Crate name override: `#[actionable(actionable = "someothername")]`. If you
46/// find yourself needing to import `actionable` as another name, this setting
47/// will replace all mentions of `actionable` with the identifier specified.
48///
49/// ## The Dispatcher Trait
50///
51/// The first trait that is generated is named `<EnumName>Dispatcher`. For
52/// example, if the enum's name is `Request`, the generated trait name will be
53/// `RequestDispatcher`. This trait has no methods for you to implement. It
54/// defines several associated types:
55///
56/// * `Output`: The `Ok` side of the `Result`.
57/// * `Error`: The `Err` side of the `Result`. Must implement
58/// `From<actionable::PermissionDenied>`.
59/// * For each variant in the enum, another Trait named `<VariantName>Handler`.
60/// For example, if the enum variant was `Request::AddUser`, the trait will be
61/// `AddUserHandler`. Each of these traits must be implemented by any type
62/// implementing `<EnumName>Dispatcher`.
63///
64/// The dispatcher trait has a method available for you to use to dispatch
65/// requests: `async fn dispatch(&self, permissions: &Permissions, request:
66/// <EnumName>) -> Result<Self::Output, Self::Error>`.
67///
68/// ## The Handler Traits
69///
70/// For each variant in the enum, a trait will be generated named
71/// `<VariantName>Handler`. Using the same example above, the enum variant
72/// `Request::AddUser` would generate the trait `AddUserHandler`. These traits
73/// are implemented using the
74/// [`async-trait`](https://crates.io/crate/async-trait) trait.
75///
76/// Each variant must have a protection method assigned using the
77/// `#[actionable]` attribute. There are three protection methods:
78///
79/// ### No Protection: `#[actionable(protection = "none")]`
80///
81/// A handler with no protection has one method:
82///
83/// ```rust
84/// # type Output = ();
85/// # type Error = ();
86/// # use actionable::{Permissions, async_trait};
87/// #[async_trait]
88/// trait Handler {
89/// type Dispatcher;
90/// async fn handle(
91/// dispatcher: &Self::Dispatcher,
92/// permissions: &Permissions,
93/// /* each field on this variant is passed
94/// as a parameter to this method */
95/// ) -> Result<Output, Error>;
96/// }
97/// ```
98///
99/// Actionable does not do any checks before invoking this handler.
100///
101/// ### Simple Protection: `#[actionable(protection = "simple")]`
102///
103/// A handler with simple protection exposes methods and types to allow
104/// specifying an `actionable::ResourceName` and an `Action` for this handler:
105///
106/// ```rust
107/// # type Output = ();
108/// # type Error = ();
109/// # use actionable::{Permissions, ResourceName, async_trait};
110/// #[async_trait]
111/// trait Handler {
112/// type Dispatcher;
113/// type Action;
114///
115/// fn resource_name<'a>(
116/// dispatcher: &Self::Dispatcher,
117/// /* each field on this variant is passed
118/// by reference as a parameter to this method */
119/// ) -> ResourceName<'a>;
120///
121/// fn action() -> Self::Action;
122///
123/// async fn handle_protected(
124/// dispatcher: &Self::Dispatcher,
125/// permissions: &Permissions,
126/// /* each field on this variant is passed
127/// as a parameter to this method */
128/// ) -> Result<Output, Error>;
129/// }
130/// ```
131///
132/// When the handler is invoked, it first checks `permissions` to ensure that
133/// `action()` is allowed to be performed on `resource_name()`. If it is not
134/// allowed, an `actionable::PermissionDenied` error will be returned. If it is
135/// allowed, `handle_protected()` will be executed.
136///
137/// ### Custom Protection: `#[actionable(protection = "custom")]`
138///
139/// A handler with custom protection has two methods, one to verify permissions
140/// and one to execute the protected code:
141///
142/// ```rust
143/// # type Output = ();
144/// # type Error = ();
145/// # use actionable::{Permissions, async_trait};
146/// #[async_trait]
147/// trait Handler {
148/// type Dispatcher;
149/// async fn verify_permissions(
150/// dispatcher: &Self::Dispatcher,
151/// permissions: &Permissions,
152/// /* each field on this variant is passed
153/// by refrence as a parameter to this method */
154/// ) -> Result<(), Error>;
155///
156/// async fn handle_protected(
157/// dispatcher: &Self::Dispatcher,
158/// permissions: &Permissions,
159/// /* each field on this variant is passed as a parameter
160/// to this method */
161/// ) -> Result<Output, Error>;
162/// }
163/// ```
164///
165/// Actionable will first call `verify_permissions()`. If you return `Ok(())`,
166/// your `handle_protected()` method is invoked.
167///
168/// ## Why should you use the built-in protection modes?
169///
170/// Actionable attempts to make permission handling easy to understand and
171/// implement while making it difficult to forget implementing permission
172/// handling. This is only effective if you use the protection levels.
173///
174/// Because Actionable includes `permissions` in every call to
175/// `handle[_protected]()`, technically you could use a protection level of
176/// `none` and implement permission handling within the `handle()` function.
177/// While it would work, you shouldn't do this.
178///
179/// Actionable encourages placing information about permission handling in the
180/// definition of the enum. By using `simple` and `custom` protection
181/// strategies, consumers of your API will be able to see at the enum level what
182/// APIs check permissions. When trying to understand what permissions are being
183/// used, this is critical.
184///
185/// By placing your permission handling code in locations that follow a
186/// repeatable patern, you're helping anyone who is reading the code separate
187/// what logic is related to permission handling and what logic is related to
188/// the API implementation.
189///
190/// ## What protection mode should you use?
191///
192/// * If your handler is operating on a single resource and performing a single
193/// action, use the `simple` protection mode.
194/// * If your handler needs to check permissions but it's more complicated than
195/// the first scenario, use the `custom` protection mode.
196/// * If you aren't enforcing permissions inside of this handler, use the `none`
197/// protection mode.
198#[proc_macro_error]
199#[proc_macro_derive(Actionable, attributes(actionable))]
200pub fn actionable_derive(input: TokenStream) -> TokenStream {
201 let input = parse_macro_input!(input as DeriveInput);
202 match actionable::derive(&input) {
203 Ok(tokens) => tokens.into(),
204 Err(err) => {
205 emit_error!(input.ident, err.to_string());
206 TokenStream::default()
207 }
208 }
209}
210
211/// Derives the `Dispatcher` trait.
212///
213/// This trait requires the `input` parameter to be specified. The full list of
214/// parameters that can be customized are:
215///
216/// * `input` Type: `#[dispatcher(input = "EnumName")]`. The enum name here
217/// needs to have had [`Actionable`](actionable_derive) derived on it.
218/// * Crate name override: `#[actionable(actionable = "someothername")]`. If you
219/// find yourself needing to import `actionable` as another name, this setting
220/// will replace all mentions of `actionable` with the identifier specified.
221///
222/// The `input` type must be in scope, as do the derived traits generated by
223/// deriving `Actionable`.
224#[proc_macro_error]
225#[proc_macro_derive(Dispatcher, attributes(dispatcher))]
226pub fn dispatcher_derive(input: TokenStream) -> TokenStream {
227 let input = parse_macro_input!(input as DeriveInput);
228 match dispatcher::derive(&input) {
229 Ok(tokens) => tokens.into(),
230 Err(err) => {
231 emit_error!(input.ident, err.to_string());
232 TokenStream::default()
233 }
234 }
235}
236
237fn actionable(actionable: Option<syn::Path>, span: proc_macro2::Span) -> syn::Path {
238 actionable.unwrap_or_else(|| {
239 let mut segments = syn::punctuated::Punctuated::new();
240 segments.push_value(syn::PathSegment {
241 ident: syn::Ident::new("actionable", span),
242 arguments: syn::PathArguments::None,
243 });
244 syn::Path {
245 leading_colon: None,
246 segments,
247 }
248 })
249}
250
251#[derive(Debug, thiserror::Error)]
252pub(crate) enum Error {
253 #[error("darling error: {0}")]
254 Darling(#[from] darling::Error),
255 #[error("syn error: {0}")]
256 Syn(#[from] syn::Error),
257}
258
259#[derive(Debug, Clone)]
260struct Actionable(syn::Path);
261
262impl Parse for Actionable {
263 fn parse(input: &'_ syn::parse::ParseBuffer<'_>) -> syn::Result<Self> {
264 let actionable: syn::Ident = input.parse()?;
265 if actionable != "actionable" {
266 abort!(
267 actionable,
268 "the only parameter action() accepts is `actionable`"
269 )
270 }
271 let _: syn::Token![=] = input.parse()?;
272 let path: syn::Path = input.parse()?;
273 Ok(Self(path))
274 }
275}
276
277#[derive(Debug)]
278struct ActionableArgs(Option<Actionable>);
279
280impl Parse for ActionableArgs {
281 fn parse(input: &'_ syn::parse::ParseBuffer<'_>) -> syn::Result<Self> {
282 let content;
283 let _ = syn::parenthesized!(content in input);
284 let mut content: syn::punctuated::Punctuated<Actionable, syn::Token![,]> =
285 content.parse_terminated(Actionable::parse)?;
286 content.pop().map_or_else(
287 || Ok(Self(None)),
288 |actionable| {
289 if !content.is_empty() {
290 abort!(
291 content.first().unwrap().0.segments.first().unwrap().ident,
292 "Only one parameter, `actionable` is allowed"
293 );
294 }
295 Ok(Self(Some(actionable.into_value())))
296 },
297 )
298 }
299}