Skip to content

Commit

Permalink
Revert context changes on error (#508)
Browse files Browse the repository at this point in the history
* 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
hdwalters authored Oct 18, 2024
1 parent 71c8902 commit 4fa2909
Show file tree
Hide file tree
Showing 17 changed files with 515 additions and 204 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 10 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,22 @@ rust-version = "1.79"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
heraclitus-compiler = "1.8.0"
similar-string = "1.4.2"
amber-meta = { path = "meta" }
chrono = "0.4.38"
clap = { version = "4.4.18", features = ["derive"] }
colored = "2.0.0"
heraclitus-compiler = "1.8.0"
include_dir = "0.7.4"
itertools = "0.13.0"
clap = { version = "4.4.18", features = ["derive"] }
chrono = "0.4.38"
similar-string = "1.4.2"
test-generator = "0.3.1"
include_dir = "0.7.4"

# test dependencies
[dev-dependencies]
tiny_http = "0.12.0"
assert_cmd = "2.0.14"
predicates = "3.1.0"
tempfile = "3.10.1"
tiny_http = "0.12.0"

[profile.release]
strip = true
Expand All @@ -37,6 +38,9 @@ lto = "thin"
[profile.test]
opt-level = 3

[workspace]
members = ["meta"]

# Config for 'cargo dist'
[workspace.metadata.dist]
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
Expand Down
12 changes: 12 additions & 0 deletions meta/Cargo.toml
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"] }
48 changes: 48 additions & 0 deletions meta/src/helper.rs
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));
}
}
}
}
}
104 changes: 104 additions & 0 deletions meta/src/lib.rs
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)
}
103 changes: 103 additions & 0 deletions meta/src/manager.rs
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));
}
}
}
}
}
38 changes: 38 additions & 0 deletions meta/src/utils.rs
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
}
}
Loading

0 comments on commit 4fa2909

Please sign in to comment.