-
Notifications
You must be signed in to change notification settings - Fork 92
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Revert context changes on error (#508)
* Reorder trait functions to match trait. * Remove unused struct field. * Add procedural macro crate. * Add procedural macro definitions. * Revert context changes on error. * Revert pushed scopes on error. * Add doc comments to Rust macro helpers. * Rename push/pop scope function. * Add doc comments to Rust macro helpers. * Use parameterised result return value. * Add doc comments to Rust macro helpers. * Add doc comments to Rust macro helpers. * Minor optimisation to util function.
- Loading branch information
Showing
17 changed files
with
515 additions
and
204 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
[package] | ||
name = "amber-meta" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[lib] | ||
proc-macro = true | ||
|
||
[dependencies] | ||
proc-macro2 = "1.0.86" | ||
quote = "1.0.36" | ||
syn = { version = "2.0.68", features = ["visit"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
use crate::utils; | ||
use proc_macro2::TokenStream as TokenStream2; | ||
use quote::quote; | ||
use syn::visit::Visit; | ||
use syn::{Field, Ident, PathSegment}; | ||
|
||
pub struct HelperVisitor { | ||
name: Ident, | ||
functions: Vec<TokenStream2>, | ||
} | ||
|
||
impl HelperVisitor { | ||
pub fn new(name: &Ident) -> Self { | ||
Self { | ||
name: name.clone(), | ||
functions: Vec::new(), | ||
} | ||
} | ||
|
||
fn make_function(name: &Ident, segment: &PathSegment) -> TokenStream2 { | ||
let concat = format!("set_{}", name); | ||
let concat = Ident::new(&concat, name.span()); | ||
quote! { | ||
/// Sets the field value and returns the previous value. | ||
pub fn #concat(&mut self, mut #name: #segment) -> #segment { | ||
use std::mem::swap; | ||
swap(&mut self.#name, &mut #name); | ||
#name | ||
} | ||
} | ||
} | ||
|
||
pub fn make_block(&self) -> TokenStream2 { | ||
utils::make_block(&self.name, &self.functions) | ||
} | ||
} | ||
|
||
impl<'a> Visit<'a> for HelperVisitor { | ||
fn visit_field(&mut self, field: &'a Field) { | ||
if field.attrs.iter().any(utils::is_context) { | ||
if let Some(name) = &field.ident { | ||
if let Some(segment) = utils::get_type(field) { | ||
self.functions.push(Self::make_function(name, segment)); | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
mod helper; | ||
mod manager; | ||
mod utils; | ||
|
||
use crate::helper::HelperVisitor; | ||
use crate::manager::ManagerVisitor; | ||
use proc_macro::TokenStream; | ||
use syn::visit::Visit; | ||
use syn::*; | ||
|
||
/// Derive macro `ContextManager` allows changes to be made to annotated | ||
/// fields on a struct, with automatic reset on early error return. | ||
/// | ||
/// In this example, we change some settings on an object, and rely on | ||
/// the context manager to reset those settings when it fails. The macro | ||
/// creates three functions for each annotated field in the `Amplifier` | ||
/// struct, and we call the following ones here: | ||
/// | ||
/// * Function `Amplifier::with_panel_ref()` swaps the existing `panel` | ||
/// field on the `Amplifier` object, passes the `Amplifier` object to | ||
/// the lambda by mutable reference, swaps the old `panel` field on | ||
/// exit, and returns the result. | ||
/// | ||
/// * Function `Amplifier::with_power()` sets the `power` field on the | ||
/// `Amplifier` object, and resets the old value on exit. Requires | ||
/// the field being modified to implement the `Copy` and `Clone` traits. | ||
/// | ||
/// * Function `Amplifier::with_panel_fn()` sets the `volume` field on | ||
/// the encapsulated `Panel` object, by calling its setter function | ||
/// `Panel::set_volume()`, and resets the old value on exit. Note, | ||
/// the setter function is created by derive macro `ContextHelper`. | ||
/// | ||
/// ```rust | ||
/// use amber_meta::{ContextHelper, ContextManager}; | ||
/// | ||
/// #[derive(ContextManager)] | ||
/// struct Amplifier { | ||
/// #[context] | ||
/// power: bool, | ||
/// input: f64, | ||
/// output: f64, | ||
/// #[context] | ||
/// panel: Panel, | ||
/// } | ||
/// | ||
/// #[derive(ContextHelper)] | ||
/// struct Panel { | ||
/// #[context] | ||
/// volume: u8, | ||
/// display: Option<String>, | ||
/// } | ||
/// | ||
/// impl Panel { | ||
/// fn new() -> Panel { | ||
/// Panel { volume: 0, display: None } | ||
/// } | ||
/// } | ||
/// | ||
/// fn demo_amplifier(amp: &mut Amplifier) -> Result<(), String> { | ||
/// // Install a new control panel. | ||
/// let mut panel = Panel::new(); | ||
/// amp.with_panel_ref(&mut panel, |amp| { | ||
/// // Turn the power on. | ||
/// amp.with_power(true, |amp| { | ||
/// // Set the volume to 11. | ||
/// amp.with_panel_fn(Panel::set_volume, 11, |amp| { | ||
/// // Strum a guitar chord. | ||
/// play_guitar(amp)?; | ||
/// Ok(()) | ||
/// })?; | ||
/// // Reset the volume on exit. | ||
/// Ok(()) | ||
/// })?; | ||
/// // Turn the power off on exit. | ||
/// Ok(()) | ||
/// })?; | ||
/// // Reinstall the old control panel on exit. | ||
/// Ok(()) | ||
/// } | ||
/// | ||
/// fn play_guitar(amp: &Amplifier) -> Result<(), String> { | ||
/// Err(String::from("Blown fuse")) | ||
/// } | ||
/// ``` | ||
#[proc_macro_derive(ContextManager, attributes(context))] | ||
pub fn context_manager(input: TokenStream) -> TokenStream { | ||
let input = parse_macro_input!(input as ItemStruct); | ||
let mut visitor = ManagerVisitor::new(&input.ident); | ||
visitor.visit_item_struct(&input); | ||
let output = visitor.make_block(); | ||
TokenStream::from(output) | ||
} | ||
|
||
/// Derive macro `ContextHelper` provides support functions for use with | ||
/// context functions created by `ContextManager`; for more information, | ||
/// see documentation for that macro. | ||
#[proc_macro_derive(ContextHelper, attributes(context))] | ||
pub fn context_helper(input: TokenStream) -> TokenStream { | ||
let input = parse_macro_input!(input as ItemStruct); | ||
let mut visitor = HelperVisitor::new(&input.ident); | ||
visitor.visit_item_struct(&input); | ||
let output = visitor.make_block(); | ||
TokenStream::from(output) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
use crate::utils; | ||
use proc_macro2::TokenStream as TokenStream2; | ||
use quote::quote; | ||
use syn::visit::Visit; | ||
use syn::{Field, Ident, PathSegment}; | ||
|
||
pub struct ManagerVisitor { | ||
name: Ident, | ||
functions: Vec<TokenStream2>, | ||
} | ||
|
||
impl ManagerVisitor { | ||
pub fn new(name: &Ident) -> Self { | ||
Self { | ||
name: name.clone(), | ||
functions: Vec::new(), | ||
} | ||
} | ||
|
||
fn make_with(name: &Ident, segment: &PathSegment) -> TokenStream2 { | ||
let concat = format!("with_{}", name); | ||
let concat = Ident::new(&concat, name.span()); | ||
quote! { | ||
/// Sets the field value (which must implement the `Copy` and | ||
/// `Clone` traits) and restores the previous value after the | ||
/// body function has returned. | ||
pub fn #concat<T, E, B>(&mut self, #name: #segment, mut body: B) -> Result<T, E> | ||
where | ||
B: FnMut(&mut Self) -> Result<T, E>, | ||
{ | ||
// Native types are implicitly copied on clone. | ||
let prev = self.#name.clone(); | ||
self.#name = #name; | ||
let result = body(self); | ||
self.#name = prev; | ||
result | ||
} | ||
} | ||
} | ||
|
||
fn make_with_ref(name: &Ident, segment: &PathSegment) -> TokenStream2 { | ||
let concat = format!("with_{}_ref", name); | ||
let concat = Ident::new(&concat, name.span()); | ||
quote! { | ||
/// Sets the field value by swapping the references, and | ||
/// restores the previous value after the body function has | ||
/// returned. | ||
pub fn #concat<T, E, B>(&mut self, #name: &mut #segment, mut body: B) -> Result<T, E> | ||
where | ||
B: FnMut(&mut Self) -> Result<T, E>, | ||
{ | ||
use std::mem::swap; | ||
swap(&mut self.#name, #name); | ||
let result = body(self); | ||
swap(&mut self.#name, #name); | ||
result | ||
} | ||
} | ||
} | ||
|
||
fn make_with_fn(name: &Ident, segment: &PathSegment) -> TokenStream2 { | ||
let concat = format!("with_{}_fn", name); | ||
let concat = Ident::new(&concat, name.span()); | ||
quote! { | ||
/// Sets the field value on the encapsulated struct using | ||
/// its member function, and restores the previous value | ||
/// after the body function has returned. | ||
/// | ||
/// Additionally, to add setter functions designed to work | ||
/// with `with_foo_fn()`, annotate the encapsulated struct | ||
/// with `#[derive(ContextHelper)`, and required fields with | ||
/// `#[context]`. | ||
pub fn #concat<V, S, T, E, B>(&mut self, mut setter: S, value: V, mut body: B) -> Result<T, E> | ||
where | ||
S: FnMut(&mut #segment, V) -> V, | ||
B: FnMut(&mut Self) -> Result<T, E>, | ||
{ | ||
let prev = setter(&mut self.#name, value); | ||
let result = body(self); | ||
setter(&mut self.#name, prev); | ||
result | ||
} | ||
} | ||
} | ||
|
||
pub fn make_block(&self) -> TokenStream2 { | ||
utils::make_block(&self.name, &self.functions) | ||
} | ||
} | ||
|
||
impl<'a> Visit<'a> for ManagerVisitor { | ||
fn visit_field(&mut self, field: &'a Field) { | ||
if field.attrs.iter().any(utils::is_context) { | ||
if let Some(name) = &field.ident { | ||
if let Some(segment) = utils::get_type(field) { | ||
self.functions.push(Self::make_with(name, segment)); | ||
self.functions.push(Self::make_with_ref(name, segment)); | ||
self.functions.push(Self::make_with_fn(name, segment)); | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
use proc_macro2::TokenStream as TokenStream2; | ||
use quote::quote; | ||
use syn::{Attribute, Field, Ident, Meta, PathSegment, Type}; | ||
|
||
/// Implements multiple functions for a struct implementation block. | ||
pub fn make_block(name: &Ident, functions: &Vec<TokenStream2>) -> TokenStream2 { | ||
// See [https://users.rust-lang.org/t/how-to-use-a-vector-of-tokenstreams-created-with-quote-within-quote/81092]. | ||
quote! { | ||
impl #name { | ||
#(#functions)* | ||
} | ||
} | ||
} | ||
|
||
/// Tests whether a given field attribute is `#[context]`, for both | ||
/// `#[derive(ContextManager)]` and `#[derive(ContextHelper)]` enhanced | ||
/// structs. | ||
pub fn is_context(attr: &Attribute) -> bool { | ||
if let Meta::Path(path) = &attr.meta { | ||
if let Some(segment) = path.segments.last() { | ||
if segment.ident == "context" { | ||
return true; | ||
} | ||
} | ||
} | ||
false | ||
} | ||
|
||
/// Gets the type of a given field. Note, we use the `PathSegment` not | ||
/// the contained `Ident`, because that supports generic field types | ||
/// like `Option<String>`. | ||
pub fn get_type(field: &Field) -> Option<&PathSegment> { | ||
if let Type::Path(path) = &field.ty { | ||
path.path.segments.last() | ||
} else { | ||
None | ||
} | ||
} |
Oops, something went wrong.