diff --git a/README.md b/README.md index 29ccae8..34b68a4 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Statum targets stable Rust and currently supports Rust `1.93+`. ```toml [dependencies] -statum = "0.6.3" +statum = "0.6.6" ``` ## 60-Second Example @@ -139,6 +139,18 @@ that starts minimal and adds features one by one, see flagship persistence story, see [docs/case-study-event-log-rebuild.md](docs/case-study-event-log-rebuild.md). +## Machine Introspection + +Statum can also emit typed machine introspection directly from the machine +definition itself. Use it when downstream tooling needs the machine structure +without rebuilding a parallel graph table by hand: CLI explainers, generated +docs, graph exports, branch-strip views, test assertions about exact legal +transitions, and replay or debug tooling. + +See [docs/introspection.md](docs/introspection.md) for the full guide and +[statum-examples/src/toy_demos/16-machine-introspection.rs](statum-examples/src/toy_demos/16-machine-introspection.rs) +for a runnable example. + ## Typed Rehydration `#[validators]` is the feature that turns stored data back into typed machines. Each `is_*` method checks whether the persisted value belongs to a state, returns `()` or state-specific data, and Statum builds the right typed output: diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 5accd7c..6263c12 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,12 @@ # Release Notes +## v0.6.6 (2026-03-21) + +### Changes +- Added first-class machine introspection emitted directly from `#[machine]`, including typed `StateId`, `TransitionId`, and a static `GRAPH` descriptor for exact transition-site queries. +- Added typed runtime transition recording so consumer crates can record the chosen branch and join it back to static machine metadata. +- Added an optional typed presentation overlay for attaching labels, descriptions, phases, and other consumer-owned metadata without rebuilding the machine graph. + ## v0.6.1 (2026-03-18) ### Changes diff --git a/docs/agents/README.md b/docs/agents/README.md index 379d0ec..cad528a 100644 --- a/docs/agents/README.md +++ b/docs/agents/README.md @@ -50,6 +50,8 @@ dynamic. - parent, child, or nested-machine structure when one workflow owns another - likely `#[transition]` impl blocks and the legal edges they encode - whether `#[validators]` or `statum::projection` should be part of the design +- whether downstream tooling should use Statum's emitted introspection instead + of a handwritten graph table - the explicit hybrid boundary: what should stay runtime-validated and why - the smallest first migration slice that improves correctness without forcing a repo-wide rewrite @@ -70,9 +72,11 @@ The templates are intentionally short. Point agents back to the canonical docs when they need detail: - [../../README.md](../../README.md) +- [../introspection.md](../introspection.md) - [../typestate-builder-design-playbook.md](../typestate-builder-design-playbook.md) - [../patterns.md](../patterns.md) - [../persistence-and-validators.md](../persistence-and-validators.md) +- [../../statum-examples/src/toy_demos/16-machine-introspection.rs](../../statum-examples/src/toy_demos/16-machine-introspection.rs) - [../../statum-examples/src/toy_demos/13-review-flow.rs](../../statum-examples/src/toy_demos/13-review-flow.rs) - [../../statum-examples/src/showcases/sqlite_event_log_rebuild.rs](../../statum-examples/src/showcases/sqlite_event_log_rebuild.rs) - [../../statum-examples/src/showcases/tokio_websocket_session.rs](../../statum-examples/src/showcases/tokio_websocket_session.rs) diff --git a/docs/agents/prompts/abstract-guide-to-statum.md b/docs/agents/prompts/abstract-guide-to-statum.md index 7f6ed92..70610ea 100644 --- a/docs/agents/prompts/abstract-guide-to-statum.md +++ b/docs/agents/prompts/abstract-guide-to-statum.md @@ -26,6 +26,8 @@ Tasks: - the legal `#[transition]` impl blocks - any parent, child, or nested machine relationships - whether `#[validators]` or `statum::projection` belongs at a persistence boundary + - whether downstream tooling should use Statum introspection instead of a + handwritten graph table 3. Call out what should explicitly stay outside Statum and why. 4. Keep the first implementation slice small enough for one focused PR. 5. If a claim is underspecified, name the missing protocol or evidence question instead of guessing. @@ -42,6 +44,7 @@ Be concrete. Do not stop at "consider typestate." Use these references if needed: - https://github.com/eboody/statum/blob/main/README.md +- https://github.com/eboody/statum/blob/main/docs/introspection.md - https://github.com/eboody/statum/blob/main/docs/typestate-builder-design-playbook.md - https://github.com/eboody/statum/blob/main/docs/patterns.md - https://github.com/eboody/statum/blob/main/docs/persistence-and-validators.md diff --git a/docs/agents/prompts/greenfield-workflow-design.md b/docs/agents/prompts/greenfield-workflow-design.md index 36412b2..62a5e4e 100644 --- a/docs/agents/prompts/greenfield-workflow-design.md +++ b/docs/agents/prompts/greenfield-workflow-design.md @@ -21,6 +21,8 @@ Tasks: - the state-specific data that should not live on the machine root - the likely `#[transition]` impl blocks - whether `#[validators]` or `statum::projection` is needed + - whether downstream docs, CLI explainers, replay tooling, graph exports, or + transition assertions should use Statum introspection 3. If it is a poor fit, say what should stay runtime-validated and why. 4. Keep the first implementation slice small enough for one PR. @@ -29,6 +31,7 @@ workflows. Use these references if needed: - https://github.com/eboody/statum/blob/main/README.md +- https://github.com/eboody/statum/blob/main/docs/introspection.md - https://github.com/eboody/statum/blob/main/docs/typestate-builder-design-playbook.md - https://github.com/eboody/statum/blob/main/docs/patterns.md - https://github.com/eboody/statum/blob/main/docs/persistence-and-validators.md diff --git a/docs/agents/prompts/pr-review-typestate-check.md b/docs/agents/prompts/pr-review-typestate-check.md index f75f474..184faeb 100644 --- a/docs/agents/prompts/pr-review-typestate-check.md +++ b/docs/agents/prompts/pr-review-typestate-check.md @@ -25,6 +25,8 @@ If you choose "Follow-up candidate" or "Refactor now", include: - the likely `#[machine]` fields and state data - the first `#[transition]` blocks worth adding - whether `#[validators]` or `statum::projection` should be involved +- whether downstream tooling should use Statum introspection instead of a + handwritten graph table - why the change would be safer or clearer with Statum Do not manufacture a typestate refactor if the new logic is still too dynamic or @@ -32,6 +34,7 @@ too early. Use these references if needed: - https://github.com/eboody/statum/blob/main/README.md +- https://github.com/eboody/statum/blob/main/docs/introspection.md - https://github.com/eboody/statum/blob/main/docs/typestate-builder-design-playbook.md - https://github.com/eboody/statum/blob/main/docs/patterns.md - https://github.com/eboody/statum/blob/main/docs/persistence-and-validators.md diff --git a/docs/agents/prompts/targeted-module-refactor.md b/docs/agents/prompts/targeted-module-refactor.md index f8e040a..fdea542 100644 --- a/docs/agents/prompts/targeted-module-refactor.md +++ b/docs/agents/prompts/targeted-module-refactor.md @@ -19,6 +19,8 @@ If Statum is a good fit, return: - the state-specific data - the legal transitions and likely `#[transition]` blocks - whether persisted rebuilds require `#[validators]` or `statum::projection` +- whether downstream tooling should use Statum introspection instead of a + handwritten graph table - the smallest safe migration slice - the tests that should move or be added @@ -29,6 +31,7 @@ Do not propose a full rewrite when a narrow first slice would prove the design. Use these references if needed: - https://github.com/eboody/statum/blob/main/README.md +- https://github.com/eboody/statum/blob/main/docs/introspection.md - https://github.com/eboody/statum/blob/main/docs/typestate-builder-design-playbook.md - https://github.com/eboody/statum/blob/main/docs/patterns.md - https://github.com/eboody/statum/blob/main/docs/persistence-and-validators.md diff --git a/docs/agents/templates/AGENTS.md b/docs/agents/templates/AGENTS.md index a60203c..a8c4429 100644 --- a/docs/agents/templates/AGENTS.md +++ b/docs/agents/templates/AGENTS.md @@ -28,6 +28,8 @@ staged workflows or protocol-heavy APIs. - `#[transition]` for legal edges - `#[validators]` and `statum::projection` if rebuilds exist - distinguish machine fields from state-specific data +- if docs, CLI explainers, replay tooling, or graph exports need machine + structure, prefer Statum introspection over a handwritten graph table - explain why plain runtime validation is weaker in this spot - keep the first migration slice small and testable @@ -37,6 +39,7 @@ staged workflows or protocol-heavy APIs. rebuild paths - read these references before proposing a non-trivial refactor: - + - - - - diff --git a/docs/agents/templates/CLAUDE.md b/docs/agents/templates/CLAUDE.md index bf4b8bf..2826c4f 100644 --- a/docs/agents/templates/CLAUDE.md +++ b/docs/agents/templates/CLAUDE.md @@ -26,12 +26,15 @@ real staged workflow or protocol whose legal edges should be encoded in types. - separate durable machine context from state-only payloads - list the likely `#[transition]` blocks - decide whether `#[validators]` or `statum::projection` is part of the design +- if downstream tooling needs machine structure, prefer Statum introspection + over a parallel handwritten graph table - cite exact files or symbols that justify the recommendation - propose a narrow first migration slice ## Read Before Refactoring - +- - - - diff --git a/docs/agents/templates/copilot-instructions.md b/docs/agents/templates/copilot-instructions.md index 1bcb4dc..fa86f28 100644 --- a/docs/agents/templates/copilot-instructions.md +++ b/docs/agents/templates/copilot-instructions.md @@ -25,11 +25,14 @@ fit for lifecycle-heavy or protocol-heavy code. - map the design to `#[state]`, `#[machine]`, `#[transition]`, and, if needed, `#[validators]` - explain the split between machine context and state data +- if docs, CLI explainers, replay tooling, or graph exports need machine + structure, prefer Statum introspection over a parallel graph table - keep the first change small enough to review and test ## References - +- - - - diff --git a/docs/agents/templates/cursor-statum.mdc b/docs/agents/templates/cursor-statum.mdc index ac9b8f0..7f0f53d 100644 --- a/docs/agents/templates/cursor-statum.mdc +++ b/docs/agents/templates/cursor-statum.mdc @@ -29,12 +29,15 @@ If you propose Statum: - cite the files and symbols that show the lifecycle - sketch `#[state]`, `#[machine]`, and `#[transition]` - decide whether `#[validators]` or `statum::projection` should be used +- if downstream tooling needs machine structure, prefer Statum introspection + over a parallel handwritten graph table - explain why runtime validation is weaker here - keep the first migration slice small References: - +- - - - diff --git a/docs/introspection.md b/docs/introspection.md new file mode 100644 index 0000000..96adc37 --- /dev/null +++ b/docs/introspection.md @@ -0,0 +1,158 @@ +# Machine Introspection + +Statum can emit typed machine introspection directly from the machine +definition itself. + +Use it when the machine definition should also drive downstream tooling: + +- CLI explainers +- generated docs +- graph exports +- branch-strip views +- test assertions about exact legal transitions +- replay or debug tooling that joins runtime events back to the static graph + +The important distinction is precision. Statum does not only expose a +machine-wide list of states. It exposes exact transition sites: + +- source state +- transition method +- exact legal target states from that site + +That means a branching transition like `Flow::validate() -> +Accepted | Rejected` can be rendered without maintaining a parallel handwritten +graph table. + +## Static Graph Access + +Use `MachineIntrospection` to get the generated graph: + +```rust +use statum::{machine, state, transition, MachineIntrospection}; + +#[state] +enum FlowState { + Fetched, + Accepted, + Rejected, +} + +#[machine] +struct Flow {} + +#[transition] +impl Flow { + fn validate(self, accept: bool) -> Result, Flow> { + if accept { + Ok(self.accept()) + } else { + Err(self.reject()) + } + } + + fn accept(self) -> Flow { + self.transition() + } + + fn reject(self) -> Flow { + self.transition() + } +} + +let graph = as MachineIntrospection>::GRAPH; +let validate = graph + .transition_from_method(flow::StateId::Fetched, "validate") + .unwrap(); + +assert_eq!( + graph.legal_targets(validate.id).unwrap(), + &[flow::StateId::Accepted, flow::StateId::Rejected] +); +``` + +From there, a consumer can ask for: + +- transitions from a state +- a transition by id +- a transition by source state and method name +- the exact legal targets for a transition site + +## Transition Identity + +State ids are generated as a machine-scoped enum like `flow::StateId`. + +Transition ids are typed and exact, but they are exposed as generated +associated consts on the source-state machine type, such as +`Flow::::VALIDATE`. + +That keeps transition identity tied to the exact source-state plus method site, +including cfg-pruned and macro-generated transitions. + +## Runtime Join Support + +If you want replay or debug tooling, record the transition that actually +happened at runtime and join it back to the static graph: + +```rust +use statum::{ + machine, state, transition, MachineTransitionRecorder, +}; + +#[state] +enum FlowState { + Fetched, + Accepted, + Rejected, +} + +#[machine] +struct Flow {} + +#[transition] +impl Flow { + fn validate(self, accept: bool) -> Result, Flow> { + if accept { + Ok(self.accept()) + } else { + Err(self.reject()) + } + } + + fn accept(self) -> Flow { + self.transition() + } + + fn reject(self) -> Flow { + self.transition() + } +} + +let event = as MachineTransitionRecorder>::try_record_transition_to::< + Flow, +>(Flow::::VALIDATE) +.unwrap(); + +assert_eq!(event.chosen, flow::StateId::Accepted); +``` + +Once you have both: + +- static graph metadata +- runtime-taken transition records + +you can render the chosen branch and the non-chosen legal branches from the +same source of truth. + +## Presentation Metadata + +Structural introspection is separate from human-facing metadata. + +If a consumer crate wants labels, descriptions, or phases for rendering, it can +add a typed `MachinePresentation` overlay keyed by the generated ids. That lets +the machine definition remain the source of truth for structure while the +consumer owns local explanation and presentation. + +## Example + +Runnable example: +[statum-examples/src/toy_demos/16-machine-introspection.rs](../statum-examples/src/toy_demos/16-machine-introspection.rs) diff --git a/docs/presentation-attribute-sugar-plan.md b/docs/presentation-attribute-sugar-plan.md new file mode 100644 index 0000000..b4846bb --- /dev/null +++ b/docs/presentation-attribute-sugar-plan.md @@ -0,0 +1,119 @@ +# Presentation Attribute Sugar Plan + +This is an optional follow-up plan for adding metadata authoring sugar on top of +Statum's existing typed introspection and presentation overlay APIs. + +## Recommendation + +Implement this in two stages. + +Start with prose-only sugar and keep the current typed overlay as the real +model. That means labels and descriptions can live near the machine +definition, but the generated output is still just a +`MachinePresentation<..., (), (), ()>` constant. If that proves useful and not +noisy, add a second opt-in layer for typed `metadata = expr` payloads. + +## Surface + +### Stage 1 + +```rust +#[machine] +#[present(label = "Flow", description = "Validation flow")] +struct Flow {} + +#[state] +enum FlowState { + #[present(label = "Fetched", description = "Ready for validation")] + Fetched, + #[present(label = "Accepted")] + Accepted, + #[present(label = "Rejected")] + Rejected, +} + +#[transition] +impl Flow { + #[present(label = "Validate", description = "Choose accepted or rejected")] + fn validate(self) -> Result, Flow> { + ... + } +} +``` + +Generated output: + +- `flow::PRESENTATION: MachinePresentation` +- emitted only when at least one `#[present(...)]` is used +- `GRAPH` stays unchanged + +### Stage 2 + +Only add this if real usage shows the manual overlay is too repetitive. + +```rust +#[presentation_types( + machine = crate::meta::MachineMeta, + state = crate::meta::StateMeta, + transition = crate::meta::TransitionMeta, +)] +#[machine] +#[present(label = "Flow", metadata = crate::meta::MachineMeta::Flow)] +struct Flow {} +``` + +Then variants and transition methods can use `metadata = ` too, and the +macro emits a typed `flow::PRESENTATION` constant using those declared metadata +types. + +## Implementation Plan + +1. Add a small shared parser in `statum-macros` for `#[present(...)]`. + It should accept only `label`, `description`, and later `metadata`. + Reject unknown keys and duplicates with precise spans. + +2. For stage 2 only, add `#[presentation_types(...)]` on the machine struct. + That keeps type declarations in one place instead of repeating them on every + state and transition. + +3. Extend existing parsers to retain presentation attrs. + - machine struct attrs for machine-level presentation + - `#[state]` enum variant attrs for state presentation + - `#[transition]` method attrs for transition presentation + +4. Reuse the ids that Statum already emits. + Generate `flow::PRESENTATION` in the same machine module that already owns + `StateId`, `TransitionId`, and `GRAPH`. + Do not add a second graph model. + +5. Keep emission conditional. + If there are no presentation attrs, emit nothing. That keeps the sugar from + adding namespace noise for users who do not want it. + +6. Keep consumer overlays first-class. + Do not make generic code depend on macro-emitted presentation. Manual + `MachinePresentation` values should remain fully supported and equally + legitimate. + +7. Add tests in this order. + - prose-only pass case + - repeated labels and descriptions across states and transitions + - duplicate-key and unknown-key compile errors + - typed `metadata = expr` pass case + - missing `#[presentation_types]` when `metadata = expr` is used + - integration test joining `GRAPH`, `PRESENTATION`, and a recorded runtime + event + +## Guardrails + +- no layout or renderer concerns in the attrs +- no impact on legality or transition generation +- no required metadata +- no stringly replacement for the typed overlay +- no second DSL beyond a thin attribute veneer over the existing model + +## Stopping Point + +If this gets implemented, stop after stage 1 unless a real consumer +immediately needs typed metadata sugar. That is where the feature is most +likely to start feeling noisy. diff --git a/macro_registry/Cargo.toml b/macro_registry/Cargo.toml index 2142cc5..d8a9040 100644 --- a/macro_registry/Cargo.toml +++ b/macro_registry/Cargo.toml @@ -1,6 +1,6 @@ [dependencies.module_path_extractor] path = "../module_path_extractor" -version = "0.6.5" +version = "0.6.6" [dependencies.syn] features = ["full"] @@ -27,7 +27,7 @@ name = "macro_registry" readme = "README.md" repository = "https://github.com/eboody/statum" rust-version = "1.93" -version = "0.6.5" +version = "0.6.6" [package.metadata.modum] weak_modules = [ diff --git a/macro_registry/src/analysis.rs b/macro_registry/src/analysis.rs index 11dae62..b08737a 100644 --- a/macro_registry/src/analysis.rs +++ b/macro_registry/src/analysis.rs @@ -1,11 +1,7 @@ -use std::cell::RefCell; -use std::collections::HashMap; use std::fs; use std::rc::Rc; -use crate::cache::{file_fingerprint, fresh_cached_value, CachedValue}; - -/// Cached enum entry extracted from a parsed source file. +/// Enum entry extracted from a parsed source file. #[derive(Clone)] pub struct EnumEntry { pub item: syn::ItemEnum, @@ -13,7 +9,7 @@ pub struct EnumEntry { pub attrs: Vec, } -/// Cached struct entry extracted from a parsed source file. +/// Struct entry extracted from a parsed source file. #[derive(Clone)] pub struct StructEntry { pub item: syn::ItemStruct, @@ -21,61 +17,55 @@ pub struct StructEntry { pub attrs: Vec, } -/// Parsed/cached representation of enums and structs in one source file. +/// Impl entry extracted from a parsed source file. +#[derive(Clone)] +pub struct ImplEntry { + pub item: syn::ItemImpl, + pub line_number: usize, + pub attrs: Vec, +} + +/// Parsed representation of enums, structs, and impls in one source file. #[derive(Clone, Default)] pub struct FileAnalysis { pub enums: Vec, pub structs: Vec, + pub impls: Vec, } -type CachedFileAnalysis = CachedValue>; - -thread_local! { - static FILE_ANALYSIS_CACHE: RefCell> = RefCell::new(HashMap::new()); -} - -/// Returns cached analysis for `file_path`, parsing and caching on first access. +/// Returns parsed analysis for `file_path`. pub fn get_file_analysis(file_path: &str) -> Option> { - let fingerprint = file_fingerprint(file_path)?; - - if let Some(cached) = fresh_cached_value( - FILE_ANALYSIS_CACHE.with(|cache| cache.borrow().get(file_path).cloned()), - fingerprint, - ) { - return Some(cached); - } - - let analysis = Rc::new(build_file_analysis(file_path)?); - FILE_ANALYSIS_CACHE.with(|cache| { - cache.borrow_mut().insert( - file_path.to_string(), - CachedFileAnalysis::new(fingerprint, analysis.clone()), - ); - }); - Some(analysis) + Some(Rc::new(build_file_analysis(file_path)?)) } fn build_file_analysis(file_path: &str) -> Option { let contents = fs::read_to_string(file_path).ok()?; let parsed = syn::parse_file(&contents).ok()?; let mut analysis = FileAnalysis::default(); + let mut next_search_line = 1usize; - collect_items(parsed.items, &contents, &mut analysis)?; + collect_items( + parsed.items, + &contents, + &mut analysis, + &mut next_search_line, + )?; Some(analysis) } -fn collect_items(items: Vec, contents: &str, analysis: &mut FileAnalysis) -> Option<()> { +fn collect_items( + items: Vec, + contents: &str, + analysis: &mut FileAnalysis, + next_search_line: &mut usize, +) -> Option<()> { for item in items { match item { syn::Item::Enum(item_enum) => { let name = item_enum.ident.to_string(); - let span_line = item_enum.ident.span().start().line; - let line_number = if span_line > 0 { - span_line - } else { - find_item_line(contents, "enum", &name)? - }; + let line_number = find_item_line_from(contents, "enum", &name, *next_search_line)?; + *next_search_line = line_number.saturating_add(1); analysis.enums.push(EnumEntry { attrs: attribute_names(&item_enum.attrs), item: item_enum, @@ -84,21 +74,27 @@ fn collect_items(items: Vec, contents: &str, analysis: &mut FileAnaly } syn::Item::Struct(item_struct) => { let name = item_struct.ident.to_string(); - let span_line = item_struct.ident.span().start().line; - let line_number = if span_line > 0 { - span_line - } else { - find_item_line(contents, "struct", &name)? - }; + let line_number = + find_item_line_from(contents, "struct", &name, *next_search_line)?; + *next_search_line = line_number.saturating_add(1); analysis.structs.push(StructEntry { attrs: attribute_names(&item_struct.attrs), item: item_struct, line_number, }); } + syn::Item::Impl(item_impl) => { + let line_number = find_impl_line_from(contents, *next_search_line)?; + *next_search_line = line_number.saturating_add(1); + analysis.impls.push(ImplEntry { + attrs: attribute_names(&item_impl.attrs), + item: item_impl, + line_number, + }); + } syn::Item::Mod(item_mod) => { if let Some((_, nested_items)) = item_mod.content { - collect_items(nested_items, contents, analysis)?; + collect_items(nested_items, contents, analysis, next_search_line)?; } } _ => {} @@ -124,8 +120,17 @@ fn attribute_names(attrs: &[syn::Attribute]) -> Vec { names } -fn find_item_line(contents: &str, kind: &str, item_name: &str) -> Option { - for (idx, line) in contents.lines().enumerate() { +fn find_item_line_from( + contents: &str, + kind: &str, + item_name: &str, + start_line: usize, +) -> Option { + for (idx, line) in contents + .lines() + .enumerate() + .skip(start_line.saturating_sub(1)) + { let trimmed = line.trim_start(); if line_starts_item_decl(trimmed, kind, item_name) { return Some(idx + 1); @@ -135,6 +140,21 @@ fn find_item_line(contents: &str, kind: &str, item_name: &str) -> Option None } +fn find_impl_line_from(contents: &str, start_line: usize) -> Option { + for (idx, line) in contents + .lines() + .enumerate() + .skip(start_line.saturating_sub(1)) + { + let trimmed = line.trim_start(); + if line_starts_impl_decl(trimmed) { + return Some(idx + 1); + } + } + + None +} + fn line_starts_item_decl(line: &str, kind: &str, item_name: &str) -> bool { let mut rest = line.trim_start(); @@ -166,6 +186,27 @@ fn line_starts_item_decl(line: &str, kind: &str, item_name: &str) -> bool { .is_none_or(|ch| ch.is_whitespace() || matches!(ch, '<' | '{' | '(' | ';' | ':')) } +fn line_starts_impl_decl(line: &str) -> bool { + let mut rest = line.trim_start(); + + if let Some(after_default) = consume_keyword(rest, "default") { + rest = after_default.trim_start(); + } + + if let Some(after_unsafe) = consume_keyword(rest, "unsafe") { + rest = after_unsafe.trim_start(); + } + + let Some(after_impl) = consume_keyword(rest, "impl") else { + return false; + }; + + after_impl + .chars() + .next() + .is_none_or(|ch| ch.is_whitespace() || matches!(ch, '<')) +} + fn consume_keyword<'a>(input: &'a str, keyword: &str) -> Option<&'a str> { let rest = input.strip_prefix(keyword)?; if rest @@ -231,20 +272,28 @@ pub(crate) enum MyState { pub struct MyMachine { id: u64, } + +impl MyMachine { + fn run(self) -> Self { + self + } +} "#, ); let analysis = build_file_analysis(path.to_str().expect("path")).expect("analysis"); assert_eq!(analysis.enums.len(), 1); assert_eq!(analysis.structs.len(), 1); + assert_eq!(analysis.impls.len(), 1); assert!(analysis.enums[0].line_number > 0); assert!(analysis.structs[0].line_number > 0); + assert!(analysis.impls[0].line_number > 0); let _ = fs::remove_file(path); } #[test] - fn file_analysis_cache_is_scoped_per_file_path() { + fn get_file_analysis_reparses_each_request() { let path_a = write_temp_rust_file( r#" #[state] @@ -262,7 +311,7 @@ enum StateB { B } let a_second = get_file_analysis(path_a.to_str().expect("a path")).expect("analysis a2"); let b_first = get_file_analysis(path_b.to_str().expect("b path")).expect("analysis b1"); - assert!(Rc::ptr_eq(&a_first, &a_second)); + assert!(!Rc::ptr_eq(&a_first, &a_second)); assert!(!Rc::ptr_eq(&a_first, &b_first)); let _ = fs::remove_file(path_a); @@ -290,6 +339,7 @@ mod workflow { let analysis = build_file_analysis(path.to_str().expect("path")).expect("analysis"); assert_eq!(analysis.enums.len(), 1); assert_eq!(analysis.structs.len(), 1); + assert_eq!(analysis.impls.len(), 0); assert_eq!(analysis.enums[0].item.ident, "TaskState"); assert_eq!(analysis.structs[0].item.ident, "TaskMachine"); @@ -297,25 +347,65 @@ mod workflow { } #[test] - fn analysis_cache_reuses_when_file_unchanged() { + fn collects_impl_blocks_from_inline_modules() { let path = write_temp_rust_file( r#" -#[state] -enum ReuseState { A } +mod workflow { + #[transition] + impl Machine { + fn run(self) -> Self { + self + } + } +} "#, ); - let path_str = path.to_str().expect("path").to_string(); - let first = get_file_analysis(&path_str).expect("analysis first"); - let second = get_file_analysis(&path_str).expect("analysis second"); + let analysis = build_file_analysis(path.to_str().expect("path")).expect("analysis"); + assert_eq!(analysis.impls.len(), 1); + assert!(analysis.impls[0] + .attrs + .iter() + .any(|attr| attr == "transition")); + assert!(analysis.impls[0].line_number > 0); + + let _ = fs::remove_file(path); + } - assert!(Rc::ptr_eq(&first, &second)); + #[test] + fn collects_distinct_line_numbers_for_same_named_structs_in_sibling_modules() { + let path = write_temp_rust_file( + r#" +mod shared { + pub struct Payload { + id: u64, + } +} + +mod workflow { + pub struct Payload { + id: u64, + } +} +"#, + ); + + let analysis = build_file_analysis(path.to_str().expect("path")).expect("analysis"); + let payload_lines = analysis + .structs + .iter() + .filter(|entry| entry.item.ident == "Payload") + .map(|entry| entry.line_number) + .collect::>(); + + assert_eq!(payload_lines.len(), 2); + assert_ne!(payload_lines[0], payload_lines[1]); let _ = fs::remove_file(path); } #[test] - fn analysis_cache_invalidates_when_file_changes() { + fn get_file_analysis_reflects_file_changes() { let path = write_temp_rust_file( r#" #[state] @@ -364,5 +454,14 @@ enum ChangedState { A, B } "struct", "Machine", )); + + assert!(line_starts_impl_decl("impl Machine {")); + assert!(line_starts_impl_decl( + "unsafe impl Trait for Machine {" + )); + assert!(line_starts_impl_decl( + "default impl Trait for Machine {" + )); + assert!(!line_starts_impl_decl("simple_machine_impl();")); } } diff --git a/macro_registry/src/cache.rs b/macro_registry/src/cache.rs deleted file mode 100644 index ae184f1..0000000 --- a/macro_registry/src/cache.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::fs; -use std::time::UNIX_EPOCH; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) struct FileFingerprint { - len: u64, - modified_ns: Option, -} - -#[derive(Clone)] -pub(crate) struct CachedValue { - pub(crate) fingerprint: FileFingerprint, - pub(crate) value: T, -} - -impl CachedValue { - pub(crate) fn new(fingerprint: FileFingerprint, value: T) -> Self { - Self { fingerprint, value } - } -} - -pub(crate) fn file_fingerprint(file_path: &str) -> Option { - let metadata = fs::metadata(file_path).ok()?; - let modified_ns = metadata - .modified() - .ok() - .and_then(|modified| modified.duration_since(UNIX_EPOCH).ok()) - .map(|duration| duration.as_nanos()); - Some(FileFingerprint { - len: metadata.len(), - modified_ns, - }) -} - -pub(crate) fn fresh_cached_value( - cached: Option>, - fingerprint: FileFingerprint, -) -> Option { - let cached = cached?; - if cached.fingerprint == fingerprint { - Some(cached.value) - } else { - None - } -} - -pub(crate) fn tracked_file_matches(tracked_file_path: Option<&str>, file_path: &str) -> bool { - tracked_file_path == Some(file_path) -} diff --git a/macro_registry/src/lib.rs b/macro_registry/src/lib.rs index 752db01..ee22481 100644 --- a/macro_registry/src/lib.rs +++ b/macro_registry/src/lib.rs @@ -1,8 +1,6 @@ #![allow(rustdoc::invalid_rust_codeblocks)] #![cfg_attr(not(doctest), doc = include_str!("../README.md"))] -mod cache; - pub mod analysis; pub mod callsite; pub mod query; diff --git a/macro_registry/src/registry.rs b/macro_registry/src/registry.rs index 971d6fe..1932eba 100644 --- a/macro_registry/src/registry.rs +++ b/macro_registry/src/registry.rs @@ -3,7 +3,6 @@ use std::hash::Hash; use std::sync::{OnceLock, RwLock}; use crate::analysis::{get_file_analysis, FileAnalysis}; -use crate::cache::tracked_file_matches; use crate::callsite::{current_source_info, module_path_for_line}; /// Key type for registry lookups. @@ -92,6 +91,10 @@ pub struct SourceContext { pub line_number: usize, } +fn tracked_file_matches(tracked_file_path: Option<&str>, file_path: &str) -> bool { + tracked_file_path == Some(file_path) +} + impl SourceContext { pub fn new(file_path: impl Into, line_number: usize) -> Self { Self { diff --git a/module_path_extractor/Cargo.toml b/module_path_extractor/Cargo.toml index 1e3b0f8..eaf917e 100644 --- a/module_path_extractor/Cargo.toml +++ b/module_path_extractor/Cargo.toml @@ -18,4 +18,4 @@ name = "module_path_extractor" readme = "README.md" repository = "https://github.com/eboody/statum" rust-version = "1.93" -version = "0.6.5" +version = "0.6.6" diff --git a/scripts/check_readme_links.sh b/scripts/check_readme_links.sh index 483ff47..cae2a51 100755 --- a/scripts/check_readme_links.sh +++ b/scripts/check_readme_links.sh @@ -1,7 +1,11 @@ #!/usr/bin/env bash set -euo pipefail -repo_root=$(git rev-parse --show-toplevel) +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +repo_root=$(cd -- "$script_dir/.." && pwd) +if [[ ! -f "$repo_root/README.md" ]]; then + repo_root=$(git rev-parse --show-toplevel) +fi cd "$repo_root" docs_files=( diff --git a/statum-core/Cargo.toml b/statum-core/Cargo.toml index ab02f9d..6f308fd 100644 --- a/statum-core/Cargo.toml +++ b/statum-core/Cargo.toml @@ -1,4 +1,5 @@ [dependencies] +linkme = "0.3" [package] authors = ["Eran Boodnero "] @@ -18,7 +19,7 @@ name = "statum-core" readme = "README.md" repository = "https://github.com/eboody/statum" rust-version = "1.93" -version = "0.6.5" +version = "0.6.6" [package.metadata.modum] weak_modules = [ diff --git a/statum-core/src/introspection.rs b/statum-core/src/introspection.rs new file mode 100644 index 0000000..866e588 --- /dev/null +++ b/statum-core/src/introspection.rs @@ -0,0 +1,678 @@ +/// Static introspection surface emitted for a generated Statum machine. +pub trait MachineIntrospection { + /// Machine-scoped state identifier emitted by `#[machine]`. + type StateId: Copy + Eq + core::hash::Hash + 'static; + + /// Machine-scoped transition-site identifier emitted by `#[machine]`. + type TransitionId: Copy + Eq + core::hash::Hash + 'static; + + /// Static graph descriptor for the machine family. + const GRAPH: &'static MachineGraph; +} + +/// Runtime accessor for transition descriptors that may be supplied by a +/// distributed registration surface. +#[derive(Clone, Copy)] +pub struct TransitionInventory { + get: fn() -> &'static [TransitionDescriptor], +} + +impl TransitionInventory { + /// Creates a transition inventory from a `'static` getter. + pub const fn new(get: fn() -> &'static [TransitionDescriptor]) -> Self { + Self { get } + } + + /// Returns the transition descriptors as a slice. + pub fn as_slice(&self) -> &'static [TransitionDescriptor] { + (self.get)() + } +} + +impl core::ops::Deref for TransitionInventory { + type Target = [TransitionDescriptor]; + + fn deref(&self) -> &Self::Target { + self.as_slice() + } +} + +impl core::fmt::Debug for TransitionInventory { + fn fmt( + &self, + formatter: &mut core::fmt::Formatter<'_>, + ) -> core::result::Result<(), core::fmt::Error> { + formatter.debug_tuple("TransitionInventory").finish() + } +} + +impl core::cmp::PartialEq for TransitionInventory { + fn eq(&self, other: &Self) -> bool { + core::ptr::eq(self.as_slice(), other.as_slice()) + } +} + +impl core::cmp::Eq for TransitionInventory {} + +/// Identity for one concrete machine state. +pub trait MachineStateIdentity: MachineIntrospection { + /// The state id for this concrete machine instantiation. + const STATE_ID: Self::StateId; +} + +/// Optional human-facing metadata layered on top of a machine graph. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct MachinePresentation< + S: 'static, + T: 'static, + MachineMeta: 'static = (), + StateMeta: 'static = (), + TransitionMeta: 'static = (), +> { + /// Optional machine-level presentation metadata. + pub machine: Option>, + /// Optional state-level presentation metadata keyed by state id. + pub states: &'static [StatePresentation], + /// Optional transition-level presentation metadata keyed by transition id. + pub transitions: &'static [TransitionPresentation], +} + +impl + MachinePresentation +where + S: Copy + Eq + 'static, + T: Copy + Eq + 'static, +{ + /// Finds state presentation metadata by id. + pub fn state(&self, id: S) -> Option<&StatePresentation> { + self.states.iter().find(|state| state.id == id) + } + + /// Finds transition presentation metadata by id. + pub fn transition(&self, id: T) -> Option<&TransitionPresentation> { + self.transitions + .iter() + .find(|transition| transition.id == id) + } +} + +/// Optional machine-level presentation metadata. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct MachinePresentationDescriptor { + /// Optional short human-facing machine label. + pub label: Option<&'static str>, + /// Optional longer human-facing machine description. + pub description: Option<&'static str>, + /// Consumer-owned typed machine metadata. + pub metadata: M, +} + +/// Optional state-level presentation metadata. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct StatePresentation { + /// Typed state identifier. + pub id: S, + /// Optional short human-facing state label. + pub label: Option<&'static str>, + /// Optional longer human-facing state description. + pub description: Option<&'static str>, + /// Consumer-owned typed state metadata. + pub metadata: M, +} + +/// Optional transition-level presentation metadata. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TransitionPresentation { + /// Typed transition-site identifier. + pub id: T, + /// Optional short human-facing transition label. + pub label: Option<&'static str>, + /// Optional longer human-facing transition description. + pub description: Option<&'static str>, + /// Consumer-owned typed transition metadata. + pub metadata: M, +} + +/// A runtime record of one chosen transition. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RecordedTransition { + /// Rust-facing identity of the machine family. + pub machine: MachineDescriptor, + /// Exact source state where the transition was taken. + pub from: S, + /// Exact transition site that was chosen. + pub transition: T, + /// Exact target state that actually happened at runtime. + pub chosen: S, +} + +impl RecordedTransition +where + S: 'static, + T: 'static, +{ + /// Builds a runtime transition record from typed machine ids. + pub const fn new(machine: MachineDescriptor, from: S, transition: T, chosen: S) -> Self { + Self { + machine, + from, + transition, + chosen, + } + } + + /// Finds the static transition descriptor for this runtime event. + pub fn transition_in<'a>( + &self, + graph: &'a MachineGraph, + ) -> Option<&'a TransitionDescriptor> + where + S: Copy + Eq, + T: Copy + Eq, + { + let descriptor = graph.transition(self.transition)?; + if descriptor.from == self.from && descriptor.to.contains(&self.chosen) { + Some(descriptor) + } else { + None + } + } + + /// Finds the static source-state descriptor for this runtime event. + pub fn source_state_in<'a>( + &self, + graph: &'a MachineGraph, + ) -> Option<&'a StateDescriptor> + where + S: Copy + Eq, + T: Copy + Eq, + { + self.transition_in(graph)?; + graph.state(self.from) + } + + /// Finds the static chosen-target descriptor for this runtime event. + pub fn chosen_state_in<'a>( + &self, + graph: &'a MachineGraph, + ) -> Option<&'a StateDescriptor> + where + S: Copy + Eq, + T: Copy + Eq, + { + self.transition_in(graph)?; + graph.state(self.chosen) + } +} + +/// Runtime recording helpers layered on top of static machine introspection. +pub trait MachineTransitionRecorder: MachineStateIdentity { + /// Records a runtime transition if `transition` is valid from `Self::STATE_ID` + /// and `chosen` is one of its legal target states. + fn try_record_transition( + transition: Self::TransitionId, + chosen: Self::StateId, + ) -> Option> { + let graph = Self::GRAPH; + let descriptor = graph.transition(transition)?; + if descriptor.from != Self::STATE_ID || !descriptor.to.contains(&chosen) { + return None; + } + + Some(RecordedTransition::new( + graph.machine, + Self::STATE_ID, + transition, + chosen, + )) + } + + /// Records a runtime transition using a typed target machine state. + fn try_record_transition_to( + transition: Self::TransitionId, + ) -> Option> + where + Next: MachineStateIdentity, + { + Self::try_record_transition(transition, Next::STATE_ID) + } +} + +impl MachineTransitionRecorder for M where M: MachineStateIdentity {} + +/// Structural machine graph emitted from macro-generated metadata. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct MachineGraph { + /// Rust-facing identity of the machine family. + pub machine: MachineDescriptor, + /// All states known to the machine. + pub states: &'static [StateDescriptor], + /// All transition sites known to the machine. + pub transitions: TransitionInventory, +} + +impl MachineGraph +where + S: Copy + Eq + 'static, + T: Copy + Eq + 'static, +{ + /// Finds a state descriptor by id. + pub fn state(&self, id: S) -> Option<&StateDescriptor> { + self.states.iter().find(|state| state.id == id) + } + + /// Finds a transition descriptor by id. + pub fn transition(&self, id: T) -> Option<&TransitionDescriptor> { + self.transitions + .iter() + .find(|transition| transition.id == id) + } + + /// Yields all transition sites originating from `state`. + pub fn transitions_from( + &self, + state: S, + ) -> impl Iterator> + '_ { + self.transitions + .iter() + .filter(move |transition| transition.from == state) + } + + /// Finds the transition site for `method_name` on `state`. + pub fn transition_from_method( + &self, + state: S, + method_name: &str, + ) -> Option<&TransitionDescriptor> { + self.transitions + .iter() + .find(|transition| transition.from == state && transition.method_name == method_name) + } + + /// Yields all transition sites that share the same method name. + pub fn transitions_named<'a>( + &'a self, + method_name: &'a str, + ) -> impl Iterator> + 'a { + self.transitions + .iter() + .filter(move |transition| transition.method_name == method_name) + } + + /// Returns the exact legal target states for a transition site. + pub fn legal_targets(&self, id: T) -> Option<&'static [S]> { + self.transition(id).map(|transition| transition.to) + } +} + +/// Rust-facing identity for a machine family. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct MachineDescriptor { + /// `module_path!()` for the source module that owns the machine. + pub module_path: &'static str, + /// Fully qualified Rust type path for the machine family. + pub rust_type_path: &'static str, +} + +/// Static descriptor for one generated state id. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct StateDescriptor { + /// Typed state identifier. + pub id: S, + /// Rust variant name of the state marker. + pub rust_name: &'static str, + /// Whether the state carries `state_data`. + pub has_data: bool, +} + +/// Static descriptor for one transition site. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TransitionDescriptor { + /// Typed transition-site identifier. + pub id: T, + /// Rust method name for the transition site. + pub method_name: &'static str, + /// Exact source state for the transition site. + pub from: S, + /// Exact legal target states for the transition site. + pub to: &'static [S], +} + +#[cfg(test)] +mod tests { + use super::{ + MachineDescriptor, MachineGraph, MachineIntrospection, MachinePresentation, + MachinePresentationDescriptor, MachineStateIdentity, MachineTransitionRecorder, + RecordedTransition, StateDescriptor, StatePresentation, TransitionDescriptor, + TransitionInventory, TransitionPresentation, + }; + use core::marker::PhantomData; + + #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] + enum StateId { + Draft, + Review, + Published, + } + + #[derive(Clone, Copy)] + struct TransitionId(&'static crate::__private::TransitionToken); + + impl TransitionId { + const fn from_token(token: &'static crate::__private::TransitionToken) -> Self { + Self(token) + } + } + + impl core::fmt::Debug for TransitionId { + fn fmt( + &self, + formatter: &mut core::fmt::Formatter<'_>, + ) -> core::result::Result<(), core::fmt::Error> { + formatter.write_str("TransitionId(..)") + } + } + + impl core::cmp::PartialEq for TransitionId { + fn eq(&self, other: &Self) -> bool { + core::ptr::eq(self.0, other.0) + } + } + + impl core::cmp::Eq for TransitionId {} + + impl core::hash::Hash for TransitionId { + fn hash(&self, state: &mut H) { + let ptr = core::ptr::from_ref(self.0) as usize; + ::hash(&ptr, state); + } + } + + static REVIEW_TARGETS: [StateId; 1] = [StateId::Review]; + static PUBLISH_TARGETS: [StateId; 1] = [StateId::Published]; + static SUBMIT_FROM_DRAFT_TOKEN: crate::__private::TransitionToken = + crate::__private::TransitionToken::new(); + static PUBLISH_FROM_REVIEW_TOKEN: crate::__private::TransitionToken = + crate::__private::TransitionToken::new(); + const SUBMIT_FROM_DRAFT: TransitionId = TransitionId::from_token(&SUBMIT_FROM_DRAFT_TOKEN); + const PUBLISH_FROM_REVIEW: TransitionId = TransitionId::from_token(&PUBLISH_FROM_REVIEW_TOKEN); + static STATES: [StateDescriptor; 3] = [ + StateDescriptor { + id: StateId::Draft, + rust_name: "Draft", + has_data: false, + }, + StateDescriptor { + id: StateId::Review, + rust_name: "Review", + has_data: true, + }, + StateDescriptor { + id: StateId::Published, + rust_name: "Published", + has_data: false, + }, + ]; + static TRANSITIONS: [TransitionDescriptor; 2] = [ + TransitionDescriptor { + id: SUBMIT_FROM_DRAFT, + method_name: "submit", + from: StateId::Draft, + to: &REVIEW_TARGETS, + }, + TransitionDescriptor { + id: PUBLISH_FROM_REVIEW, + method_name: "publish", + from: StateId::Review, + to: &PUBLISH_TARGETS, + }, + ]; + + struct Workflow(PhantomData); + struct DraftMarker; + struct ReviewMarker; + struct PublishedMarker; + + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + enum Phase { + Intake, + Review, + Output, + } + + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct MachineMeta { + phase: Phase, + } + + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct StateMeta { + phase: Phase, + term: &'static str, + } + + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct TransitionMeta { + phase: Phase, + branch: bool, + } + + static PRESENTATION: MachinePresentation< + StateId, + TransitionId, + MachineMeta, + StateMeta, + TransitionMeta, + > = MachinePresentation { + machine: Some(MachinePresentationDescriptor { + label: Some("Workflow"), + description: Some("Example presentation metadata for introspection."), + metadata: MachineMeta { + phase: Phase::Intake, + }, + }), + states: &[ + StatePresentation { + id: StateId::Draft, + label: Some("Draft"), + description: Some("Work has not been submitted yet."), + metadata: StateMeta { + phase: Phase::Intake, + term: "draft", + }, + }, + StatePresentation { + id: StateId::Review, + label: Some("Review"), + description: Some("Work is awaiting review."), + metadata: StateMeta { + phase: Phase::Review, + term: "review", + }, + }, + StatePresentation { + id: StateId::Published, + label: Some("Published"), + description: Some("Work is complete."), + metadata: StateMeta { + phase: Phase::Output, + term: "published", + }, + }, + ], + transitions: &[ + TransitionPresentation { + id: SUBMIT_FROM_DRAFT, + label: Some("Submit"), + description: Some("Move work into review."), + metadata: TransitionMeta { + phase: Phase::Review, + branch: false, + }, + }, + TransitionPresentation { + id: PUBLISH_FROM_REVIEW, + label: Some("Publish"), + description: Some("Complete the workflow."), + metadata: TransitionMeta { + phase: Phase::Output, + branch: false, + }, + }, + ], + }; + + impl MachineIntrospection for Workflow { + type StateId = StateId; + type TransitionId = TransitionId; + + const GRAPH: &'static MachineGraph = &MachineGraph { + machine: MachineDescriptor { + module_path: "workflow", + rust_type_path: "workflow::Machine", + }, + states: &STATES, + transitions: TransitionInventory::new(|| &TRANSITIONS), + }; + } + + impl MachineStateIdentity for Workflow { + const STATE_ID: Self::StateId = StateId::Draft; + } + + impl MachineStateIdentity for Workflow { + const STATE_ID: Self::StateId = StateId::Review; + } + + impl MachineStateIdentity for Workflow { + const STATE_ID: Self::StateId = StateId::Published; + } + + #[test] + fn query_helpers_find_expected_items() { + let graph = MachineGraph { + machine: MachineDescriptor { + module_path: "workflow", + rust_type_path: "workflow::Machine", + }, + states: &STATES, + transitions: TransitionInventory::new(|| &TRANSITIONS), + }; + + assert_eq!( + graph.state(StateId::Review).map(|state| state.rust_name), + Some("Review") + ); + assert_eq!( + graph + .transition(PUBLISH_FROM_REVIEW) + .map(|transition| transition.method_name), + Some("publish") + ); + assert_eq!( + graph + .transition_from_method(StateId::Draft, "submit") + .map(|transition| transition.id), + Some(SUBMIT_FROM_DRAFT) + ); + assert_eq!( + graph.legal_targets(SUBMIT_FROM_DRAFT), + Some(REVIEW_TARGETS.as_slice()) + ); + assert_eq!(graph.transitions_from(StateId::Draft).count(), 1); + assert_eq!(graph.transitions_named("publish").count(), 1); + } + + #[test] + fn runtime_transition_recording_joins_back_to_static_graph() { + let event = Workflow::::try_record_transition_to::>( + SUBMIT_FROM_DRAFT, + ) + .expect("valid runtime transition"); + + assert_eq!( + event, + RecordedTransition::new( + MachineDescriptor { + module_path: "workflow", + rust_type_path: "workflow::Machine", + }, + StateId::Draft, + SUBMIT_FROM_DRAFT, + StateId::Review, + ) + ); + assert_eq!( + Workflow::::GRAPH + .transition(event.transition) + .map(|transition| (transition.from, transition.to)), + Some((StateId::Draft, REVIEW_TARGETS.as_slice())) + ); + assert_eq!( + event.source_state_in(Workflow::::GRAPH), + Some(&StateDescriptor { + id: StateId::Draft, + rust_name: "Draft", + has_data: false, + }) + ); + } + + #[test] + fn runtime_transition_recording_rejects_illegal_target_or_site() { + assert!(Workflow::::try_record_transition( + PUBLISH_FROM_REVIEW, + StateId::Published, + ) + .is_none()); + assert!( + Workflow::::try_record_transition_to::>( + SUBMIT_FROM_DRAFT, + ) + .is_none() + ); + } + + #[test] + fn presentation_queries_join_with_runtime_transitions() { + let event = Workflow::::try_record_transition_to::>( + SUBMIT_FROM_DRAFT, + ) + .expect("valid runtime transition"); + + assert_eq!( + PRESENTATION.machine, + Some(MachinePresentationDescriptor { + label: Some("Workflow"), + description: Some("Example presentation metadata for introspection."), + metadata: MachineMeta { + phase: Phase::Intake, + }, + }) + ); + assert_eq!( + PRESENTATION.transition(event.transition), + Some(&TransitionPresentation { + id: SUBMIT_FROM_DRAFT, + label: Some("Submit"), + description: Some("Move work into review."), + metadata: TransitionMeta { + phase: Phase::Review, + branch: false, + }, + }) + ); + assert_eq!( + PRESENTATION.state(event.chosen), + Some(&StatePresentation { + id: StateId::Review, + label: Some("Review"), + description: Some("Work is awaiting review."), + metadata: StateMeta { + phase: Phase::Review, + term: "review", + }, + }) + ); + } +} diff --git a/statum-core/src/lib.rs b/statum-core/src/lib.rs index a07a455..53293a9 100644 --- a/statum-core/src/lib.rs +++ b/statum-core/src/lib.rs @@ -8,8 +8,39 @@ //! - runtime error and result types //! - projection helpers for event-log style rebuilds +mod introspection; + pub mod projection; +#[doc(hidden)] +pub mod __private { + pub use linkme; + + #[derive(Debug)] + pub struct TransitionToken { + _private: u8, + } + + impl Default for TransitionToken { + fn default() -> Self { + Self::new() + } + } + + impl TransitionToken { + pub const fn new() -> Self { + Self { _private: 0 } + } + } +} + +pub use introspection::{ + MachineDescriptor, MachineGraph, MachineIntrospection, MachinePresentation, + MachinePresentationDescriptor, MachineStateIdentity, MachineTransitionRecorder, + RecordedTransition, StateDescriptor, StatePresentation, TransitionDescriptor, + TransitionInventory, TransitionPresentation, +}; + /// A generated state marker type. /// /// Every `#[state]` variant produces one marker type that implements diff --git a/statum-examples/Cargo.toml b/statum-examples/Cargo.toml index a36c74f..b2adb78 100644 --- a/statum-examples/Cargo.toml +++ b/statum-examples/Cargo.toml @@ -22,7 +22,7 @@ version = "0.8" [dependencies.statum] path = "../statum" -version = "0.6.5" +version = "0.6.6" [dependencies.tokio] features = ["full"] @@ -38,7 +38,7 @@ license = "MIT" name = "statum-examples" publish = false rust-version = "1.93" -version = "0.6.5" +version = "0.6.6" [package.metadata.modum] weak_modules = [ diff --git a/statum-examples/README.md b/statum-examples/README.md index c673891..a848eda 100644 --- a/statum-examples/README.md +++ b/statum-examples/README.md @@ -21,8 +21,9 @@ cargo run -p statum-examples --bin tokio-websocket-session ## Contents - Toy demos: - - `example_01_setup.rs` through `15-transition-map.rs` + - `example_01_setup.rs` through `16-machine-introspection.rs` - best when you are learning the macros or one helper at a time + - includes an introspection example that shows exact branch alternatives and runtime transition recording - Showcases: - `axum-sqlite-review`: HTTP + SQLite + typed rehydration - `clap-sqlite-deploy-pipeline`: multi-invocation CLI workflow diff --git a/statum-examples/src/toy_demos/16-machine-introspection.rs b/statum-examples/src/toy_demos/16-machine-introspection.rs new file mode 100644 index 0000000..f723ede --- /dev/null +++ b/statum-examples/src/toy_demos/16-machine-introspection.rs @@ -0,0 +1,68 @@ +use statum::{ + MachineIntrospection, MachineStateIdentity, MachineTransitionRecorder, machine, state, + transition, +}; + +#[state] +enum FlowState { + Fetched, + Accepted, + Rejected, +} + +#[machine] +struct Flow { + request_id: u64, +} + +#[transition] +impl Flow { + fn validate(self, accept: bool) -> Result, Flow> { + if accept { + Ok(self.accept()) + } else { + Err(self.reject()) + } + } + + fn accept(self) -> Flow { + self.transition() + } + + fn reject(self) -> Flow { + self.transition() + } +} + +pub fn run() { + let graph = as MachineIntrospection>::GRAPH; + + let validate = graph + .transition_from_method(flow::StateId::Fetched, "validate") + .expect("validate transition should exist"); + assert_eq!(validate.id, Flow::::VALIDATE); + assert_eq!( + graph.legal_targets(validate.id).unwrap(), + &[flow::StateId::Accepted, flow::StateId::Rejected] + ); + + let fetched = Flow::::builder().request_id(7).build(); + let accepted = match fetched.validate(true) { + Ok(accepted) => accepted, + Err(_) => panic!("expected accepted branch"), + }; + assert_eq!(accepted.request_id, 7); + + let event = as MachineTransitionRecorder>::try_record_transition_to::< + Flow, + >(Flow::::VALIDATE) + .expect("runtime event should match the static graph"); + + let transition = event.transition_in(graph).expect("transition metadata"); + assert_eq!(transition.method_name, "validate"); + assert_eq!( + transition.from, + as MachineStateIdentity>::STATE_ID + ); + assert_eq!(event.chosen, flow::StateId::Accepted); +} diff --git a/statum-examples/src/toy_demos/mod.rs b/statum-examples/src/toy_demos/mod.rs index d291b03..03ae497 100644 --- a/statum-examples/src/toy_demos/mod.rs +++ b/statum-examples/src/toy_demos/mod.rs @@ -27,3 +27,5 @@ pub mod example_13_review_flow; pub mod example_14_batch_machine_fields; #[path = "15-transition-map.rs"] pub mod example_15_transition_map; +#[path = "16-machine-introspection.rs"] +pub mod example_16_machine_introspection; diff --git a/statum-examples/tests/toy_demos.rs b/statum-examples/tests/toy_demos.rs index b3f6642..952d04e 100644 --- a/statum-examples/tests/toy_demos.rs +++ b/statum-examples/tests/toy_demos.rs @@ -74,3 +74,8 @@ fn example_14_batch_machine_fields() { fn example_15_transition_map() { toy_demos::example_15_transition_map::run(); } + +#[test] +fn example_16_machine_introspection() { + toy_demos::example_16_machine_introspection::run(); +} diff --git a/statum-macros/Cargo.toml b/statum-macros/Cargo.toml index 67e9b75..c5c413f 100644 --- a/statum-macros/Cargo.toml +++ b/statum-macros/Cargo.toml @@ -3,7 +3,7 @@ quote = "1.0" [dependencies.macro_registry] path = "../macro_registry" -version = "0.6.5" +version = "0.6.6" [dependencies.moddef] version = "0.2.6" @@ -26,7 +26,7 @@ trybuild = "1.0" [dev-dependencies.statum-core] path = "../statum-core" -version = "0.6.5" +version = "0.6.6" [features] default = [] @@ -52,4 +52,4 @@ name = "statum-macros" readme = "README.md" repository = "https://github.com/eboody/statum" rust-version = "1.93" -version = "0.6.5" +version = "0.6.6" diff --git a/statum-macros/src/lib.rs b/statum-macros/src/lib.rs index ad25f05..405a126 100644 --- a/statum-macros/src/lib.rs +++ b/statum-macros/src/lib.rs @@ -27,7 +27,7 @@ moddef::moddef!( pub(crate) use syntax::{ItemTarget, ModulePath, extract_derives}; -use crate::{MachinePath, ensure_machine_loaded_by_name}; +use crate::{MachinePath, ensure_machine_loaded_by_name, unique_loaded_machine_elsewhere}; use macro_registry::callsite::current_module_path_opt; use proc_macro::TokenStream; use proc_macro2::Span; @@ -123,7 +123,10 @@ pub fn transition( }; let machine_path: MachinePath = module_path.clone().into(); - let machine_info_owned = ensure_machine_loaded_by_name(&machine_path, &tr_impl.machine_name); + // `include!` gives the transition macro the included file as its source context, + // so exact module lookup can miss the already-loaded parent machine. + let machine_info_owned = ensure_machine_loaded_by_name(&machine_path, &tr_impl.machine_name) + .or_else(|| unique_loaded_machine_elsewhere(&tr_impl.machine_name)); let machine_info = match machine_info_owned.as_ref() { Some(info) => info, None => { @@ -141,7 +144,7 @@ pub fn transition( } // -- Step 3: Generate new code - let expanded = generate_transition_impl(&input, &tr_impl, machine_info, &module_path); + let expanded = generate_transition_impl(&input, &tr_impl, machine_info); // Combine expanded code with the original `impl` if needed // or simply return the expanded code diff --git a/statum-macros/src/machine/emission.rs b/statum-macros/src/machine/emission.rs index 0eca87d..7ecf820 100644 --- a/statum-macros/src/machine/emission.rs +++ b/statum-macros/src/machine/emission.rs @@ -1,13 +1,13 @@ use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote}; -use syn::{GenericParam, Generics, Ident, ItemStruct, Visibility}; +use syn::{GenericParam, Generics, Ident, ItemStruct, LitStr, Visibility}; use crate::state::{ParsedEnumInfo, ParsedVariantInfo}; use crate::{EnumInfo, to_snake_case}; use super::metadata::{ParsedMachineInfo, field_type_alias_name, is_rust_analyzer}; use super::registry::get_machine_map; -use super::MachineInfo; +use super::{MachineInfo, transition_slice_ident}; pub fn generate_machine_impls(machine_info: &MachineInfo, item: &ItemStruct) -> proc_macro2::TokenStream { let map_guard = match get_machine_map().read() { @@ -77,6 +77,12 @@ pub fn generate_machine_impls(machine_info: &MachineInfo, item: &ItemStruct) -> Ok(surface) => surface, Err(err) => return err, }; + let introspection_impls = generate_machine_introspection_impls( + machine_info, + &state_enum, + &parsed_state, + &machine_ident, + ); quote! { #transition_support @@ -84,6 +90,7 @@ pub fn generate_machine_impls(machine_info: &MachineInfo, item: &ItemStruct) -> #struct_def #builder_methods #machine_state_surface + #introspection_impls } } @@ -256,7 +263,8 @@ fn generate_machine_state_surface( } } }); - let module_ident = format_ident!("{}", to_snake_case(&machine_info.name)); + let module_ident = machine_state_module_ident(machine_info); + let introspection_surface = generate_machine_module_introspection(machine_info, parsed_state); Ok(quote! { #vis mod #module_ident { @@ -284,6 +292,8 @@ fn generate_machine_state_surface( impl SomeState { #(#is_methods)* } + + #introspection_surface } }) } @@ -314,6 +324,136 @@ pub(crate) fn transition_support_module_ident(machine_info: &MachineInfo) -> Ide ) } +fn machine_state_module_ident(machine_info: &MachineInfo) -> Ident { + format_ident!("{}", to_snake_case(&machine_info.name)) +} + +fn generate_machine_module_introspection( + machine_info: &MachineInfo, + parsed_state: &ParsedEnumInfo, +) -> TokenStream { + let transition_slice_ident = transition_slice_ident( + &machine_info.name, + machine_info.file_path.as_deref(), + machine_info.line_number, + ); + let state_id_variants = parsed_state.variants.iter().map(|variant| { + let variant_ident = format_ident!("{}", variant.name); + quote! { #variant_ident } + }); + let state_descriptors = parsed_state.variants.iter().map(|variant| { + let variant_ident = format_ident!("{}", variant.name); + let rust_name = LitStr::new(&variant.name, Span::call_site()); + let has_data = variant.data_type.is_some(); + quote! { + statum::StateDescriptor { + id: StateId::#variant_ident, + rust_name: #rust_name, + has_data: #has_data, + } + } + }); + let module_path = LitStr::new(machine_info.module_path.as_ref(), Span::call_site()); + let rust_type_path = LitStr::new( + &format!("{}::{}", machine_info.module_path, machine_info.name), + Span::call_site(), + ); + let state_count = parsed_state.variants.len(); + + quote! { + #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] + pub enum StateId { + #(#state_id_variants),* + } + + #[derive(Clone, Copy)] + pub struct TransitionId(&'static statum::__private::TransitionToken); + + impl TransitionId { + #[doc(hidden)] + pub const fn from_token(token: &'static statum::__private::TransitionToken) -> Self { + Self(token) + } + } + + impl core::fmt::Debug for TransitionId { + fn fmt( + &self, + formatter: &mut core::fmt::Formatter<'_>, + ) -> core::result::Result<(), core::fmt::Error> { + formatter.write_str("TransitionId(..)") + } + } + + impl core::cmp::PartialEq for TransitionId { + fn eq(&self, other: &Self) -> bool { + core::ptr::eq(self.0, other.0) + } + } + + impl core::cmp::Eq for TransitionId {} + + impl core::hash::Hash for TransitionId { + fn hash(&self, state: &mut H) { + let ptr = core::ptr::from_ref(self.0) as usize; + ::hash(&ptr, state); + } + } + + static __STATUM_STATES: [statum::StateDescriptor; #state_count] = [ + #(#state_descriptors),* + ]; + + #[doc(hidden)] + #[statum::__private::linkme::distributed_slice] + #[linkme(crate = statum::__private::linkme)] + pub static #transition_slice_ident: [statum::TransitionDescriptor]; + + fn __statum_transitions() -> &'static [statum::TransitionDescriptor] { + &#transition_slice_ident + } + + pub static GRAPH: statum::MachineGraph = statum::MachineGraph { + machine: statum::MachineDescriptor { + module_path: #module_path, + rust_type_path: #rust_type_path, + }, + states: &__STATUM_STATES, + transitions: statum::TransitionInventory::new(__statum_transitions), + }; + } +} + +fn generate_machine_introspection_impls( + machine_info: &MachineInfo, + state_enum: &EnumInfo, + parsed_state: &ParsedEnumInfo, + machine_ident: &Ident, +) -> TokenStream { + let module_ident = machine_state_module_ident(machine_info); + let state_trait_ident = state_enum.get_trait_name(); + let state_identity_impls = parsed_state.variants.iter().map(|variant| { + let variant_ident = format_ident!("{}", variant.name); + quote! { + impl statum::MachineStateIdentity for #machine_ident<#variant_ident> { + const STATE_ID: Self::StateId = #module_ident::StateId::#variant_ident; + } + } + }); + + quote! { + impl statum::MachineIntrospection for #machine_ident { + type StateId = #module_ident::StateId; + type TransitionId = #module_ident::TransitionId; + + const GRAPH: &'static statum::MachineGraph = + &#module_ident::GRAPH; + } + + #(#state_identity_impls)* + } +} + fn generate_struct_definition( parsed_machine: &ParsedMachineInfo, machine_ident: &Ident, diff --git a/statum-macros/src/machine/introspection.rs b/statum-macros/src/machine/introspection.rs new file mode 100644 index 0000000..bb103e4 --- /dev/null +++ b/statum-macros/src/machine/introspection.rs @@ -0,0 +1,70 @@ +use quote::format_ident; +use syn::Ident; + +pub(crate) fn to_shouty_snake_identifier(value: &str) -> String { + let mut result = String::new(); + + for (idx, segment) in value.trim_start_matches("r#").split('_').enumerate() { + if segment.is_empty() { + continue; + } + + if idx > 0 { + result.push('_'); + } + + for ch in segment.chars() { + for upper in ch.to_uppercase() { + result.push(upper); + } + } + } + + result +} + +pub(crate) fn transition_slice_ident( + machine_name: &str, + file_path: Option<&str>, + line_number: usize, +) -> Ident { + let key = format!( + "{machine_name}::{}::{line_number}", + file_path.unwrap_or_default() + ); + format_ident!("__statum_transitions_{:016x}", stable_hash(&key)) +} + +fn stable_hash(input: &str) -> u64 { + let mut hash = 0xcbf29ce484222325u64; + for byte in input.as_bytes() { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x100000001b3); + } + hash +} + +#[cfg(test)] +mod tests { + use super::{to_shouty_snake_identifier, transition_slice_ident}; + + #[test] + fn shouty_snake_identifier_handles_snake_case_and_raw_prefixes() { + assert_eq!(to_shouty_snake_identifier("validate"), "VALIDATE"); + assert_eq!(to_shouty_snake_identifier("start_review"), "START_REVIEW"); + assert_eq!(to_shouty_snake_identifier("r#await"), "AWAIT"); + } + + #[test] + fn transition_slice_ident_tracks_machine_source() { + let first = transition_slice_ident("ReviewFlow", Some("src/alpha.rs"), 40).to_string(); + let second = transition_slice_ident("ReviewFlow", Some("src/beta.rs"), 40).to_string(); + let third = transition_slice_ident("ReviewFlow", Some("src/alpha.rs"), 91).to_string(); + + assert!(first.starts_with("__statum_transitions_")); + assert!(second.starts_with("__statum_transitions_")); + assert!(third.starts_with("__statum_transitions_")); + assert_ne!(first, second); + assert_ne!(first, third); + } +} diff --git a/statum-macros/src/machine/metadata.rs b/statum-macros/src/machine/metadata.rs index f554fce..76025f4 100644 --- a/statum-macros/src/machine/metadata.rs +++ b/statum-macros/src/machine/metadata.rs @@ -1,4 +1,5 @@ use macro_registry::callsite::{current_source_info, module_path_for_line}; +use macro_registry::registry::SourceContext; use macro_registry::query; use macro_registry::registry; use proc_macro2::{Span, TokenStream}; @@ -7,7 +8,8 @@ use syn::{Generics, Ident, ItemStruct, LitStr, Type, Visibility}; use crate::{ EnumInfo, ModulePath, StateModulePath, ensure_state_enum_loaded, - ensure_state_enum_loaded_by_name, extract_derives, + ensure_state_enum_loaded_by_name, ensure_state_enum_loaded_by_name_from_source, + ensure_state_enum_loaded_from_source, extract_derives, }; pub type MachinePath = ModulePath; @@ -19,6 +21,7 @@ pub struct MachineInfo { pub derives: Vec, pub fields: Vec, pub module_path: MachinePath, + pub line_number: usize, pub generics: String, pub state_generic_name: Option, pub file_path: Option, @@ -64,11 +67,25 @@ impl MachineInfo { } pub fn get_matching_state_enum(&self) -> Result { - let state_path: StateModulePath = self.module_path.clone().into(); + let state_path: StateModulePath = self.module_path.clone(); + // Included transition fragments must resolve the state enum against the + // machine's source file, not the include file's pseudo-module context. + let source = self + .file_path + .as_ref() + .map(|file_path| SourceContext::new(file_path.clone(), self.line_number)); let state_enum = if let Some(expected_name) = self.state_generic_name.as_deref() { - ensure_state_enum_loaded_by_name(&state_path, expected_name) + source + .as_ref() + .and_then(|source| { + ensure_state_enum_loaded_by_name_from_source(&state_path, expected_name, source) + }) + .or_else(|| ensure_state_enum_loaded_by_name(&state_path, expected_name)) } else { - ensure_state_enum_loaded(&state_path) + source + .as_ref() + .and_then(|source| ensure_state_enum_loaded_from_source(&state_path, source)) + .or_else(|| ensure_state_enum_loaded(&state_path)) }; let Some(state_enum) = state_enum else { return Err(missing_state_enum_error(self)); @@ -109,6 +126,7 @@ impl MachineInfo { .flatten() .collect(), module_path, + line_number, fields, generics: item.generics.to_token_stream().to_string(), state_generic_name: extract_state_generic_name(&item.generics), @@ -121,6 +139,7 @@ impl MachineInfo { return None; } + let line_number = current_source_info().map(|(_, line)| line).unwrap_or_default(); Some(Self { name: item.ident.to_string(), vis: item.vis.to_token_stream().to_string(), @@ -132,6 +151,7 @@ impl MachineInfo { .collect(), fields: collect_fields(item), module_path: module_path.clone(), + line_number, generics: item.generics.to_token_stream().to_string(), state_generic_name: extract_state_generic_name(&item.generics), file_path: current_source_info().map(|(path, _)| path), @@ -228,7 +248,10 @@ fn missing_state_enum_error(machine_info: &MachineInfo) -> TokenStream { machine_info.name ) }); - let available = available_state_candidates_in_module(machine_info.module_path.as_ref()); + let available = available_state_candidates_in_module( + machine_info.file_path.as_deref(), + machine_info.module_path.as_ref(), + ); let available_line = if available.is_empty() { "No `#[state]` enums were found in that module.".to_string() } else { @@ -238,7 +261,13 @@ fn missing_state_enum_error(machine_info: &MachineInfo) -> TokenStream { ) }; let elsewhere_line = expected - .and_then(|name| same_named_state_candidates_elsewhere(name, machine_info.module_path.as_ref())) + .and_then(|name| { + same_named_state_candidates_elsewhere( + machine_info.file_path.as_deref(), + name, + machine_info.module_path.as_ref(), + ) + }) .map(|candidates| { format!( "Same-named `#[state]` enums elsewhere in this file: {}.", @@ -247,7 +276,7 @@ fn missing_state_enum_error(machine_info: &MachineInfo) -> TokenStream { }) .unwrap_or_else(|| "No same-named `#[state]` enums were found in other modules of this file.".to_string()); let missing_attr_line = expected.and_then(|name| { - plain_enum_line_in_module(machine_info.module_path.as_ref(), name).map(|line| { + plain_enum_line_in_module(machine_info.file_path.as_deref(), machine_info.module_path.as_ref(), name).map(|line| { format!("An enum named `{name}` exists on line {line}, but it is not annotated with `#[state]`.") }) }); @@ -265,15 +294,27 @@ fn missing_state_enum_error(machine_info: &MachineInfo) -> TokenStream { quote! { compile_error!(#message); } } -fn available_state_candidates_in_module(module_path: &str) -> Vec { - let Some((file_path, _)) = current_source_info() else { +fn available_state_candidates_in_module( + file_path: Option<&str>, + module_path: &str, +) -> Vec { + let Some(file_path) = file_path + .map(str::to_owned) + .or_else(|| current_source_info().map(|(path, _)| path)) + else { return Vec::new(); }; query::candidates_in_module(&file_path, module_path, query::ItemKind::Enum, Some("state")) } -fn plain_enum_line_in_module(module_path: &str, enum_name: &str) -> Option { - let (file_path, _) = current_source_info()?; +fn plain_enum_line_in_module( + file_path: Option<&str>, + module_path: &str, + enum_name: &str, +) -> Option { + let file_path = file_path + .map(str::to_owned) + .or_else(|| current_source_info().map(|(path, _)| path))?; query::plain_item_line_in_module( &file_path, module_path, @@ -284,10 +325,13 @@ fn plain_enum_line_in_module(module_path: &str, enum_name: &str) -> Option, enum_name: &str, module_path: &str, ) -> Option> { - let (file_path, _) = current_source_info()?; + let file_path = file_path + .map(str::to_owned) + .or_else(|| current_source_info().map(|(path, _)| path))?; let candidates = query::same_named_candidates_elsewhere( &file_path, module_path, diff --git a/statum-macros/src/machine/mod.rs b/statum-macros/src/machine/mod.rs index 605e552..3e4c2cd 100644 --- a/statum-macros/src/machine/mod.rs +++ b/statum-macros/src/machine/mod.rs @@ -1,10 +1,12 @@ mod emission; +mod introspection; mod metadata; mod registry; mod validation; pub(crate) use emission::transition_support_module_ident; pub use emission::generate_machine_impls; +pub(crate) use introspection::{to_shouty_snake_identifier, transition_slice_ident}; pub use metadata::{MachineInfo, MachinePath}; -pub use registry::{ensure_machine_loaded_by_name, store_machine_struct}; +pub use registry::{ensure_machine_loaded_by_name, store_machine_struct, unique_loaded_machine_elsewhere}; pub use validation::{invalid_machine_target_error, validate_machine_struct}; diff --git a/statum-macros/src/machine/registry.rs b/statum-macros/src/machine/registry.rs index 3ff561f..4834e12 100644 --- a/statum-macros/src/machine/registry.rs +++ b/statum-macros/src/machine/registry.rs @@ -23,7 +23,9 @@ impl registry::RegistryDomain for MachineRegistryDomain { } fn build_value(entry: &Self::Entry, module_path: &Self::Key) -> Option { - MachineInfo::from_item_struct_with_module(&entry.item, module_path) + let mut value = MachineInfo::from_item_struct_with_module(&entry.item, module_path)?; + value.line_number = entry.line_number; + Some(value) } fn matches_entry(entry: &Self::Entry) -> bool { @@ -66,6 +68,40 @@ pub fn ensure_machine_loaded_by_name( registry::ensure_loaded_by_name::(&MACHINE_MAP, machine_path, machine_name) } +pub fn unique_loaded_machine_elsewhere(machine_name: &str) -> Option { + let source = registry::SourceContext::current()?; + let map = MACHINE_MAP.map().read().ok()?; + + let mut matches = map + .values() + .filter(|machine| { + machine.name == machine_name + && machine.file_path.as_deref() != Some(source.file_path.as_str()) + }) + .cloned() + .collect::>(); + + matches.sort_by(|left, right| { + left.name + .cmp(&right.name) + .then(left.module_path.as_ref().cmp(right.module_path.as_ref())) + .then(left.file_path.cmp(&right.file_path)) + .then(left.line_number.cmp(&right.line_number)) + }); + matches.dedup_by(|left, right| { + left.name == right.name + && left.module_path.as_ref() == right.module_path.as_ref() + && left.file_path == right.file_path + && left.line_number == right.line_number + }); + + if matches.len() == 1 { + matches.pop() + } else { + None + } +} + pub fn store_machine_struct(machine_info: &MachineInfo) { MACHINE_MAP.insert(machine_info.module_path.clone(), machine_info.clone()); } diff --git a/statum-macros/src/machine/validation.rs b/statum-macros/src/machine/validation.rs index 5f53672..1a7db62 100644 --- a/statum-macros/src/machine/validation.rs +++ b/statum-macros/src/machine/validation.rs @@ -38,7 +38,7 @@ pub fn validate_machine_struct(item: &ItemStruct, machine_info: &MachineInfo) -> ); }; - let state_path: StateModulePath = machine_info.module_path.clone().into(); + let state_path: StateModulePath = machine_info.module_path.clone(); let matching_state_enum = ensure_state_enum_loaded(&state_path); if item.generics.params.len() > 1 { diff --git a/statum-macros/src/state.rs b/statum-macros/src/state.rs index 94d833f..edbb64c 100644 --- a/statum-macros/src/state.rs +++ b/statum-macros/src/state.rs @@ -208,12 +208,40 @@ pub fn ensure_state_enum_loaded(enum_path: &StateModulePath) -> Option registry::ensure_loaded::(&STATE_ENUMS, enum_path) } +pub fn ensure_state_enum_loaded_from_source( + enum_path: &StateModulePath, + source: ®istry::SourceContext, +) -> Option { + registry::try_ensure_loaded_from_source::( + &STATE_ENUMS, + registry::LookupMode::from_key(enum_path), + source, + ) + .ok() + .map(|loaded| loaded.value) +} + pub fn ensure_state_enum_loaded_by_name( enum_path: &StateModulePath, enum_name: &str, ) -> Option { registry::ensure_loaded_by_name::(&STATE_ENUMS, enum_path, enum_name) } + +pub fn ensure_state_enum_loaded_by_name_from_source( + enum_path: &StateModulePath, + enum_name: &str, + source: ®istry::SourceContext, +) -> Option { + registry::try_ensure_loaded_by_name_from_source::( + &STATE_ENUMS, + registry::LookupMode::from_key(enum_path), + enum_name, + source, + ) + .ok() + .map(|loaded| loaded.value) +} impl EnumInfo { pub fn from_item_enum(item: &ItemEnum) -> syn::Result { let Some((file_path, line_number)) = current_source_info() else { diff --git a/statum-macros/src/transition.rs b/statum-macros/src/transition.rs index 65a63e5..b4fb44a 100644 --- a/statum-macros/src/transition.rs +++ b/statum-macros/src/transition.rs @@ -9,13 +9,14 @@ use syn::{ ItemImpl, LitStr, PathArguments, ReturnType, Type, TypePath, }; -use crate::machine::transition_support_module_ident; -use crate::{EnumInfo, MachineInfo}; +use crate::machine::{to_shouty_snake_identifier, transition_slice_ident, transition_support_module_ident}; +use crate::{EnumInfo, MachineInfo, to_snake_case}; /// Stores all metadata for a single transition method in an `impl` block #[allow(unused)] pub struct TransitionFn { pub name: Ident, + pub attrs: Vec, pub has_receiver: bool, pub return_type: Option, pub return_type_span: Option, @@ -43,6 +44,25 @@ impl TransitionFn { Ok(return_state) } + + pub fn return_states(&self) -> Result, TokenStream> { + let Some(return_type) = self.return_type.as_ref() else { + return Err(invalid_return_type_error(self, "missing return type")); + }; + let machine_ident = format_ident!("{}", self.machine_name); + let return_states = collect_machine_and_states(return_type, machine_ident) + .into_iter() + .map(|(_, state)| state) + .collect::>(); + if return_states.is_empty() { + return Err(invalid_return_type_error( + self, + "expected return type like `Machine` (optionally wrapped in `Option`/`Result`)", + )); + } + + Ok(return_states) + } } /// Represents the entire `impl` block of our `transition` macro @@ -55,6 +75,7 @@ pub struct TransitionImpl { /// The source state extracted from `target_type` (e.g. `Draft`) pub source_state: String, pub source_state_span: Span, + pub attrs: Vec, /// All transition methods extracted from the `impl` pub functions: Vec, } @@ -86,6 +107,7 @@ pub fn parse_transition_impl(item_impl: &ItemImpl) -> Result states, + Err(err) => return Some(err), + }; + for return_state in return_states { + if state_enum_info.get_variant_from_name(&return_state).is_none() { + return Some(invalid_transition_method_state_error( + func, + &tr_impl.machine_name, + &return_state, + &state_enum_info, + )); + } + } } None @@ -215,12 +253,17 @@ pub fn generate_transition_impl( input: &ItemImpl, tr_impl: &TransitionImpl, target_machine_info: &MachineInfo, - _module_path: &str, ) -> TokenStream { let target_type = &tr_impl.target_type; let machine_target_ident = format_ident!("{}", target_machine_info.name); let transition_support_module_ident = transition_support_module_ident(target_machine_info); let field_names = target_machine_info.field_names(); + let machine_module_ident = format_ident!("{}", to_snake_case(&target_machine_info.name)); + let transition_slice_ident = transition_slice_ident( + &target_machine_info.name, + target_machine_info.file_path.as_deref(), + target_machine_info.line_number, + ); let state_enum_info = match target_machine_info.get_matching_state_enum() { Ok(enum_info) => enum_info, Err(err) => return err, @@ -329,9 +372,61 @@ pub fn generate_transition_impl( Err(err) => err, }) }); + let transition_registrations = tr_impl.functions.iter().enumerate().map(|(idx, function)| { + let return_states = match function.return_states() { + Ok(states) => states, + Err(err) => return err, + }; + let unique_suffix = transition_site_unique_suffix(tr_impl, function, idx); + let token_ident = format_ident!("__STATUM_TRANSITION_TOKEN_{}", unique_suffix); + let targets_ident = format_ident!("__STATUM_TRANSITION_TARGETS_{}", unique_suffix); + let registration_ident = format_ident!("__STATUM_TRANSITION_SITE_{}", unique_suffix); + let id_const_ident = format_ident!( + "{}", + to_shouty_snake_identifier(&function.name.to_string()) + ); + let method_name = LitStr::new(&function.name.to_string(), function.name.span()); + let source_state_ident = format_ident!("{}", tr_impl.source_state); + let target_state_idents = return_states.iter().map(|state| { + let state_ident = format_ident!("{}", state); + quote! { #machine_module_ident::StateId::#state_ident } + }); + let target_state_count = return_states.len(); + let cfg_attrs = propagated_cfg_attrs(&tr_impl.attrs, &function.attrs); + + quote! { + #(#cfg_attrs)* + static #targets_ident: [#machine_module_ident::StateId; #target_state_count] = [ + #(#target_state_idents),* + ]; + + #(#cfg_attrs)* + static #token_ident: statum::__private::TransitionToken = + statum::__private::TransitionToken::new(); + + #(#cfg_attrs)* + #[statum::__private::linkme::distributed_slice(#machine_module_ident::#transition_slice_ident)] + #[linkme(crate = statum::__private::linkme)] + static #registration_ident: + statum::TransitionDescriptor<#machine_module_ident::StateId, #machine_module_ident::TransitionId> = + statum::TransitionDescriptor { + id: #machine_module_ident::TransitionId::from_token(&#token_ident), + method_name: #method_name, + from: #machine_module_ident::StateId::#source_state_ident, + to: &#targets_ident, + }; + + #(#cfg_attrs)* + impl #target_type { + pub const #id_const_ident: #machine_module_ident::TransitionId = + #machine_module_ident::TransitionId::from_token(&#token_ident); + } + } + }); quote! { #(#transition_impls)* + #(#transition_registrations)* #input } } @@ -456,6 +551,60 @@ fn machine_return_signature(machine_name: &str) -> String { format!("{machine_name}") } +fn propagated_cfg_attrs( + impl_attrs: &[syn::Attribute], + function_attrs: &[syn::Attribute], +) -> Vec { + impl_attrs + .iter() + .chain(function_attrs.iter()) + .filter(|attr| { + attr.path() + .get_ident() + .is_some_and(|ident| ident == "cfg" || ident == "cfg_attr") + }) + .cloned() + .collect() +} + +fn transition_site_unique_suffix( + tr_impl: &TransitionImpl, + function: &TransitionFn, + index: usize, +) -> String { + let attrs = function + .attrs + .iter() + .map(|attr| attr.to_token_stream().to_string()) + .collect::>() + .join("|"); + let return_type = function + .return_type + .as_ref() + .map(|ty| ty.to_token_stream().to_string()) + .unwrap_or_default(); + let signature = format!( + "{}::{}::{}::{}::{}::{}", + tr_impl.machine_name, + tr_impl.source_state, + function.name, + index, + attrs, + return_type, + ); + + format!("{:016x}", stable_hash(&signature)) +} + +fn stable_hash(input: &str) -> u64 { + let mut hash = 0xcbf29ce484222325u64; + for byte in input.as_bytes() { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x100000001b3); + } + hash +} + fn compile_error_at(span: Span, message: &str) -> TokenStream { let message = LitStr::new(message, span); quote::quote_spanned! { span => @@ -474,50 +623,120 @@ fn compile_error_at(span: Span, message: &str) -> TokenStream { /// /// Walks through wrapper types (`Option`/`Result`) via their first generic argument. pub fn parse_machine_and_state(ty: &Type, target_machine_ident: Ident) -> Option<(String, String)> { + parse_primary_machine_and_state(ty, target_machine_ident) +} + +/// Attempts to parse the primary visible next state from `ty`. +/// +/// This preserves the existing transition helper behavior by following the first +/// generic argument through supported wrappers until it reaches `Machine`. +pub fn parse_primary_machine_and_state( + ty: &Type, + target_machine_ident: Ident, +) -> Option<(String, String)> { let mut current = ty; loop { - match classify_return_wrapper(current, &target_machine_ident)? { - ReturnWrapper::Machine(segment) => { + match classify_primary_return_wrapper(current, &target_machine_ident)? { + PrimaryReturnWrapper::Machine(segment) => { return extract_machine_generic(&segment.arguments, target_machine_ident) .map(|(machine, state, _)| (machine, state)); } - ReturnWrapper::Option(inner) | ReturnWrapper::Result(inner) => { + PrimaryReturnWrapper::Option(inner) | PrimaryReturnWrapper::Result(inner) => { current = inner; } } } } -enum ReturnWrapper<'a> { +/// Collects every `Machine` target mentioned in supported wrapper trees. +/// +/// This is used for exact branch introspection and intentionally inspects both +/// sides of `Result` while still ignoring arbitrary custom decision enums. +pub fn collect_machine_and_states( + ty: &Type, + target_machine_ident: Ident, +) -> Vec<(String, String)> { + let mut targets = Vec::new(); + collect_machine_targets(ty, &target_machine_ident, &mut targets); + targets +} + +enum PrimaryReturnWrapper<'a> { Machine(&'a syn::PathSegment), Option(&'a Type), Result(&'a Type), } -fn classify_return_wrapper<'a>( +fn classify_primary_return_wrapper<'a>( ty: &'a Type, target_machine_ident: &Ident, -) -> Option> { +) -> Option> { let Type::Path(TypePath { path, .. }) = ty else { return None; }; let segment = path.segments.last()?; if &segment.ident == target_machine_ident { - return Some(ReturnWrapper::Machine(segment)); + return Some(PrimaryReturnWrapper::Machine(segment)); } if segment.ident == "Option" { - return extract_first_generic_type_ref(&segment.arguments).map(ReturnWrapper::Option); + return extract_first_generic_type_ref(&segment.arguments).map(PrimaryReturnWrapper::Option); } if segment.ident == "Result" { - return extract_first_generic_type_ref(&segment.arguments).map(ReturnWrapper::Result); + return extract_first_generic_type_ref(&segment.arguments).map(PrimaryReturnWrapper::Result); } None } +fn collect_machine_targets( + ty: &Type, + target_machine_ident: &Ident, + targets: &mut Vec<(String, String)>, +) { + let Type::Path(TypePath { path, .. }) = ty else { + return; + }; + let Some(segment) = path.segments.last() else { + return; + }; + + if &segment.ident == target_machine_ident { + if let Some((machine, state, _)) = + extract_machine_generic(&segment.arguments, target_machine_ident.clone()) + { + push_unique_target(targets, machine, state); + } + return; + } + + if segment.ident == "Option" { + if let Some(inner) = extract_first_generic_type_ref(&segment.arguments) { + collect_machine_targets(inner, target_machine_ident, targets); + } + return; + } + + if segment.ident == "Result" + && let Some(types) = extract_generic_type_refs(&segment.arguments) + { + for inner in types { + collect_machine_targets(inner, target_machine_ident, targets); + } + } +} + +fn push_unique_target(targets: &mut Vec<(String, String)>, machine: String, state: String) { + if !targets + .iter() + .any(|(existing_machine, existing_state)| existing_machine == &machine && existing_state == &state) + { + targets.push((machine, state)); + } +} + fn extract_machine_generic( args: &PathArguments, target_machine_ident: Ident, @@ -558,17 +777,88 @@ fn same_named_machine_candidates_elsewhere( } fn extract_first_generic_type_ref(args: &PathArguments) -> Option<&Type> { + extract_generic_type_refs(args)?.into_iter().next() +} + +fn extract_generic_type_refs(args: &PathArguments) -> Option> { let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args: generic_args, .. }) = args else { return None; }; - if generic_args.is_empty() { + + let types = generic_args + .iter() + .filter_map(|arg| match arg { + GenericArgument::Type(ty) => Some(ty), + _ => None, + }) + .collect::>(); + if types.is_empty() { return None; } - let GenericArgument::Type(ty) = &generic_args[0] else { - return None; - }; - Some(ty) + + Some(types) +} + +#[cfg(test)] +mod tests { + use super::{collect_machine_and_states, parse_machine_and_state, parse_primary_machine_and_state}; + use quote::format_ident; + use syn::Type; + + fn parse_type(source: &str) -> Type { + syn::parse_str(source).expect("valid type") + } + + #[test] + fn primary_parser_preserves_existing_result_behavior() { + let ty = parse_type("Result, Machine>"); + + assert_eq!( + parse_primary_machine_and_state(&ty, format_ident!("Machine")), + Some(("Machine".to_owned(), "Accepted".to_owned())) + ); + assert_eq!( + parse_machine_and_state(&ty, format_ident!("Machine")), + Some(("Machine".to_owned(), "Accepted".to_owned())) + ); + } + + #[test] + fn target_collector_reads_both_result_branches() { + let ty = parse_type("Result, Machine>"); + + assert_eq!( + collect_machine_and_states(&ty, format_ident!("Machine")), + vec![ + ("Machine".to_owned(), "Accepted".to_owned()), + ("Machine".to_owned(), "Rejected".to_owned()), + ] + ); + } + + #[test] + fn target_collector_reads_nested_wrappers() { + let ty = parse_type("Option, Machine>>"); + + assert_eq!( + collect_machine_and_states(&ty, format_ident!("Machine")), + vec![ + ("Machine".to_owned(), "Accepted".to_owned()), + ("Machine".to_owned(), "Rejected".to_owned()), + ] + ); + } + + #[test] + fn target_collector_ignores_non_machine_payloads_and_dedups() { + let ty = parse_type("Result>, Result, Error>>"); + + assert_eq!( + collect_machine_and_states(&ty, format_ident!("Machine")), + vec![("Machine".to_owned(), "Accepted".to_owned())] + ); + } } diff --git a/statum-macros/tests/macro_errors.rs b/statum-macros/tests/macro_errors.rs index 3b6bf02..9229d33 100644 --- a/statum-macros/tests/macro_errors.rs +++ b/statum-macros/tests/macro_errors.rs @@ -32,6 +32,7 @@ fn test_invalid_transition_usage() { t.compile_fail("tests/ui/invalid_transition_plain_struct_machine_name.rs"); t.compile_fail("tests/ui/invalid_transition_unknown_source_state.rs"); t.compile_fail("tests/ui/invalid_transition_unknown_return_state.rs"); + t.compile_fail("tests/ui/invalid_transition_unknown_secondary_return_state.rs"); t.compile_fail("tests/ui/invalid_transition_map_undeclared_edge.rs"); t.compile_fail("tests/ui/invalid_legacy_transition_helper_trait.rs"); } @@ -71,6 +72,8 @@ fn test_valid_macro_usage() { t.pass("tests/ui/valid_transition_nested_wrappers.rs"); t.pass("tests/ui/valid_into_machines_by.rs"); t.pass("tests/ui/valid_transition_map.rs"); + t.pass("tests/ui/valid_machine_introspection.rs"); + t.pass("tests/ui/valid_machine_introspection_cfg_dedup.rs"); t.pass("tests/ui/valid_visibility_and_reconstruction.rs"); t.pass("tests/ui/valid_multiple_machines_same_module.rs"); t.pass("tests/ui/valid_machine_field_aliases.rs"); diff --git a/statum-macros/tests/ui/invalid_legacy_machine_builder.rs b/statum-macros/tests/ui/invalid_legacy_machine_builder.rs index 1c7b758..065f11e 100644 --- a/statum-macros/tests/ui/invalid_legacy_machine_builder.rs +++ b/statum-macros/tests/ui/invalid_legacy_machine_builder.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, validators}; diff --git a/statum-macros/tests/ui/invalid_legacy_machine_builder.stderr b/statum-macros/tests/ui/invalid_legacy_machine_builder.stderr index d4b8925..3cd3074 100644 --- a/statum-macros/tests/ui/invalid_legacy_machine_builder.stderr +++ b/statum-macros/tests/ui/invalid_legacy_machine_builder.stderr @@ -1,8 +1,8 @@ error[E0599]: no method named `machine_builder` found for struct `Row` in the current scope - --> tests/ui/invalid_legacy_machine_builder.rs:47:17 + --> tests/ui/invalid_legacy_machine_builder.rs:53:17 | -20 | struct Row { +26 | struct Row { | ---------- method `machine_builder` not found for this struct ... -47 | let _ = row.machine_builder().name("todo".to_string()).build(); +53 | let _ = row.machine_builder().name("todo".to_string()).build(); | ^^^^^^^^^^^^^^^ method not found in `Row` diff --git a/statum-macros/tests/ui/invalid_legacy_machines_builder.rs b/statum-macros/tests/ui/invalid_legacy_machines_builder.rs index 9f6d9c2..b22797d 100644 --- a/statum-macros/tests/ui/invalid_legacy_machines_builder.rs +++ b/statum-macros/tests/ui/invalid_legacy_machines_builder.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, validators}; diff --git a/statum-macros/tests/ui/invalid_legacy_machines_builder.stderr b/statum-macros/tests/ui/invalid_legacy_machines_builder.stderr index c62b404..d627306 100644 --- a/statum-macros/tests/ui/invalid_legacy_machines_builder.stderr +++ b/statum-macros/tests/ui/invalid_legacy_machines_builder.stderr @@ -1,5 +1,5 @@ error[E0599]: no method named `machines_builder` found for struct `Vec` in the current scope - --> tests/ui/invalid_legacy_machines_builder.rs:49:18 + --> tests/ui/invalid_legacy_machines_builder.rs:55:18 | -49 | let _ = rows.machines_builder().name("todo".to_string()).build(); +55 | let _ = rows.machines_builder().name("todo".to_string()).build(); | ^^^^^^^^^^^^^^^^ method not found in `Vec` diff --git a/statum-macros/tests/ui/invalid_legacy_state_helper_traits.rs b/statum-macros/tests/ui/invalid_legacy_state_helper_traits.rs index b47f95b..f4a0d35 100644 --- a/statum-macros/tests/ui/invalid_legacy_state_helper_traits.rs +++ b/statum-macros/tests/ui/invalid_legacy_state_helper_traits.rs @@ -1,7 +1,13 @@ #![allow(unused_imports)] extern crate self as statum; - -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; + +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::state; diff --git a/statum-macros/tests/ui/invalid_legacy_state_helper_traits.stderr b/statum-macros/tests/ui/invalid_legacy_state_helper_traits.stderr index f186cad..fee5291 100644 --- a/statum-macros/tests/ui/invalid_legacy_state_helper_traits.stderr +++ b/statum-macros/tests/ui/invalid_legacy_state_helper_traits.stderr @@ -1,17 +1,17 @@ error[E0405]: cannot find trait `StateVariant` in this scope - --> tests/ui/invalid_legacy_state_helper_traits.rs:15:28 + --> tests/ui/invalid_legacy_state_helper_traits.rs:21:28 | -15 | fn assert_state_variant() {} +21 | fn assert_state_variant() {} | ^^^^^^^^^^^^ not found in this scope error[E0405]: cannot find trait `RequiresStateData` in this scope - --> tests/ui/invalid_legacy_state_helper_traits.rs:17:34 + --> tests/ui/invalid_legacy_state_helper_traits.rs:23:34 | -17 | fn assert_requires_state_data() {} +23 | fn assert_requires_state_data() {} | ^^^^^^^^^^^^^^^^^ not found in this scope error[E0405]: cannot find trait `DoesNotRequireStateData` in this scope - --> tests/ui/invalid_legacy_state_helper_traits.rs:19:42 + --> tests/ui/invalid_legacy_state_helper_traits.rs:25:42 | -19 | fn assert_does_not_require_state_data() {} +25 | fn assert_does_not_require_state_data() {} | ^^^^^^^^^^^^^^^^^^^^^^^ not found in this scope diff --git a/statum-macros/tests/ui/invalid_legacy_superstate.rs b/statum-macros/tests/ui/invalid_legacy_superstate.rs index 140864f..c7b54d3 100644 --- a/statum-macros/tests/ui/invalid_legacy_superstate.rs +++ b/statum-macros/tests/ui/invalid_legacy_superstate.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, validators}; diff --git a/statum-macros/tests/ui/invalid_legacy_superstate.stderr b/statum-macros/tests/ui/invalid_legacy_superstate.stderr index e72a3c4..217a244 100644 --- a/statum-macros/tests/ui/invalid_legacy_superstate.stderr +++ b/statum-macros/tests/ui/invalid_legacy_superstate.stderr @@ -1,5 +1,5 @@ error[E0425]: cannot find type `TaskMachineSuperState` in this scope - --> tests/ui/invalid_legacy_superstate.rs:46:17 + --> tests/ui/invalid_legacy_superstate.rs:52:17 | -46 | let _state: TaskMachineSuperState = Row { status: "draft" } +52 | let _state: TaskMachineSuperState = Row { status: "draft" } | ^^^^^^^^^^^^^^^^^^^^^ not found in this scope diff --git a/statum-macros/tests/ui/invalid_legacy_transition_helper_trait.rs b/statum-macros/tests/ui/invalid_legacy_transition_helper_trait.rs index 6cde0c4..194ed59 100644 --- a/statum-macros/tests/ui/invalid_legacy_transition_helper_trait.rs +++ b/statum-macros/tests/ui/invalid_legacy_transition_helper_trait.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, transition}; diff --git a/statum-macros/tests/ui/invalid_legacy_transition_helper_trait.stderr b/statum-macros/tests/ui/invalid_legacy_transition_helper_trait.stderr index 159a74c..23b5d90 100644 --- a/statum-macros/tests/ui/invalid_legacy_transition_helper_trait.stderr +++ b/statum-macros/tests/ui/invalid_legacy_transition_helper_trait.stderr @@ -1,5 +1,5 @@ error[E0405]: cannot find trait `WorkflowMachineTransitionTo` in this scope - --> tests/ui/invalid_legacy_transition_helper_trait.rs:25:31 + --> tests/ui/invalid_legacy_transition_helper_trait.rs:31:31 | -25 | fn assert_transition_trait>(_machine: T) {} +31 | fn assert_transition_trait>(_machine: T) {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ not found in this scope diff --git a/statum-macros/tests/ui/invalid_machine_generic_not_first.rs b/statum-macros/tests/ui/invalid_machine_generic_not_first.rs index 456030c..42a3cd3 100644 --- a/statum-macros/tests/ui/invalid_machine_generic_not_first.rs +++ b/statum-macros/tests/ui/invalid_machine_generic_not_first.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state}; diff --git a/statum-macros/tests/ui/invalid_machine_generic_not_first.stderr b/statum-macros/tests/ui/invalid_machine_generic_not_first.stderr index 1d29cb1..901642c 100644 --- a/statum-macros/tests/ui/invalid_machine_generic_not_first.stderr +++ b/statum-macros/tests/ui/invalid_machine_generic_not_first.stderr @@ -2,13 +2,13 @@ error: Error: machine `BadMachine` declares unsupported generics `< T, FooState Statum requires exactly one generic, and it must name the `#[state]` enum `FooState`. Found first generic `T` and additional generics `FooState`. Fix: rewrite this as `struct BadMachine { ... }` and move other generic data into fields or payload types. - --> tests/ui/invalid_machine_generic_not_first.rs:14:18 + --> tests/ui/invalid_machine_generic_not_first.rs:20:18 | -14 | struct BadMachine { +20 | struct BadMachine { | ^^^^^^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_machine_generic_not_first.rs:16:2 + --> tests/ui/invalid_machine_generic_not_first.rs:22:2 | -16 | } +22 | } | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_machine_generic_not_first.rs` diff --git a/statum-macros/tests/ui/invalid_machine_missing_state_derive.rs b/statum-macros/tests/ui/invalid_machine_missing_state_derive.rs index f4d8807..64524f8 100644 --- a/statum-macros/tests/ui/invalid_machine_missing_state_derive.rs +++ b/statum-macros/tests/ui/invalid_machine_missing_state_derive.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state}; diff --git a/statum-macros/tests/ui/invalid_machine_missing_state_derive.stderr b/statum-macros/tests/ui/invalid_machine_missing_state_derive.stderr index 63d0218..e9d58ac 100644 --- a/statum-macros/tests/ui/invalid_machine_missing_state_derive.stderr +++ b/statum-macros/tests/ui/invalid_machine_missing_state_derive.stderr @@ -1,12 +1,12 @@ error: Error: machine `BuildMachine` derives `Clone`, but `#[state]` enum `BuildState` does not. Fix: add `#[derive(Clone)]` to `BuildState` so the generated state markers and `BuildMachine` stay compatible. - --> tests/ui/invalid_machine_missing_state_derive.rs:17:8 + --> tests/ui/invalid_machine_missing_state_derive.rs:23:8 | -17 | struct BuildMachine { +23 | struct BuildMachine { | ^^^^^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_machine_missing_state_derive.rs:19:2 + --> tests/ui/invalid_machine_missing_state_derive.rs:25:2 | -19 | } +25 | } | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_machine_missing_state_derive.rs` diff --git a/statum-macros/tests/ui/invalid_machine_multiple_generics.rs b/statum-macros/tests/ui/invalid_machine_multiple_generics.rs index 87081e1..0471fd2 100644 --- a/statum-macros/tests/ui/invalid_machine_multiple_generics.rs +++ b/statum-macros/tests/ui/invalid_machine_multiple_generics.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state}; diff --git a/statum-macros/tests/ui/invalid_machine_multiple_generics.stderr b/statum-macros/tests/ui/invalid_machine_multiple_generics.stderr index fbde060..6c57082 100644 --- a/statum-macros/tests/ui/invalid_machine_multiple_generics.stderr +++ b/statum-macros/tests/ui/invalid_machine_multiple_generics.stderr @@ -2,7 +2,7 @@ error: Error: machine `Workflow` declares unsupported generics `< WorkflowState, Statum requires exactly one generic, and it must name the `#[state]` enum `WorkflowState`. Found first generic `WorkflowState` and additional generics `Context`. Fix: rewrite this as `struct Workflow { ... }` and move other generic data into fields or payload types. - --> tests/ui/invalid_machine_multiple_generics.rs:14:16 + --> tests/ui/invalid_machine_multiple_generics.rs:20:16 | -14 | struct Workflow { +20 | struct Workflow { | ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/statum-macros/tests/ui/invalid_machine_no_state_generic.rs b/statum-macros/tests/ui/invalid_machine_no_state_generic.rs index 53ac20a..9e962f2 100644 --- a/statum-macros/tests/ui/invalid_machine_no_state_generic.rs +++ b/statum-macros/tests/ui/invalid_machine_no_state_generic.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::machine; diff --git a/statum-macros/tests/ui/invalid_machine_no_state_generic.stderr b/statum-macros/tests/ui/invalid_machine_no_state_generic.stderr index 25c03d7..6707edd 100644 --- a/statum-macros/tests/ui/invalid_machine_no_state_generic.stderr +++ b/statum-macros/tests/ui/invalid_machine_no_state_generic.stderr @@ -1,12 +1,12 @@ error: Error: machine `Machine` is missing its `#[state]` generic. Fix: declare `Machine` where `State` is the `#[state]` enum in this module. - --> tests/ui/invalid_machine_no_state_generic.rs:9:8 - | -9 | struct Machine { - | ^^^^^^^ + --> tests/ui/invalid_machine_no_state_generic.rs:15:8 + | +15 | struct Machine { + | ^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_machine_no_state_generic.rs:11:2 + --> tests/ui/invalid_machine_no_state_generic.rs:17:2 | -11 | } +17 | } | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_machine_no_state_generic.rs` diff --git a/statum-macros/tests/ui/invalid_machine_not_struct.rs b/statum-macros/tests/ui/invalid_machine_not_struct.rs index f936f47..f609356 100644 --- a/statum-macros/tests/ui/invalid_machine_not_struct.rs +++ b/statum-macros/tests/ui/invalid_machine_not_struct.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::machine; diff --git a/statum-macros/tests/ui/invalid_machine_not_struct.stderr b/statum-macros/tests/ui/invalid_machine_not_struct.stderr index 5172fa2..3705da4 100644 --- a/statum-macros/tests/ui/invalid_machine_not_struct.stderr +++ b/statum-macros/tests/ui/invalid_machine_not_struct.stderr @@ -1,12 +1,12 @@ error: Error: #[machine] must be applied to a struct, but `NotAStruct` is an enum. Fix: declare `struct NotAStruct { ... }` and apply `#[machine]` to that struct. - --> tests/ui/invalid_machine_not_struct.rs:9:6 - | -9 | enum NotAStruct { - | ^^^^^^^^^^ + --> tests/ui/invalid_machine_not_struct.rs:15:6 + | +15 | enum NotAStruct { + | ^^^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_machine_not_struct.rs:11:2 + --> tests/ui/invalid_machine_not_struct.rs:17:2 | -11 | } +17 | } | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_machine_not_struct.rs` diff --git a/statum-macros/tests/ui/invalid_machine_plain_enum_missing_state_attr.rs b/statum-macros/tests/ui/invalid_machine_plain_enum_missing_state_attr.rs index 7e8860a..308c677 100644 --- a/statum-macros/tests/ui/invalid_machine_plain_enum_missing_state_attr.rs +++ b/statum-macros/tests/ui/invalid_machine_plain_enum_missing_state_attr.rs @@ -1,7 +1,11 @@ #![allow(unused_imports)] extern crate self as statum; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; pub use statum_core::{ - CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState, + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, }; use statum_macros::machine; diff --git a/statum-macros/tests/ui/invalid_machine_plain_enum_missing_state_attr.stderr b/statum-macros/tests/ui/invalid_machine_plain_enum_missing_state_attr.stderr index 57f605f..fa497be 100644 --- a/statum-macros/tests/ui/invalid_machine_plain_enum_missing_state_attr.stderr +++ b/statum-macros/tests/ui/invalid_machine_plain_enum_missing_state_attr.stderr @@ -5,9 +5,9 @@ error: Failed to resolve the #[state] enum for machine `WorkflowMachine`. No `#[state]` enums were found in that module. Help: make sure the machine's first generic names the right `#[state]` enum in this module. Correct shape: `struct WorkflowMachine { ... }` where `ExpectedState` is a `#[state]` enum in `invalid_machine_plain_enum_missing_state_attr`. - --> tests/ui/invalid_machine_plain_enum_missing_state_attr.rs:14:1 + --> tests/ui/invalid_machine_plain_enum_missing_state_attr.rs:18:1 | -14 | #[machine] +18 | #[machine] | ^^^^^^^^^^ | = note: this error originates in the attribute macro `machine` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/statum-macros/tests/ui/invalid_machine_private_field_access.rs b/statum-macros/tests/ui/invalid_machine_private_field_access.rs index 562695a..b7cb293 100644 --- a/statum-macros/tests/ui/invalid_machine_private_field_access.rs +++ b/statum-macros/tests/ui/invalid_machine_private_field_access.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state}; diff --git a/statum-macros/tests/ui/invalid_machine_private_field_access.stderr b/statum-macros/tests/ui/invalid_machine_private_field_access.stderr index f3cedca..e50fe07 100644 --- a/statum-macros/tests/ui/invalid_machine_private_field_access.stderr +++ b/statum-macros/tests/ui/invalid_machine_private_field_access.stderr @@ -1,5 +1,5 @@ error[E0616]: field `secret` of struct `demo::LightSwitch` is private - --> tests/ui/invalid_machine_private_field_access.rs:30:19 + --> tests/ui/invalid_machine_private_field_access.rs:36:19 | -30 | let _ = light.secret; +36 | let _ = light.secret; | ^^^^^^ private field diff --git a/statum-macros/tests/ui/invalid_machine_wrong_generic.rs b/statum-macros/tests/ui/invalid_machine_wrong_generic.rs index fc96564..45775aa 100644 --- a/statum-macros/tests/ui/invalid_machine_wrong_generic.rs +++ b/statum-macros/tests/ui/invalid_machine_wrong_generic.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state}; diff --git a/statum-macros/tests/ui/invalid_machine_wrong_generic.stderr b/statum-macros/tests/ui/invalid_machine_wrong_generic.stderr index f00a98c..26f7e65 100644 --- a/statum-macros/tests/ui/invalid_machine_wrong_generic.stderr +++ b/statum-macros/tests/ui/invalid_machine_wrong_generic.stderr @@ -1,13 +1,13 @@ error: Error: machine `Machine` uses `S : Clone` as its state generic, but the `#[state]` enum in this module is `MachineState`. Fix: declare `Machine`. Found: `struct Machine< S : Clone > { ... }`. - --> tests/ui/invalid_machine_wrong_generic.rs:14:16 + --> tests/ui/invalid_machine_wrong_generic.rs:20:16 | -14 | struct Machine { +20 | struct Machine { | ^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_machine_wrong_generic.rs:16:2 + --> tests/ui/invalid_machine_wrong_generic.rs:22:2 | -16 | } +22 | } | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_machine_wrong_generic.rs` diff --git a/statum-macros/tests/ui/invalid_state_empty_enum.rs b/statum-macros/tests/ui/invalid_state_empty_enum.rs index 39e2054..aa25cda 100644 --- a/statum-macros/tests/ui/invalid_state_empty_enum.rs +++ b/statum-macros/tests/ui/invalid_state_empty_enum.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::state; #[state] diff --git a/statum-macros/tests/ui/invalid_state_empty_enum.stderr b/statum-macros/tests/ui/invalid_state_empty_enum.stderr index e83335d..7ed48b2 100644 --- a/statum-macros/tests/ui/invalid_state_empty_enum.stderr +++ b/statum-macros/tests/ui/invalid_state_empty_enum.stderr @@ -1,12 +1,12 @@ error: Error: #[state] enum `EmptyState` must declare at least one variant. Fix: add unit variants like `Draft` or single-payload variants like `InReview(ReviewData)`. - --> tests/ui/invalid_state_empty_enum.rs:7:6 - | -7 | enum EmptyState {} - | ^^^^^^^^^^ + --> tests/ui/invalid_state_empty_enum.rs:13:6 + | +13 | enum EmptyState {} + | ^^^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_state_empty_enum.rs:7:19 - | -7 | enum EmptyState {} - | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_state_empty_enum.rs` + --> tests/ui/invalid_state_empty_enum.rs:13:19 + | +13 | enum EmptyState {} + | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_state_empty_enum.rs` diff --git a/statum-macros/tests/ui/invalid_state_not_enum.rs b/statum-macros/tests/ui/invalid_state_not_enum.rs index f4e403c..5b827d6 100644 --- a/statum-macros/tests/ui/invalid_state_not_enum.rs +++ b/statum-macros/tests/ui/invalid_state_not_enum.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::state; #[state] diff --git a/statum-macros/tests/ui/invalid_state_not_enum.stderr b/statum-macros/tests/ui/invalid_state_not_enum.stderr index 0b765f8..7e1f2a4 100644 --- a/statum-macros/tests/ui/invalid_state_not_enum.stderr +++ b/statum-macros/tests/ui/invalid_state_not_enum.stderr @@ -1,12 +1,12 @@ error: Error: #[state] must be applied to an enum, but `NotAnEnum` is a struct. Fix: declare `enum NotAnEnum { ... }` with unit variants like `Draft` or single-payload variants like `InReview(ReviewData)`, or remove `#[state]`. - --> tests/ui/invalid_state_not_enum.rs:7:8 - | -7 | struct NotAnEnum { - | ^^^^^^^^^ + --> tests/ui/invalid_state_not_enum.rs:13:8 + | +13 | struct NotAnEnum { + | ^^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_state_not_enum.rs:9:2 - | -9 | } - | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_state_not_enum.rs` + --> tests/ui/invalid_state_not_enum.rs:15:2 + | +15 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_state_not_enum.rs` diff --git a/statum-macros/tests/ui/invalid_state_struct_variant.rs b/statum-macros/tests/ui/invalid_state_struct_variant.rs index d410f0f..90422cd 100644 --- a/statum-macros/tests/ui/invalid_state_struct_variant.rs +++ b/statum-macros/tests/ui/invalid_state_struct_variant.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::state; #[state] diff --git a/statum-macros/tests/ui/invalid_state_struct_variant.stderr b/statum-macros/tests/ui/invalid_state_struct_variant.stderr index 61b358e..096621b 100644 --- a/statum-macros/tests/ui/invalid_state_struct_variant.stderr +++ b/statum-macros/tests/ui/invalid_state_struct_variant.stderr @@ -1,12 +1,12 @@ error: Error: #[state] enum `BadState` variant `Draft` uses named fields, but Statum state variants must be unit variants like `Draft` or single-payload tuple variants like `Draft(DraftData)`. Fix: move the named fields into a payload type and reference that type as the only tuple field. - --> tests/ui/invalid_state_struct_variant.rs:8:5 - | -8 | Draft { version: u32 }, - | ^^^^^^^^^^^^^^^^^^^^^^ + --> tests/ui/invalid_state_struct_variant.rs:14:5 + | +14 | Draft { version: u32 }, + | ^^^^^^^^^^^^^^^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_state_struct_variant.rs:9:2 - | -9 | } - | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_state_struct_variant.rs` + --> tests/ui/invalid_state_struct_variant.rs:15:2 + | +15 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_state_struct_variant.rs` diff --git a/statum-macros/tests/ui/invalid_state_tuple_variant.rs b/statum-macros/tests/ui/invalid_state_tuple_variant.rs index 952c560..9f23bcb 100644 --- a/statum-macros/tests/ui/invalid_state_tuple_variant.rs +++ b/statum-macros/tests/ui/invalid_state_tuple_variant.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::state; #[state] diff --git a/statum-macros/tests/ui/invalid_state_tuple_variant.stderr b/statum-macros/tests/ui/invalid_state_tuple_variant.stderr index 2abb225..63a4568 100644 --- a/statum-macros/tests/ui/invalid_state_tuple_variant.stderr +++ b/statum-macros/tests/ui/invalid_state_tuple_variant.stderr @@ -1,12 +1,12 @@ error: Error: #[state] enum `BadState` variant `Draft` carries 2 fields, but Statum supports at most one payload type per state. Fix: wrap those fields in a separate payload type and use `Draft(DraftData)`. - --> tests/ui/invalid_state_tuple_variant.rs:8:5 - | -8 | Draft(u32, u32), - | ^^^^^^^^^^^^^^^ + --> tests/ui/invalid_state_tuple_variant.rs:14:5 + | +14 | Draft(u32, u32), + | ^^^^^^^^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_state_tuple_variant.rs:9:2 - | -9 | } - | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_state_tuple_variant.rs` + --> tests/ui/invalid_state_tuple_variant.rs:15:2 + | +15 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_state_tuple_variant.rs` diff --git a/statum-macros/tests/ui/invalid_state_with_generics.rs b/statum-macros/tests/ui/invalid_state_with_generics.rs index db46aa3..0695b71 100644 --- a/statum-macros/tests/ui/invalid_state_with_generics.rs +++ b/statum-macros/tests/ui/invalid_state_with_generics.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::state; #[state] diff --git a/statum-macros/tests/ui/invalid_state_with_generics.stderr b/statum-macros/tests/ui/invalid_state_with_generics.stderr index 3fd7017..44bb307 100644 --- a/statum-macros/tests/ui/invalid_state_with_generics.stderr +++ b/statum-macros/tests/ui/invalid_state_with_generics.stderr @@ -1,13 +1,13 @@ error: Error: #[state] enum `GenericState` cannot declare generics. Fix: keep `GenericState` non-generic and move generic data into payload types. Found: `enum GenericState< 'a, T > { ... }`. - --> tests/ui/invalid_state_with_generics.rs:7:18 - | -7 | enum GenericState<'a, T> { - | ^^^^^^^ + --> tests/ui/invalid_state_with_generics.rs:13:18 + | +13 | enum GenericState<'a, T> { + | ^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_state_with_generics.rs:10:2 + --> tests/ui/invalid_state_with_generics.rs:16:2 | -10 | } +16 | } | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_state_with_generics.rs` diff --git a/statum-macros/tests/ui/invalid_transition_conditional.rs b/statum-macros/tests/ui/invalid_transition_conditional.rs index 3fad516..1fef311 100644 --- a/statum-macros/tests/ui/invalid_transition_conditional.rs +++ b/statum-macros/tests/ui/invalid_transition_conditional.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, transition}; diff --git a/statum-macros/tests/ui/invalid_transition_conditional.stderr b/statum-macros/tests/ui/invalid_transition_conditional.stderr index a246c9f..419ee22 100644 --- a/statum-macros/tests/ui/invalid_transition_conditional.stderr +++ b/statum-macros/tests/ui/invalid_transition_conditional.stderr @@ -8,13 +8,13 @@ error: Invalid transition return type for `ProcessMachine::decide`: expect Help: return `ProcessMachine` directly, or wrap it in `Option<...>` / `Result<..., E>` and build the next state with `self.transition()` or `self.transition_with(...)`. - --> tests/ui/invalid_transition_conditional.rs:28:35 + --> tests/ui/invalid_transition_conditional.rs:34:35 | -28 | fn decide(self, event: u8) -> Decision { +34 | fn decide(self, event: u8) -> Decision { | ^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_transition_conditional.rs:35:2 + --> tests/ui/invalid_transition_conditional.rs:41:2 | -35 | } +41 | } | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_transition_conditional.rs` diff --git a/statum-macros/tests/ui/invalid_transition_map_undeclared_edge.rs b/statum-macros/tests/ui/invalid_transition_map_undeclared_edge.rs index 6e0db2d..a02072d 100644 --- a/statum-macros/tests/ui/invalid_transition_map_undeclared_edge.rs +++ b/statum-macros/tests/ui/invalid_transition_map_undeclared_edge.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, transition}; diff --git a/statum-macros/tests/ui/invalid_transition_map_undeclared_edge.stderr b/statum-macros/tests/ui/invalid_transition_map_undeclared_edge.stderr index ca838ac..2ba9d55 100644 --- a/statum-macros/tests/ui/invalid_transition_map_undeclared_edge.stderr +++ b/statum-macros/tests/ui/invalid_transition_map_undeclared_edge.stderr @@ -1,14 +1,14 @@ error[E0277]: the trait bound `WorkflowMachine: CanTransitionMap<_>` is not satisfied - --> tests/ui/invalid_transition_map_undeclared_edge.rs:53:20 + --> tests/ui/invalid_transition_map_undeclared_edge.rs:59:20 | -53 | let _ = review.transition_map(|data| data); +59 | let _ = review.transition_map(|data| data); | ^^^^^^^^^^^^^^ unsatisfied trait bound | help: the trait `CanTransitionMap<_>` is not implemented for `WorkflowMachine` but trait `CanTransitionMap` is implemented for `WorkflowMachine` - --> tests/ui/invalid_transition_map_undeclared_edge.rs:28:1 + --> tests/ui/invalid_transition_map_undeclared_edge.rs:34:1 | -28 | #[transition] +34 | #[transition] | ^^^^^^^^^^^^^ = help: for that trait implementation, expected `Draft`, found `Review` = note: this error originates in the attribute macro `transition` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/statum-macros/tests/ui/invalid_transition_no_methods.rs b/statum-macros/tests/ui/invalid_transition_no_methods.rs index d4431fc..47b446d 100644 --- a/statum-macros/tests/ui/invalid_transition_no_methods.rs +++ b/statum-macros/tests/ui/invalid_transition_no_methods.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, transition}; diff --git a/statum-macros/tests/ui/invalid_transition_no_methods.stderr b/statum-macros/tests/ui/invalid_transition_no_methods.stderr index eb4797c..95e0453 100644 --- a/statum-macros/tests/ui/invalid_transition_no_methods.stderr +++ b/statum-macros/tests/ui/invalid_transition_no_methods.stderr @@ -1,11 +1,11 @@ error: Error: #[transition] impl for `Machine` must contain at least one method returning `Machine` or a supported wrapper like `Option>` / `Result, E>`. - --> tests/ui/invalid_transition_no_methods.rs:18:6 + --> tests/ui/invalid_transition_no_methods.rs:24:6 | -18 | impl Machine {} +24 | impl Machine {} | ^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_transition_no_methods.rs:18:19 + --> tests/ui/invalid_transition_no_methods.rs:24:19 | -18 | impl Machine {} +24 | impl Machine {} | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_transition_no_methods.rs` diff --git a/statum-macros/tests/ui/invalid_transition_not_method.rs b/statum-macros/tests/ui/invalid_transition_not_method.rs index d8d18e8..79a02e4 100644 --- a/statum-macros/tests/ui/invalid_transition_not_method.rs +++ b/statum-macros/tests/ui/invalid_transition_not_method.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, transition}; diff --git a/statum-macros/tests/ui/invalid_transition_not_method.stderr b/statum-macros/tests/ui/invalid_transition_not_method.stderr index 389ada2..4e8aafa 100644 --- a/statum-macros/tests/ui/invalid_transition_not_method.stderr +++ b/statum-macros/tests/ui/invalid_transition_not_method.stderr @@ -1,11 +1,11 @@ error: Error: `#[transition]` method `Machine::to_b` must take `self` or `mut self` as its receiver. - --> tests/ui/invalid_transition_not_method.rs:19:5 + --> tests/ui/invalid_transition_not_method.rs:25:5 | -19 | fn to_b(_value: u64) -> Machine { +25 | fn to_b(_value: u64) -> Machine { | ^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_transition_not_method.rs:22:2 + --> tests/ui/invalid_transition_not_method.rs:28:2 | -22 | } +28 | } | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_transition_not_method.rs` diff --git a/statum-macros/tests/ui/invalid_transition_plain_struct_machine_name.rs b/statum-macros/tests/ui/invalid_transition_plain_struct_machine_name.rs index a951c32..8858ffb 100644 --- a/statum-macros/tests/ui/invalid_transition_plain_struct_machine_name.rs +++ b/statum-macros/tests/ui/invalid_transition_plain_struct_machine_name.rs @@ -1,7 +1,11 @@ #![allow(unused_imports)] extern crate self as statum; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; pub use statum_core::{ - CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState, + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, }; use statum_macros::{state, transition}; diff --git a/statum-macros/tests/ui/invalid_transition_plain_struct_machine_name.stderr b/statum-macros/tests/ui/invalid_transition_plain_struct_machine_name.stderr index 0526cc3..63ff2f8 100644 --- a/statum-macros/tests/ui/invalid_transition_plain_struct_machine_name.stderr +++ b/statum-macros/tests/ui/invalid_transition_plain_struct_machine_name.stderr @@ -1,10 +1,10 @@ error: Error: no `#[machine]` named `Machine` was found in module `invalid_transition_plain_struct_machine_name`. - A struct named `Machine` exists on line 18, but it is not annotated with `#[machine]`. + A struct named `Machine` exists on line 20, but it is not annotated with `#[machine]`. No same-named `#[machine]` items were found in other modules of this file. No `#[machine]` items were found in this module. Help: apply `#[transition]` to an impl for the machine type generated by `#[machine]` in this module. Correct shape: `#[transition] impl Machine { ... }` where `Machine` is declared with `#[machine]` in `invalid_transition_plain_struct_machine_name`. - --> tests/ui/invalid_transition_plain_struct_machine_name.rs:19:6 + --> tests/ui/invalid_transition_plain_struct_machine_name.rs:23:6 | -19 | impl Machine { +23 | impl Machine { | ^^^^^^^ diff --git a/statum-macros/tests/ui/invalid_transition_unknown_machine.rs b/statum-macros/tests/ui/invalid_transition_unknown_machine.rs index 0872899..87133c2 100644 --- a/statum-macros/tests/ui/invalid_transition_unknown_machine.rs +++ b/statum-macros/tests/ui/invalid_transition_unknown_machine.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, transition}; diff --git a/statum-macros/tests/ui/invalid_transition_unknown_machine.stderr b/statum-macros/tests/ui/invalid_transition_unknown_machine.stderr index 91f63aa..bcc6086 100644 --- a/statum-macros/tests/ui/invalid_transition_unknown_machine.stderr +++ b/statum-macros/tests/ui/invalid_transition_unknown_machine.stderr @@ -1,10 +1,10 @@ error: Error: no `#[machine]` named `DoesNotExist` was found in module `invalid_transition_unknown_machine`. - A struct named `DoesNotExist` exists on line 19, but it is not annotated with `#[machine]`. + A struct named `DoesNotExist` exists on line 23, but it is not annotated with `#[machine]`. No same-named `#[machine]` items were found in other modules of this file. - Available `#[machine]` items in this module: `Machine` in `invalid_transition_unknown_machine` (line 19). + Available `#[machine]` items in this module: `Machine` in `invalid_transition_unknown_machine` (line 21). Help: apply `#[transition]` to an impl for the machine type generated by `#[machine]` in this module. Correct shape: `#[transition] impl Machine { ... }` where `Machine` is declared with `#[machine]` in `invalid_transition_unknown_machine`. - --> tests/ui/invalid_transition_unknown_machine.rs:20:6 + --> tests/ui/invalid_transition_unknown_machine.rs:26:6 | -20 | impl DoesNotExist { +26 | impl DoesNotExist { | ^^^^^^^^^^^^ diff --git a/statum-macros/tests/ui/invalid_transition_unknown_return_state.rs b/statum-macros/tests/ui/invalid_transition_unknown_return_state.rs index 20ebd23..d451a2d 100644 --- a/statum-macros/tests/ui/invalid_transition_unknown_return_state.rs +++ b/statum-macros/tests/ui/invalid_transition_unknown_return_state.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, transition}; diff --git a/statum-macros/tests/ui/invalid_transition_unknown_return_state.stderr b/statum-macros/tests/ui/invalid_transition_unknown_return_state.stderr index 77cfc5a..ac71e1b 100644 --- a/statum-macros/tests/ui/invalid_transition_unknown_return_state.stderr +++ b/statum-macros/tests/ui/invalid_transition_unknown_return_state.stderr @@ -1,7 +1,7 @@ error: Error: transition method `to_ghost` returns state `Ghost`, but `Ghost` is not a variant of `#[state]` enum `State`. Valid next states for `Machine` are: A, B. Help: return `Machine` using one of those variants, or call `self.transition()` / `self.transition_with(...)`. - --> tests/ui/invalid_transition_unknown_return_state.rs:21:26 + --> tests/ui/invalid_transition_unknown_return_state.rs:27:26 | -21 | fn to_ghost(self) -> Machine { +27 | fn to_ghost(self) -> Machine { | ^^^^^^^ diff --git a/statum-macros/tests/ui/invalid_transition_unknown_secondary_return_state.rs b/statum-macros/tests/ui/invalid_transition_unknown_secondary_return_state.rs new file mode 100644 index 0000000..abfa343 --- /dev/null +++ b/statum-macros/tests/ui/invalid_transition_unknown_secondary_return_state.rs @@ -0,0 +1,41 @@ +#![allow(unused_imports)] +extern crate self as statum; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; + +use statum_macros::{machine, state, transition}; + +#[state] +enum State { + A, + B, +} + +#[machine] +struct Machine {} + +#[transition] +impl Machine { + fn to_b_or_ghost(self) -> Result, Machine> { + if true { + Ok(self.to_b()) + } else { + Err(self.to_ghost()) + } + } + + fn to_b(self) -> Machine { + self.transition() + } + + fn to_ghost(self) -> Machine { + self.transition() + } +} + +fn main() {} diff --git a/statum-macros/tests/ui/invalid_transition_unknown_secondary_return_state.stderr b/statum-macros/tests/ui/invalid_transition_unknown_secondary_return_state.stderr new file mode 100644 index 0000000..7b97d57 --- /dev/null +++ b/statum-macros/tests/ui/invalid_transition_unknown_secondary_return_state.stderr @@ -0,0 +1,7 @@ +error: Error: transition method `to_b_or_ghost` returns state `Ghost`, but `Ghost` is not a variant of `#[state]` enum `State`. + Valid next states for `Machine` are: A, B. + Help: return `Machine` using one of those variants, or call `self.transition()` / `self.transition_with(...)`. + --> tests/ui/invalid_transition_unknown_secondary_return_state.rs:24:31 + | +24 | fn to_b_or_ghost(self) -> Result, Machine> { + | ^^^^^^ diff --git a/statum-macros/tests/ui/invalid_transition_unknown_source_state.rs b/statum-macros/tests/ui/invalid_transition_unknown_source_state.rs index f4263d3..51133ad 100644 --- a/statum-macros/tests/ui/invalid_transition_unknown_source_state.rs +++ b/statum-macros/tests/ui/invalid_transition_unknown_source_state.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, transition}; diff --git a/statum-macros/tests/ui/invalid_transition_unknown_source_state.stderr b/statum-macros/tests/ui/invalid_transition_unknown_source_state.stderr index 061d2dd..25cefcd 100644 --- a/statum-macros/tests/ui/invalid_transition_unknown_source_state.stderr +++ b/statum-macros/tests/ui/invalid_transition_unknown_source_state.stderr @@ -1,7 +1,7 @@ error: Error: source state `Ghost` in `#[transition]` target `Machine` is not a variant of `#[state]` enum `State`. Valid states for `Machine` are: A, B. Help: change the impl target to `impl Machine` using one of those variants. - --> tests/ui/invalid_transition_unknown_source_state.rs:20:14 + --> tests/ui/invalid_transition_unknown_source_state.rs:26:14 | -20 | impl Machine { +26 | impl Machine { | ^^^^^ diff --git a/statum-macros/tests/ui/invalid_transition_wrong_return.rs b/statum-macros/tests/ui/invalid_transition_wrong_return.rs index c1f42a3..55a800e 100644 --- a/statum-macros/tests/ui/invalid_transition_wrong_return.rs +++ b/statum-macros/tests/ui/invalid_transition_wrong_return.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, transition}; diff --git a/statum-macros/tests/ui/invalid_transition_wrong_return.stderr b/statum-macros/tests/ui/invalid_transition_wrong_return.stderr index b424460..b61b370 100644 --- a/statum-macros/tests/ui/invalid_transition_wrong_return.stderr +++ b/statum-macros/tests/ui/invalid_transition_wrong_return.stderr @@ -8,13 +8,13 @@ error: Invalid transition return type for `Machine::to_b`: expected return ty Help: return `Machine` directly, or wrap it in `Option<...>` / `Result<..., E>` and build the next state with `self.transition()` or `self.transition_with(...)`. - --> tests/ui/invalid_transition_wrong_return.rs:19:22 + --> tests/ui/invalid_transition_wrong_return.rs:25:22 | -19 | fn to_b(self) -> u64 { +25 | fn to_b(self) -> u64 { | ^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_transition_wrong_return.rs:22:2 + --> tests/ui/invalid_transition_wrong_return.rs:28:2 | -22 | } +28 | } | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_transition_wrong_return.rs` diff --git a/statum-macros/tests/ui/invalid_validators_alias_wrong_payload.rs b/statum-macros/tests/ui/invalid_validators_alias_wrong_payload.rs index fcdc4db..68ae828 100644 --- a/statum-macros/tests/ui/invalid_validators_alias_wrong_payload.rs +++ b/statum-macros/tests/ui/invalid_validators_alias_wrong_payload.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; pub type Result = core::result::Result; diff --git a/statum-macros/tests/ui/invalid_validators_alias_wrong_payload.stderr b/statum-macros/tests/ui/invalid_validators_alias_wrong_payload.stderr index 3b152ca..99aa4b9 100644 --- a/statum-macros/tests/ui/invalid_validators_alias_wrong_payload.stderr +++ b/statum-macros/tests/ui/invalid_validators_alias_wrong_payload.stderr @@ -1,11 +1,11 @@ error: Error: validator `is_in_progress` for `impl DbRow` rebuilding `TaskMachine` state `TaskState::InProgress` must return `Result` (or an equivalent alias), but found `statum :: Result < () >` with payload `()`. - --> tests/ui/invalid_validators_alias_wrong_payload.rs:40:33 + --> tests/ui/invalid_validators_alias_wrong_payload.rs:46:33 | -40 | fn is_in_progress(&self) -> statum::Result<()> { +46 | fn is_in_progress(&self) -> statum::Result<()> { | ^^^^^^^^^^^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_validators_alias_wrong_payload.rs:48:2 + --> tests/ui/invalid_validators_alias_wrong_payload.rs:54:2 | -48 | } +54 | } | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_validators_alias_wrong_payload.rs` diff --git a/statum-macros/tests/ui/invalid_validators_missing_variant.rs b/statum-macros/tests/ui/invalid_validators_missing_variant.rs index 343a5fa..9707678 100644 --- a/statum-macros/tests/ui/invalid_validators_missing_variant.rs +++ b/statum-macros/tests/ui/invalid_validators_missing_variant.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, validators}; diff --git a/statum-macros/tests/ui/invalid_validators_missing_variant.stderr b/statum-macros/tests/ui/invalid_validators_missing_variant.stderr index c40ba76..604c828 100644 --- a/statum-macros/tests/ui/invalid_validators_missing_variant.stderr +++ b/statum-macros/tests/ui/invalid_validators_missing_variant.stderr @@ -1,14 +1,14 @@ error: Error: `#[validators(TaskMachine)]` on `impl DbRow` is missing validator methods for `TaskState`: is_done. Fix: add one validator per state variant (snake_case), e.g. `fn is_draft(&self) -> Result<()>`. - --> tests/ui/invalid_validators_missing_variant.rs:23:1 + --> tests/ui/invalid_validators_missing_variant.rs:29:1 | -23 | #[validators(TaskMachine)] +29 | #[validators(TaskMachine)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the attribute macro `validators` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_validators_missing_variant.rs:33:2 + --> tests/ui/invalid_validators_missing_variant.rs:39:2 | -33 | } +39 | } | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_validators_missing_variant.rs` diff --git a/statum-macros/tests/ui/invalid_validators_no_methods.rs b/statum-macros/tests/ui/invalid_validators_no_methods.rs index 9fcac88..0856ac2 100644 --- a/statum-macros/tests/ui/invalid_validators_no_methods.rs +++ b/statum-macros/tests/ui/invalid_validators_no_methods.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, validators}; diff --git a/statum-macros/tests/ui/invalid_validators_no_methods.stderr b/statum-macros/tests/ui/invalid_validators_no_methods.stderr index 5f84004..f157141 100644 --- a/statum-macros/tests/ui/invalid_validators_no_methods.stderr +++ b/statum-macros/tests/ui/invalid_validators_no_methods.stderr @@ -1,14 +1,14 @@ error: Error: `#[validators(TaskMachine)]` on `impl DbRow` must define at least one validator method. Expected one method per `TaskState` variant: is_draft. - --> tests/ui/invalid_validators_no_methods.rs:22:1 + --> tests/ui/invalid_validators_no_methods.rs:28:1 | -22 | #[validators(TaskMachine)] +28 | #[validators(TaskMachine)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the attribute macro `validators` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_validators_no_methods.rs:23:14 + --> tests/ui/invalid_validators_no_methods.rs:29:14 | -23 | impl DbRow {} +29 | impl DbRow {} | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_validators_no_methods.rs` diff --git a/statum-macros/tests/ui/invalid_validators_parameter_name_collision.rs b/statum-macros/tests/ui/invalid_validators_parameter_name_collision.rs index a4718a0..e94234c 100644 --- a/statum-macros/tests/ui/invalid_validators_parameter_name_collision.rs +++ b/statum-macros/tests/ui/invalid_validators_parameter_name_collision.rs @@ -1,7 +1,11 @@ #![allow(unused_imports)] extern crate self as statum; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; pub use statum_core::{ - CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState, + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, }; use statum_macros::{machine, state, validators}; diff --git a/statum-macros/tests/ui/invalid_validators_parameter_name_collision.stderr b/statum-macros/tests/ui/invalid_validators_parameter_name_collision.stderr index 8020ca8..c831c72 100644 --- a/statum-macros/tests/ui/invalid_validators_parameter_name_collision.stderr +++ b/statum-macros/tests/ui/invalid_validators_parameter_name_collision.stderr @@ -3,7 +3,7 @@ error: Error: validator `is_draft` for `impl Row` rebuilding `TaskMachine` state Parameter name collision: `name` collides with injected machine field binding. Machine `TaskMachine` injects these fields by bare name inside validator bodies: `name`. Remove explicit parameters and use those bindings directly. Correct shape: `fn is_draft(&self) -> Result<(), _>`. - --> tests/ui/invalid_validators_parameter_name_collision.rs:26:24 + --> tests/ui/invalid_validators_parameter_name_collision.rs:30:24 | -26 | fn is_draft(&self, name: &str) -> Result<(), statum_core::Error> { +30 | fn is_draft(&self, name: &str) -> Result<(), statum_core::Error> { | ^^^^^^^^^^ diff --git a/statum-macros/tests/ui/invalid_validators_plain_struct_machine_name.rs b/statum-macros/tests/ui/invalid_validators_plain_struct_machine_name.rs index e896ecc..001c1a1 100644 --- a/statum-macros/tests/ui/invalid_validators_plain_struct_machine_name.rs +++ b/statum-macros/tests/ui/invalid_validators_plain_struct_machine_name.rs @@ -1,7 +1,11 @@ #![allow(unused_imports)] extern crate self as statum; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; pub use statum_core::{ - CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState, + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, }; use statum_macros::{state, validators}; diff --git a/statum-macros/tests/ui/invalid_validators_plain_struct_machine_name.stderr b/statum-macros/tests/ui/invalid_validators_plain_struct_machine_name.stderr index 2c534a3..e0ad418 100644 --- a/statum-macros/tests/ui/invalid_validators_plain_struct_machine_name.stderr +++ b/statum-macros/tests/ui/invalid_validators_plain_struct_machine_name.stderr @@ -1,12 +1,12 @@ error: Error: no `#[machine]` named `TaskMachine` was found in module `invalid_validators_plain_struct_machine_name`. - A struct named `TaskMachine` exists on line 24, but it is not annotated with `#[machine]`. + A struct named `TaskMachine` exists on line 19, but it is not annotated with `#[machine]`. No same-named `#[machine]` items were found in other modules of this file. No `#[machine]` items were found in this module. Help: point `#[validators(...)]` at the Statum machine type in this module. Correct shape: `#[validators(TaskMachine)] impl PersistedRow { ... }` where `TaskMachine` is declared with `#[machine]` in `invalid_validators_plain_struct_machine_name`. - --> tests/ui/invalid_validators_plain_struct_machine_name.rs:24:1 + --> tests/ui/invalid_validators_plain_struct_machine_name.rs:28:1 | -24 | #[validators(TaskMachine)] +28 | #[validators(TaskMachine)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the attribute macro `validators` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/statum-macros/tests/ui/invalid_validators_unknown_machine.rs b/statum-macros/tests/ui/invalid_validators_unknown_machine.rs index 4480501..299f0c8 100644 --- a/statum-macros/tests/ui/invalid_validators_unknown_machine.rs +++ b/statum-macros/tests/ui/invalid_validators_unknown_machine.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, validators}; diff --git a/statum-macros/tests/ui/invalid_validators_unknown_machine.stderr b/statum-macros/tests/ui/invalid_validators_unknown_machine.stderr index 1cad475..6d342b9 100644 --- a/statum-macros/tests/ui/invalid_validators_unknown_machine.stderr +++ b/statum-macros/tests/ui/invalid_validators_unknown_machine.stderr @@ -1,12 +1,12 @@ error: Error: no `#[machine]` named `DoesNotExist` was found in module `invalid_validators_unknown_machine`. No plain struct with that name was found in this module either. No same-named `#[machine]` items were found in other modules of this file. - Available `#[machine]` items in this module: `TaskMachine` in `invalid_validators_unknown_machine` (line 22). + Available `#[machine]` items in this module: `TaskMachine` in `invalid_validators_unknown_machine` (line 20). Help: point `#[validators(...)]` at the Statum machine type in this module. Correct shape: `#[validators(TaskMachine)] impl PersistedRow { ... }` where `TaskMachine` is declared with `#[machine]` in `invalid_validators_unknown_machine`. - --> tests/ui/invalid_validators_unknown_machine.rs:22:1 + --> tests/ui/invalid_validators_unknown_machine.rs:28:1 | -22 | #[validators(DoesNotExist)] +28 | #[validators(DoesNotExist)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the attribute macro `validators` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/statum-macros/tests/ui/invalid_validators_unknown_state_method.rs b/statum-macros/tests/ui/invalid_validators_unknown_state_method.rs index 799ca5f..1c74211 100644 --- a/statum-macros/tests/ui/invalid_validators_unknown_state_method.rs +++ b/statum-macros/tests/ui/invalid_validators_unknown_state_method.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, validators}; diff --git a/statum-macros/tests/ui/invalid_validators_unknown_state_method.stderr b/statum-macros/tests/ui/invalid_validators_unknown_state_method.stderr index 1ffa51d..c45fd98 100644 --- a/statum-macros/tests/ui/invalid_validators_unknown_state_method.stderr +++ b/statum-macros/tests/ui/invalid_validators_unknown_state_method.stderr @@ -1,14 +1,14 @@ error: Error: `#[validators(TaskMachine)]` on `impl DbRow` defines methods that do not match any variant in `TaskState`: is_archived. Valid validator methods for `TaskMachine` are: is_draft, is_done. - --> tests/ui/invalid_validators_unknown_state_method.rs:23:1 + --> tests/ui/invalid_validators_unknown_state_method.rs:29:1 | -23 | #[validators(TaskMachine)] +29 | #[validators(TaskMachine)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the attribute macro `validators` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_validators_unknown_state_method.rs:42:2 + --> tests/ui/invalid_validators_unknown_state_method.rs:48:2 | -42 | } +48 | } | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_validators_unknown_state_method.rs` diff --git a/statum-macros/tests/ui/invalid_validators_wrong_receiver.rs b/statum-macros/tests/ui/invalid_validators_wrong_receiver.rs index 231319f..709ab42 100644 --- a/statum-macros/tests/ui/invalid_validators_wrong_receiver.rs +++ b/statum-macros/tests/ui/invalid_validators_wrong_receiver.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, validators}; diff --git a/statum-macros/tests/ui/invalid_validators_wrong_receiver.stderr b/statum-macros/tests/ui/invalid_validators_wrong_receiver.stderr index b56f4c8..48cc7aa 100644 --- a/statum-macros/tests/ui/invalid_validators_wrong_receiver.stderr +++ b/statum-macros/tests/ui/invalid_validators_wrong_receiver.stderr @@ -1,7 +1,7 @@ error: Error: validator `is_draft` for `impl DbRow` rebuilding `TaskMachine` state `TaskState::Draft` must take `&self`, not `self`. Machine `TaskMachine` injects these fields by bare name inside validator bodies: `name`. Remove explicit parameters and use those bindings directly. Correct shape: `fn is_draft(&self) -> Result<(), _>`. - --> tests/ui/invalid_validators_wrong_receiver.rs:24:17 + --> tests/ui/invalid_validators_wrong_receiver.rs:30:17 | -24 | fn is_draft(self) -> Result<(), statum_core::Error> { +30 | fn is_draft(self) -> Result<(), statum_core::Error> { | ^^^^ diff --git a/statum-macros/tests/ui/invalid_validators_wrong_return.rs b/statum-macros/tests/ui/invalid_validators_wrong_return.rs index 8ed9ff7..4b7e78c 100644 --- a/statum-macros/tests/ui/invalid_validators_wrong_return.rs +++ b/statum-macros/tests/ui/invalid_validators_wrong_return.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, validators}; diff --git a/statum-macros/tests/ui/invalid_validators_wrong_return.stderr b/statum-macros/tests/ui/invalid_validators_wrong_return.stderr index c184f82..eae53cd 100644 --- a/statum-macros/tests/ui/invalid_validators_wrong_return.stderr +++ b/statum-macros/tests/ui/invalid_validators_wrong_return.stderr @@ -1,11 +1,11 @@ error: Error: validator `is_in_progress` for `impl DbRow` rebuilding `TaskMachine` state `TaskState::InProgress` must return `Result` (or an equivalent alias), but found `Result < (), statum_core :: Error >` with payload `()`. - --> tests/ui/invalid_validators_wrong_return.rs:38:33 + --> tests/ui/invalid_validators_wrong_return.rs:44:33 | -38 | fn is_in_progress(&self) -> Result<(), statum_core::Error> { +44 | fn is_in_progress(&self) -> Result<(), statum_core::Error> { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_validators_wrong_return.rs:46:2 + --> tests/ui/invalid_validators_wrong_return.rs:52:2 | -46 | } +52 | } | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_validators_wrong_return.rs` diff --git a/statum-macros/tests/ui/invalid_validators_wrong_signature.rs b/statum-macros/tests/ui/invalid_validators_wrong_signature.rs index 9590214..0046a57 100644 --- a/statum-macros/tests/ui/invalid_validators_wrong_signature.rs +++ b/statum-macros/tests/ui/invalid_validators_wrong_signature.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, validators}; diff --git a/statum-macros/tests/ui/invalid_validators_wrong_signature.stderr b/statum-macros/tests/ui/invalid_validators_wrong_signature.stderr index c3caaed..714a563 100644 --- a/statum-macros/tests/ui/invalid_validators_wrong_signature.stderr +++ b/statum-macros/tests/ui/invalid_validators_wrong_signature.stderr @@ -3,13 +3,13 @@ error: Error: validator `is_draft` for `impl DbRow` rebuilding `TaskMachine` sta Validator methods do not accept explicit machine-field parameters. Machine `TaskMachine` injects these fields by bare name inside validator bodies: `name`. Remove explicit parameters and use those bindings directly. Correct shape: `fn is_draft(&self) -> Result<(), _>`. - --> tests/ui/invalid_validators_wrong_signature.rs:24:24 + --> tests/ui/invalid_validators_wrong_signature.rs:30:24 | -24 | fn is_draft(&self, extra: u8) -> Result<(), statum_core::Error> { +30 | fn is_draft(&self, extra: u8) -> Result<(), statum_core::Error> { | ^^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/invalid_validators_wrong_signature.rs:33:2 + --> tests/ui/invalid_validators_wrong_signature.rs:39:2 | -33 | } +39 | } | ^ consider adding a `main` function to `$DIR/tests/ui/invalid_validators_wrong_signature.rs` diff --git a/statum-macros/tests/ui/valid_advanced_traits.rs b/statum-macros/tests/ui/valid_advanced_traits.rs index 13fefaa..8be5c03 100644 --- a/statum-macros/tests/ui/valid_advanced_traits.rs +++ b/statum-macros/tests/ui/valid_advanced_traits.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, transition}; diff --git a/statum-macros/tests/ui/valid_builder_overwrite.rs b/statum-macros/tests/ui/valid_builder_overwrite.rs index 5f5712e..bfe3533 100644 --- a/statum-macros/tests/ui/valid_builder_overwrite.rs +++ b/statum-macros/tests/ui/valid_builder_overwrite.rs @@ -1,7 +1,11 @@ #![allow(unused_imports)] extern crate self as statum; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; pub use statum_core::{ - CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState, + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, }; use statum_macros::{machine, state, validators}; diff --git a/statum-macros/tests/ui/valid_helper_trait_visibility.rs b/statum-macros/tests/ui/valid_helper_trait_visibility.rs index d77e831..b0638a1 100644 --- a/statum-macros/tests/ui/valid_helper_trait_visibility.rs +++ b/statum-macros/tests/ui/valid_helper_trait_visibility.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, transition, validators}; diff --git a/statum-macros/tests/ui/valid_into_machines_by.rs b/statum-macros/tests/ui/valid_into_machines_by.rs index 06f2d68..77887a9 100644 --- a/statum-macros/tests/ui/valid_into_machines_by.rs +++ b/statum-macros/tests/ui/valid_into_machines_by.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, validators}; diff --git a/statum-macros/tests/ui/valid_machine_field_aliases.rs b/statum-macros/tests/ui/valid_machine_field_aliases.rs index 093ea04..284c91e 100644 --- a/statum-macros/tests/ui/valid_machine_field_aliases.rs +++ b/statum-macros/tests/ui/valid_machine_field_aliases.rs @@ -1,7 +1,11 @@ #![allow(unused_imports)] extern crate self as statum; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; pub use statum_core::{ - CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState, + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, }; diff --git a/statum-macros/tests/ui/valid_machine_field_aliases_batch.rs b/statum-macros/tests/ui/valid_machine_field_aliases_batch.rs index 7e5c370..82effa5 100644 --- a/statum-macros/tests/ui/valid_machine_field_aliases_batch.rs +++ b/statum-macros/tests/ui/valid_machine_field_aliases_batch.rs @@ -1,7 +1,11 @@ #![allow(unused_imports)] extern crate self as statum; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; pub use statum_core::{ - CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState, + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, }; diff --git a/statum-macros/tests/ui/valid_machine_field_aliases_local_validators.rs b/statum-macros/tests/ui/valid_machine_field_aliases_local_validators.rs index 222463e..b5f1b68 100644 --- a/statum-macros/tests/ui/valid_machine_field_aliases_local_validators.rs +++ b/statum-macros/tests/ui/valid_machine_field_aliases_local_validators.rs @@ -1,7 +1,11 @@ #![allow(unused_imports)] extern crate self as statum; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; pub use statum_core::{ - CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState, + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, }; diff --git a/statum-macros/tests/ui/valid_machine_field_aliases_renamed_import.rs b/statum-macros/tests/ui/valid_machine_field_aliases_renamed_import.rs index 83954f6..ef785c2 100644 --- a/statum-macros/tests/ui/valid_machine_field_aliases_renamed_import.rs +++ b/statum-macros/tests/ui/valid_machine_field_aliases_renamed_import.rs @@ -1,7 +1,11 @@ #![allow(unused_imports)] extern crate self as statum; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; pub use statum_core::{ - CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState, + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, }; diff --git a/statum-macros/tests/ui/valid_machine_field_module_paths.rs b/statum-macros/tests/ui/valid_machine_field_module_paths.rs index 034463b..dfec7bd 100644 --- a/statum-macros/tests/ui/valid_machine_field_module_paths.rs +++ b/statum-macros/tests/ui/valid_machine_field_module_paths.rs @@ -1,7 +1,11 @@ #![allow(unused_imports)] extern crate self as statum; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; pub use statum_core::{ - CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState, + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, }; diff --git a/statum-macros/tests/ui/valid_machine_introspection.rs b/statum-macros/tests/ui/valid_machine_introspection.rs new file mode 100644 index 0000000..165889c --- /dev/null +++ b/statum-macros/tests/ui/valid_machine_introspection.rs @@ -0,0 +1,84 @@ +#![allow(unused_imports)] +extern crate self as statum; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; + +use statum_macros::{machine, state, transition}; + +#[state] +enum FlowState { + Draft, + Review, + Accepted, + Rejected, + Published, +} + +#[machine] +struct Flow {} + +#[transition] +impl Flow { + fn submit(self) -> Flow { + self.transition() + } +} + +#[transition] +impl Flow { + fn maybe_decide(self) -> Option, Flow>> { + if true { + Some(Ok(self.accept())) + } else { + Some(Err(self.reject())) + } + } + + fn accept(self) -> Flow { + self.transition() + } + + fn reject(self) -> Flow { + self.transition() + } +} + +#[transition] +impl Flow { + fn explain(self) -> Flow { + self.transition() + } +} + +#[transition] +impl Flow { + fn explain(self) -> Flow { + self.transition() + } +} + +fn main() { + let graph = as statum::MachineIntrospection>::GRAPH; + let maybe_decide = graph + .transition_from_method(flow::StateId::Review, "maybe_decide") + .expect("maybe_decide"); + + assert_eq!( + as statum::MachineStateIdentity>::STATE_ID, + flow::StateId::Review + ); + assert_eq!( + maybe_decide.id, + Flow::::MAYBE_DECIDE + ); + assert_eq!( + graph.legal_targets(maybe_decide.id).unwrap(), + &[flow::StateId::Accepted, flow::StateId::Rejected] + ); + assert_eq!(graph.transitions_named("explain").count(), 2); +} diff --git a/statum-macros/tests/ui/valid_machine_introspection_cfg_dedup.rs b/statum-macros/tests/ui/valid_machine_introspection_cfg_dedup.rs new file mode 100644 index 0000000..9566478 --- /dev/null +++ b/statum-macros/tests/ui/valid_machine_introspection_cfg_dedup.rs @@ -0,0 +1,41 @@ +#![allow(unused_imports)] +extern crate self as statum; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; + +use statum_macros::{machine, state, transition}; + +#[state] +enum FlowState { + Draft, + Accepted, + Rejected, +} + +#[machine] +struct Flow {} + +#[transition] +impl Flow { + #[cfg(any())] + fn validate(self) -> Flow { + self.transition() + } + + #[cfg(not(any()))] + fn validate(self) -> Flow { + self.transition() + } +} + +fn main() { + let graph = as statum::MachineIntrospection>::GRAPH; + let _validate = graph + .transition_from_method(flow::StateId::Draft, "validate") + .expect("validate"); +} diff --git a/statum-macros/tests/ui/valid_machine_no_fields.rs b/statum-macros/tests/ui/valid_machine_no_fields.rs index aaebc35..3676599 100644 --- a/statum-macros/tests/ui/valid_machine_no_fields.rs +++ b/statum-macros/tests/ui/valid_machine_no_fields.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state}; diff --git a/statum-macros/tests/ui/valid_machine_state_surface.rs b/statum-macros/tests/ui/valid_machine_state_surface.rs index f26c082..cee0750 100644 --- a/statum-macros/tests/ui/valid_machine_state_surface.rs +++ b/statum-macros/tests/ui/valid_machine_state_surface.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, validators}; diff --git a/statum-macros/tests/ui/valid_matrix.rs b/statum-macros/tests/ui/valid_matrix.rs index bebb1ff..e97c21a 100644 --- a/statum-macros/tests/ui/valid_matrix.rs +++ b/statum-macros/tests/ui/valid_matrix.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, transition, validators}; diff --git a/statum-macros/tests/ui/valid_multiple_machines_same_module.rs b/statum-macros/tests/ui/valid_multiple_machines_same_module.rs index 5a11014..8d20776 100644 --- a/statum-macros/tests/ui/valid_multiple_machines_same_module.rs +++ b/statum-macros/tests/ui/valid_multiple_machines_same_module.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, transition}; diff --git a/statum-macros/tests/ui/valid_same_names_different_modules.rs b/statum-macros/tests/ui/valid_same_names_different_modules.rs index de47f45..4b8205d 100644 --- a/statum-macros/tests/ui/valid_same_names_different_modules.rs +++ b/statum-macros/tests/ui/valid_same_names_different_modules.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; pub use statum_core::Result; diff --git a/statum-macros/tests/ui/valid_state_unit_only.rs b/statum-macros/tests/ui/valid_state_unit_only.rs index a5a1af8..5b4ec50 100644 --- a/statum-macros/tests/ui/valid_state_unit_only.rs +++ b/statum-macros/tests/ui/valid_state_unit_only.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state}; diff --git a/statum-macros/tests/ui/valid_state_with_data.rs b/statum-macros/tests/ui/valid_state_with_data.rs index 356afef..df2bb35 100644 --- a/statum-macros/tests/ui/valid_state_with_data.rs +++ b/statum-macros/tests/ui/valid_state_with_data.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state}; diff --git a/statum-macros/tests/ui/valid_transition_map.rs b/statum-macros/tests/ui/valid_transition_map.rs index cd138e0..1b87adb 100644 --- a/statum-macros/tests/ui/valid_transition_map.rs +++ b/statum-macros/tests/ui/valid_transition_map.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, transition}; diff --git a/statum-macros/tests/ui/valid_transition_nested_wrappers.rs b/statum-macros/tests/ui/valid_transition_nested_wrappers.rs index e68113f..bca2d07 100644 --- a/statum-macros/tests/ui/valid_transition_nested_wrappers.rs +++ b/statum-macros/tests/ui/valid_transition_nested_wrappers.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, transition}; diff --git a/statum-macros/tests/ui/valid_validators_async.rs b/statum-macros/tests/ui/valid_validators_async.rs index 280f94b..e7c050e 100644 --- a/statum-macros/tests/ui/valid_validators_async.rs +++ b/statum-macros/tests/ui/valid_validators_async.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, validators}; diff --git a/statum-macros/tests/ui/valid_validators_generic_payload.rs b/statum-macros/tests/ui/valid_validators_generic_payload.rs index bbbd5f5..e19312d 100644 --- a/statum-macros/tests/ui/valid_validators_generic_payload.rs +++ b/statum-macros/tests/ui/valid_validators_generic_payload.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; pub type Result = core::result::Result; diff --git a/statum-macros/tests/ui/valid_validators_result_aliases.rs b/statum-macros/tests/ui/valid_validators_result_aliases.rs index fe1471d..875ddb4 100644 --- a/statum-macros/tests/ui/valid_validators_result_aliases.rs +++ b/statum-macros/tests/ui/valid_validators_result_aliases.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; pub type Result = core::result::Result; diff --git a/statum-macros/tests/ui/valid_validators_sync.rs b/statum-macros/tests/ui/valid_validators_sync.rs index 424cecd..dddc137 100644 --- a/statum-macros/tests/ui/valid_validators_sync.rs +++ b/statum-macros/tests/ui/valid_validators_sync.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, validators}; diff --git a/statum-macros/tests/ui/valid_visibility_and_reconstruction.rs b/statum-macros/tests/ui/valid_visibility_and_reconstruction.rs index 07e1819..d725604 100644 --- a/statum-macros/tests/ui/valid_visibility_and_reconstruction.rs +++ b/statum-macros/tests/ui/valid_visibility_and_reconstruction.rs @@ -1,6 +1,12 @@ #![allow(unused_imports)] extern crate self as statum; -pub use statum_core::{CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, StateMarker, UnitState}; +pub use statum_core::__private; +pub use statum_core::TransitionInventory; +pub use statum_core::{ + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachineStateIdentity, StateDescriptor, StateMarker, + TransitionDescriptor, UnitState, +}; use statum_macros::{machine, state, validators}; diff --git a/statum/Cargo.toml b/statum/Cargo.toml index 6b7161e..b132633 100644 --- a/statum/Cargo.toml +++ b/statum/Cargo.toml @@ -1,10 +1,10 @@ [dependencies.statum-core] path = "../statum-core" -version = "0.6.5" +version = "0.6.6" [dependencies.statum-macros] path = "../statum-macros" -version = "0.6.5" +version = "0.6.6" [dev-dependencies] futures = "0.3" @@ -31,7 +31,7 @@ name = "statum" readme = "README.md" repository = "https://github.com/eboody/statum" rust-version = "1.93" -version = "0.6.5" +version = "0.6.6" [package.metadata.modum] generic_nouns = [ diff --git a/statum/README.md b/statum/README.md index ba21748..67cea79 100644 --- a/statum/README.md +++ b/statum/README.md @@ -12,13 +12,14 @@ This crate re-exports: - attribute macros: `#[state]`, `#[machine]`, `#[transition]`, `#[validators]` - runtime types: `statum::Error`, `statum::Result` - advanced traits: `StateMarker`, `UnitState`, `DataState`, `CanTransition*` +- typed introspection and runtime-join surfaces: `MachineIntrospection`, `MachineGraph`, `MachineTransitionRecorder`, `MachinePresentation` - projection helpers: `statum::projection` ## Install ```toml [dependencies] -statum = "0.6.3" +statum = "0.6.6" ``` Statum targets stable Rust and currently supports Rust `1.93+`. @@ -63,6 +64,10 @@ impl Light { ## Docs +- Machine introspection is useful when the machine definition should also drive + CLI explainers, graph exports, generated docs, branch-strip views, or runtime + replay/debug tooling. Statum exposes exact transition sites instead of a + coarse machine-wide state list. - API docs: - Repository README: - Coding-agent kit: diff --git a/statum/src/lib.rs b/statum/src/lib.rs index 189fe0b..8b11308 100644 --- a/statum/src/lib.rs +++ b/statum/src/lib.rs @@ -153,6 +153,79 @@ //! let _ = light.switch_off(); // no such method on Light //! ``` //! +//! # Machine Introspection +//! +//! Statum can also expose the static machine structure as typed metadata. +//! This is useful when the same machine definition should drive: +//! +//! - CLI explainers +//! - generated docs +//! - graph exports +//! - exact transition assertions in tests +//! - runtime replay or debug tooling +//! +//! The important detail is that the graph is exact at the transition-site +//! level. A consumer can ask for the legal targets of one specific method on +//! one specific source state. +//! +//! ```rust +//! use statum::{ +//! machine, state, transition, MachineIntrospection, MachineTransitionRecorder, +//! }; +//! +//! #[state] +//! enum FlowState { +//! Fetched, +//! Accepted, +//! Rejected, +//! } +//! +//! #[machine] +//! struct Flow {} +//! +//! #[transition] +//! impl Flow { +//! fn validate(self, accept: bool) -> Result, Flow> { +//! if accept { +//! Ok(self.accept()) +//! } else { +//! Err(self.reject()) +//! } +//! } +//! +//! fn accept(self) -> Flow { +//! self.transition() +//! } +//! +//! fn reject(self) -> Flow { +//! self.transition() +//! } +//! } +//! +//! fn main() { +//! let graph = as MachineIntrospection>::GRAPH; +//! let validate = graph +//! .transition_from_method(flow::StateId::Fetched, "validate") +//! .unwrap(); +//! +//! assert_eq!( +//! graph.legal_targets(validate.id).unwrap(), +//! &[flow::StateId::Accepted, flow::StateId::Rejected] +//! ); +//! +//! let event = as MachineTransitionRecorder>::try_record_transition_to::< +//! Flow, +//! >(Flow::::VALIDATE) +//! .unwrap(); +//! +//! assert_eq!(event.chosen, flow::StateId::Accepted); +//! } +//! ``` +//! +//! Transition ids are exact and typed, but they are exposed as generated +//! associated consts on the source-state machine type, such as +//! `Flow::::VALIDATE`. +//! //! # Where To Look Next //! //! - Start with [`state`](macro@state), [`machine`](macro@machine), and @@ -162,12 +235,17 @@ //! - The repository README and `docs/` directory contain longer guides and //! showcase applications. +#[doc(hidden)] +pub use statum_core::__private; #[doc(inline)] pub use statum_core::projection; #[doc(inline)] pub use statum_core::{ - CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, Result, StateMarker, - UnitState, + CanTransitionMap, CanTransitionTo, CanTransitionWith, DataState, Error, MachineDescriptor, + MachineGraph, MachineIntrospection, MachinePresentation, MachinePresentationDescriptor, + MachineStateIdentity, MachineTransitionRecorder, RecordedTransition, Result, StateDescriptor, + StateMarker, StatePresentation, TransitionDescriptor, TransitionInventory, + TransitionPresentation, UnitState, }; /// Define the legal lifecycle phases for a machine. diff --git a/statum/tests/introspection.rs b/statum/tests/introspection.rs new file mode 100644 index 0000000..7007e6f --- /dev/null +++ b/statum/tests/introspection.rs @@ -0,0 +1,350 @@ +#![allow(dead_code)] + +use statum::{ + machine, state, transition, MachineIntrospection, MachinePresentation, + MachinePresentationDescriptor, MachineStateIdentity, MachineTransitionRecorder, + StatePresentation, TransitionPresentation, +}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum Phase { + Intake, + Review, + Decision, + Output, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct FlowMachineMeta { + phase: Phase, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct FlowStateMeta { + phase: Phase, + source_term: &'static str, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct FlowTransitionMeta { + phase: Phase, + branching: bool, +} + +#[state] +enum SharedState { + Draft, + Review, + Accepted, + Rejected, + Published, +} + +#[machine] +struct Flow {} + +#[transition] +impl Flow { + fn submit(self) -> Flow { + self.transition() + } +} + +#[transition] +impl Flow { + fn maybe_decide(self) -> Option, Flow>> { + if true { + Some(Ok(self.accept())) + } else { + Some(Err(self.reject())) + } + } + + fn accept(self) -> Flow { + self.transition() + } + + fn reject(self) -> Flow { + self.transition() + } +} + +#[transition] +impl Flow { + fn explain(self) -> Flow { + self.transition() + } +} + +#[transition] +impl Flow { + fn explain(self) -> Flow { + self.transition() + } +} + +#[machine] +struct AlphaMachine {} + +#[transition] +impl AlphaMachine { + fn finish(self) -> AlphaMachine { + self.transition() + } +} + +#[machine] +struct BetaMachine {} + +#[transition] +impl BetaMachine { + fn finish(self) -> BetaMachine { + self.transition() + } +} + +static FLOW_PRESENTATION: MachinePresentation< + flow::StateId, + flow::TransitionId, + FlowMachineMeta, + FlowStateMeta, + FlowTransitionMeta, +> = MachinePresentation { + machine: Some(MachinePresentationDescriptor { + label: Some("Flow"), + description: Some("Example consumer-owned presentation metadata."), + metadata: FlowMachineMeta { + phase: Phase::Intake, + }, + }), + states: &[ + StatePresentation { + id: flow::StateId::Draft, + label: Some("Draft"), + description: Some("Initial work before submission."), + metadata: FlowStateMeta { + phase: Phase::Intake, + source_term: "draft", + }, + }, + StatePresentation { + id: flow::StateId::Review, + label: Some("Review"), + description: Some("Work is under review."), + metadata: FlowStateMeta { + phase: Phase::Review, + source_term: "review", + }, + }, + StatePresentation { + id: flow::StateId::Accepted, + label: Some("Accepted"), + description: Some("Review approved the work."), + metadata: FlowStateMeta { + phase: Phase::Decision, + source_term: "accepted", + }, + }, + StatePresentation { + id: flow::StateId::Rejected, + label: Some("Rejected"), + description: Some("Review rejected the work."), + metadata: FlowStateMeta { + phase: Phase::Decision, + source_term: "rejected", + }, + }, + StatePresentation { + id: flow::StateId::Published, + label: Some("Published"), + description: Some("Work has been published."), + metadata: FlowStateMeta { + phase: Phase::Output, + source_term: "published", + }, + }, + ], + transitions: &[ + TransitionPresentation { + id: Flow::::SUBMIT, + label: Some("Submit"), + description: Some("Move draft work into review."), + metadata: FlowTransitionMeta { + phase: Phase::Review, + branching: false, + }, + }, + TransitionPresentation { + id: Flow::::MAYBE_DECIDE, + label: Some("Validate"), + description: Some("Choose whether the reviewed work is accepted or rejected."), + metadata: FlowTransitionMeta { + phase: Phase::Decision, + branching: true, + }, + }, + TransitionPresentation { + id: Flow::::ACCEPT, + label: Some("Accept"), + description: Some("Approve the work."), + metadata: FlowTransitionMeta { + phase: Phase::Decision, + branching: false, + }, + }, + TransitionPresentation { + id: Flow::::REJECT, + label: Some("Reject"), + description: Some("Reject the work."), + metadata: FlowTransitionMeta { + phase: Phase::Decision, + branching: false, + }, + }, + TransitionPresentation { + id: Flow::::EXPLAIN, + label: Some("Publish"), + description: Some("Move accepted work into published."), + metadata: FlowTransitionMeta { + phase: Phase::Output, + branching: false, + }, + }, + TransitionPresentation { + id: Flow::::EXPLAIN, + label: Some("Rework"), + description: Some("Loop rejected work back to draft."), + metadata: FlowTransitionMeta { + phase: Phase::Output, + branching: false, + }, + }, + ], +}; + +#[test] +fn graph_exposes_exact_transition_sites() { + let graph = as MachineIntrospection>::GRAPH; + + assert_eq!( + as MachineStateIdentity>::STATE_ID, + flow::StateId::Review + ); + assert!(graph.state(flow::StateId::Accepted).is_some()); + assert!(!graph.state(flow::StateId::Published).unwrap().has_data); + + let review_methods = graph + .transitions_from(flow::StateId::Review) + .map(|transition| transition.method_name) + .collect::>(); + let mut review_methods = review_methods; + review_methods.sort_unstable(); + assert_eq!(review_methods, vec!["accept", "maybe_decide", "reject"]); + + let maybe_decide = graph + .transition_from_method(flow::StateId::Review, "maybe_decide") + .unwrap(); + assert_eq!(maybe_decide.id, Flow::::MAYBE_DECIDE); + assert_eq!( + graph.legal_targets(maybe_decide.id).unwrap(), + &[flow::StateId::Accepted, flow::StateId::Rejected] + ); + + let accepted_explain = graph + .transition_from_method(flow::StateId::Accepted, "explain") + .unwrap(); + assert_eq!(accepted_explain.id, Flow::::EXPLAIN); + let rejected_explain = graph + .transition_from_method(flow::StateId::Rejected, "explain") + .unwrap(); + assert_eq!(rejected_explain.id, Flow::::EXPLAIN); + assert_eq!(graph.transitions_named("explain").count(), 2); +} + +#[test] +fn graph_collection_is_scoped_per_machine() { + let alpha_graph = as MachineIntrospection>::GRAPH; + let beta_graph = as MachineIntrospection>::GRAPH; + + let alpha_finish = alpha_graph + .transition(AlphaMachine::::FINISH) + .unwrap(); + assert_eq!( + alpha_graph.legal_targets(alpha_finish.id).unwrap(), + &[alpha_machine::StateId::Published] + ); + + let beta_finish = beta_graph.transition(BetaMachine::::FINISH).unwrap(); + assert_eq!( + beta_graph.legal_targets(beta_finish.id).unwrap(), + &[beta_machine::StateId::Rejected] + ); +} + +#[test] +fn runtime_transition_recording_joins_to_static_metadata() { + let event = + Flow::::try_record_transition_to::>(Flow::::MAYBE_DECIDE) + .unwrap(); + let graph = as MachineIntrospection>::GRAPH; + let transition = graph.transition(event.transition).unwrap(); + + assert_eq!(event.machine, graph.machine); + assert_eq!(event.from, flow::StateId::Review); + assert_eq!(event.chosen, flow::StateId::Accepted); + assert_eq!(transition.from, event.from); + assert!(transition.to.contains(&event.chosen)); +} + +#[test] +fn runtime_transition_recording_rejects_illegal_runtime_join() { + assert!( + Flow::::try_record_transition(Flow::::SUBMIT, flow::StateId::Accepted,) + .is_none() + ); + assert!(Flow::::try_record_transition_to::>( + Flow::::MAYBE_DECIDE, + ) + .is_none()); +} + +#[test] +fn consumer_owned_presentation_metadata_joins_with_graph_and_runtime_event() { + let graph = as MachineIntrospection>::GRAPH; + let event = + Flow::::try_record_transition_to::>(Flow::::MAYBE_DECIDE) + .unwrap(); + + assert_eq!( + FLOW_PRESENTATION.machine, + Some(MachinePresentationDescriptor { + label: Some("Flow"), + description: Some("Example consumer-owned presentation metadata."), + metadata: FlowMachineMeta { + phase: Phase::Intake, + }, + }) + ); + assert_eq!( + event.transition_in(graph).unwrap().method_name, + "maybe_decide" + ); + assert_eq!( + FLOW_PRESENTATION + .transition(event.transition) + .unwrap() + .label, + Some("Validate") + ); + assert_eq!( + FLOW_PRESENTATION.state(event.chosen).unwrap().metadata, + FlowStateMeta { + phase: Phase::Decision, + source_term: "accepted", + } + ); + assert_eq!( + graph.legal_targets(event.transition).unwrap(), + &[flow::StateId::Accepted, flow::StateId::Rejected] + ); +} diff --git a/statum/tests/introspection_authority.rs b/statum/tests/introspection_authority.rs new file mode 100644 index 0000000..d3b933e --- /dev/null +++ b/statum/tests/introspection_authority.rs @@ -0,0 +1,84 @@ +#![allow(dead_code)] + +use statum::{machine, state, transition, MachineIntrospection}; + +#[state] +enum GeneratedState { + Start, + Enabled, + MacroTarget, + Included, + Hidden, +} + +#[machine] +struct GeneratedFlow {} + +#[cfg(any())] +#[transition] +impl GeneratedFlow { + fn cfg_impl_hidden(self) -> GeneratedFlow { + self.transition() + } +} + +#[transition] +impl GeneratedFlow { + fn enable(self) -> GeneratedFlow { + self.transition() + } + + #[cfg(any())] + fn cfg_method_hidden(self) -> GeneratedFlow { + self.transition() + } +} + +macro_rules! generated_transitions { + () => { + #[transition] + impl GeneratedFlow { + fn via_macro(self) -> GeneratedFlow { + self.transition() + } + } + }; +} + +generated_transitions!(); + +include!("support/generated_flow_include.rs"); + +#[test] +fn graph_respects_cfg_pruning_and_macro_generated_transitions() { + let graph = as MachineIntrospection>::GRAPH; + + let mut start_methods = graph + .transitions_from(generated_flow::StateId::Start) + .map(|transition| transition.method_name) + .collect::>(); + start_methods.sort_unstable(); + assert_eq!(start_methods, vec!["enable"]); + assert!(graph + .transition_from_method(generated_flow::StateId::Start, "cfg_impl_hidden") + .is_none()); + assert!(graph + .transition_from_method(generated_flow::StateId::Start, "cfg_method_hidden") + .is_none()); + + let via_macro = graph + .transition_from_method(generated_flow::StateId::Enabled, "via_macro") + .expect("macro-generated transition should be in GRAPH"); + assert_eq!( + graph.legal_targets(via_macro.id).unwrap(), + &[generated_flow::StateId::MacroTarget] + ); + + let via_include = graph + .transition_from_method(generated_flow::StateId::MacroTarget, "via_include") + .expect("include-generated transition should be in GRAPH"); + assert_eq!( + graph.legal_targets(via_include.id).unwrap(), + &[generated_flow::StateId::Included] + ); +} diff --git a/statum/tests/support/generated_flow_include.rs b/statum/tests/support/generated_flow_include.rs new file mode 100644 index 0000000..84f70c8 --- /dev/null +++ b/statum/tests/support/generated_flow_include.rs @@ -0,0 +1,6 @@ +#[transition] +impl GeneratedFlow { + fn via_include(self) -> GeneratedFlow { + self.transition() + } +}