Intro
First of all, thanks a lot for creating this library! The usefulness and UX is awesome, it motivated me as a user to implement an additional feature that dribbles past the compiler's inability to correctly handle the ..self expression when constructing a state-shift'ed function output. Since the implementation scope grew more than anticipated, I would like to discuss whether this might benefit upstream.
Summary
Add a new optional #[auto_assign] macro that seamlessly integrates into the feature set and UX of state-shift, fully eliminating the need to hand-craft the state-shift'ed return values in most cases.
Quick UX demo:
BEFORE
#[require(Initial, B, C)]
#[switch_to(RaceSet, B, C)]
fn set_race(self, race: Race) -> PlayerBuilder {
PlayerBuilder {
race: Some(race),
level: self.level,
skill_slots: self.skill_slots,
spell_slots: self.spell_slots,
}
}
AFTER
#[require(Initial, B, C)]
#[switch_to(RaceSet, B, C)]
#[auto_assign(race = race)]
fn set_race(self, race: Race) -> PlayerBuilder {/*magic*/}
Motivation
The status-quo version already eliminates the difficult, ugly and verbose parts when creating an FSM-style builder for a struct. As explained in the library docs, the compiler does not support the shorthand ..self with generics yet. It follows, that the returned builder struct must be constructed in an explicit way by hand. This gets old fast, approximately after the second copy+paste+modify repetition.
Implementation
The implementation can be found on this feature branch, but beware!
Over the past few days of extensive tinkering, the overall architecture has undergone increasingly more changes. This was mainly driven by a perceived need to make the design of #[auto_assign] as robust as possible without breaking backwards compatibility. I created a small write-up and added a cargo expand --ugly --test complex_example showcase in the notes file. A short description about introduced changes:
#[auto_assign]
- lives in
src/auto_assign.rs
- follows the same principle as
#require and #switch_to -> it's consumed by #impl_state
- consists of two parts:
- The "meta" factory that generates a struct-specific factory macro, invoked by
#type_state to encode information about struct fields in internal helper macros.
- The struct factory invoker that is injected at the bottom of the function body on functions, where
#[auto_assign(field_id = value, ..., field_id_i = value_i)] is declared
changes in library design
- generalized macro handling of
#require, #switch_to, and #auto_assign by introducing the trait IsSupportedMacro -> the definitions and implementations can be found in src/helper.rs
- introduced various ergonomic definitions/functions in
src/helper.rs to help with debugging, ToTokenstream transformations and parsing
- moved some of the logic around:
src/impl_state.rs is now the orchestrator of the function-level macros -> I decided against adding more unrelated logic to src/require.rs.
- This orchestrator mainly checks relevant conditions like "
#switch_to cannot run unless #require is defined", executes the respective macro logic and eventually stitches all collected tokenstreams together into the final impl block
- introduced
src/errors.rs to streamline and unify error handling, making it more consistent across the code base
- intro'd functional pipelines (where it made sense) in the hopes of making functionality more contained and easier to test
Compatibility & Tests
All tests are still passing and there should be no impact whatsoever on current users. Nevertheless, it would be adviseable to create additional edge case tests.
Discussion
I'd love some feedback on:
- the design changes
- the integration of the new feature and if it plays well with the current state
- in case of an upstream integration ->
- what's the best approach? Splitting the introduced changes into two or more PRs?
- any specific things to improve first?
Intro
First of all, thanks a lot for creating this library! The usefulness and UX is awesome, it motivated me as a user to implement an additional feature that dribbles past the compiler's inability to correctly handle the
..selfexpression when constructing a state-shift'ed function output. Since the implementation scope grew more than anticipated, I would like to discuss whether this might benefit upstream.Summary
Add a new optional
#[auto_assign]macro that seamlessly integrates into the feature set and UX ofstate-shift, fully eliminating the need to hand-craft the state-shift'ed return values in most cases.Quick UX demo:
BEFORE
AFTER
Motivation
The status-quo version already eliminates the difficult, ugly and verbose parts when creating an FSM-style builder for a struct. As explained in the library docs, the compiler does not support the shorthand
..selfwith generics yet. It follows, that the returned builder struct must be constructed in an explicit way by hand. This gets old fast, approximately after the second copy+paste+modify repetition.Implementation
The implementation can be found on this feature branch, but beware!
Over the past few days of extensive tinkering, the overall architecture has undergone increasingly more changes. This was mainly driven by a perceived need to make the design of
#[auto_assign]as robust as possible without breaking backwards compatibility. I created a small write-up and added acargo expand --ugly --test complex_exampleshowcase in the notes file. A short description about introduced changes:#[auto_assign]src/auto_assign.rs#requireand#switch_to-> it's consumed by#impl_state#type_stateto encode information about struct fields in internal helper macros.#[auto_assign(field_id = value, ..., field_id_i = value_i)]is declaredchanges in library design
#require,#switch_to, and#auto_assignby introducing the trait IsSupportedMacro -> the definitions and implementations can be found insrc/helper.rssrc/helper.rsto help with debugging, ToTokenstream transformations and parsingsrc/impl_state.rsis now the orchestrator of the function-level macros -> I decided against adding more unrelated logic tosrc/require.rs.#switch_tocannot run unless#requireis defined", executes the respective macro logic and eventually stitches all collected tokenstreams together into the final impl blocksrc/errors.rsto streamline and unify error handling, making it more consistent across the code baseCompatibility & Tests
All tests are still passing and there should be no impact whatsoever on current users. Nevertheless, it would be adviseable to create additional edge case tests.
Discussion
I'd love some feedback on: