Attribute-like macro 'methods_enum::gen'

Lightweight (no dependencies) attribute-like macro for “state” and “state machine” design patterns without dyn Trait (based on enum) with decoding output in doc-comments.

The macro attribute is set before the direct impl block (no trait). Based on the method signatures of the impl block, it generates: enum with options from argument tuples, and generates the {} bodies of these methods with the call of the argument handler method from this enum .

This allows the handler method to control the behavior of the methods depending on the context.

There are two syntax options:

  1. For the case where methods returning a value have the same return type:

#[methods_enum::gen(EnumName: handler_name)]

where:

  1. In case of more than one meaningful return type:

#[methods_enum::gen(EnumName: handler_name = OutName)]

where:

In the second case, you can also specify an expression for the default return value after the method signature.

Usage example

Chapter 17.3 “Implementing an Object-Oriented Design Pattern” of the rust-book shows an implementation of the state pattern in rust that provides the following behavior:

fn main() {
    let mut post = blog::Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());

    post.request_review();
    assert_eq!("", post.content());

    post.approve();
    assert_eq!("I ate a salad for lunch today", post.content());
}

The dyn Trait option proposed in the book requires dynamic binding and duplication of logic. The option on different types is not applicable in cases where a single interface is required for states.

By setting in Cargo.toml:

[dependencies]
methods-enum = "0.1.1"

this can be solved, for example, like this:

mod blog {
    enum State {
        Draft,
        PendingReview,
        Published,
    }

    pub struct Post {
        state: State,
        content: String,
    }

    #[methods_enum::gen(Meth: run_methods)]
    impl Post {
        pub fn add_text(&mut self, text: &str);
        pub fn request_review(&mut self);
        pub fn approve(&mut self);
        pub fn content(&mut self) -> &str;

        fn run_methods(&mut self, method: Meth) -> &str {
            match self.state {
                State::Draft => match method {
                    Meth::add_text(text) => { self.content.push_str(text); "" }
                    Meth::request_review() => { self.state = State::PendingReview; "" }
                    _ => "",
                },
                State::PendingReview => match method {
                    Meth::approve() => { self.state = State::Published; "" }
                    _ => "",
                },
                State::Published => match method {
                    Meth::content() => &self.content,
                    _ => "",
                },
            }
        }

        pub fn new() -> Post {
            Post {
                state: State::Draft,
                content: String::new(),
            }
        }
    }
}

In the handler method (in this case, run_methods), simply write for each state which methods should work and how.

The macro duplicates the output for the compiler in the doc-comments. Therefore, in the IDE1, you can always see the declaration of the generated enum and the generated method bodies, in the popup hint above the enum name:

enum popup hint

enum popup: bodies

Alternatively, the entire result of a macro can be output to the console at compile time by setting the session environment variable M_ENUM_DBG to a value other than “0”. PowerShell example:

PS > $Env:M_ENUM_DBG=1
PS > cargo build

This is worth doing when the compiler messages are not clear and referring to the macro line , so that for debugging, replace the impl block along with the attribute with the output of the macro.

Restrictions

Details of the macro and use cases

The macro reads only its impl block and only up to the name of the handler method. From which it follows that all method signatures for enum must be located before the handler method or in a separate from it impl block.

The following example demonstrates the use of methods with self in the form of a move, in a separate impl block from their handler, which also contains the signatures of the &mut self methods and both handlers.

Let’s say that in the blog::Post task, the state-changing methods require the form self move, to work with dot notation, while the rest of the methods need to be left on the form &mut self, or:

fn main() {
    let mut post = blog::Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());

    assert_eq!(
        "I ate a salad for lunch today",
        post.request_review().approve().content()
    );
}

// In this case, the solution might be:

mod blog {
    enum State {
        Draft,
        PendingReview,
        Published,
    }

    pub struct Post {
        state: State,
        content: String,
    }

    #[methods_enum::gen(Move: run_move)]
    impl Post {
        pub fn request_review(self) -> Post;
        pub fn approve(self) -> Post;
    }

    #[methods_enum::gen(Meth: run_methods)]
    impl Post {
        pub fn add_text(&mut self, text: &str);
        pub fn content(&mut self) -> &str;

        fn run_methods(&mut self, method: Meth) -> &str {
            match self.state {
                State::Draft => match method {
                    Meth::add_text(text) => { self.content.push_str(text); "" }
                    _ => "",
                },
                State::PendingReview => "",
                State::Published => match method {
                    Meth::content() => &self.content,
                    _ => "",
                },
            }
        }

        fn run_move(mut self, method: Move) -> Post {
            match self.state {
                State::Draft => match method {
                    Move::request_review() => { self.state = State::PendingReview; self }
                    _ => self,
                },
                State::PendingReview => match method {
                    Move::approve() => { self.state = State::Published; self }
                    _ => self,
                },
                State::Published => self,
            }
        }

        pub fn new() -> Post {
            Post {
                state: State::Draft,
                content: String::new(),
            }
        }
    }
}

Here fn run_move and/or fn run_methods can also be placed at the end of the first impl block.

Associated functions (for the syntax without OutName also and regular methods) can be in the impl block and before the handler method, interspersed with method signatures, but this worsens readability.

Methods arguments with &mut types work the same way. For example, to extend the blog::Post task to:

fn main() {
    let mut post = blog::Post::new();

    let mut ext_content = "external content: ".to_string();

    post.add_text("I ate a salad for lunch today", &mut ext_content);
    assert_eq!("", post.content());
    assert_eq!("external content: I ate a salad for lunch today", ext_content);

    post.request_review();
    post.approve();
    assert_eq!("I ate a salad for lunch today", post.content());
}

// the solution might look like this:

mod blog {
// . . .                    
// . . .                    
    #[methods_enum::gen(Meth: run_methods)]
    impl Post {
        pub fn add_text(&mut self, text: &str, ex_content: &mut String);
        pub fn request_review(&mut self);
        pub fn approve(&mut self);
        pub fn content(&mut self) -> &str;

        fn run_methods(&mut self, method: Meth) -> &str {
            match self.state {
                State::Draft => match method {
                    Meth::add_text(text, ex_cont) => {
                        self.content.push_str(text);
                        ex_cont.push_str(text);
                        ""
                    }
// . . .                    
// . . .    
}

2nd syntax option: with OutName

#[methods_enum::gen(EnumName: handler_name = OutName)]

where:


  1. IDE support tested on ‘rust-analyzer for VS Code v0.3.1083’ - everything works: highlighting, tooltips, transitions, renames.