├── crates ├── narrative-macros │ ├── src │ │ ├── error.rs │ │ ├── step_usage.rs │ │ ├── validation │ │ │ └── no_foreign_types.rs │ │ ├── no_foreign_type_validation.rs │ │ ├── story_attr_syntax.rs │ │ ├── item_story │ │ │ ├── story_const.rs │ │ │ ├── story_item.rs │ │ │ └── story_step.rs │ │ ├── output │ │ │ ├── local_type_impls.rs │ │ │ ├── local_type_assertions.rs │ │ │ ├── story_trait.rs │ │ │ ├── base_trait.rs │ │ │ ├── dummy_environment.rs │ │ │ ├── step_fn.rs │ │ │ ├── story_context.rs │ │ │ └── story_consts.rs │ │ ├── item_story.rs │ │ ├── lib.rs │ │ ├── output.rs │ │ ├── local_type_for.rs │ │ ├── step_attr_syntax.rs │ │ └── extract_types_for_assertion.rs │ └── Cargo.toml └── narrative │ ├── tests │ ├── .gitignore │ ├── compile-fail │ │ ├── non-trait-story.stderr │ │ ├── non-trait-story.rs │ │ ├── missing-step-text.stderr │ │ ├── invalid-item-in-story.stderr │ │ ├── const-without-value.stderr │ │ ├── missing-step-text.rs │ │ ├── missing-step-parameter.rs │ │ ├── invalid-item-in-story.rs │ │ ├── const-without-value.rs │ │ ├── non-local-type.rs │ │ ├── local_type_nested.rs │ │ ├── non-local-type.stderr │ │ ├── local_type_nested.stderr │ │ └── missing-step-parameter.stderr │ ├── compile_tests.rs │ └── compile-pass │ │ ├── unused_step_arg.rs │ │ ├── basic-story.rs │ │ └── doc_comment_story.rs │ ├── src │ ├── environment.rs │ ├── value.rs │ ├── lib.rs │ ├── independent_type.rs │ ├── step.rs │ ├── runner.rs │ └── story.rs │ └── Cargo.toml ├── test-suite ├── src │ ├── step_arg.rs │ ├── story_consts │ │ └── literal_types.rs │ ├── story_consts.rs │ ├── sub_story_from_other_module.rs │ ├── step_arg │ │ └── complex_type.rs │ ├── lib.rs │ └── multiple_stories_in_one_file.rs └── Cargo.toml ├── Cargo.toml ├── .gitignore ├── examples ├── src │ ├── chain.rs │ ├── lib.rs │ ├── empty_story.rs │ ├── hello_world.rs │ ├── async_story.rs │ ├── consts.rs │ ├── my_first_story.rs │ ├── third_party_crates.rs │ ├── sub_story.rs │ ├── async_sub_story.rs │ ├── custom_data_type.rs │ └── story_runner.rs └── Cargo.toml ├── LICENSE-MIT ├── LICENSE-APACHE └── README.md /crates/narrative-macros/src/error.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/step_usage.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/narrative/tests/.gitignore: -------------------------------------------------------------------------------- 1 | !*.stderr 2 | -------------------------------------------------------------------------------- /test-suite/src/step_arg.rs: -------------------------------------------------------------------------------- 1 | mod complex_type; 2 | -------------------------------------------------------------------------------- /test-suite/src/story_consts/literal_types.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/validation/no_foreign_types.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/no_foreign_type_validation.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | resolver = "2" 4 | members = ["crates/*", "examples", "test-suite"] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !*/ 3 | 4 | /target 5 | Cargo.lock 6 | 7 | !*.rs 8 | 9 | !.gitignore 10 | !README.md 11 | !Cargo.toml 12 | !LICENSE-* 13 | 14 | !.github/*.yml 15 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-fail/non-trait-story.stderr: -------------------------------------------------------------------------------- 1 | error: expected `trait` 2 | --> tests/compile-fail/non-trait-story.rs:2:1 3 | | 4 | 2 | struct NotATrait { 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-fail/non-trait-story.rs: -------------------------------------------------------------------------------- 1 | #[narrative::story("This should fail because it's not a trait")] 2 | struct NotATrait { 3 | field: String, 4 | } 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-fail/missing-step-text.stderr: -------------------------------------------------------------------------------- 1 | error: expected a step or const 2 | --> tests/compile-fail/missing-step-text.rs:4:5 3 | | 4 | 4 | fn invalid_step(&self); 5 | | ^^ 6 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-fail/invalid-item-in-story.stderr: -------------------------------------------------------------------------------- 1 | error: expected a step or const 2 | --> tests/compile-fail/invalid-item-in-story.rs:4:5 3 | | 4 | 4 | type SomeType = String; 5 | | ^^^^ 6 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile_tests.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn compile_tests() { 3 | let t = trybuild::TestCases::new(); 4 | t.compile_fail("tests/compile-fail/*.rs"); 5 | t.pass("tests/compile-pass/*.rs"); 6 | } -------------------------------------------------------------------------------- /test-suite/src/story_consts.rs: -------------------------------------------------------------------------------- 1 | mod literal_types; 2 | 3 | #[narrative::story("Story consts")] 4 | trait StoryConsts { 5 | const NUMBER: u32 = 42; 6 | 7 | #[step("Step")] 8 | fn step(&self); 9 | } 10 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-fail/const-without-value.stderr: -------------------------------------------------------------------------------- 1 | error: in a story, all consts must have a value 2 | --> tests/compile-fail/const-without-value.rs:5:5 3 | | 4 | 5 | #[step("When using const")] 5 | | ^ 6 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-fail/missing-step-text.rs: -------------------------------------------------------------------------------- 1 | #[narrative::story("Story with invalid step")] 2 | trait InvalidStepStory { 3 | #[step] // Missing required text parameter 4 | fn invalid_step(&self); 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /test-suite/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-suite" 3 | edition = "2024" 4 | publish = false 5 | 6 | [dependencies] 7 | narrative = { path = "../crates/narrative" } 8 | futures = "0.3" 9 | serde = { version = "1", features = ["derive"] } 10 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-pass/unused_step_arg.rs: -------------------------------------------------------------------------------- 1 | #[narrative::story("Story with unbound parameter")] 2 | trait UnboundParameterStory { 3 | #[step("When I do something", unused_param = "unused")] 4 | fn do_something(&self); // unused_param is not used 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-fail/missing-step-parameter.rs: -------------------------------------------------------------------------------- 1 | #[narrative::story("Story with missing step parameter")] 2 | trait MissingParameterStory { 3 | #[step("When I perform an action")] 4 | fn perform_action(&self, action: &str); // Missing 'action' parameter 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /examples/src/chain.rs: -------------------------------------------------------------------------------- 1 | // #[narrative::transformation("Chain steps")] 2 | // trait Chain { 3 | // #[step("Step 1")] 4 | // fn parse() -> Ast; 5 | // #[step("Step 2")] 6 | // fn analysis() -> (Ast, Analysis); 7 | // #[step("Step 3")] 8 | // fn codegen() -> Code; 9 | // } 10 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-fail/invalid-item-in-story.rs: -------------------------------------------------------------------------------- 1 | #[narrative::story("Story with invalid item")] 2 | trait InvalidItemStory { 3 | // Type aliases are not allowed in stories 4 | type SomeType = String; 5 | 6 | #[step("When using type")] 7 | fn use_type(&self); 8 | } 9 | 10 | fn main() {} 11 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-fail/const-without-value.rs: -------------------------------------------------------------------------------- 1 | #[narrative::story("Story with const missing value")] 2 | trait ConstWithoutValueStory { 3 | const INVALID_CONST: i32; // Error: in a story, all consts must have a value 4 | 5 | #[step("When using const")] 6 | fn use_const(&self); 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /examples/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![cfg_attr(test, deny(dead_code, warnings))] 3 | 4 | mod async_story; 5 | mod async_sub_story; 6 | mod chain; 7 | mod consts; 8 | mod custom_data_type; 9 | mod empty_story; 10 | mod hello_world; 11 | mod my_first_story; 12 | mod story_runner; 13 | mod sub_story; 14 | mod third_party_crates; 15 | -------------------------------------------------------------------------------- /examples/src/empty_story.rs: -------------------------------------------------------------------------------- 1 | #[narrative::story("Empty story is valid")] 2 | trait EmptyStory {} 3 | 4 | #[allow(dead_code)] 5 | struct Env; 6 | 7 | impl EmptyStory for Env { 8 | type Error = std::convert::Infallible; 9 | } 10 | 11 | #[test] 12 | fn test_empty_story() { 13 | use narrative::story::RunStory as _; 14 | let mut env = Env; 15 | EmptyStoryContext.run_story(&mut env).unwrap(); 16 | } 17 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-fail/non-local-type.rs: -------------------------------------------------------------------------------- 1 | // This type is defined outside the story trait 2 | // but does not implement StoryOwnedType or TestStoryLocalType 3 | #[derive(Debug, Clone, serde::Serialize)] 4 | struct NotLocalType; 5 | 6 | #[narrative::story("Test non-local type error")] 7 | trait TestStory { 8 | #[step("Given a non-local type", arg = NotLocalType)] 9 | fn use_non_local_type(&self, arg: NotLocalType); 10 | } 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-pass/basic-story.rs: -------------------------------------------------------------------------------- 1 | #[narrative::story("My First Story")] 2 | trait MyFirstStory { 3 | const NAME: &str = "Ryo"; 4 | #[step("Hi, I'm a user: {NAME}")] 5 | fn as_a_user(); 6 | #[step("I have an apple", count = 1)] 7 | fn have_one_apple(count: u32); 8 | #[step("I have {count} oranges", count = 2)] 9 | fn have_two_oranges(count: u32); 10 | #[step("I should have {total} fruits", total = 3)] 11 | fn should_have_three_fruits(total: u32); 12 | } 13 | 14 | fn main() {} 15 | -------------------------------------------------------------------------------- /crates/narrative/src/environment.rs: -------------------------------------------------------------------------------- 1 | /// A dummy environment that does nothing on each step. Every story can be run on this environment. 2 | pub struct DummyEnvironment(std::marker::PhantomData); 3 | 4 | impl Default for DummyEnvironment { 5 | fn default() -> Self { 6 | Self(std::marker::PhantomData) 7 | } 8 | } 9 | 10 | impl std::fmt::Debug for DummyEnvironment { 11 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 12 | f.debug_tuple("DummyEnvironment").finish() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-fail/local_type_nested.rs: -------------------------------------------------------------------------------- 1 | #[narrative::story("Story with local types")] 2 | trait ConstWithoutValueStory { 3 | } 4 | 5 | #[derive(Debug, Clone, serde::Serialize)] 6 | #[narrative::local_type_for(ConstWithoutValueStory)] 7 | struct LocalTypeStruct { 8 | field: LocalTypeEnum, 9 | } 10 | 11 | #[derive(Debug, Clone, serde::Serialize)] 12 | #[narrative::local_type_for(ConstWithoutValueStory)] 13 | enum LocalTypeEnum { 14 | Variant1, 15 | Variant2(NonLocalType), 16 | } 17 | 18 | #[derive(Debug, Clone, serde::Serialize)] 19 | struct NonLocalType; 20 | 21 | fn main() {} 22 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "narrative-examples" 3 | edition = "2024" 4 | publish = false 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | chrono = { version = "0.4", features = ["serde"] } 10 | futures = "0.3" 11 | narrative = { path = "../crates/narrative", features = [ 12 | "chrono", 13 | "serde_json", 14 | "uuid", 15 | ] } 16 | serde = { version = "1", features = ["derive"] } 17 | serde_json = { version = "1" } 18 | uuid = { version = "1", features = ["serde"] } 19 | 20 | [dev-dependencies] 21 | pretty_assertions = "1" 22 | -------------------------------------------------------------------------------- /test-suite/src/sub_story_from_other_module.rs: -------------------------------------------------------------------------------- 1 | mod sub_story { 2 | #[narrative::story("Sub story from other module")] 3 | trait SubStory { 4 | #[step("Sub step 1")] 5 | fn sub_step_1(); 6 | } 7 | } 8 | 9 | use narrative::step::Step; 10 | use sub_story::*; 11 | 12 | #[narrative::story("Main story")] 13 | trait MainStory { 14 | #[step(story: SubStory, "Run sub story")] 15 | fn main_step_1(); 16 | } 17 | 18 | #[test] 19 | fn test_sub_story_from_other_module() { 20 | let steps = MainStoryContext.steps().collect::>(); 21 | assert_eq!(steps.len(), 1); 22 | assert_eq!(steps[0].step_text(), "Run sub story"); 23 | } 24 | -------------------------------------------------------------------------------- /test-suite/src/step_arg/complex_type.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use narrative::{environment::DummyEnvironment, story::RunStory}; 4 | 5 | use crate::TestRunner; 6 | 7 | #[narrative::story("Complex step type")] 8 | trait ComplexStepType { 9 | #[step("Complex step", arg = &[ 10 | &["a", "b"], 11 | &["c", "d", "e"], 12 | ])] 13 | fn complex_step(&self, arg: &[&[&str]]); 14 | } 15 | 16 | #[test] 17 | fn test_complex_step_type() { 18 | let mut env = DummyEnvironment::::default(); 19 | let mut runner = TestRunner::default(); 20 | ComplexStepTypeContext 21 | .run_story_with_runner(&mut env, &mut runner) 22 | .unwrap(); 23 | } 24 | -------------------------------------------------------------------------------- /crates/narrative-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "narrative-macros" 3 | version = "0.12.2" 4 | edition = "2024" 5 | description = "Procedural macros for the narrative crate" 6 | readme = "../../README.md" 7 | repository = "https://github.com/ryo33/narrative" 8 | license = "MIT OR Apache-2.0" 9 | keywords = [] 10 | categories = [] 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | prettyplease = "0.2" 17 | proc-macro2 = "1" 18 | quote = "1" 19 | # extra-traits for PartialEq of Pat 20 | syn = { version = "2", features = [ 21 | "extra-traits", 22 | "full", 23 | "parsing", 24 | "visit", 25 | "visit-mut", 26 | ] } 27 | 28 | [dev-dependencies] 29 | pretty_assertions = "1.4.0" 30 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/story_attr_syntax.rs: -------------------------------------------------------------------------------- 1 | use syn::parse::{Parse, ParseStream}; 2 | 3 | mod kw { 4 | syn::custom_keyword!(story); 5 | } 6 | 7 | pub struct StoryAttr { 8 | pub title: syn::LitStr, 9 | } 10 | 11 | impl Parse for StoryAttr { 12 | fn parse(input: ParseStream) -> syn::Result { 13 | let title = input.parse()?; 14 | Ok(Self { title }) 15 | } 16 | } 17 | 18 | #[cfg(test)] 19 | mod tests { 20 | use super::*; 21 | 22 | #[test] 23 | fn test_story_attr() { 24 | let input: StoryAttr = syn::parse_quote! { 25 | "Hello, world!" 26 | }; 27 | assert_eq!(input.title.value(), "Hello, world!".to_string()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-pass/doc_comment_story.rs: -------------------------------------------------------------------------------- 1 | /// This is a my first story. 2 | #[narrative::story("My First Story")] 3 | trait MyFirstStory { 4 | /// This is a name. 5 | const NAME: &str = "Ryo"; 6 | /// This is a step to say hello to the user. 7 | #[step("Hi, I'm a user: {NAME}")] 8 | fn as_a_user(); 9 | /// This is a step to have one apple. 10 | #[step("I have an apple", count = 1)] 11 | fn have_one_apple(count: u32); 12 | /// This is a step to have two oranges. 13 | #[step("I have {count} orages", count = 2)] 14 | fn have_two_oranges(count: u32); 15 | /// This is a step to should have three fruits. 16 | #[step("I should have {total} fruits", total = 3)] 17 | fn should_have_three_fruits(total: u32); 18 | } 19 | 20 | fn main() {} 21 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/item_story/story_const.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | 4 | pub struct StoryConst { 5 | pub raw: syn::TraitItemConst, 6 | pub default: (syn::Token![=], syn::Expr), 7 | } 8 | 9 | impl StoryConst { 10 | pub fn to_pub_const(&self) -> TokenStream { 11 | let syn::TraitItemConst { 12 | attrs, 13 | const_token, 14 | ident, 15 | generics, 16 | colon_token, 17 | ty, 18 | default: _, 19 | semi_token: _, 20 | } = &self.raw; 21 | let (eq, default) = self.default.clone(); 22 | quote! { 23 | #(#attrs)* 24 | pub #const_token #ident #generics #colon_token #ty #eq #default; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /crates/narrative/src/value.rs: -------------------------------------------------------------------------------- 1 | pub trait Value: std::any::Any + std::fmt::Debug + serde::Serialize {} 2 | 3 | pub trait DynValue: std::any::Any + std::fmt::Debug + erased_serde::Serialize {} 4 | 5 | impl Value for T {} 6 | impl DynValue for T {} 7 | 8 | pub struct BoxedValue(Box); 9 | 10 | impl BoxedValue { 11 | pub fn new(value: impl DynValue) -> Self { 12 | Self(Box::new(value)) 13 | } 14 | } 15 | 16 | impl std::fmt::Debug for BoxedValue { 17 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 18 | self.0.fmt(f) 19 | } 20 | } 21 | 22 | impl serde::Serialize for BoxedValue { 23 | fn serialize(&self, serializer: S) -> Result 24 | where 25 | S: serde::Serializer, 26 | { 27 | (&*self.0 as &dyn erased_serde::Serialize).serialize(serializer) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-fail/non-local-type.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the type `NotLocalType` cannot be used in this story 2 | --> tests/compile-fail/non-local-type.rs:9:39 3 | | 4 | 9 | fn use_non_local_type(&self, arg: NotLocalType); 5 | | ^^^^^^^^^^^^ this type is not allowed in stories 6 | | 7 | help: the trait `TestStoryLocalType` is not implemented for `NotLocalType` 8 | --> tests/compile-fail/non-local-type.rs:4:1 9 | | 10 | 4 | struct NotLocalType; 11 | | ^^^^^^^^^^^^^^^^^^^ 12 | = note: only types from the standard library or types defined with #[local_type_for] are allowed 13 | note: required by a bound in `assert_local_type` 14 | --> tests/compile-fail/non-local-type.rs:7:7 15 | | 16 | 6 | #[narrative::story("Test non-local type error")] 17 | | ------------------------------------------------ required by a bound in this function 18 | 7 | trait TestStory { 19 | | ^^^^^^^^^ required by this bound in `assert_local_type` 20 | -------------------------------------------------------------------------------- /crates/narrative/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "narrative" 3 | version = "0.12.2" 4 | edition = "2024" 5 | description = "An immensely simple library for story-driven development" 6 | readme = "../../README.md" 7 | repository = "https://github.com/ryo33/narrative" 8 | license = "MIT OR Apache-2.0" 9 | keywords = ["atdd", "bdd", "story-driven", "testing"] 10 | categories = ["development-tools::testing"] 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | chrono = { version = "0.4", features = ["serde"], optional = true } 16 | erased-serde = { version = "0.4" } 17 | narrative-macros = { path = "../narrative-macros", version = "0.12.0" } 18 | serde = { version = "1", features = ["derive", "rc"] } 19 | serde_json = { version = "1", optional = true } 20 | uuid = { version = "1", features = ["serde"], optional = true } 21 | 22 | [dev-dependencies] 23 | trybuild = "1" 24 | 25 | [features] 26 | chrono = ["dep:chrono"] 27 | serde_json = ["dep:serde_json"] 28 | uuid = ["dep:uuid"] 29 | -------------------------------------------------------------------------------- /examples/src/hello_world.rs: -------------------------------------------------------------------------------- 1 | use std::io::{BufWriter, Write as _}; 2 | 3 | /// This is a hello world story. 4 | #[narrative::story("Say hello world")] 5 | trait HelloWorld { 6 | /// This is a step to say hello. 7 | #[step("Say hello")] 8 | fn say_hello(); 9 | /// This is a step to say world. 10 | #[step("Say world")] 11 | fn say_world(); 12 | } 13 | 14 | #[allow(dead_code)] 15 | struct Env { 16 | buf: BufWriter>, 17 | } 18 | 19 | impl HelloWorld for Env { 20 | type Error = std::io::Error; 21 | 22 | fn say_hello(&mut self) -> Result<(), Self::Error> { 23 | write!(self.buf, "Hello, ") 24 | } 25 | 26 | fn say_world(&mut self) -> Result<(), Self::Error> { 27 | write!(self.buf, "World!") 28 | } 29 | } 30 | 31 | #[test] 32 | fn test() { 33 | use narrative::story::RunStory as _; 34 | let mut env = Env { 35 | buf: BufWriter::new(Vec::new()), 36 | }; 37 | HelloWorldContext.run_story(&mut env).unwrap(); 38 | assert_eq!(env.buf.into_inner().unwrap(), b"Hello, World!"); 39 | } 40 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-fail/local_type_nested.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the type `NonLocalType` cannot be used in this story 2 | --> tests/compile-fail/local_type_nested.rs:15:14 3 | | 4 | 15 | Variant2(NonLocalType), 5 | | ^^^^^^^^^^^^ this type is not allowed in stories 6 | | 7 | help: the trait `ConstWithoutValueStoryLocalType` is not implemented for `NonLocalType` 8 | --> tests/compile-fail/local_type_nested.rs:19:1 9 | | 10 | 19 | struct NonLocalType; 11 | | ^^^^^^^^^^^^^^^^^^^ 12 | = note: only types from the standard library or types defined with #[local_type_for] are allowed 13 | = help: the following other types implement trait `ConstWithoutValueStoryLocalType`: 14 | LocalTypeEnum 15 | LocalTypeStruct 16 | note: required by a bound in `_local_type_assertions_LocalTypeEnum::assert_local_type` 17 | --> tests/compile-fail/local_type_nested.rs:12:29 18 | | 19 | 12 | #[narrative::local_type_for(ConstWithoutValueStory)] 20 | | ^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `assert_local_type` 21 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ryo Hirayama 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/narrative/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod environment; 2 | mod independent_type; 3 | pub mod runner; 4 | pub mod step; 5 | pub mod story; 6 | pub mod value; 7 | 8 | pub use independent_type::IndependentType; 9 | pub use narrative_macros::*; 10 | 11 | /// Marker trait for types owned by a single story. 12 | /// Due to the Orphan Rule, using `#[local_type_for]` on remote types (types from other crates) 13 | /// will result in a compilation error. 14 | /// Additionally, attempting to use `#[local_type_for]` on the same type for multiple stories 15 | /// will cause conflicting trait implementations. 16 | pub trait StoryOwnedType: std::fmt::Debug + Clone + serde::Serialize {} 17 | 18 | pub mod serde { 19 | pub use serde::*; 20 | } 21 | 22 | pub mod prelude { 23 | pub use crate::step::Run as _; 24 | pub use crate::step::RunAsync as _; 25 | pub use crate::step::Step as _; 26 | pub use crate::step::StepArg as _; 27 | pub use crate::story::RunStory as _; 28 | pub use crate::story::RunStoryAsync as _; 29 | pub use crate::story::StoryConst as _; 30 | pub use crate::story::StoryContext as _; 31 | pub use crate::story::StoryContext as _; 32 | } 33 | -------------------------------------------------------------------------------- /examples/src/async_story.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | #[narrative::story("My First Story")] 4 | trait MyFirstStory { 5 | #[step("Hi, I'm a user")] 6 | fn as_a_user(); 7 | #[step("I have an apple", count = 1)] 8 | fn have_one_apple(count: u32); 9 | #[step("I have {count} oranges", count = 2)] 10 | fn have_two_oranges(count: u32); 11 | #[step("I should have {total} fruits", total = 3)] 12 | fn should_have_three_fruits(total: u32); 13 | } 14 | 15 | #[allow(dead_code)] 16 | struct MyFirstStoryEnv { 17 | sum: u32, 18 | } 19 | 20 | impl AsyncMyFirstStory for MyFirstStoryEnv { 21 | type Error = Infallible; 22 | 23 | async fn as_a_user(&mut self) -> Result<(), Self::Error> { 24 | Ok(()) 25 | } 26 | 27 | async fn have_one_apple(&mut self, count: u32) -> Result<(), Self::Error> { 28 | self.sum += count; 29 | Ok(()) 30 | } 31 | 32 | async fn have_two_oranges(&mut self, count: u32) -> Result<(), Self::Error> { 33 | self.sum += count; 34 | Ok(()) 35 | } 36 | 37 | async fn should_have_three_fruits(&mut self, total: u32) -> Result<(), Self::Error> { 38 | assert_eq!(self.sum, total); 39 | Ok(()) 40 | } 41 | } 42 | 43 | #[test] 44 | fn test() { 45 | use narrative::story::RunStoryAsync as _; 46 | let mut env = MyFirstStoryEnv { sum: 0 }; 47 | let _ = futures::executor::block_on(MyFirstStoryContext.run_story_async(&mut env)); 48 | } 49 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/item_story/story_item.rs: -------------------------------------------------------------------------------- 1 | use syn::parse::{Parse, ParseStream}; 2 | 3 | use super::{story_const::StoryConst, StoryStep}; 4 | 5 | #[allow(clippy::large_enum_variant)] 6 | pub enum StoryItem { 7 | Step(StoryStep), 8 | Const(StoryConst), 9 | } 10 | 11 | impl Parse for StoryItem { 12 | fn parse(input: ParseStream) -> syn::Result { 13 | if let Ok(step) = input.parse().map(Self::Step) { 14 | Ok(step) 15 | } else if let Ok(const_) = input.parse::() { 16 | let Some(default) = const_.default.clone() else { 17 | return Err(input.error("in a story, all consts must have a value")); 18 | }; 19 | Ok(Self::Const(StoryConst { 20 | raw: const_, 21 | default, 22 | })) 23 | } else { 24 | // I want to return more helpful error by looking ahead some tokens. 25 | Err(input.error("expected a step or const")) 26 | } 27 | } 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use super::*; 33 | use pretty_assertions::assert_eq; 34 | use quote::quote; 35 | 36 | #[test] 37 | fn parse_step() { 38 | let input = quote! { 39 | #[step("Hi, I'm a user")] 40 | fn as_a_user(); 41 | }; 42 | let StoryItem::Step(step) = syn::parse2(input).unwrap() else { 43 | panic!("Expected a step"); 44 | }; 45 | assert_eq!(step.inner.sig.ident, "as_a_user"); 46 | } 47 | 48 | 49 | #[test] 50 | fn parse_const() { 51 | let input = quote! { 52 | const user_id: UserId = UserId::new_v4(); 53 | }; 54 | let StoryItem::Const(StoryConst { raw: const_, .. }) = syn::parse2(input).unwrap() else { 55 | panic!("Expected a const"); 56 | }; 57 | assert_eq!(const_.ident.to_string(), "user_id"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/output/local_type_impls.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{format_ident, quote}; 3 | 4 | use crate::item_story::ItemStory; 5 | 6 | pub fn generate(input: &ItemStory) -> TokenStream { 7 | let story_name = &input.ident; 8 | let local_type_trait = format_ident!("{}LocalType", story_name); 9 | 10 | quote! { 11 | #[diagnostic::on_unimplemented( 12 | message = "the type `{Self}` cannot be used in this story", 13 | label = "this type is not allowed in stories", 14 | note = "only types from the standard library or types defined with #[local_type_for] are allowed" 15 | )] 16 | pub trait #local_type_trait {} 17 | 18 | // Blanket impl for standard library types that already implement IndependentType 19 | #[diagnostic::do_not_recommend] 20 | impl #local_type_trait for T {} 21 | } 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use super::*; 27 | use pretty_assertions::assert_eq; 28 | 29 | #[test] 30 | fn test_independent_type() { 31 | let input = syn::parse_quote! { 32 | trait User { 33 | #[step("Step 1")] 34 | fn step1(); 35 | #[step("Step 2")] 36 | fn step2(); 37 | } 38 | }; 39 | let actual = generate(&input); 40 | let expected = quote! { 41 | #[diagnostic::on_unimplemented( 42 | message = "the type `{Self}` cannot be used in this story", 43 | label = "this type is not allowed in stories", 44 | note = "only types from the standard library or types defined with #[local_type_for] are allowed" 45 | )] 46 | pub trait UserLocalType {} 47 | 48 | #[diagnostic::do_not_recommend] 49 | impl UserLocalType for T {} 50 | }; 51 | assert_eq!(actual.to_string(), expected.to_string()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test-suite/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | #![deny(clippy::all)] 3 | 4 | use narrative::{runner::StoryRunner, step::Run}; 5 | 6 | mod multiple_stories_in_one_file; 7 | mod step_arg; 8 | mod story_consts; 9 | mod sub_story_from_other_module; 10 | 11 | #[derive(Default)] 12 | pub struct TestRunner { 13 | story_queue_depth: usize, 14 | } 15 | 16 | impl StoryRunner for TestRunner { 17 | fn start_story(&mut self, story: impl narrative::story::StoryContext) -> Result<(), E> { 18 | self.story_queue_depth += 1; 19 | eprintln!( 20 | "{}story: {}", 21 | " ".repeat(self.story_queue_depth), 22 | story.story_title() 23 | ); 24 | Ok(()) 25 | } 26 | 27 | fn end_story(&mut self, _story: impl narrative::story::StoryContext) -> Result<(), E> { 28 | self.story_queue_depth -= 1; 29 | Ok(()) 30 | } 31 | 32 | fn run_step(&mut self, step: T, state: &mut S) -> Result<(), E> 33 | where 34 | T: narrative::step::Step + narrative::step::Run, 35 | { 36 | eprintln!( 37 | "{}step: {}", 38 | " ".repeat(self.story_queue_depth + 1), 39 | step.step_text() 40 | ); 41 | if let Err(err) = step.run(state) { 42 | panic!("step {} failed: {}", step.step_text(), err); 43 | } 44 | Ok(()) 45 | } 46 | 47 | fn run_nested_story( 48 | &mut self, 49 | _step: impl narrative::step::Step, 50 | nested_story: S, 51 | env: &mut Env, 52 | ) -> Result<(), E> 53 | where 54 | S::Step: narrative::step::Run, 55 | S: narrative::story::StoryContext + narrative::story::RunStory, 56 | { 57 | eprintln!( 58 | "{}nested story: {}", 59 | " ".repeat(self.story_queue_depth + 1), 60 | nested_story.story_title() 61 | ); 62 | // Test that we can run a nested story step by step by using the provided bound. 63 | if false { 64 | for step in nested_story.steps() { 65 | step.run_with_runner(env, self)?; 66 | } 67 | } 68 | nested_story.run_story_with_runner(env, self) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/output/local_type_assertions.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{format_ident, quote}; 3 | 4 | use crate::{ 5 | extract_types_for_assertion::extract_types_for_assertion, 6 | item_story::{ItemStory, StoryItem}, 7 | }; 8 | 9 | pub fn generate(input: &ItemStory) -> TokenStream { 10 | let story_name = &input.ident; 11 | let local_type_trait = format_ident!("{}LocalType", story_name); 12 | 13 | let assertions = input.items.iter().map(|item| match item { 14 | StoryItem::Step(step) => { 15 | let args = &step.inner.sig.inputs; 16 | let types: Vec<_> = args 17 | .iter() 18 | .filter_map(|arg| match arg { 19 | syn::FnArg::Typed(pat_type) => Some(&*pat_type.ty), 20 | _ => None, 21 | }) 22 | .flat_map(extract_types_for_assertion) 23 | .collect(); 24 | 25 | let assertions = types.iter().map(|ty| { 26 | quote! { 27 | assert_local_type::<#ty>(); 28 | } 29 | }); 30 | quote! { 31 | #(#assertions)* 32 | } 33 | } 34 | _ => Default::default(), 35 | }); 36 | quote! { 37 | fn _local_type_assertions() { 38 | fn assert_local_type() {} 39 | #(#assertions)* 40 | } 41 | } 42 | } 43 | 44 | #[cfg(test)] 45 | mod tests { 46 | use super::*; 47 | use pretty_assertions::assert_eq; 48 | 49 | #[test] 50 | fn test_full() { 51 | let input = syn::parse_quote! { 52 | trait User { 53 | #[step("Step 1")] 54 | fn step1(); 55 | #[step("Step 2")] 56 | fn step2(id: UserId, name: &str); 57 | } 58 | }; 59 | let actual = generate(&input); 60 | let expected = quote! { 61 | fn _local_type_assertions() { 62 | fn assert_local_type() {} 63 | assert_local_type::(); 64 | assert_local_type::<&str>(); 65 | } 66 | }; 67 | assert_eq!(actual.to_string(), expected.to_string()); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/src/consts.rs: -------------------------------------------------------------------------------- 1 | #[narrative::story("Consts")] 2 | trait Consts { 3 | const NAME: &str = "Ryo"; 4 | const ID: &str = "ryo33"; 5 | #[step("dummy", name = NAME)] 6 | fn dummy_step(name: &str); 7 | 8 | #[step("use const in format!", url = format!("https://example.com/{ID}"))] 9 | fn format_step(url: String); 10 | 11 | #[step("use const in step text name: {NAME}")] 12 | fn format_step_in_step_text(); 13 | } 14 | 15 | struct Env; 16 | 17 | impl Consts for Env { 18 | type Error = std::convert::Infallible; 19 | 20 | fn dummy_step(&mut self, name: &str) -> Result<(), Self::Error> { 21 | assert_eq!(name, ConstsContext::NAME); 22 | assert_eq!(name, Self::NAME); 23 | Ok(()) 24 | } 25 | 26 | fn format_step(&mut self, url: String) -> Result<(), Self::Error> { 27 | assert_eq!(url, format!("https://example.com/{}", ConstsContext::ID)); 28 | assert_eq!(url, format!("https://example.com/{}", Self::ID)); 29 | Ok(()) 30 | } 31 | 32 | fn format_step_in_step_text(&mut self) -> Result<(), Self::Error> { 33 | Ok(()) 34 | } 35 | } 36 | 37 | #[test] 38 | fn accessible_through_context() { 39 | use narrative::step::Step; 40 | use narrative::step::StepArg; 41 | use serde_json::json; 42 | assert_eq!(ConstsContext::ID, "ryo33"); 43 | assert_eq!(ConstsContext::NAME, "Ryo"); 44 | 45 | let steps = ConstsContext.steps().collect::>(); 46 | assert_eq!(steps.len(), 3); 47 | assert_eq!(steps[0].args().count(), 1); 48 | let arg = steps[0].args().next().unwrap(); 49 | assert_eq!(format!("{:?}", arg.value()), r#""Ryo""#); 50 | assert_eq!(serde_json::to_value(arg.value()).unwrap(), json!("Ryo")); 51 | 52 | assert_eq!(steps[1].args().count(), 1); 53 | let arg = steps[1].args().next().unwrap(); 54 | assert_eq!( 55 | format!("{:?}", arg.value()), 56 | r#""https://example.com/ryo33""# 57 | ); 58 | assert_eq!( 59 | serde_json::to_value(arg.value()).unwrap(), 60 | json!("https://example.com/ryo33") 61 | ); 62 | 63 | assert_eq!(steps[2].args().count(), 0); 64 | assert_eq!(steps[2].step_text(), "use const in step text name: Ryo"); 65 | } 66 | 67 | #[test] 68 | fn accessible_in_impl() { 69 | let mut env = Env; 70 | ConstsContext.run_story(&mut env).unwrap(); 71 | } 72 | -------------------------------------------------------------------------------- /examples/src/my_first_story.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | #[narrative::story("My First Story")] 4 | trait MyFirstStory { 5 | const NAME: &str = "Ryo"; 6 | #[step("Hi, I'm a user: {NAME}")] 7 | fn as_a_user(); 8 | #[step("I have an apple", count = 1)] 9 | fn have_one_apple(count: u32); 10 | #[step("I have {count} oranges", count = 2)] 11 | fn have_two_oranges(count: u32); 12 | #[step("I should have {total} fruits", total = 3)] 13 | fn should_have_three_fruits(total: u32); 14 | } 15 | 16 | #[allow(dead_code)] 17 | struct MyFirstStoryEnv { 18 | sum: u32, 19 | } 20 | 21 | impl MyFirstStory for MyFirstStoryEnv { 22 | type Error = Infallible; 23 | 24 | fn as_a_user(&mut self) -> Result<(), Self::Error> { 25 | println!("Hi, I'm a user: {}", Self::NAME); 26 | Ok(()) 27 | } 28 | 29 | fn have_one_apple(&mut self, count: u32) -> Result<(), Self::Error> { 30 | self.sum += count; 31 | Ok(()) 32 | } 33 | 34 | fn have_two_oranges(&mut self, count: u32) -> Result<(), Self::Error> { 35 | self.sum += count; 36 | Ok(()) 37 | } 38 | 39 | fn should_have_three_fruits(&mut self, total: u32) -> Result<(), Self::Error> { 40 | assert_eq!(self.sum, total); 41 | Ok(()) 42 | } 43 | } 44 | 45 | #[test] 46 | fn test() { 47 | use narrative::story::RunStory as _; 48 | let mut env = MyFirstStoryEnv { sum: 0 }; 49 | MyFirstStoryContext.run_story(&mut env).unwrap(); 50 | } 51 | 52 | #[test] 53 | fn test_context() { 54 | use narrative::prelude::*; 55 | assert_eq!(MyFirstStoryContext::NAME, "Ryo"); 56 | let consts = MyFirstStoryContext.consts().collect::>(); 57 | assert_eq!(consts.len(), 1); 58 | assert_eq!(format!("{:?}", consts[0]), "NAME: &str = \"Ryo\""); 59 | let steps = MyFirstStoryContext.steps().collect::>(); 60 | assert_eq!(steps.len(), 4); 61 | assert_eq!(steps[0].args().count(), 0); 62 | assert_eq!(steps[0].step_text(), "Hi, I'm a user: Ryo"); 63 | let args = steps[1].args().collect::>(); 64 | assert_eq!(args.len(), 1); 65 | assert_eq!(format!("{:?}", args[0]), "count: u32 = 1"); 66 | assert_eq!(steps[1].step_text(), "I have an apple"); 67 | assert_eq!(steps[2].step_text(), "I have 2 oranges"); 68 | assert_eq!(steps[3].step_text(), "I should have 3 fruits"); 69 | } 70 | -------------------------------------------------------------------------------- /examples/src/third_party_crates.rs: -------------------------------------------------------------------------------- 1 | use uuid::uuid; 2 | 3 | #[narrative::story("Using third party crates")] 4 | trait ThirdPartyCrates { 5 | #[step("Generate a uuid", uuid = uuid!("14f95cf3-4302-4e59-9b49-e40cdc4c6ba3"))] 6 | fn generate_uuid(&self, uuid: uuid::Uuid); 7 | 8 | #[step("Now is {now}", now = chrono::DateTime::::parse_from_rfc3339("2025-04-28T00:00:00Z").unwrap())] 9 | fn jump_to_tomorrow(&self, now: chrono::DateTime); 10 | 11 | #[step("Print the json", json = serde_json::json!({ 12 | "key": "value" 13 | }))] 14 | fn print_json(&self, json: serde_json::Value); 15 | } 16 | 17 | #[allow(dead_code)] 18 | struct ThirdPartyCratesImpl { 19 | state: Vec, 20 | } 21 | 22 | impl ThirdPartyCrates for ThirdPartyCratesImpl { 23 | type Error = std::convert::Infallible; 24 | 25 | fn generate_uuid(&mut self, uuid: uuid::Uuid) -> Result<(), Self::Error> { 26 | self.state.push(format!("Generated UUID: {uuid}")); 27 | Ok(()) 28 | } 29 | 30 | fn jump_to_tomorrow( 31 | &mut self, 32 | now: chrono::DateTime, 33 | ) -> Result<(), Self::Error> { 34 | self.state.push(format!("Time jumped to: {now}")); 35 | Ok(()) 36 | } 37 | 38 | fn print_json(&mut self, json: serde_json::Value) -> Result<(), Self::Error> { 39 | self.state.push(format!("JSON: {json}")); 40 | Ok(()) 41 | } 42 | } 43 | 44 | #[test] 45 | fn test_third_party_crates() { 46 | use narrative::story::RunStory as _; 47 | let mut env = ThirdPartyCratesImpl { state: vec![] }; 48 | ThirdPartyCratesContext.run_story(&mut env).unwrap(); 49 | assert_eq!( 50 | env.state, 51 | vec![ 52 | "Generated UUID: 14f95cf3-4302-4e59-9b49-e40cdc4c6ba3", 53 | "Time jumped to: 2025-04-28 00:00:00 +00:00", 54 | "JSON: {\"key\":\"value\"}" 55 | ] 56 | ); 57 | } 58 | 59 | #[test] 60 | fn test_path() { 61 | use narrative::prelude::*; 62 | let steps = ThirdPartyCratesContext.steps().collect::>(); 63 | assert_eq!(steps.len(), 3); 64 | let args = steps[0].args().collect::>(); 65 | assert_eq!(args.len(), 1); 66 | assert_eq!( 67 | format!("{:?}", args[0]), 68 | r#"uuid: uuid::Uuid = uuid!("14f95cf3-4302-4e59-9b49-e40cdc4c6ba3")"# 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /examples/src/sub_story.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | #[narrative::story("This is a sub story")] 4 | trait SubStory { 5 | #[step("sub_step_1")] 6 | fn sub_step_1(); 7 | #[step("sub_step_2")] 8 | fn sub_step_2(); 9 | } 10 | 11 | #[narrative::story("This is a sub story 2")] 12 | trait SubStory2 { 13 | #[step("sub_step_3")] 14 | fn sub_step_3(); 15 | #[step("sub_step_4")] 16 | fn sub_step_4(); 17 | } 18 | 19 | #[narrative::story("This is a main story")] 20 | trait MainStory { 21 | #[step(story: SubStory, "do sub story")] 22 | fn main_step_1(); 23 | #[step(story: SubStory2, "do sub story with args", arg = 2)] 24 | fn main_step_2(arg: i32); 25 | } 26 | 27 | #[allow(dead_code)] 28 | struct SubStoryImpl<'a> { 29 | state: &'a mut Vec, 30 | arg: Option, 31 | } 32 | 33 | impl SubStory for SubStoryImpl<'_> { 34 | type Error = Infallible; 35 | 36 | fn sub_step_1(&mut self) -> Result<(), Self::Error> { 37 | self.state.push(format!("sub_step_1: {:?}", self.arg)); 38 | Ok(()) 39 | } 40 | 41 | fn sub_step_2(&mut self) -> Result<(), Self::Error> { 42 | self.state.push(format!("sub_step_2: {:?}", self.arg)); 43 | Ok(()) 44 | } 45 | } 46 | 47 | impl SubStory2 for SubStoryImpl<'_> { 48 | type Error = Infallible; 49 | 50 | fn sub_step_3(&mut self) -> Result<(), Self::Error> { 51 | self.state.push(format!("sub_step_3: {:?}", self.arg)); 52 | Ok(()) 53 | } 54 | 55 | fn sub_step_4(&mut self) -> Result<(), Self::Error> { 56 | self.state.push(format!("sub_step_4: {:?}", self.arg)); 57 | Ok(()) 58 | } 59 | } 60 | 61 | #[allow(dead_code)] 62 | struct MainStoryImpl { 63 | state: Vec, 64 | } 65 | 66 | impl MainStory for MainStoryImpl { 67 | type Error = Infallible; 68 | 69 | fn main_step_1(&mut self) -> Result, Self::Error> { 70 | Ok(SubStoryImpl { 71 | state: &mut self.state, 72 | arg: None, 73 | }) 74 | } 75 | 76 | fn main_step_2( 77 | &mut self, 78 | arg: i32, 79 | ) -> Result, Self::Error> { 80 | Ok(SubStoryImpl { 81 | state: &mut self.state, 82 | arg: Some(arg), 83 | }) 84 | } 85 | } 86 | 87 | #[test] 88 | fn test() { 89 | use narrative::story::RunStory as _; 90 | let mut env = MainStoryImpl { state: vec![] }; 91 | MainStoryContext.run_story(&mut env).unwrap(); 92 | assert_eq!( 93 | env.state, 94 | vec![ 95 | "sub_step_1: None", 96 | "sub_step_2: None", 97 | "sub_step_3: Some(2)", 98 | "sub_step_4: Some(2)", 99 | ] 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /examples/src/async_sub_story.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | #[narrative::story("This is a sub story")] 4 | trait SubStory { 5 | #[step("sub_step_1")] 6 | fn sub_step_1(); 7 | #[step("sub_step_2")] 8 | fn sub_step_2(); 9 | } 10 | 11 | #[narrative::story("This is a sub story 2")] 12 | trait SubStory2 { 13 | #[step("sub_step_3")] 14 | fn sub_step_3(); 15 | #[step("sub_step_4")] 16 | fn sub_step_4(); 17 | } 18 | 19 | #[narrative::story("This is a main story")] 20 | trait MainStory { 21 | #[step(story: SubStory, "do sub story")] 22 | fn main_step_1(); 23 | #[step(story: SubStory2, "do sub story with args", arg = 2)] 24 | fn main_step_2(arg: i32); 25 | } 26 | 27 | #[allow(dead_code)] 28 | struct SubStoryImpl<'a> { 29 | state: &'a mut Vec, 30 | arg: Option, 31 | } 32 | 33 | impl AsyncSubStory for SubStoryImpl<'_> { 34 | type Error = Infallible; 35 | 36 | async fn sub_step_1(&mut self) -> Result<(), Self::Error> { 37 | self.state.push(format!("sub_step_1: {:?}", self.arg)); 38 | Ok(()) 39 | } 40 | 41 | async fn sub_step_2(&mut self) -> Result<(), Self::Error> { 42 | self.state.push(format!("sub_step_2: {:?}", self.arg)); 43 | Ok(()) 44 | } 45 | } 46 | 47 | impl AsyncSubStory2 for SubStoryImpl<'_> { 48 | type Error = Infallible; 49 | 50 | async fn sub_step_3(&mut self) -> Result<(), Self::Error> { 51 | self.state.push(format!("sub_step_3: {:?}", self.arg)); 52 | Ok(()) 53 | } 54 | 55 | async fn sub_step_4(&mut self) -> Result<(), Self::Error> { 56 | self.state.push(format!("sub_step_4: {:?}", self.arg)); 57 | Ok(()) 58 | } 59 | } 60 | 61 | #[allow(dead_code)] 62 | struct MainStoryImpl { 63 | state: Vec, 64 | } 65 | 66 | impl AsyncMainStory for MainStoryImpl { 67 | type Error = Infallible; 68 | 69 | fn main_step_1(&mut self) -> Result, Self::Error> { 70 | Ok(SubStoryImpl { 71 | state: &mut self.state, 72 | arg: None, 73 | }) 74 | } 75 | 76 | fn main_step_2( 77 | &mut self, 78 | arg: i32, 79 | ) -> Result, Self::Error> { 80 | Ok(SubStoryImpl { 81 | state: &mut self.state, 82 | arg: Some(arg), 83 | }) 84 | } 85 | } 86 | 87 | #[test] 88 | fn test_async() { 89 | use narrative::story::RunStoryAsync as _; 90 | let mut env = MainStoryImpl { state: vec![] }; 91 | futures::executor::block_on(MainStoryContext.run_story_async(&mut env)).unwrap(); 92 | assert_eq!( 93 | env.state, 94 | vec![ 95 | "sub_step_1: None", 96 | "sub_step_2: None", 97 | "sub_step_3: Some(2)", 98 | "sub_step_4: Some(2)", 99 | ] 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /examples/src/custom_data_type.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use std::convert::Infallible; 3 | 4 | trait IndependentType {} 5 | trait StoryLocalType {} 6 | 7 | struct ExampleMyType; 8 | 9 | impl StoryLocalType for T {} 10 | impl StoryLocalType for ExampleMyType {} 11 | 12 | // Story trait that will generate UserStoryLocalType 13 | #[narrative::story("User Story with Custom Data Types")] 14 | trait UserStory { 15 | const USER_ID: UserId = UserId::new("user123"); 16 | 17 | #[step("User {id:?} logs in as {role:?}", id = USER_ID, role = UserRole::Admin)] 18 | fn user_logs_in(id: UserId, role: UserRole); 19 | 20 | #[step("User has access to admin panel", has_access = true)] 21 | fn check_admin_access(has_access: bool); 22 | 23 | #[step("List all users", users = vec![UserRecord::new("user123", "Alice", UserRole::Admin), UserRecord::new("user456", "Bob", UserRole::User)])] 24 | fn list_all_users(users: Vec); 25 | } 26 | 27 | // Custom data types with #[local_type_for] macro 28 | // This implements both IndependentType and UserStoryLocalType 29 | #[derive(Debug, Clone, serde::Serialize)] 30 | #[narrative::local_type_for(UserStory)] 31 | pub struct UserId(&'static str); 32 | 33 | impl UserId { 34 | pub const fn new(id: &'static str) -> Self { 35 | Self(id) 36 | } 37 | } 38 | 39 | #[derive(Debug, Clone, serde::Serialize)] 40 | #[narrative::local_type_for(UserStory)] 41 | pub enum UserRole { 42 | Admin, 43 | #[allow(dead_code)] 44 | User, 45 | #[allow(dead_code)] 46 | Guest, 47 | } 48 | 49 | #[derive(Debug, Clone, serde::Serialize)] 50 | #[narrative::local_type_for(UserStory)] 51 | pub struct UserRecord { 52 | id: UserId, 53 | name: &'static str, 54 | role: UserRole, 55 | } 56 | 57 | impl UserRecord { 58 | pub const fn new(id: &'static str, name: &'static str, role: UserRole) -> Self { 59 | Self { id: UserId::new(id), name, role } 60 | } 61 | } 62 | 63 | struct UserStoryEnv { 64 | current_user_id: Option<&'static str>, 65 | current_role: Option, 66 | } 67 | 68 | impl UserStory for UserStoryEnv { 69 | type Error = Infallible; 70 | 71 | fn user_logs_in(&mut self, id: UserId, role: UserRole) -> Result<(), Self::Error> { 72 | self.current_user_id = Some(id.0); 73 | self.current_role = Some(role); 74 | Ok(()) 75 | } 76 | 77 | fn check_admin_access(&mut self, has_access: bool) -> Result<(), Self::Error> { 78 | if let Some(role) = &self.current_role { 79 | match role { 80 | UserRole::Admin => assert!(has_access), 81 | _ => assert!(!has_access), 82 | } 83 | } 84 | Ok(()) 85 | } 86 | 87 | fn list_all_users(&mut self, users: Vec) -> Result<(), Self::Error> { 88 | for user in users { 89 | println!("User: {} ({:?})", user.name, user.id); 90 | } 91 | Ok(()) 92 | } 93 | } 94 | 95 | #[test] 96 | fn test() { 97 | use narrative::story::RunStory as _; 98 | let mut env = UserStoryEnv { 99 | current_user_id: None, 100 | current_role: None, 101 | }; 102 | UserStoryContext.run_story(&mut env).unwrap(); 103 | } 104 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/output/story_trait.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{format_ident, quote}; 3 | 4 | use crate::{Asyncness, item_story::ItemStory, output::step_fn}; 5 | 6 | pub(crate) fn generate(input: &ItemStory, asyncness: Asyncness) -> TokenStream { 7 | let ident = match asyncness { 8 | Asyncness::Sync => input.ident.clone(), 9 | Asyncness::Async => format_ident!("Async{}", input.ident), 10 | }; 11 | let steps = input.items.iter().filter_map(|item| match item { 12 | crate::item_story::StoryItem::Step(step) => Some(step_fn::generate(step, asyncness)), 13 | _ => None, 14 | }); 15 | let attrs = &input.attrs; 16 | quote! { 17 | #(#attrs)* 18 | pub trait #ident { 19 | // no std::error::Error bound here for flexibility in use 20 | type Error; 21 | #(#steps;)* 22 | } 23 | } 24 | } 25 | 26 | #[cfg(test)] 27 | mod tests { 28 | use super::*; 29 | use pretty_assertions::assert_eq; 30 | 31 | #[test] 32 | fn test_trait_visibility() { 33 | let input = syn::parse_quote! { 34 | trait UserStory { 35 | #[step("Step 1")] 36 | fn step1(); 37 | #[step("Step 2", user_id = UserId::new())] 38 | fn step2(user_id: UserId); 39 | } 40 | }; 41 | let actual = generate(&input, Asyncness::Sync); 42 | let expected = quote! { 43 | pub trait UserStory { 44 | type Error; 45 | fn step1(&mut self) -> Result<(), Self::Error>; 46 | fn step2(&mut self, user_id: UserId) -> Result<(), Self::Error>; 47 | } 48 | }; 49 | assert_eq!(actual.to_string(), expected.to_string()); 50 | } 51 | 52 | #[test] 53 | fn test_async() { 54 | let input = syn::parse_quote! { 55 | trait UserStory { 56 | #[step("Step 1")] 57 | fn step1(); 58 | #[step("Step 2", user_id = UserId::new())] 59 | fn step2(user_id: UserId); 60 | } 61 | }; 62 | let actual = generate(&input, Asyncness::Async); 63 | let expected = quote! { 64 | pub trait AsyncUserStory { 65 | type Error; 66 | fn step1(&mut self) -> impl std::future::Future> + Send; 67 | fn step2(&mut self, user_id: UserId) -> impl std::future::Future> + Send; 68 | } 69 | }; 70 | assert_eq!(actual.to_string(), expected.to_string()); 71 | } 72 | 73 | #[test] 74 | fn test_trait_with_doc_attr() { 75 | let input = syn::parse_quote! { 76 | /// This is a my first story. 77 | trait MyFirstStory { 78 | #[step("Step 1")] 79 | fn step1(); 80 | } 81 | }; 82 | let actual = generate(&input, Asyncness::Sync); 83 | let expected = quote! { 84 | /// This is a my first story. 85 | pub trait MyFirstStory { 86 | type Error; 87 | fn step1(&mut self) -> Result<(), Self::Error>; 88 | } 89 | }; 90 | assert_eq!(actual.to_string(), expected.to_string()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/output/base_trait.rs: -------------------------------------------------------------------------------- 1 | // The generated trait is used as a super trait of the story trait, and provides `Self::*` items 2 | // for coupled data types. 3 | // Sealing the trait is not necessary because we have blanket impls of this for all types. 4 | 5 | use proc_macro2::TokenStream; 6 | use quote::{format_ident, quote}; 7 | 8 | use crate::{ 9 | item_story::{story_const::StoryConst, ItemStory}, 10 | Asyncness, 11 | }; 12 | 13 | pub fn generate(input: &ItemStory, asyncness: Asyncness) -> TokenStream { 14 | let trait_ident = match asyncness { 15 | Asyncness::Sync => quote!(BaseTrait), 16 | Asyncness::Async => quote!(AsyncBaseTrait), 17 | }; 18 | let story_trait_ident = match asyncness { 19 | Asyncness::Sync => input.ident.clone(), 20 | Asyncness::Async => format_ident!("Async{}", input.ident), 21 | }; 22 | let consts = input.consts().map(|StoryConst { raw, .. }| { 23 | let ident = &raw.ident; 24 | let ty = &raw.ty; 25 | Some(quote! { 26 | const #ident: #ty; 27 | }) 28 | }); 29 | let consts_assigns = input.consts().map(|StoryConst { raw, .. }| raw); 30 | quote! { 31 | pub trait #trait_ident { 32 | #(#consts)* 33 | type Context: narrative::story::StoryContext; 34 | const CONTEXT: Self::Context; 35 | } 36 | impl #trait_ident for T { 37 | #(#consts_assigns)* 38 | type Context = StoryContext; 39 | const CONTEXT: StoryContext = StoryContext; 40 | } 41 | } 42 | } 43 | 44 | #[cfg(test)] 45 | mod tests { 46 | use super::*; 47 | use pretty_assertions::assert_eq; 48 | 49 | #[test] 50 | fn test_empty() { 51 | let story_syntax = syn::parse_quote! { 52 | trait User { 53 | #[step("Step 1")] 54 | fn step1(); 55 | #[step("Step 2")] 56 | fn step2(); 57 | } 58 | }; 59 | let actual = generate(&story_syntax, Asyncness::Sync); 60 | let expected = quote! { 61 | pub trait BaseTrait { 62 | type Context: narrative::story::StoryContext; 63 | const CONTEXT: Self::Context; 64 | } 65 | impl BaseTrait for T { 66 | type Context = StoryContext; 67 | const CONTEXT: StoryContext = StoryContext; 68 | } 69 | }; 70 | assert_eq!(actual.to_string(), expected.to_string()); 71 | } 72 | 73 | 74 | #[test] 75 | fn test_async() { 76 | let story_syntax = syn::parse_quote! { 77 | trait User { 78 | #[step("Step 1")] 79 | async fn step1(); 80 | #[step("Step 2")] 81 | async fn step2(); 82 | } 83 | }; 84 | let actual = generate(&story_syntax, Asyncness::Async); 85 | let expected = quote! { 86 | pub trait AsyncBaseTrait { 87 | type Context: narrative::story::StoryContext; 88 | const CONTEXT: Self::Context; 89 | } 90 | impl AsyncBaseTrait for T { 91 | type Context = StoryContext; 92 | const CONTEXT: StoryContext = StoryContext; 93 | } 94 | }; 95 | assert_eq!(actual.to_string(), expected.to_string()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/item_story.rs: -------------------------------------------------------------------------------- 1 | pub mod story_const; 2 | pub mod story_item; 3 | pub mod story_step; 4 | 5 | use syn::{ 6 | Token, braced, 7 | parse::{Parse, ParseStream}, 8 | }; 9 | 10 | pub use story_item::StoryItem; 11 | pub use story_step::StoryStep; 12 | 13 | use self::story_const::StoryConst; 14 | 15 | pub struct ItemStory { 16 | pub attrs: Vec, 17 | #[allow(dead_code)] 18 | pub trait_token: Token![trait], 19 | pub ident: syn::Ident, 20 | #[allow(dead_code)] 21 | pub brace_token: syn::token::Brace, 22 | pub items: Vec, 23 | } 24 | 25 | impl Parse for ItemStory { 26 | fn parse(input: ParseStream) -> syn::Result { 27 | let attrs = input.call(syn::Attribute::parse_outer)?; 28 | let story_token = input.parse::()?; 29 | let ident = input.parse()?; 30 | let content; 31 | let brace_token = braced!(content in input); 32 | let mut items = Vec::new(); 33 | while !content.is_empty() { 34 | items.push(content.parse()?); 35 | } 36 | Ok(Self { 37 | attrs, 38 | trait_token: story_token, 39 | ident, 40 | brace_token, 41 | items, 42 | }) 43 | } 44 | } 45 | 46 | impl ItemStory { 47 | pub(crate) fn consts(&self) -> impl Iterator { 48 | self.items.iter().filter_map(|item| match item { 49 | StoryItem::Const(item) => Some(item), 50 | _ => None, 51 | }) 52 | } 53 | pub(crate) fn steps(&self) -> impl Iterator { 54 | self.items.iter().filter_map(|item| match item { 55 | StoryItem::Step(step) => Some(step), 56 | _ => None, 57 | }) 58 | } 59 | pub(crate) fn find_assignments<'a>(&'a self, ident: &'a syn::Ident) -> Option<&'a syn::Expr> { 60 | self.consts().find_map(|StoryConst { raw, default }| { 61 | if raw.ident == *ident { 62 | Some(&default.1) 63 | } else { 64 | None 65 | } 66 | }) 67 | } 68 | } 69 | 70 | #[cfg(test)] 71 | mod tests { 72 | use super::*; 73 | use quote::quote; 74 | 75 | #[test] 76 | fn parse_story() { 77 | let input = quote! { 78 | trait MyFirstStory { 79 | const user_id: String = "test-id".to_string(); 80 | 81 | #[step("Hi, I'm a user")] 82 | fn as_a_user(); 83 | #[step("I have an apple", count = 1)] 84 | fn have_one_apple(count: u32); 85 | } 86 | }; 87 | let ItemStory { 88 | attrs: _, 89 | trait_token: _, 90 | ident, 91 | brace_token: _, 92 | items, 93 | } = syn::parse2(input).expect("parse a story"); 94 | assert_eq!(ident, "MyFirstStory"); 95 | assert_eq!(items.len(), 3); 96 | assert!(matches!(items[0], StoryItem::Const { .. })); 97 | assert!(matches!(items[1], StoryItem::Step(_))); 98 | assert!(matches!(items[2], StoryItem::Step(_))); 99 | } 100 | 101 | #[test] 102 | fn parse_story_with_doc_attr() { 103 | let input = quote! { 104 | /// This is a my first story. 105 | trait MyFirstStory { 106 | const user_id: String = "test-id".to_string(); 107 | 108 | #[step("Hi, I'm a user")] 109 | fn as_a_user(); 110 | } 111 | }; 112 | let ItemStory { 113 | attrs, 114 | ident, 115 | items, 116 | .. 117 | } = syn::parse2(input).expect("parse a story with doc attr"); 118 | assert_eq!(ident, "MyFirstStory"); 119 | assert_eq!(attrs.len(), 1); 120 | assert!(attrs[0].path().is_ident("doc")); 121 | assert_eq!(items.len(), 2); 122 | assert!(matches!(items[0], StoryItem::Const { .. })); 123 | assert!(matches!(items[1], StoryItem::Step(_))); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /test-suite/src/multiple_stories_in_one_file.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use narrative::environment::DummyEnvironment; 4 | 5 | #[narrative::story("Say hello world")] 6 | trait SubHelloWorld { 7 | #[step("Say hello")] 8 | fn say_hello(); 9 | #[step("Say world")] 10 | fn say_world(); 11 | } 12 | 13 | #[narrative::story("Say hello world")] 14 | trait HelloWorld { 15 | const A: &str = "a"; 16 | #[step("Say hello")] 17 | fn say_hello(); 18 | #[step("Say world")] 19 | fn say_world(); 20 | #[step(story: SubHelloWorld, "Say hello world in sub story")] 21 | fn say_hello_world(); 22 | } 23 | 24 | #[narrative::story("Say hello world 2")] 25 | trait HelloWorld2 { 26 | const A: &str = "a"; 27 | #[step("Say hello")] 28 | fn say_hello(); 29 | #[step("Say world")] 30 | fn say_world(); 31 | #[step(story: SubHelloWorld, "Say hello world in sub story")] 32 | fn say_hello_world(); 33 | } 34 | 35 | #[narrative::story("Say hello world 3")] 36 | trait HelloWorld3 { 37 | const A: &str = "a"; 38 | #[step("Say hello")] 39 | fn say_hello(); 40 | #[step("Say world")] 41 | fn say_world(); 42 | #[step(story: SubHelloWorld, "Say hello world in sub story")] 43 | fn say_hello_world(); 44 | } 45 | 46 | #[allow(dead_code)] 47 | pub struct Env; 48 | 49 | impl HelloWorld for Env { 50 | type Error = Infallible; 51 | 52 | fn say_hello(&mut self) -> Result<(), Self::Error> { 53 | todo!() 54 | } 55 | 56 | fn say_world(&mut self) -> Result<(), Self::Error> { 57 | todo!() 58 | } 59 | 60 | fn say_hello_world(&mut self) -> Result, Self::Error> { 61 | Ok(DummyEnvironment::::default()) 62 | } 63 | } 64 | 65 | impl AsyncHelloWorld for Env { 66 | type Error = Infallible; 67 | 68 | async fn say_hello(&mut self) -> Result<(), Self::Error> { 69 | todo!() 70 | } 71 | 72 | async fn say_world(&mut self) -> Result<(), Self::Error> { 73 | todo!() 74 | } 75 | 76 | fn say_hello_world( 77 | &mut self, 78 | ) -> Result, Self::Error> { 79 | Ok(DummyEnvironment::::default()) 80 | } 81 | } 82 | 83 | impl HelloWorld2 for Env { 84 | type Error = Infallible; 85 | 86 | fn say_hello(&mut self) -> Result<(), Self::Error> { 87 | todo!() 88 | } 89 | 90 | fn say_world(&mut self) -> Result<(), Self::Error> { 91 | todo!() 92 | } 93 | 94 | fn say_hello_world(&mut self) -> Result, Self::Error> { 95 | Ok(DummyEnvironment::::default()) 96 | } 97 | } 98 | 99 | impl AsyncHelloWorld2 for Env { 100 | type Error = Infallible; 101 | 102 | async fn say_hello(&mut self) -> Result<(), Self::Error> { 103 | todo!() 104 | } 105 | 106 | async fn say_world(&mut self) -> Result<(), Self::Error> { 107 | todo!() 108 | } 109 | 110 | fn say_hello_world( 111 | &mut self, 112 | ) -> Result, Self::Error> { 113 | Ok(DummyEnvironment::::default()) 114 | } 115 | } 116 | 117 | impl HelloWorld3 for Env { 118 | type Error = Infallible; 119 | 120 | fn say_hello(&mut self) -> Result<(), Self::Error> { 121 | todo!() 122 | } 123 | 124 | fn say_world(&mut self) -> Result<(), Self::Error> { 125 | todo!() 126 | } 127 | 128 | fn say_hello_world(&mut self) -> Result, Self::Error> { 129 | Ok(DummyEnvironment::::default()) 130 | } 131 | } 132 | 133 | impl AsyncHelloWorld3 for Env { 134 | type Error = Infallible; 135 | 136 | async fn say_hello(&mut self) -> Result<(), Self::Error> { 137 | todo!() 138 | } 139 | 140 | async fn say_world(&mut self) -> Result<(), Self::Error> { 141 | todo!() 142 | } 143 | 144 | fn say_hello_world( 145 | &mut self, 146 | ) -> Result, Self::Error> { 147 | Ok(DummyEnvironment::::default()) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /crates/narrative/src/independent_type.rs: -------------------------------------------------------------------------------- 1 | // important! this must be sealed to prevent downstream crates from implementing it. 2 | mod private { 3 | // Theorically, supertraits are not required, but it helps to maintain the list of supported types. 4 | // This is a trait implemented for types that can be used without dependencies. 5 | // Debug is required to format step arguments. 6 | // Clone is required to ensure the semantics of shared arguments are replicated. 7 | // Serialize is required to send arguments to external runners. 8 | pub trait SealedIndependentType: std::fmt::Debug + Clone + serde::Serialize {} 9 | } 10 | use private::SealedIndependentType; 11 | 12 | pub trait IndependentType: SealedIndependentType {} 13 | 14 | impl IndependentType for T {} 15 | 16 | macro_rules! local { 17 | ($($ty:ty),*) => { 18 | $( 19 | impl SealedIndependentType for $ty {} 20 | )* 21 | }; 22 | ($gen1:tt; $($ty:ty),*) => { 23 | $( 24 | impl<$gen1: SealedIndependentType> SealedIndependentType for $ty {} 25 | )* 26 | }; 27 | ($gen1:ident, $gen2:ident; $($ty:ty),*) => { 28 | $( 29 | impl<$gen1: SealedIndependentType, $gen2: SealedIndependentType> SealedIndependentType for $ty {} 30 | )* 31 | }; 32 | } 33 | 34 | macro_rules! local_tuple { 35 | ($($ty:ident),*) => { 36 | impl<$($ty: SealedIndependentType),*> SealedIndependentType for ($($ty,)*) {} 37 | }; 38 | } 39 | 40 | macro_rules! local_array { 41 | ($($num:tt),*) => { 42 | $( 43 | impl SealedIndependentType for [T; $num] {} 44 | )* 45 | }; 46 | } 47 | 48 | local!( 49 | String, 50 | &str, 51 | (), 52 | bool, 53 | char, 54 | u8, 55 | u16, 56 | u32, 57 | u64, 58 | u128, 59 | usize, 60 | i8, 61 | i16, 62 | i32, 63 | i64, 64 | i128, 65 | isize, 66 | f32, 67 | f64 68 | ); 69 | local!( 70 | std::num::NonZeroU8, 71 | std::num::NonZeroU16, 72 | std::num::NonZeroU32, 73 | std::num::NonZeroU64, 74 | std::num::NonZeroU128, 75 | std::num::NonZeroUsize, 76 | std::num::NonZeroI8, 77 | std::num::NonZeroI16, 78 | std::num::NonZeroI32, 79 | std::num::NonZeroI64, 80 | std::num::NonZeroI128, 81 | std::num::NonZeroIsize 82 | ); 83 | local!( 84 | std::time::Duration, 85 | std::time::SystemTime, 86 | std::path::PathBuf 87 | ); 88 | local!(std::ffi::OsString, std::ffi::CString); 89 | local!( 90 | std::net::IpAddr, 91 | std::net::Ipv4Addr, 92 | std::net::Ipv6Addr, 93 | std::net::SocketAddr, 94 | std::net::SocketAddrV4, 95 | std::net::SocketAddrV6 96 | ); 97 | 98 | local!(T; std::ops::Range, std::ops::RangeFrom, std::ops::RangeTo, std::ops::RangeInclusive, std::ops::Bound); 99 | 100 | local_tuple!(A); 101 | local_tuple!(A, B); 102 | local_tuple!(A, B, C); 103 | local_tuple!(A, B, C, D); 104 | 105 | local_tuple!(A, B, C, D, E); 106 | local_tuple!(A, B, C, D, E, F); 107 | local_tuple!(A, B, C, D, E, F, G); 108 | local_tuple!(A, B, C, D, E, F, G, H); 109 | 110 | local_tuple!(A, B, C, D, E, F, G, H, I); 111 | local_tuple!(A, B, C, D, E, F, G, H, I, J); 112 | local_tuple!(A, B, C, D, E, F, G, H, I, J, K); 113 | local_tuple!(A, B, C, D, E, F, G, H, I, J, K, L); 114 | 115 | local_array!( 116 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 117 | 26, 27, 28, 29, 30, 31, 32 118 | ); 119 | 120 | #[cfg(feature = "serde_json")] 121 | impl SealedIndependentType for serde_json::Value {} 122 | 123 | #[cfg(feature = "chrono")] 124 | mod chrono_impls { 125 | use super::*; 126 | impl SealedIndependentType for chrono::NaiveDate {} 127 | impl SealedIndependentType for chrono::NaiveTime {} 128 | impl SealedIndependentType for chrono::NaiveDateTime {} 129 | impl SealedIndependentType for chrono::DateTime {} 130 | impl SealedIndependentType for chrono::DateTime {} 131 | impl SealedIndependentType for chrono::DateTime {} 132 | impl SealedIndependentType for chrono::Duration {} 133 | } 134 | 135 | #[cfg(feature = "uuid")] 136 | impl SealedIndependentType for uuid::Uuid {} 137 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod extract_types_for_assertion; 3 | mod item_story; 4 | mod local_type_for; 5 | mod no_foreign_type_validation; 6 | mod output; 7 | mod step_attr_syntax; 8 | mod step_usage; 9 | mod story_attr_syntax; 10 | 11 | use item_story::ItemStory; 12 | use proc_macro2::TokenStream; 13 | use story_attr_syntax::StoryAttr; 14 | use syn::parse_macro_input; 15 | 16 | #[proc_macro_attribute] 17 | /// TODO: Add documentation. 18 | pub fn story( 19 | attr: proc_macro::TokenStream, 20 | input: proc_macro::TokenStream, 21 | ) -> proc_macro::TokenStream { 22 | let attr = parse_macro_input!(attr as StoryAttr); 23 | let story = parse_macro_input!(input as ItemStory); 24 | process_story(attr, story).into() 25 | } 26 | 27 | #[proc_macro_attribute] 28 | /// Marks a data type as a local type for a specific story. 29 | /// This implements both `IndependentType` and `LocalType` for the type. 30 | pub fn local_type_for( 31 | attr: proc_macro::TokenStream, 32 | input: proc_macro::TokenStream, 33 | ) -> proc_macro::TokenStream { 34 | let story_name = parse_macro_input!(attr as syn::Ident); 35 | let input_item = parse_macro_input!(input as syn::Item); 36 | 37 | local_type_for::generate(&story_name, &input_item).into() 38 | } 39 | 40 | // In general, we don't do caching some intermediate results to keep the implementation simple. 41 | // However, we should avoid to have heavy computation in this crate, to keep the story compilation 42 | // fast. So, modules have their own functionality which is simple. 43 | fn process_story(attr: StoryAttr, story: ItemStory) -> TokenStream { 44 | output::generate(&attr, &story) 45 | } 46 | 47 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 48 | pub(crate) enum Asyncness { 49 | Sync, 50 | Async, 51 | } 52 | 53 | impl quote::ToTokens for Asyncness { 54 | fn to_tokens(&self, tokens: &mut TokenStream) { 55 | match self { 56 | Asyncness::Sync => quote::quote!().to_tokens(tokens), 57 | Asyncness::Async => quote::quote!(async).to_tokens(tokens), 58 | } 59 | } 60 | } 61 | 62 | pub(crate) fn collect_format_args(lit_str: &syn::LitStr) -> Vec { 63 | lit_str 64 | .value() 65 | // remove escaped braces 66 | .split("{{") 67 | .flat_map(|part| part.split("}}")) 68 | // iter parts that start with '{' by skipping the first split 69 | .flat_map(|part| part.split('{').skip(1)) 70 | // take the part before the first '}' 71 | .filter_map(|part| part.split_once('}').map(|(head, _)| head)) 72 | // remove parts after the first ':' 73 | .map(|format| { 74 | format 75 | .split_once(':') 76 | .map(|(head, _)| head) 77 | .unwrap_or(format) 78 | }) 79 | .map(ToOwned::to_owned) 80 | .collect() 81 | } 82 | 83 | struct MakeStaticWalker; 84 | 85 | impl syn::visit_mut::VisitMut for MakeStaticWalker { 86 | fn visit_type_reference_mut(&mut self, i: &mut syn::TypeReference) { 87 | i.lifetime = Some(syn::Lifetime::new( 88 | "'static", 89 | proc_macro2::Span::mixed_site(), 90 | )); 91 | self.visit_type_mut(&mut i.elem); 92 | } 93 | } 94 | 95 | pub(crate) fn make_static(ty: &syn::Type) -> syn::Type { 96 | use syn::visit_mut::VisitMut; 97 | let mut walker = MakeStaticWalker; 98 | let mut static_ty = ty.clone(); 99 | walker.visit_type_mut(&mut static_ty); 100 | static_ty 101 | } 102 | 103 | pub(crate) fn pretty_print_expr(expr: &syn::Expr) -> String { 104 | prettyplease::unparse( 105 | &syn::parse_file( 106 | "e::quote! { 107 | const IDENT: String = #expr; 108 | } 109 | .to_string(), 110 | ) 111 | .unwrap(), 112 | ) 113 | .replace("const IDENT: String = ", "") 114 | .replace(";", "") 115 | .trim() 116 | .to_string() 117 | } 118 | 119 | pub(crate) fn pretty_print_type(ty: &syn::Type) -> String { 120 | prettyplease::unparse( 121 | &syn::parse_file( 122 | "e::quote! { 123 | const IDENT: #ty = 1; 124 | } 125 | .to_string(), 126 | ) 127 | .unwrap(), 128 | ) 129 | .replace("const IDENT: ", "") 130 | .replace(" = 1;", "") 131 | .trim() 132 | .to_string() 133 | } 134 | -------------------------------------------------------------------------------- /crates/narrative/src/step.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | runner::{AsyncStoryRunner, StoryRunner}, 3 | story::{DynStoryContext, StoryContext}, 4 | value::{BoxedValue, Value}, 5 | }; 6 | 7 | // T and E can be any type by implementing the story trait in any way. 8 | pub trait Step { 9 | /// Returns the text representation of the step. 10 | fn step_text(&self) -> String; 11 | /// Returns the id, which is the method name, of the step. 12 | fn step_id(&self) -> &'static str; 13 | /// Returns the arguments of the step. 14 | fn args( 15 | &self, 16 | ) -> impl Iterator + Send + Sync + 'static; 17 | /// Returns the parent story of the step. 18 | fn story(&self) -> impl StoryContext + Send + Sync + 'static; 19 | /// Returns the sub story that this step references. 20 | fn nested_story(&self) -> Option; 21 | } 22 | 23 | pub trait Run: Step { 24 | /// Runs the step. 25 | fn run(&self, story: &mut T) -> Result<(), E>; 26 | /// Runs the step, but with a runner if the step has a sub story. 27 | fn run_with_runner(&self, story: &mut T, runner: &mut impl StoryRunner) -> Result<(), E>; 28 | } 29 | 30 | pub trait RunAsync: Step { 31 | /// Runs the step asynchronously. 32 | fn run_async(&self, story: &mut T) -> impl std::future::Future> + Send; 33 | /// Runs the step asynchronously, but with a runner if the step has a sub story. 34 | fn run_with_runner_async( 35 | &self, 36 | story: &mut T, 37 | runner: &mut (impl AsyncStoryRunner + Send), 38 | ) -> impl std::future::Future> + Send; 39 | } 40 | 41 | pub trait StepArg: Clone + std::fmt::Debug { 42 | /// Returns the name of the argument. 43 | fn name(&self) -> &'static str; 44 | /// Returns the type of the argument. 45 | fn ty(&self) -> &'static str; 46 | /// Returns the real expression of the argument. 47 | fn expr(&self) -> &'static str; 48 | /// Returns the actual value of the argument. 49 | fn value(&self) -> impl Value; 50 | // TODO: fn schema() -> Schema; 51 | } 52 | 53 | #[derive(Clone, Copy)] 54 | pub struct DynStep { 55 | step_text: fn() -> String, 56 | step_id: &'static str, 57 | args: fn() -> Box + Send + Sync>, 58 | story: fn() -> DynStoryContext, 59 | nested_story: fn() -> Option, 60 | } 61 | 62 | impl DynStep { 63 | pub const fn new( 64 | step_text: fn() -> String, 65 | step_id: &'static str, 66 | args: fn() -> Box + Send + Sync>, 67 | story: fn() -> DynStoryContext, 68 | nested_story: fn() -> Option, 69 | ) -> Self { 70 | Self { 71 | step_text, 72 | step_id, 73 | args, 74 | story, 75 | nested_story, 76 | } 77 | } 78 | } 79 | 80 | #[derive(Clone, Copy)] 81 | pub struct DynStepArg { 82 | name: &'static str, 83 | ty: &'static str, 84 | expr: &'static str, 85 | value: fn() -> BoxedValue, 86 | step_value: fn() -> BoxedValue, 87 | } 88 | 89 | impl DynStepArg { 90 | pub const fn new( 91 | name: &'static str, 92 | ty: &'static str, 93 | expr: &'static str, 94 | value: fn() -> BoxedValue, 95 | step_value: fn() -> BoxedValue, 96 | ) -> Self { 97 | Self { 98 | name, 99 | ty, 100 | expr, 101 | value, 102 | step_value, 103 | } 104 | } 105 | } 106 | 107 | impl std::fmt::Debug for DynStepArg { 108 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 109 | (self.step_value)().fmt(f) 110 | } 111 | } 112 | 113 | impl Step for DynStep { 114 | fn step_text(&self) -> String { 115 | (self.step_text)() 116 | } 117 | 118 | fn step_id(&self) -> &'static str { 119 | self.step_id 120 | } 121 | 122 | fn args( 123 | &self, 124 | ) -> impl Iterator + Send + Sync + 'static 125 | { 126 | (self.args)() 127 | } 128 | 129 | fn story(&self) -> impl StoryContext + Send + Sync + 'static { 130 | (self.story)() 131 | } 132 | 133 | fn nested_story(&self) -> Option { 134 | (self.nested_story)() 135 | } 136 | } 137 | 138 | impl StepArg for DynStepArg { 139 | fn name(&self) -> &'static str { 140 | self.name 141 | } 142 | 143 | fn ty(&self) -> &'static str { 144 | self.ty 145 | } 146 | 147 | fn expr(&self) -> &'static str { 148 | self.expr 149 | } 150 | 151 | fn value(&self) -> impl Value { 152 | (self.value)() 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/output.rs: -------------------------------------------------------------------------------- 1 | // Separation of impls and assertions leads not only simple implementation but also good error 2 | // messages that indicate an outer dependency. 3 | mod local_type_assertions; 4 | mod local_type_impls; 5 | 6 | mod base_trait; 7 | mod step_fn; 8 | mod story_trait; 9 | 10 | mod step_args; 11 | mod step_types; 12 | mod story_consts; 13 | mod story_context; 14 | 15 | mod dummy_environment; 16 | 17 | use proc_macro2::TokenStream; 18 | use quote::{format_ident, quote, ToTokens}; 19 | 20 | use crate::{item_story::ItemStory, story_attr_syntax::StoryAttr, Asyncness}; 21 | 22 | pub(crate) fn generate(attr: &StoryAttr, item: &ItemStory) -> TokenStream { 23 | let mod_ident = format_ident!("mod_{}", item.ident); 24 | let ident = &item.ident; 25 | let async_ident = format_ident!("Async{}", item.ident); 26 | let context_ident = format_ident!("{}Context", item.ident); 27 | let base_trait = base_trait::generate(item, Asyncness::Sync); 28 | let async_base_trait = base_trait::generate(item, Asyncness::Async); 29 | let story_trait = story_trait::generate(item, Asyncness::Sync); 30 | let async_story_trait = story_trait::generate(item, Asyncness::Async); 31 | let step_args = step_args::generate(item); 32 | let step_types = step_types::generate(item); 33 | let story_consts = story_consts::generate(item); 34 | let story_context = story_context::generate(attr, item); 35 | let context_ext = story_context::generate_ext(item); 36 | let local_type_impls = local_type_impls::generate(item); 37 | let local_type_assertions = local_type_assertions::generate(item); 38 | let dummy_environment = dummy_environment::generate(item, Asyncness::Sync); 39 | let async_dummy_environment = dummy_environment::generate(item, Asyncness::Async); 40 | let local_type_trait = format_ident!("{}LocalType", ident); 41 | quote! { 42 | #[allow(non_snake_case, unused_imports)] 43 | mod #mod_ident { 44 | use super::*; 45 | #base_trait 46 | #async_base_trait 47 | #story_trait 48 | #async_story_trait 49 | #step_args 50 | #step_types 51 | #story_consts 52 | #story_context 53 | #context_ext 54 | #local_type_impls 55 | #local_type_assertions 56 | #dummy_environment 57 | #async_dummy_environment 58 | } 59 | #[allow(unused_imports)] 60 | use narrative::prelude::*; 61 | #[allow(unused_imports)] 62 | pub use #mod_ident::#ident; 63 | #[allow(unused_imports)] 64 | pub use #mod_ident::#async_ident; 65 | #[allow(unused_imports)] 66 | pub use #mod_ident::StoryContext as #context_ident; 67 | #[allow(unused_imports)] 68 | pub use #mod_ident::#local_type_trait; 69 | pub use #mod_ident::ContextExt as _; 70 | pub use #mod_ident::AsyncContextExt as _; 71 | pub use #mod_ident::BaseTrait as _; 72 | } 73 | } 74 | 75 | #[derive(Default)] 76 | struct MatchArms { 77 | arms: Vec, 78 | cast_as: Option, 79 | match_target: Option, 80 | fallback: Option, 81 | } 82 | 83 | impl MatchArms { 84 | pub fn cast_as(mut self, cast_as: TokenStream) -> Self { 85 | self.cast_as = Some(cast_as); 86 | self 87 | } 88 | 89 | pub fn match_target(mut self, match_target: TokenStream) -> Self { 90 | self.match_target = Some(match_target); 91 | self 92 | } 93 | } 94 | impl FromIterator for MatchArms { 95 | fn from_iter>(iter: T) -> Self { 96 | Self { 97 | arms: iter.into_iter().collect(), 98 | ..Default::default() 99 | } 100 | } 101 | } 102 | 103 | impl<'a> FromIterator<&'a TokenStream> for MatchArms { 104 | fn from_iter>(iter: T) -> Self { 105 | Self { 106 | arms: iter.into_iter().cloned().collect(), 107 | ..Default::default() 108 | } 109 | } 110 | } 111 | 112 | impl ToTokens for MatchArms { 113 | fn to_tokens(&self, tokens: &mut TokenStream) { 114 | let extend = if self.arms.is_empty() { 115 | if let Some(cast_as) = &self.cast_as { 116 | quote! { 117 | #[allow(unreachable_code)] 118 | { 119 | unreachable!() as #cast_as 120 | } 121 | } 122 | } else { 123 | quote! { 124 | unreachable!() 125 | } 126 | } 127 | } else { 128 | let arms = &self.arms; 129 | let match_target = self.match_target.clone().unwrap_or_else(|| quote!(self)); 130 | let fallback = self.fallback.as_ref().map(|fallback| { 131 | quote! { 132 | _ => #fallback, 133 | } 134 | }); 135 | quote! { 136 | match #match_target { 137 | #(#arms)* 138 | #fallback 139 | } 140 | } 141 | }; 142 | tokens.extend(::core::iter::once(extend)); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /crates/narrative/src/runner.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | 3 | use crate::{ 4 | step::{Run, RunAsync, Step}, 5 | story::{RunStory, RunStoryAsync, StoryContext}, 6 | }; 7 | 8 | /// A trait for running a story. 9 | pub trait StoryRunner { 10 | /// Called when the root or a nested story starts. 11 | fn start_story(&mut self, story: impl StoryContext) -> Result<(), E>; 12 | /// Called when the root or a nested story ends. 13 | fn end_story(&mut self, story: impl StoryContext) -> Result<(), E>; 14 | /// Executes a step. 15 | /// If you call `step.run_with_runner(env, self)`, the runner will be passed to the nested story if it exists. 16 | fn run_step(&mut self, step: T, state: &mut S) -> Result<(), E> 17 | where 18 | T: Step + Run; 19 | /// Executes a nested story referenced by a parent step. 20 | /// You can run a nested story step by step with the `Run` trait or all at once with the `RunStory` trait. 21 | /// If you call those methods with `_with_runner` variants with `self`, the runner will be passed to the nested story. 22 | /// 23 | /// # Example 24 | /// ```rust,ignore 25 | /// fn run_nested_story( 26 | /// &mut self, 27 | /// _step: impl narrative::step::Step, 28 | /// nested_story: S, 29 | /// env: &mut Env, 30 | /// ) -> Result<(), E> 31 | /// where 32 | /// S::Step: narrative::step::Run, 33 | /// S: narrative::story::StoryContext + narrative::story::RunStory, 34 | /// { 35 | /// if self.run_nested_story_step_by_step { 36 | /// for step in nested_story.steps() { 37 | /// step.run_with_runner(env, self)?; 38 | /// } 39 | /// } else { 40 | /// nested_story.run_story_with_runner(env, self) 41 | /// } 42 | /// } 43 | /// ``` 44 | fn run_nested_story( 45 | &mut self, 46 | step: impl Step, 47 | nested_story: S, 48 | env: &mut Env, 49 | ) -> Result<(), E> 50 | where 51 | S::Step: Run, 52 | S: StoryContext + RunStory; 53 | } 54 | 55 | /// A trait for running a story asynchronously. 56 | pub trait AsyncStoryRunner { 57 | /// Called when the root or a nested story starts. 58 | fn start_story(&mut self, story: impl StoryContext) -> Result<(), E>; 59 | /// Called when the root or a nested story ends. 60 | fn end_story(&mut self, story: impl StoryContext) -> Result<(), E>; 61 | /// Executes a step asynchronously. 62 | /// If you call `step.run_with_runner_async(env, self)`, the runner will be passed to the nested story if it exists. 63 | fn run_step_async( 64 | &mut self, 65 | step: T, 66 | state: &mut Env, 67 | ) -> impl Future> + Send 68 | where 69 | T: Step + RunAsync + Send + Sync, 70 | Env: Send; 71 | /// Executes a nested story referenced by a parent step asynchronously. 72 | /// See [StoryRunner::run_nested_story] for more details. 73 | fn run_nested_story_async( 74 | &mut self, 75 | step: impl Step + Send, 76 | nested_story: S, 77 | env: &mut Env, 78 | ) -> impl Future> + Send 79 | where 80 | S: StoryContext + RunStoryAsync + Send + Sync, 81 | Env: Send, 82 | S::Step: RunAsync + Send + Sync; 83 | } 84 | 85 | /// The default story runner that executes steps sequentially without extra logic. 86 | pub struct DefaultStoryRunner; 87 | 88 | impl StoryRunner for DefaultStoryRunner { 89 | #[inline] 90 | fn start_story(&mut self, _story: impl StoryContext) -> Result<(), E> { 91 | Ok(()) 92 | } 93 | 94 | #[inline] 95 | fn end_story(&mut self, _story: impl StoryContext) -> Result<(), E> { 96 | Ok(()) 97 | } 98 | 99 | #[inline] 100 | fn run_step(&mut self, step: T, state: &mut S) -> Result<(), E> 101 | where 102 | T: Step + Run, 103 | { 104 | step.run(state) 105 | } 106 | 107 | #[inline] 108 | fn run_nested_story, NestedImpl>( 109 | &mut self, 110 | _parent_step: impl Step, 111 | nested_context: NestedCtx, 112 | nested_impl: &mut NestedImpl, 113 | ) -> Result<(), E> 114 | where 115 | NestedCtx::Step: Run, 116 | { 117 | nested_context.run_story(nested_impl) 118 | } 119 | } 120 | 121 | impl AsyncStoryRunner for DefaultStoryRunner { 122 | #[inline] 123 | fn start_story(&mut self, _story: impl StoryContext) -> Result<(), E> { 124 | Ok(()) 125 | } 126 | 127 | #[inline] 128 | fn end_story(&mut self, _story: impl StoryContext) -> Result<(), E> { 129 | Ok(()) 130 | } 131 | 132 | #[inline] 133 | async fn run_step_async(&mut self, step: T, state: &mut Env) -> Result<(), E> 134 | where 135 | T: Step + RunAsync + Send + Sync, 136 | Env: Send, 137 | { 138 | step.run_async(state).await 139 | } 140 | 141 | #[inline] 142 | async fn run_nested_story_async( 143 | &mut self, 144 | _step: impl Step + Send, 145 | nested_story: S, 146 | env: &mut Env, 147 | ) -> Result<(), E> 148 | where 149 | S: StoryContext + RunStoryAsync + Send + Sync, 150 | Env: Send, 151 | S::Step: RunAsync + Send + Sync, 152 | { 153 | nested_story.run_story_async(env).await 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/item_story/story_step.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::ToTokens; 3 | use syn::parse::{Parse, ParseStream}; 4 | 5 | use crate::{ 6 | collect_format_args, 7 | step_attr_syntax::{StepAttr, StoryType}, 8 | }; 9 | 10 | pub struct StoryStep { 11 | pub step_attr: StepAttr, 12 | pub other_attrs: Vec, 13 | pub inner: syn::TraitItemFn, 14 | } 15 | 16 | impl Parse for StoryStep { 17 | fn parse(input: ParseStream) -> syn::Result { 18 | let mut attrs = input.call(syn::Attribute::parse_outer)?; 19 | let Some(position) = attrs.iter().position(|attr| attr.path().is_ident("step")) else { 20 | return Err(syn::Error::new( 21 | input.span(), 22 | "expected #[step(...)] attribute", 23 | )); 24 | }; 25 | let step_attr = attrs.remove(position); 26 | let step_attr = syn::parse2::(quote::quote! { #step_attr })?; 27 | let inner = input.parse()?; 28 | Ok(Self { 29 | step_attr, 30 | other_attrs: attrs, 31 | inner, 32 | }) 33 | } 34 | } 35 | 36 | impl ToTokens for StoryStep { 37 | fn to_tokens(&self, tokens: &mut TokenStream) { 38 | self.step_attr.to_tokens(tokens); 39 | self.inner.to_tokens(tokens); 40 | } 41 | } 42 | 43 | impl StoryStep { 44 | pub(crate) fn attr_args(&self) -> impl Iterator { 45 | self.step_attr 46 | .args 47 | .iter() 48 | .map(|arg| (&arg.ident, &arg.value)) 49 | } 50 | 51 | /// This ignores patterns. 52 | pub(crate) fn fn_args(&self) -> impl Iterator { 53 | self.inner 54 | .sig 55 | .inputs 56 | .iter() 57 | .filter_map(|input| match input { 58 | syn::FnArg::Typed(pat_type) => { 59 | if let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() { 60 | Some((&pat_ident.ident, pat_type.ty.as_ref())) 61 | } else { 62 | None 63 | } 64 | } 65 | syn::FnArg::Receiver(_) => None, 66 | }) 67 | } 68 | 69 | pub(crate) fn find_attr_arg<'a>(&'a self, ident: &'a syn::Ident) -> Option<&'a syn::Expr> { 70 | self.attr_args().find_map(move |(arg_ident, arg_value)| { 71 | if arg_ident == ident { 72 | Some(arg_value) 73 | } else { 74 | None 75 | } 76 | }) 77 | } 78 | 79 | pub(crate) fn extract_format_args(&self) -> Vec { 80 | collect_format_args(&self.step_attr.text) 81 | } 82 | 83 | pub(crate) fn has_sub_story(&self) -> bool { 84 | self.step_attr.story_type.is_some() 85 | } 86 | 87 | /// Gets the path to the sub-story type if this is a sub-story step 88 | pub(crate) fn sub_story_path(&self) -> Option<&StoryType> { 89 | self.step_attr.story_type.as_ref() 90 | } 91 | } 92 | 93 | #[cfg(test)] 94 | mod tests { 95 | use super::*; 96 | use quote::quote; 97 | 98 | #[test] 99 | fn parse_step() { 100 | let input = quote! { 101 | #[step("Step 1")] 102 | fn step1(); 103 | }; 104 | let actual = syn::parse2::(input).unwrap(); 105 | assert_eq!(actual.step_attr.text.value(), "Step 1".to_string()); 106 | assert_eq!(actual.inner.sig.ident.to_string(), "step1".to_string()); 107 | assert!(actual.sub_story_path().is_none()); 108 | } 109 | 110 | #[test] 111 | fn parse_sub_story_step() { 112 | let input = quote! { 113 | #[step(story: SubStory, "do sub story")] 114 | fn step_with_sub(); 115 | }; 116 | let actual = syn::parse2::(input).unwrap(); 117 | assert_eq!(actual.step_attr.text.value(), "do sub story".to_string()); 118 | assert_eq!( 119 | actual.inner.sig.ident.to_string(), 120 | "step_with_sub".to_string() 121 | ); 122 | assert!(actual.sub_story_path().unwrap().path.is_ident("SubStory")); 123 | } 124 | 125 | #[test] 126 | fn to_tokens() { 127 | let input = quote! { 128 | #[step("Step 1")] 129 | fn step1(); 130 | }; 131 | let actual = syn::parse2::(input).unwrap(); 132 | let actual = quote! { 133 | #actual 134 | }; 135 | let expected = quote! { 136 | #[step("Step 1")] 137 | fn step1(); 138 | }; 139 | assert_eq!(actual.to_string(), expected.to_string()); 140 | } 141 | 142 | #[test] 143 | fn to_tokens_sub_story() { 144 | let input = quote! { 145 | #[step(story: SubStory, "do sub story")] 146 | fn step_with_sub(); 147 | }; 148 | let actual = syn::parse2::(input).unwrap(); 149 | let actual = quote! { 150 | #actual 151 | }; 152 | let expected = quote! { 153 | #[step(story: SubStory, "do sub story")] 154 | fn step_with_sub(); 155 | }; 156 | assert_eq!(actual.to_string(), expected.to_string()); 157 | } 158 | 159 | #[test] 160 | fn parse_step_with_other_attrs() { 161 | let input = quote! { 162 | #[doc("This is a step")] 163 | #[step("Step 1")] 164 | fn step1(); 165 | }; 166 | let actual = syn::parse2::(input).unwrap(); 167 | assert_eq!(actual.other_attrs.len(), 1); 168 | assert!(actual.other_attrs[0].path().is_ident("doc")); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/output/dummy_environment.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{format_ident, quote}; 3 | 4 | use crate::{item_story::ItemStory, output::step_fn, Asyncness}; 5 | 6 | pub(crate) fn generate(input: &ItemStory, asyncness: Asyncness) -> TokenStream { 7 | let ident = match asyncness { 8 | Asyncness::Sync => input.ident.clone(), 9 | Asyncness::Async => format_ident!("Async{}", input.ident), 10 | }; 11 | let generics = match asyncness { 12 | Asyncness::Sync => quote!(), 13 | Asyncness::Async => quote!(), 14 | }; 15 | let steps = input.steps().map(|step| { 16 | let step_fn = step_fn::generate(step, asyncness); 17 | let body = if step.has_sub_story() { 18 | quote!(Ok( 19 | narrative::environment::DummyEnvironment::::default() 20 | )) 21 | } else { 22 | match asyncness { 23 | Asyncness::Sync => quote!(Ok(())), 24 | Asyncness::Async => quote!(async { Ok(()) }), 25 | } 26 | }; 27 | quote! { 28 | #[inline] 29 | #[allow(clippy::manual_async_fn)] 30 | #step_fn { 31 | #body 32 | } 33 | } 34 | }); 35 | 36 | quote! { 37 | #[allow(unused_variables)] 38 | impl #generics #ident for narrative::environment::DummyEnvironment { 39 | type Error = E; 40 | #(#steps)* 41 | } 42 | } 43 | } 44 | 45 | #[cfg(test)] 46 | mod tests { 47 | use super::*; 48 | use pretty_assertions::assert_eq; 49 | 50 | #[test] 51 | fn test_empty() { 52 | let story_syntax = syn::parse_quote! { 53 | trait UserStory { 54 | #[step("Step 1")] 55 | fn step1(); 56 | #[step("Step 2")] 57 | fn step2(); 58 | } 59 | }; 60 | let actual = generate(&story_syntax, Asyncness::Sync); 61 | let expected = quote! { 62 | #[allow(unused_variables)] 63 | impl UserStory for narrative::environment::DummyEnvironment { 64 | type Error = E; 65 | #[inline] 66 | #[allow(clippy::manual_async_fn)] 67 | fn step1(&mut self) -> Result<(), Self::Error> { 68 | Ok(()) 69 | } 70 | #[inline] 71 | #[allow(clippy::manual_async_fn)] 72 | fn step2(&mut self) -> Result<(), Self::Error> { 73 | Ok(()) 74 | } 75 | } 76 | }; 77 | assert_eq!(actual.to_string(), expected.to_string()); 78 | } 79 | 80 | #[test] 81 | fn test_async() { 82 | let story_syntax = syn::parse_quote! { 83 | trait UserStory { 84 | #[step("Step 1")] 85 | fn step1(); 86 | #[step("Step 2")] 87 | fn step2(); 88 | } 89 | }; 90 | let actual = generate(&story_syntax, Asyncness::Async); 91 | let expected = quote! { 92 | #[allow(unused_variables)] 93 | impl AsyncUserStory for narrative::environment::DummyEnvironment { 94 | type Error = E; 95 | #[inline] 96 | #[allow(clippy::manual_async_fn)] 97 | fn step1(&mut self) -> impl std::future::Future> + Send { 98 | async { Ok(()) } 99 | } 100 | #[inline] 101 | #[allow(clippy::manual_async_fn)] 102 | fn step2(&mut self) -> impl std::future::Future> + Send { 103 | async { Ok(()) } 104 | } 105 | } 106 | }; 107 | assert_eq!(actual.to_string(), expected.to_string()); 108 | } 109 | 110 | #[test] 111 | fn test_with_sub_story() { 112 | let story_syntax = syn::parse_quote! { 113 | trait StoryDef { 114 | #[step(story: OtherStory, "Sub Step 1")] 115 | fn sub_step_1(); 116 | } 117 | }; 118 | let actual = generate(&story_syntax, Asyncness::Sync); 119 | let expected = quote! { 120 | #[allow(unused_variables)] 121 | impl StoryDef for narrative::environment::DummyEnvironment { 122 | type Error = E; 123 | #[inline] 124 | #[allow(clippy::manual_async_fn)] 125 | fn sub_step_1(&mut self) -> Result, Self::Error> { 126 | Ok(narrative::environment::DummyEnvironment::::default()) 127 | } 128 | } 129 | }; 130 | assert_eq!(actual.to_string(), expected.to_string()); 131 | } 132 | 133 | #[test] 134 | fn test_async_with_sub_story() { 135 | let story_syntax = syn::parse_quote! { 136 | trait StoryDef { 137 | #[step(story: OtherStory, "Sub Step 1")] 138 | fn sub_step_1(); 139 | } 140 | }; 141 | let actual = generate(&story_syntax, Asyncness::Async); 142 | let expected = quote! { 143 | #[allow(unused_variables)] 144 | impl AsyncStoryDef for narrative::environment::DummyEnvironment { 145 | type Error = E; 146 | #[inline] 147 | #[allow(clippy::manual_async_fn)] 148 | fn sub_step_1(&mut self) -> Result + Send, Self::Error> { 149 | Ok(narrative::environment::DummyEnvironment::::default()) 150 | } 151 | } 152 | }; 153 | assert_eq!(actual.to_string(), expected.to_string()); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /crates/narrative/src/story.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | 3 | use crate::{ 4 | runner::{AsyncStoryRunner, DefaultStoryRunner, StoryRunner}, 5 | step::{DynStep, Run, RunAsync, Step}, 6 | value::{BoxedValue, Value}, 7 | }; 8 | 9 | /// A trait for handing a story in general. 10 | // `&self` is not actually used, and is for future compatibility and friendly API. 11 | pub trait StoryContext { 12 | type Step: Step + 'static; 13 | /// Returns the title of the story. 14 | fn story_title(&self) -> String; 15 | /// Returns the identifier of the story. 16 | fn story_id(&self) -> &'static str; 17 | fn consts( 18 | &self, 19 | ) -> impl Iterator + Send + Sync + 'static; 20 | /// Returns the steps of the story. 21 | fn steps(&self) -> impl Iterator + Send + Sync + 'static; 22 | } 23 | 24 | pub trait StoryConst: Clone + std::fmt::Debug { 25 | /// Returns the name of the constant value. 26 | fn name(&self) -> &'static str; 27 | /// Returns the type of the constant value. 28 | fn ty(&self) -> &'static str; 29 | /// Returns the real expression of the constant value. 30 | fn expr(&self) -> &'static str; 31 | /// Returns the value of the constant. 32 | fn value(&self) -> impl Value; 33 | } 34 | 35 | #[derive(Clone, Copy)] 36 | pub struct DynStoryContext { 37 | story_title: &'static str, 38 | story_id: &'static str, 39 | consts: fn() -> Box + Send + Sync>, 40 | steps: fn() -> Box + Send + Sync>, 41 | } 42 | 43 | impl DynStoryContext { 44 | pub const fn new( 45 | story_title: &'static str, 46 | story_id: &'static str, 47 | consts: fn() -> Box + Send + Sync>, 48 | steps: fn() -> Box + Send + Sync>, 49 | ) -> Self { 50 | Self { 51 | story_title, 52 | story_id, 53 | consts, 54 | steps, 55 | } 56 | } 57 | } 58 | 59 | #[derive(Clone, Copy)] 60 | pub struct DynStoryConst { 61 | name: &'static str, 62 | ty: &'static str, 63 | expr: &'static str, 64 | value: fn() -> BoxedValue, 65 | obj_value: fn() -> BoxedValue, 66 | } 67 | 68 | impl DynStoryConst { 69 | pub const fn new( 70 | name: &'static str, 71 | ty: &'static str, 72 | expr: &'static str, 73 | value: fn() -> BoxedValue, 74 | obj_value: fn() -> BoxedValue, 75 | ) -> Self { 76 | Self { 77 | name, 78 | ty, 79 | expr, 80 | value, 81 | obj_value, 82 | } 83 | } 84 | } 85 | 86 | impl std::fmt::Debug for DynStoryConst { 87 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 88 | (self.obj_value)().fmt(f) 89 | } 90 | } 91 | 92 | impl StoryContext for DynStoryContext { 93 | type Step = DynStep; 94 | 95 | fn story_title(&self) -> String { 96 | self.story_title.to_string() 97 | } 98 | 99 | fn story_id(&self) -> &'static str { 100 | self.story_id 101 | } 102 | 103 | fn consts( 104 | &self, 105 | ) -> impl Iterator + Send + Sync + 'static { 106 | (self.consts)() 107 | } 108 | 109 | fn steps(&self) -> impl Iterator + Send + Sync + 'static { 110 | (self.steps)() 111 | } 112 | } 113 | 114 | impl StoryConst for DynStoryConst { 115 | fn name(&self) -> &'static str { 116 | self.name 117 | } 118 | 119 | fn ty(&self) -> &'static str { 120 | self.ty 121 | } 122 | 123 | fn expr(&self) -> &'static str { 124 | self.expr 125 | } 126 | 127 | fn value(&self) -> impl Value { 128 | (self.value)() 129 | } 130 | } 131 | 132 | pub trait RunStory { 133 | fn run_story(&self, env: &mut S) -> Result<(), E>; 134 | fn run_story_with_runner(&self, env: &mut S, runner: &mut impl StoryRunner) 135 | -> Result<(), E>; 136 | } 137 | 138 | impl RunStory for T 139 | where 140 | T: StoryContext + Copy, 141 | T::Step: Run, 142 | { 143 | fn run_story(&self, env: &mut S) -> Result<(), E> { 144 | let mut runner = DefaultStoryRunner; 145 | Self::run_story_with_runner(self, env, &mut runner) 146 | } 147 | fn run_story_with_runner( 148 | &self, 149 | env: &mut S, 150 | runner: &mut impl StoryRunner, 151 | ) -> Result<(), E> { 152 | runner.start_story(*self)?; 153 | for step in self.steps() { 154 | runner.run_step(step, env)?; 155 | } 156 | runner.end_story(*self)?; 157 | Ok(()) 158 | } 159 | } 160 | 161 | pub trait RunStoryAsync { 162 | fn run_story_async(&self, env: &mut S) -> impl Future> + Send; 163 | fn run_story_with_runner_async( 164 | &self, 165 | env: &mut S, 166 | runner: &mut (impl AsyncStoryRunner + Send), 167 | ) -> impl Future> + Send; 168 | } 169 | 170 | impl RunStoryAsync for T 171 | where 172 | T: StoryContext + Copy + Send + Sync, 173 | T::Step: RunAsync + Send + Sync, 174 | Env: Send, 175 | { 176 | async fn run_story_async(&self, env: &mut Env) -> Result<(), E> { 177 | let mut runner = DefaultStoryRunner; 178 | Self::run_story_with_runner_async(self, env, &mut runner).await 179 | } 180 | async fn run_story_with_runner_async( 181 | &self, 182 | env: &mut Env, 183 | runner: &mut (impl AsyncStoryRunner + Send), 184 | ) -> Result<(), E> { 185 | runner.start_story(*self)?; 186 | for step in self.steps() { 187 | runner.run_step_async(step, env).await?; 188 | } 189 | runner.end_story(*self)?; 190 | Ok(()) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /examples/src/story_runner.rs: -------------------------------------------------------------------------------- 1 | use narrative::{ 2 | runner::{AsyncStoryRunner, StoryRunner}, 3 | step::{Run, RunAsync, Step}, 4 | story::{RunStory, RunStoryAsync, StoryContext}, 5 | }; 6 | 7 | #[allow(dead_code)] 8 | struct LoggingStoryRunner { 9 | log: Vec, 10 | } 11 | 12 | impl StoryRunner for LoggingStoryRunner { 13 | fn start_story(&mut self, story: impl narrative::story::StoryContext) -> Result<(), E> { 14 | self.log 15 | .push(format!("Starting story: {}", story.story_title())); 16 | Ok(()) 17 | } 18 | 19 | fn end_story(&mut self, story: impl narrative::story::StoryContext) -> Result<(), E> { 20 | self.log 21 | .push(format!("Ending story: {}", story.story_title())); 22 | Ok(()) 23 | } 24 | 25 | fn run_step(&mut self, step: T, state: &mut S) -> Result<(), E> 26 | where 27 | T: Step + Run, 28 | { 29 | self.log.push(format!("Running step: {}", step.step_text())); 30 | step.run_with_runner(state, self) 31 | } 32 | 33 | fn run_nested_story( 34 | &mut self, 35 | step: impl Step, 36 | nested_story: S, 37 | env: &mut Env, 38 | ) -> Result<(), E> 39 | where 40 | S::Step: Run, 41 | S: StoryContext + RunStory, 42 | { 43 | self.log.push(format!( 44 | "Starting nested story: {}; {}", 45 | step.step_text(), 46 | nested_story.story_title(), 47 | )); 48 | nested_story.run_story_with_runner(env, self) 49 | } 50 | } 51 | 52 | impl AsyncStoryRunner for LoggingStoryRunner { 53 | fn start_story(&mut self, story: impl narrative::story::StoryContext) -> Result<(), E> { 54 | self.log 55 | .push(format!("Starting story: {}", story.story_title())); 56 | Ok(()) 57 | } 58 | 59 | fn end_story(&mut self, story: impl narrative::story::StoryContext) -> Result<(), E> { 60 | self.log 61 | .push(format!("Ending story: {}", story.story_title())); 62 | Ok(()) 63 | } 64 | 65 | fn run_step_async( 66 | &mut self, 67 | step: T, 68 | state: &mut S, 69 | ) -> impl std::future::Future> + Send 70 | where 71 | T: Step + RunAsync + Send, 72 | S: Send, 73 | { 74 | // Async recursion happens here by passing `self`, so Box::pin is required. 75 | Box::pin(async move { 76 | self.log.push(format!("Running step: {}", step.step_text())); 77 | step.run_with_runner_async(state, self).await 78 | }) 79 | } 80 | 81 | async fn run_nested_story_async( 82 | &mut self, 83 | step: impl Step + Send, 84 | nested_story: S, 85 | env: &mut Env, 86 | ) -> Result<(), E> 87 | where 88 | S: StoryContext + RunStoryAsync + Send, 89 | Env: Send, 90 | S::Step: RunAsync + Send, 91 | { 92 | self.log.push(format!( 93 | "Starting nested story: {}; {}", 94 | step.step_text(), 95 | nested_story.story_title(), 96 | )); 97 | nested_story.run_story_async(env).await 98 | } 99 | } 100 | 101 | #[test] 102 | fn test() { 103 | use crate::my_first_story::MyFirstStoryContext; 104 | use narrative::{environment::DummyEnvironment, story::RunStory}; 105 | use std::convert::Infallible; 106 | let mut runner = LoggingStoryRunner { log: vec![] }; 107 | let mut env = DummyEnvironment::::default(); 108 | MyFirstStoryContext 109 | .run_story_with_runner(&mut env, &mut runner) 110 | .unwrap(); 111 | assert_eq!( 112 | runner.log, 113 | vec![ 114 | "Starting story: My First Story", 115 | "Running step: Hi, I'm a user: Ryo", 116 | "Running step: I have an apple", 117 | "Running step: I have 2 oranges", 118 | "Running step: I should have 3 fruits", 119 | "Ending story: My First Story", 120 | ] 121 | ); 122 | } 123 | 124 | #[test] 125 | fn test_async() { 126 | use crate::async_story::MyFirstStoryContext; 127 | use narrative::{environment::DummyEnvironment, story::RunStoryAsync}; 128 | use std::convert::Infallible; 129 | let mut runner = LoggingStoryRunner { log: vec![] }; 130 | let mut env = DummyEnvironment::::default(); 131 | futures::executor::block_on( 132 | MyFirstStoryContext.run_story_with_runner_async(&mut env, &mut runner), 133 | ) 134 | .unwrap(); 135 | assert_eq!( 136 | runner.log, 137 | vec![ 138 | "Starting story: My First Story", 139 | "Running step: Hi, I'm a user", 140 | "Running step: I have an apple", 141 | "Running step: I have 2 oranges", 142 | "Running step: I should have 3 fruits", 143 | "Ending story: My First Story", 144 | ] 145 | ); 146 | } 147 | 148 | #[test] 149 | fn test_sub_story() { 150 | use crate::sub_story::MainStoryContext; 151 | use narrative::{environment::DummyEnvironment, story::RunStory}; 152 | use pretty_assertions::assert_eq; 153 | use std::convert::Infallible; 154 | let mut runner = LoggingStoryRunner { log: vec![] }; 155 | let mut env = DummyEnvironment::::default(); 156 | MainStoryContext 157 | .run_story_with_runner(&mut env, &mut runner) 158 | .unwrap(); 159 | assert_eq!( 160 | runner.log, 161 | vec![ 162 | "Starting story: This is a main story", 163 | "Running step: do sub story", 164 | "Starting nested story: do sub story; This is a sub story", 165 | "Starting story: This is a sub story", 166 | "Running step: sub_step_1", 167 | "Running step: sub_step_2", 168 | "Ending story: This is a sub story", 169 | "Running step: do sub story with args", 170 | "Starting nested story: do sub story with args; This is a sub story 2", 171 | "Starting story: This is a sub story 2", 172 | "Running step: sub_step_3", 173 | "Running step: sub_step_4", 174 | "Ending story: This is a sub story 2", 175 | "Ending story: This is a main story", 176 | ] 177 | ); 178 | } 179 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/local_type_for.rs: -------------------------------------------------------------------------------- 1 | use quote::format_ident; 2 | 3 | use crate::extract_types_for_assertion::extract_types_for_assertion; 4 | 5 | /// Extract all field types from a struct or enum 6 | fn extract_field_types(input_item: &syn::Item) -> Vec<&syn::Type> { 7 | match input_item { 8 | syn::Item::Struct(item_struct) => { 9 | let field_types: Vec<_> = match &item_struct.fields { 10 | syn::Fields::Named(fields) => fields.named.iter().map(|f| &f.ty).collect(), 11 | syn::Fields::Unnamed(fields) => fields.unnamed.iter().map(|f| &f.ty).collect(), 12 | syn::Fields::Unit => vec![], 13 | }; 14 | field_types 15 | .into_iter() 16 | .flat_map(extract_types_for_assertion) 17 | .collect() 18 | } 19 | syn::Item::Enum(item_enum) => { 20 | let field_types: Vec<_> = item_enum 21 | .variants 22 | .iter() 23 | .flat_map(|variant| match &variant.fields { 24 | syn::Fields::Named(fields) => { 25 | fields.named.iter().map(|f| &f.ty).collect::>() 26 | } 27 | syn::Fields::Unnamed(fields) => { 28 | fields.unnamed.iter().map(|f| &f.ty).collect::>() 29 | } 30 | syn::Fields::Unit => vec![], 31 | }) 32 | .collect(); 33 | field_types 34 | .into_iter() 35 | .flat_map(extract_types_for_assertion) 36 | .collect() 37 | } 38 | _ => vec![], 39 | } 40 | } 41 | 42 | pub(crate) fn generate( 43 | story_name: &syn::Ident, 44 | input_item: &syn::Item, 45 | ) -> proc_macro2::TokenStream { 46 | let (type_name, generics) = match &input_item { 47 | syn::Item::Struct(item) => (&item.ident, &item.generics), 48 | syn::Item::Enum(item) => (&item.ident, &item.generics), 49 | _ => { 50 | return syn::Error::new_spanned( 51 | input_item, 52 | "local_type_for can only be applied to structs or enums", 53 | ) 54 | .to_compile_error(); 55 | } 56 | }; 57 | 58 | let local_type_trait = format_ident!("{}LocalType", story_name); 59 | 60 | // Add StoryOwnedType bound to all type parameters 61 | let mut impl_generics = generics.clone(); 62 | impl_generics.type_params_mut().for_each(|param| { 63 | param 64 | .bounds 65 | .push(syn::parse_quote!(narrative::StoryOwnedType)); 66 | param.bounds.push(syn::parse_quote!(#local_type_trait)); 67 | }); 68 | 69 | // Type parameters without bounds for usage 70 | let mut type_generics = generics.clone(); 71 | type_generics.type_params_mut().for_each(|param| { 72 | param.bounds.clear(); 73 | }); 74 | 75 | // Extract field types and generate assertions 76 | let field_types = extract_field_types(input_item); 77 | let assertions = field_types 78 | .iter() 79 | .map(|ty| quote::quote!(assert_local_type::<#ty>();)); 80 | 81 | // Generate unique assertion function name based on type name 82 | let assertion_fn_name = format_ident!("_local_type_assertions_{}", type_name); 83 | 84 | quote::quote! { 85 | #input_item 86 | 87 | // Implement StoryOwnedType for this type 88 | // This will conflict if #[local_type_for] is applied to the same type twice, 89 | // preventing a data type from being a local type for multiple stories 90 | impl #impl_generics narrative::StoryOwnedType for #type_name #type_generics {} 91 | 92 | // Implement StoryLocalType for this type 93 | impl #impl_generics #local_type_trait for #type_name #type_generics {} 94 | 95 | // Type assertions for field types 96 | #[allow(non_snake_case)] 97 | fn #assertion_fn_name() { 98 | fn assert_local_type() {} 99 | #(#assertions)* 100 | } 101 | } 102 | } 103 | 104 | #[cfg(test)] 105 | mod tests { 106 | use super::*; 107 | use pretty_assertions::assert_eq; 108 | use quote::quote; 109 | 110 | #[test] 111 | fn test_struct_with_fields() { 112 | let story_name: syn::Ident = syn::parse_quote!(User); 113 | let input: syn::Item = syn::parse_quote! { 114 | struct UserId { 115 | id: u64, 116 | tags: Vec, 117 | } 118 | }; 119 | let actual = generate(&story_name, &input); 120 | let expected = quote! { 121 | struct UserId { 122 | id: u64, 123 | tags: Vec, 124 | } 125 | 126 | impl narrative::StoryOwnedType for UserId {} 127 | 128 | impl UserLocalType for UserId {} 129 | 130 | #[allow(non_snake_case)] 131 | fn _local_type_assertions_UserId() { 132 | fn assert_local_type() {} 133 | assert_local_type::(); 134 | assert_local_type::(); 135 | } 136 | }; 137 | assert_eq!(actual.to_string(), expected.to_string()); 138 | } 139 | 140 | #[test] 141 | fn test_enum_with_variants() { 142 | let story_name: syn::Ident = syn::parse_quote!(User); 143 | let input: syn::Item = syn::parse_quote! { 144 | enum UserEvent { 145 | Created(UserId), 146 | Updated { id: UserId, name: String }, 147 | } 148 | }; 149 | let actual = generate(&story_name, &input); 150 | let expected = quote! { 151 | enum UserEvent { 152 | Created(UserId), 153 | Updated { id: UserId, name: String }, 154 | } 155 | 156 | impl narrative::StoryOwnedType for UserEvent {} 157 | 158 | impl UserLocalType for UserEvent {} 159 | 160 | #[allow(non_snake_case)] 161 | fn _local_type_assertions_UserEvent() { 162 | fn assert_local_type() {} 163 | assert_local_type::(); 164 | assert_local_type::(); 165 | assert_local_type::(); 166 | } 167 | }; 168 | assert_eq!(actual.to_string(), expected.to_string()); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/output/step_fn.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | 4 | use crate::{Asyncness, item_story::StoryStep}; 5 | 6 | /// This does not emits `;` or body. 7 | pub(crate) fn generate(step: &StoryStep, asyncness: Asyncness) -> TokenStream { 8 | let fn_name = &step.inner.sig.ident; 9 | let other_attrs = &step.other_attrs; 10 | let inputs_tokens = step 11 | .inner 12 | .sig 13 | .inputs 14 | .iter() 15 | .filter(|input| matches!(input, syn::FnArg::Typed(_))); 16 | 17 | let bounds = if asyncness == Asyncness::Async { 18 | quote!(+ Send) 19 | } else { 20 | quote!() 21 | }; 22 | 23 | // Check if this is a sub-story step 24 | if let Some(sub_story_path) = step.sub_story_path() { 25 | let path = sub_story_path.path(); 26 | let async_path = sub_story_path.async_path(); 27 | // Generate different outputs based on asyncness 28 | let trait_name = match asyncness { 29 | Asyncness::Sync => quote!(#path), 30 | Asyncness::Async => quote!(#async_path), 31 | }; 32 | 33 | quote! { 34 | #(#other_attrs)* 35 | fn #fn_name(&mut self #(,#inputs_tokens)*) -> Result #bounds, Self::Error> 36 | } 37 | } else { 38 | // Regular step function 39 | let output = match asyncness { 40 | Asyncness::Sync => quote!(Result<(), Self::Error>), 41 | Asyncness::Async => { 42 | quote!(impl std::future::Future> + Send) 43 | } 44 | }; 45 | 46 | quote! { 47 | #(#other_attrs)* 48 | fn #fn_name(&mut self #(,#inputs_tokens)*) -> #output 49 | } 50 | } 51 | } 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use super::*; 56 | use pretty_assertions::assert_eq; 57 | 58 | #[test] 59 | fn test_generate_step_fn_blank() { 60 | let item_story = syn::parse_quote! { 61 | #[step("Step 1")] 62 | fn step1(); 63 | }; 64 | let actual = generate(&item_story, Asyncness::Sync); 65 | let expected = quote! { 66 | fn step1(&mut self) -> Result<(), Self::Error> 67 | }; 68 | assert_eq!(actual.to_string(), expected.to_string()); 69 | } 70 | 71 | #[test] 72 | fn test_generate_step_fn_with_inputs() { 73 | let item_story = syn::parse_quote! { 74 | #[step("Step 1")] 75 | fn step1(a: i32, b: i32); 76 | }; 77 | let actual = generate(&item_story, Asyncness::Sync); 78 | let expected = quote! { 79 | fn step1(&mut self, a: i32, b: i32) -> Result<(), Self::Error> 80 | }; 81 | assert_eq!(actual.to_string(), expected.to_string()); 82 | } 83 | 84 | #[test] 85 | fn test_ignore_receiver() { 86 | let item_story = syn::parse_quote! { 87 | #[step("Step 1")] 88 | fn step1(&self, a: i32, b: i32); 89 | }; 90 | let actual = generate(&item_story, Asyncness::Sync); 91 | let expected = quote! { 92 | fn step1(&mut self, a: i32, b: i32) -> Result<(), Self::Error> 93 | }; 94 | assert_eq!(actual.to_string(), expected.to_string()); 95 | } 96 | 97 | #[test] 98 | fn test_async() { 99 | let item_story = syn::parse_quote! { 100 | #[step("Step 1")] 101 | fn step1(); 102 | }; 103 | let actual = generate(&item_story, Asyncness::Async); 104 | let expected = quote! { 105 | fn step1(&mut self) -> impl std::future::Future> + Send 106 | }; 107 | assert_eq!(actual.to_string(), expected.to_string()); 108 | } 109 | 110 | #[test] 111 | fn test_generate_substory_step_fn() { 112 | let item_story = syn::parse_quote! { 113 | #[step(story: SubStory, "do sub story")] 114 | fn step_with_sub(); 115 | }; 116 | let actual = generate(&item_story, Asyncness::Sync); 117 | let expected = quote! { 118 | fn step_with_sub(&mut self) -> Result, Self::Error> 119 | }; 120 | assert_eq!(actual.to_string(), expected.to_string()); 121 | } 122 | 123 | #[test] 124 | fn test_generate_substory_step_fn_with_inputs() { 125 | let item_story = syn::parse_quote! { 126 | #[step(story: SubStory, "do sub story")] 127 | fn step_with_sub(arg: i32); 128 | }; 129 | let actual = generate(&item_story, Asyncness::Sync); 130 | let expected = quote! { 131 | fn step_with_sub(&mut self, arg: i32) -> Result, Self::Error> 132 | }; 133 | assert_eq!(actual.to_string(), expected.to_string()); 134 | } 135 | 136 | #[test] 137 | fn test_generate_substory_step_fn_async() { 138 | let item_story = syn::parse_quote! { 139 | #[step(story: SubStory, "do sub story")] 140 | fn step_with_sub(); 141 | }; 142 | let actual = generate(&item_story, Asyncness::Async); 143 | let expected = quote! { 144 | fn step_with_sub(&mut self) -> Result + Send, Self::Error> 145 | }; 146 | assert_eq!(actual.to_string(), expected.to_string()); 147 | } 148 | 149 | #[test] 150 | fn test_generate_step_fn_with_other_attrs() { 151 | let item_story = syn::parse_quote! { 152 | /// This is a step 153 | #[step("Step 1")] 154 | fn step1(); 155 | }; 156 | let actual = generate(&item_story, Asyncness::Sync); 157 | let expected = quote! { 158 | /// This is a step 159 | fn step1(&mut self) -> Result<(), Self::Error> 160 | }; 161 | assert_eq!(actual.to_string(), expected.to_string()); 162 | } 163 | 164 | #[test] 165 | fn test_generate_step_fn_with_other_attrs_async() { 166 | let item_story = syn::parse_quote! { 167 | /// This is a step 168 | #[step("Step 1")] 169 | fn step1(); 170 | }; 171 | let actual = generate(&item_story, Asyncness::Async); 172 | let expected = quote! { 173 | /// This is a step 174 | fn step1(&mut self) -> impl std::future::Future> + Send 175 | }; 176 | assert_eq!(actual.to_string(), expected.to_string()); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/output/story_context.rs: -------------------------------------------------------------------------------- 1 | // &self in these methods are not necessary but it's for future extensibility and friendly API. 2 | 3 | use proc_macro2::TokenStream; 4 | use quote::{format_ident, quote}; 5 | 6 | use crate::{ 7 | item_story::{story_const::StoryConst, ItemStory}, 8 | story_attr_syntax::StoryAttr, 9 | }; 10 | 11 | pub(crate) fn generate(attr: &StoryAttr, input: &ItemStory) -> TokenStream { 12 | let title = &attr.title; 13 | let ident = &input.ident; 14 | let steps = input.steps().map(|step| { 15 | let step_name = &step.inner.sig.ident; 16 | quote! { 17 | #[inline] 18 | pub fn #step_name(&self) -> Step { 19 | Step::#step_name 20 | } 21 | } 22 | }); 23 | let step_names = input.steps().map(|step| &step.inner.sig.ident); 24 | let consts = input.consts().map(|item| &item.raw.ident); 25 | let consts_defs = input.consts().map( 26 | |StoryConst { 27 | raw, 28 | default: (eq, default), 29 | }| { 30 | let ident = &raw.ident; 31 | let ty = &raw.ty; 32 | Some(quote! { 33 | pub const #ident: #ty #eq #default; 34 | }) 35 | }, 36 | ); 37 | let steps_len = input.steps().count(); 38 | let const_len = input.consts().count(); 39 | let dyn_consts = if const_len == 0 { 40 | quote!(Box::new(std::iter::empty())) 41 | } else { 42 | quote!(Box::new(__CONSTS.into_iter().map(|c| c.to_dyn()))) 43 | }; 44 | let dyn_steps = if steps_len == 0 { 45 | quote!(Box::new(std::iter::empty())) 46 | } else { 47 | quote!(Box::new(__STEPS.into_iter().map(|s| s.to_dyn()))) 48 | }; 49 | quote! { 50 | #[derive(Default, Clone, Copy)] 51 | pub struct StoryContext; 52 | pub const __STORY_TITLE: &str = #title; 53 | pub const __STORY_ID: &str = stringify!(#ident); 54 | pub const __STEPS: [Step; #steps_len] = [#(Step::#step_names),*]; 55 | pub const __CONSTS: [StoryConst; #const_len] = [#(StoryConst::#consts),*]; 56 | impl StoryContext { 57 | #(#consts_defs)* 58 | #(#steps)* 59 | 60 | pub fn to_dyn(&self) -> narrative::story::DynStoryContext { 61 | narrative::story::DynStoryContext::new( 62 | __STORY_TITLE, 63 | __STORY_ID, 64 | || #dyn_consts, 65 | || #dyn_steps, 66 | ) 67 | } 68 | } 69 | impl narrative::story::StoryContext for StoryContext { 70 | type Step = Step; 71 | 72 | #[inline] 73 | fn story_title(&self) -> String { 74 | __STORY_TITLE.to_string() 75 | } 76 | #[inline] 77 | fn story_id(&self) -> &'static str { 78 | __STORY_ID 79 | } 80 | #[inline] 81 | fn steps(&self) -> impl Iterator + 'static + Send { 82 | __STEPS.into_iter() 83 | } 84 | #[inline] 85 | fn consts(&self) -> impl Iterator + 'static 86 | { 87 | __CONSTS.into_iter() 88 | } 89 | } 90 | } 91 | } 92 | 93 | pub(crate) fn generate_ext(input: &ItemStory) -> TokenStream { 94 | let ident = &input.ident; 95 | let async_ident = format_ident!("Async{}", input.ident); 96 | quote! { 97 | pub trait ContextExt { 98 | fn context() -> StoryContext; 99 | fn get_context(&self) -> StoryContext; 100 | } 101 | pub trait AsyncContextExt { 102 | fn context() -> StoryContext; 103 | fn get_context(&self) -> StoryContext; 104 | } 105 | impl ContextExt for T { 106 | #[inline] 107 | fn context() -> StoryContext { 108 | StoryContext::default() 109 | } 110 | #[inline] 111 | fn get_context(&self) -> StoryContext { 112 | StoryContext::default() 113 | } 114 | } 115 | impl AsyncContextExt for T { 116 | #[inline] 117 | fn context() -> StoryContext { 118 | StoryContext::default() 119 | } 120 | #[inline] 121 | fn get_context(&self) -> StoryContext { 122 | StoryContext::default() 123 | } 124 | } 125 | } 126 | } 127 | 128 | #[cfg(test)] 129 | mod tests { 130 | use super::*; 131 | use pretty_assertions::assert_eq; 132 | 133 | #[test] 134 | fn test_generate() { 135 | let attr = syn::parse_quote! { 136 | "Story Title" 137 | }; 138 | let story_syntax = syn::parse_quote! { 139 | trait UserStory { 140 | const NAME: &str = "Ryo"; 141 | const AGE: u32 = 20; 142 | 143 | #[step("step1")] 144 | fn step1(); 145 | #[step("step2: {name}", name = "ryo")] 146 | fn step2(name: &str); 147 | } 148 | }; 149 | let actual = generate(&attr, &story_syntax); 150 | let expected = quote! { 151 | #[derive(Default, Clone, Copy)] 152 | pub struct StoryContext; 153 | pub const __STORY_TITLE: &str = "Story Title"; 154 | pub const __STORY_ID: &str = stringify!(UserStory); 155 | pub const __STEPS: [Step; 2usize] = [Step::step1, Step::step2]; 156 | pub const __CONSTS: [StoryConst; 2usize] = [StoryConst::NAME, StoryConst::AGE]; 157 | impl StoryContext { 158 | pub const NAME: &str = "Ryo"; 159 | pub const AGE: u32 = 20; 160 | 161 | #[inline] 162 | pub fn step1(&self) -> Step { 163 | Step::step1 164 | } 165 | #[inline] 166 | pub fn step2(&self) -> Step { 167 | Step::step2 168 | } 169 | 170 | pub fn to_dyn(&self) -> narrative::story::DynStoryContext { 171 | narrative::story::DynStoryContext::new( 172 | __STORY_TITLE, 173 | __STORY_ID, 174 | || Box::new(__CONSTS.into_iter().map(|c| c.to_dyn())), 175 | || Box::new(__STEPS.into_iter().map(|s| s.to_dyn())), 176 | ) 177 | } 178 | } 179 | impl narrative::story::StoryContext for StoryContext { 180 | type Step = Step; 181 | 182 | #[inline] 183 | fn story_title(&self) -> String { 184 | __STORY_TITLE.to_string() 185 | } 186 | #[inline] 187 | fn story_id(&self) -> &'static str { 188 | __STORY_ID 189 | } 190 | #[inline] 191 | fn steps(&self) -> impl Iterator + 'static + Send { 192 | __STEPS.into_iter() 193 | } 194 | #[inline] 195 | fn consts(&self) -> impl Iterator + 'static 196 | { 197 | __CONSTS.into_iter() 198 | } 199 | } 200 | }; 201 | assert_eq!(actual.to_string(), expected.to_string()); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/step_attr_syntax.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{ToTokens, format_ident}; 3 | use syn::parse::Parse; 4 | 5 | mod kw { 6 | syn::custom_keyword!(step); 7 | syn::custom_keyword!(story); 8 | } 9 | 10 | pub struct StepAttr { 11 | pub pound_symbol: syn::Token![#], 12 | pub bracket: syn::token::Bracket, 13 | pub step: kw::step, 14 | pub paren: syn::token::Paren, 15 | pub story_type: Option, 16 | pub text: syn::LitStr, 17 | pub args: Vec, 18 | } 19 | 20 | pub struct StoryType { 21 | pub story_kw: kw::story, 22 | pub colon_token: syn::Token![:], 23 | pub path: syn::Path, 24 | pub comma_token: syn::Token![,], 25 | } 26 | 27 | pub struct StepAttrArgs { 28 | pub comma_token: Option, 29 | pub ident: syn::Ident, 30 | pub equal_token: syn::Token![=], 31 | pub value: syn::Expr, 32 | } 33 | 34 | impl StoryType { 35 | pub fn path(&self) -> &syn::Path { 36 | &self.path 37 | } 38 | 39 | pub fn async_path(&self) -> syn::Path { 40 | let mut cloned = self.path.clone(); 41 | if let Some(seg) = cloned.segments.last_mut() { 42 | seg.ident = format_ident!("Async{}", seg.ident); 43 | } 44 | cloned 45 | } 46 | 47 | pub fn context_path(&self) -> syn::Path { 48 | let mut cloned = self.path.clone(); 49 | if let Some(seg) = cloned.segments.last_mut() { 50 | seg.ident = format_ident!("{}Context", seg.ident); 51 | } 52 | cloned 53 | } 54 | } 55 | 56 | impl Parse for StepAttr { 57 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 58 | let pound_symbol = input.parse::()?; 59 | let attr_content; 60 | let bracket = syn::bracketed!(attr_content in input); 61 | let step = attr_content.parse::()?; 62 | let step_content; 63 | let paren = syn::parenthesized!(step_content in attr_content); 64 | 65 | // Try to parse a story type if available 66 | let story_type = if step_content.peek(kw::story) { 67 | Some(StoryType { 68 | story_kw: step_content.parse()?, 69 | colon_token: step_content.parse()?, 70 | path: step_content.parse()?, 71 | comma_token: step_content.parse()?, 72 | }) 73 | } else { 74 | None 75 | }; 76 | 77 | let text = step_content.parse()?; 78 | let mut args = Vec::new(); 79 | while !step_content.is_empty() { 80 | args.push(step_content.parse()?); 81 | } 82 | 83 | Ok(Self { 84 | pound_symbol, 85 | bracket, 86 | step, 87 | paren, 88 | story_type, 89 | text, 90 | args, 91 | }) 92 | } 93 | } 94 | 95 | impl Parse for StepAttrArgs { 96 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 97 | let comma_token = input.parse::>()?; 98 | let ident = input.parse::()?; 99 | let equal_token = input.parse::()?; 100 | let value = input.parse::()?; 101 | Ok(Self { 102 | comma_token, 103 | ident, 104 | equal_token, 105 | value, 106 | }) 107 | } 108 | } 109 | 110 | impl ToTokens for StepAttr { 111 | fn to_tokens(&self, tokens: &mut TokenStream) { 112 | self.pound_symbol.to_tokens(tokens); 113 | self.bracket.surround(tokens, |tokens| { 114 | self.step.to_tokens(tokens); 115 | self.paren.surround(tokens, |tokens| { 116 | if let Some(story_type) = &self.story_type { 117 | story_type.to_tokens(tokens); 118 | } 119 | self.text.to_tokens(tokens); 120 | for arg in &self.args { 121 | arg.to_tokens(tokens); 122 | } 123 | }); 124 | }); 125 | } 126 | } 127 | 128 | impl ToTokens for StoryType { 129 | fn to_tokens(&self, tokens: &mut TokenStream) { 130 | self.story_kw.to_tokens(tokens); 131 | self.colon_token.to_tokens(tokens); 132 | self.path.to_tokens(tokens); 133 | self.comma_token.to_tokens(tokens); 134 | } 135 | } 136 | 137 | impl ToTokens for StepAttrArgs { 138 | fn to_tokens(&self, tokens: &mut TokenStream) { 139 | self.comma_token.to_tokens(tokens); 140 | self.ident.to_tokens(tokens); 141 | self.equal_token.to_tokens(tokens); 142 | self.value.to_tokens(tokens); 143 | } 144 | } 145 | 146 | #[cfg(test)] 147 | mod tests { 148 | use super::*; 149 | use quote::quote; 150 | 151 | #[test] 152 | fn test_step_attr() { 153 | let input: StepAttr = syn::parse_quote! { 154 | #[step("Hello, world!")] 155 | }; 156 | assert_eq!(input.text.value(), "Hello, world!".to_string()); 157 | assert_eq!(input.args.len(), 0); 158 | assert!(input.story_type.is_none()); 159 | } 160 | 161 | #[test] 162 | fn test_step_attr_with_args() { 163 | let input: StepAttr = syn::parse_quote! { 164 | #[step("Hello, world!", arg1 = 1, arg2 = "2", arg3 = UserId::new_v4())] 165 | }; 166 | assert_eq!(input.text.value(), "Hello, world!".to_string()); 167 | assert_eq!(input.args.len(), 3); 168 | assert_eq!(input.args[0].ident, "arg1".to_string()); 169 | assert_eq!(input.args[1].ident, "arg2".to_string()); 170 | assert_eq!(input.args[2].ident, "arg3".to_string()); 171 | assert!(matches!( 172 | &input.args[0].value, 173 | syn::Expr::Lit(syn::ExprLit { 174 | lit: syn::Lit::Int(value), 175 | .. 176 | }) if value.to_string() == "1" 177 | )); 178 | assert!(matches!( 179 | &input.args[1].value, 180 | syn::Expr::Lit(syn::ExprLit { 181 | lit: syn::Lit::Str(value), 182 | .. 183 | }) if value.value() == "2" 184 | )); 185 | assert!(matches!( 186 | &input.args[2].value, 187 | syn::Expr::Call(syn::ExprCall { func, .. }) if matches!(func.as_ref(), syn::Expr::Path(syn::ExprPath { path, .. }) if path.segments.len() == 2 && path.segments[0].ident == "UserId") 188 | )); 189 | } 190 | 191 | #[test] 192 | fn test_step_attr_with_story() { 193 | let input: StepAttr = syn::parse_quote! { 194 | #[step(story: SubStory, "do sub story")] 195 | }; 196 | assert_eq!(input.text.value(), "do sub story".to_string()); 197 | assert_eq!(input.args.len(), 0); 198 | assert!(input.story_type.is_some()); 199 | let story_type = input.story_type.as_ref().unwrap(); 200 | assert!(story_type.path.is_ident("SubStory")); 201 | } 202 | 203 | #[test] 204 | fn test_step_attr_with_story_and_args() { 205 | let input: StepAttr = syn::parse_quote! { 206 | #[step(story: SubStory, "do sub story with args", arg = 2)] 207 | }; 208 | assert_eq!(input.text.value(), "do sub story with args".to_string()); 209 | assert_eq!(input.args.len(), 1); 210 | assert_eq!(input.args[0].ident, "arg".to_string()); 211 | assert!(input.story_type.is_some()); 212 | let story_type = input.story_type.as_ref().unwrap(); 213 | assert!(story_type.path.is_ident("SubStory")); 214 | } 215 | 216 | #[test] 217 | fn test_to_tokens() { 218 | let input: StepAttr = syn::parse_quote! { 219 | #[step("Hello, world!", arg1 = 1, arg2 = "2", arg3 = UserId::new_v4())] 220 | }; 221 | let actual = quote! { 222 | #input 223 | }; 224 | let expected = quote! { 225 | #[step("Hello, world!", arg1 = 1, arg2 = "2", arg3 = UserId::new_v4())] 226 | }; 227 | assert_eq!(actual.to_string(), expected.to_string()); 228 | } 229 | 230 | #[test] 231 | fn test_to_tokens_with_story() { 232 | let input: StepAttr = syn::parse_quote! { 233 | #[step(story: SubStory, "do sub story", arg = 2)] 234 | }; 235 | let actual = quote! { 236 | #input 237 | }; 238 | let expected = quote! { 239 | #[step(story: SubStory, "do sub story", arg = 2)] 240 | }; 241 | assert_eq!(actual.to_string(), expected.to_string()); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /crates/narrative/tests/compile-fail/missing-step-parameter.stderr: -------------------------------------------------------------------------------- 1 | error: macros that expand to items must be delimited with braces or followed by a semicolon 2 | --> tests/compile-fail/missing-step-parameter.rs:4:30 3 | | 4 | 4 | fn perform_action(&self, action: &str); // Missing 'action' parameter 5 | | ^^^^^^ 6 | | 7 | help: change the delimiters to curly braces 8 | | 9 | 4 - fn perform_action(&self, action: &str); // Missing 'action' parameter 10 | 4 + fn perform_action(&self, {}: &str); // Missing 'action' parameter 11 | | 12 | help: add a semicolon 13 | | 14 | 4 | fn perform_action(&self, action;: &str); // Missing 'action' parameter 15 | | + 16 | 17 | error: No attr arg or assignment found 18 | --> tests/compile-fail/missing-step-parameter.rs:4:30 19 | | 20 | 4 | fn perform_action(&self, action: &str); // Missing 'action' parameter 21 | | ^^^^^^ 22 | 23 | error[E0425]: cannot find value `action` in this scope 24 | --> tests/compile-fail/missing-step-parameter.rs:4:30 25 | | 26 | 4 | fn perform_action(&self, action: &str); // Missing 'action' parameter 27 | | ^^^^^^ not found in this scope 28 | | 29 | = help: consider importing one of these items: 30 | crate::mod_MissingParameterStory::arg_values::perform_action::action 31 | crate::mod_MissingParameterStory::args::perform_action::action 32 | 33 | error[E0599]: no method named `value` found for enum `mod_MissingParameterStory::args::perform_action` in the current scope 34 | --> tests/compile-fail/missing-step-parameter.rs:1:1 35 | | 36 | 1 | #[narrative::story("Story with missing step parameter")] 37 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 38 | | | 39 | | method not found in `mod_MissingParameterStory::args::perform_action` 40 | | method `value` not found for this enum 41 | | 42 | = help: items from traits can only be used if the trait is implemented and in scope 43 | = note: the following traits define an item `value`, perhaps you need to implement one of them: 44 | candidate #1: `narrative::step::StepArg` 45 | candidate #2: `narrative::story::StoryConst` 46 | = note: this error originates in the attribute macro `narrative::story` (in Nightly builds, run with -Z macro-backtrace for more info) 47 | 48 | error[E0599]: no method named `name` found for enum `mod_MissingParameterStory::args::perform_action` in the current scope 49 | --> tests/compile-fail/missing-step-parameter.rs:1:1 50 | | 51 | 1 | #[narrative::story("Story with missing step parameter")] 52 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 53 | | | 54 | | method not found in `mod_MissingParameterStory::args::perform_action` 55 | | method `name` not found for this enum 56 | | 57 | = help: items from traits can only be used if the trait is implemented and in scope 58 | = note: the following traits define an item `name`, perhaps you need to implement one of them: 59 | candidate #1: `narrative::step::StepArg` 60 | candidate #2: `narrative::story::StoryConst` 61 | = note: this error originates in the attribute macro `narrative::story` (in Nightly builds, run with -Z macro-backtrace for more info) 62 | 63 | error[E0599]: no method named `ty` found for enum `mod_MissingParameterStory::args::perform_action` in the current scope 64 | --> tests/compile-fail/missing-step-parameter.rs:1:1 65 | | 66 | 1 | #[narrative::story("Story with missing step parameter")] 67 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 68 | | | 69 | | method not found in `mod_MissingParameterStory::args::perform_action` 70 | | method `ty` not found for this enum 71 | | 72 | = help: items from traits can only be used if the trait is implemented and in scope 73 | = note: the following traits define an item `ty`, perhaps you need to implement one of them: 74 | candidate #1: `narrative::step::StepArg` 75 | candidate #2: `narrative::story::StoryConst` 76 | = note: this error originates in the attribute macro `narrative::story` (in Nightly builds, run with -Z macro-backtrace for more info) 77 | 78 | error[E0599]: no method named `expr` found for enum `mod_MissingParameterStory::args::perform_action` in the current scope 79 | --> tests/compile-fail/missing-step-parameter.rs:1:1 80 | | 81 | 1 | #[narrative::story("Story with missing step parameter")] 82 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 83 | | | 84 | | method not found in `mod_MissingParameterStory::args::perform_action` 85 | | method `expr` not found for this enum 86 | | 87 | = help: items from traits can only be used if the trait is implemented and in scope 88 | = note: the following traits define an item `expr`, perhaps you need to implement one of them: 89 | candidate #1: `narrative::step::StepArg` 90 | candidate #2: `narrative::story::StoryConst` 91 | = note: this error originates in the attribute macro `narrative::story` (in Nightly builds, run with -Z macro-backtrace for more info) 92 | 93 | error[E0599]: no method named `to_dyn` found for enum `mod_MissingParameterStory::args::perform_action` in the current scope 94 | --> tests/compile-fail/missing-step-parameter.rs:1:1 95 | | 96 | 1 | #[narrative::story("Story with missing step parameter")] 97 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 98 | | | 99 | | method not found in `mod_MissingParameterStory::args::perform_action` 100 | | method `to_dyn` not found for this enum 101 | | 102 | = note: this error originates in the attribute macro `narrative::story` (in Nightly builds, run with -Z macro-backtrace for more info) 103 | 104 | error[E0599]: no method named `name` found for reference `&mod_MissingParameterStory::args::perform_action` in the current scope 105 | --> tests/compile-fail/missing-step-parameter.rs:1:1 106 | | 107 | 1 | #[narrative::story("Story with missing step parameter")] 108 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ method not found in `&mod_MissingParameterStory::args::perform_action` 109 | | 110 | = help: items from traits can only be used if the trait is implemented and in scope 111 | = note: the following traits define an item `name`, perhaps you need to implement one of them: 112 | candidate #1: `narrative::step::StepArg` 113 | candidate #2: `narrative::story::StoryConst` 114 | = note: this error originates in the attribute macro `narrative::story` (in Nightly builds, run with -Z macro-backtrace for more info) 115 | 116 | error[E0599]: no method named `ty` found for reference `&mod_MissingParameterStory::args::perform_action` in the current scope 117 | --> tests/compile-fail/missing-step-parameter.rs:1:1 118 | | 119 | 1 | #[narrative::story("Story with missing step parameter")] 120 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ method not found in `&mod_MissingParameterStory::args::perform_action` 121 | | 122 | = help: items from traits can only be used if the trait is implemented and in scope 123 | = note: the following traits define an item `ty`, perhaps you need to implement one of them: 124 | candidate #1: `narrative::step::StepArg` 125 | candidate #2: `narrative::story::StoryConst` 126 | = note: this error originates in the attribute macro `narrative::story` (in Nightly builds, run with -Z macro-backtrace for more info) 127 | 128 | error[E0599]: no method named `expr` found for reference `&mod_MissingParameterStory::args::perform_action` in the current scope 129 | --> tests/compile-fail/missing-step-parameter.rs:1:1 130 | | 131 | 1 | #[narrative::story("Story with missing step parameter")] 132 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ method not found in `&mod_MissingParameterStory::args::perform_action` 133 | | 134 | = help: items from traits can only be used if the trait is implemented and in scope 135 | = note: the following traits define an item `expr`, perhaps you need to implement one of them: 136 | candidate #1: `narrative::step::StepArg` 137 | candidate #2: `narrative::story::StoryConst` 138 | = note: this error originates in the attribute macro `narrative::story` (in Nightly builds, run with -Z macro-backtrace for more info) 139 | 140 | error[E0599]: no method named `value` found for reference `&mod_MissingParameterStory::args::perform_action` in the current scope 141 | --> tests/compile-fail/missing-step-parameter.rs:1:1 142 | | 143 | 1 | #[narrative::story("Story with missing step parameter")] 144 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ method not found in `&mod_MissingParameterStory::args::perform_action` 145 | | 146 | = help: items from traits can only be used if the trait is implemented and in scope 147 | = note: the following traits define an item `value`, perhaps you need to implement one of them: 148 | candidate #1: `narrative::step::StepArg` 149 | candidate #2: `narrative::story::StoryConst` 150 | = note: this error originates in the attribute macro `narrative::story` (in Nightly builds, run with -Z macro-backtrace for more info) 151 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/output/story_consts.rs: -------------------------------------------------------------------------------- 1 | // important! We don't make story const types camel case, to support rich rename experience on rust-analyzer. 2 | // To avoid name conflict, we use dedicated module for consts. 3 | // enum dispatched by const name 4 | 5 | use proc_macro2::TokenStream; 6 | use quote::quote; 7 | 8 | use crate::{ 9 | item_story::ItemStory, make_static, output::MatchArms, pretty_print_expr, pretty_print_type, 10 | }; 11 | 12 | pub(crate) fn generate(story: &ItemStory) -> TokenStream { 13 | let const_defs = story.consts().map(|item| item.to_pub_const()); 14 | let const_names = story 15 | .consts() 16 | .map(|item| &item.raw.ident) 17 | .collect::>(); 18 | if const_names.is_empty() { 19 | return quote! { 20 | /// This is placeholder and never be constructed. 21 | pub type StoryConst = narrative::story::DynStoryConst; 22 | }; 23 | } 24 | let const_variants = story.consts().map(|item| { 25 | let ident = &item.raw.ident; 26 | let ty = &item.raw.ty; 27 | let static_ty = make_static(ty); 28 | quote!(#ident(#static_ty)) 29 | }); 30 | 31 | let const_value_debug_arms = story 32 | .consts() 33 | .map(|item| { 34 | let ident = &item.raw.ident; 35 | quote!(Self::#ident(value) => value.fmt(f),) 36 | }) 37 | .collect::(); 38 | 39 | let const_mod_defs = story.consts().map(|item| { 40 | let ident = &item.raw.ident; 41 | let ty = pretty_print_type(&item.raw.ty); 42 | let expr = &item.default.1; 43 | let expr_str = pretty_print_expr(expr); 44 | 45 | let static_ty = make_static(&item.raw.ty); 46 | 47 | quote! { 48 | pub mod #ident { 49 | use super::*; 50 | pub const __NAME: &str = stringify!(#ident); 51 | pub const __TY: &str = #ty; 52 | pub const __EXPR: &str = #expr_str; 53 | #[inline] 54 | pub fn value() -> #static_ty { 55 | #expr 56 | } 57 | pub const DYN_STORY_CONST: narrative::story::DynStoryConst = narrative::story::DynStoryConst::new( 58 | __NAME, 59 | __TY, 60 | __EXPR, 61 | || narrative::value::BoxedValue::new(value()), 62 | || narrative::value::BoxedValue::new(StoryConst::#ident) 63 | ); 64 | } 65 | } 66 | }); 67 | 68 | let name_arms = story 69 | .consts() 70 | .map(|item| { 71 | let ident = &item.raw.ident; 72 | quote!(Self::#ident => story_consts::#ident::__NAME,) 73 | }) 74 | .collect::(); 75 | 76 | let ty_arms = story 77 | .consts() 78 | .map(|item| { 79 | let ident = &item.raw.ident; 80 | quote!(Self::#ident => story_consts::#ident::__TY,) 81 | }) 82 | .collect::(); 83 | 84 | let expr_arms = story 85 | .consts() 86 | .map(|item| { 87 | let ident = &item.raw.ident; 88 | quote!(Self::#ident => story_consts::#ident::__EXPR,) 89 | }) 90 | .collect::(); 91 | 92 | let value_arms = story 93 | .consts() 94 | .map(|item| { 95 | let ident = &item.raw.ident; 96 | quote!(Self::#ident => ConstValue::#ident(story_consts::#ident::value()),) 97 | }) 98 | .collect::() 99 | .cast_as(quote!(narrative::value::BoxedValue)); 100 | 101 | let impl_body = quote! { 102 | #[inline] 103 | fn name(&self) -> &'static str { 104 | #name_arms 105 | } 106 | #[inline] 107 | fn ty(&self) -> &'static str { 108 | #ty_arms 109 | } 110 | #[inline] 111 | fn expr(&self) -> &'static str { 112 | #expr_arms 113 | } 114 | #[inline] 115 | fn value(&self) -> impl narrative::value::Value { 116 | #value_arms 117 | } 118 | }; 119 | 120 | quote! { 121 | #(#const_defs)* 122 | 123 | #[derive(Clone, Copy)] 124 | #[allow(non_camel_case_types)] 125 | pub enum StoryConst { 126 | #(#const_names),* 127 | } 128 | 129 | #[derive(Clone, narrative::serde::Serialize)] 130 | #[allow(non_camel_case_types)] 131 | enum ConstValue { 132 | #(#const_variants),* 133 | } 134 | 135 | impl std::fmt::Debug for ConstValue { 136 | #[inline] 137 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 138 | #const_value_debug_arms 139 | } 140 | } 141 | 142 | mod story_consts { 143 | use super::*; 144 | #(#const_mod_defs)* 145 | } 146 | 147 | impl StoryConst { 148 | pub fn to_dyn(&self) -> narrative::story::DynStoryConst { 149 | match self { 150 | #(Self::#const_names => story_consts::#const_names::DYN_STORY_CONST,)* 151 | } 152 | } 153 | } 154 | 155 | impl narrative::story::StoryConst for StoryConst { 156 | #impl_body 157 | } 158 | 159 | impl std::fmt::Debug for StoryConst { 160 | #[inline] 161 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 162 | use narrative::story::StoryConst; 163 | write!(f, "{name}: {ty} = {expr}", name = self.name(), ty = self.ty(), expr = self.expr()) 164 | } 165 | } 166 | 167 | impl narrative::serde::Serialize for StoryConst { 168 | #[inline] 169 | fn serialize(&self, serializer: T) -> Result { 170 | use narrative::story::StoryConst; 171 | use narrative::serde::ser::SerializeMap; 172 | let mut map = serializer.serialize_map(Some(4))?; 173 | map.serialize_entry("name", self.name())?; 174 | map.serialize_entry("ty", self.ty())?; 175 | map.serialize_entry("expr", self.expr())?; 176 | map.serialize_entry("value", &self.value())?; 177 | map.end() 178 | } 179 | } 180 | } 181 | } 182 | 183 | #[cfg(test)] 184 | mod tests { 185 | use super::*; 186 | use pretty_assertions::assert_eq; 187 | 188 | use syn::parse_quote; 189 | 190 | #[test] 191 | fn empty() { 192 | let story = parse_quote! { 193 | trait User { 194 | } 195 | }; 196 | 197 | let actual = generate(&story); 198 | 199 | assert_eq!( 200 | actual.to_string(), 201 | quote! { 202 | /// This is placeholder and never be constructed. 203 | pub type StoryConst = narrative::story::DynStoryConst; 204 | } 205 | .to_string() 206 | ); 207 | } 208 | 209 | #[test] 210 | fn consts() { 211 | let story = parse_quote! { 212 | trait User { 213 | const NUMBER: u32 = 42; 214 | } 215 | }; 216 | 217 | let actual = generate(&story); 218 | 219 | assert_eq!( 220 | actual.to_string(), 221 | quote! { 222 | pub const NUMBER: u32 = 42; 223 | 224 | #[derive(Clone, Copy)] 225 | #[allow(non_camel_case_types)] 226 | pub enum StoryConst { 227 | NUMBER 228 | } 229 | 230 | #[derive(Clone, narrative::serde::Serialize)] 231 | #[allow(non_camel_case_types)] 232 | enum ConstValue { 233 | NUMBER(u32) 234 | } 235 | 236 | impl std::fmt::Debug for ConstValue { 237 | #[inline] 238 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 239 | match self { 240 | Self::NUMBER(value) => value.fmt(f), 241 | } 242 | } 243 | } 244 | 245 | mod story_consts { 246 | use super::*; 247 | pub mod NUMBER { 248 | use super::*; 249 | pub const __NAME: &str = stringify!(NUMBER); 250 | pub const __TY: &str = "u32"; 251 | pub const __EXPR: &str = "42"; 252 | #[inline] 253 | pub fn value() -> u32 { 254 | 42 255 | } 256 | pub const DYN_STORY_CONST: narrative::story::DynStoryConst = narrative::story::DynStoryConst::new( 257 | __NAME, 258 | __TY, 259 | __EXPR, 260 | || narrative::value::BoxedValue::new(value()), 261 | || narrative::value::BoxedValue::new(StoryConst::NUMBER) 262 | ); 263 | } 264 | } 265 | 266 | impl StoryConst { 267 | pub fn to_dyn(&self) -> narrative::story::DynStoryConst { 268 | match self { 269 | Self::NUMBER => story_consts::NUMBER::DYN_STORY_CONST, 270 | } 271 | } 272 | } 273 | 274 | impl narrative::story::StoryConst for StoryConst { 275 | #[inline] 276 | fn name(&self) -> &'static str { 277 | match self { 278 | Self::NUMBER => story_consts::NUMBER::__NAME, 279 | } 280 | } 281 | #[inline] 282 | fn ty(&self) -> &'static str { 283 | match self { 284 | Self::NUMBER => story_consts::NUMBER::__TY, 285 | } 286 | } 287 | #[inline] 288 | fn expr(&self) -> &'static str { 289 | match self { 290 | Self::NUMBER => story_consts::NUMBER::__EXPR, 291 | } 292 | } 293 | #[inline] 294 | fn value(&self) -> impl narrative::value::Value { 295 | match self { 296 | Self::NUMBER => ConstValue::NUMBER(story_consts::NUMBER::value()), 297 | } 298 | } 299 | } 300 | 301 | impl std::fmt::Debug for StoryConst { 302 | #[inline] 303 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 304 | use narrative::story::StoryConst; 305 | write!(f, "{name}: {ty} = {expr}", name = self.name(), ty = self.ty(), expr = self.expr()) 306 | } 307 | } 308 | 309 | impl narrative::serde::Serialize for StoryConst { 310 | #[inline] 311 | fn serialize(&self, serializer: T) -> Result { 312 | use narrative::story::StoryConst; 313 | use narrative::serde::ser::SerializeMap; 314 | let mut map = serializer.serialize_map(Some(4))?; 315 | map.serialize_entry("name", self.name())?; 316 | map.serialize_entry("ty", self.ty())?; 317 | map.serialize_entry("expr", self.expr())?; 318 | map.serialize_entry("value", &self.value())?; 319 | map.end() 320 | } 321 | } 322 | } 323 | .to_string() 324 | ); 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /crates/narrative-macros/src/extract_types_for_assertion.rs: -------------------------------------------------------------------------------- 1 | /// Extracts types that should be asserted from a given type. 2 | /// 3 | /// This function recursively extracts types from container types like Vec, Option, Result, 4 | /// arrays, slices, and tuples. For other types, it returns the type itself. 5 | /// 6 | /// Types starting with `Self` are filtered out and return an empty vector. 7 | pub fn extract_types_for_assertion(ty: &syn::Type) -> Vec<&syn::Type> { 8 | // Check if this is a Self type 9 | if let syn::Type::Path(path) = ty 10 | && let Some(first_segment) = path.path.segments.first() 11 | && first_segment.ident == "Self" 12 | { 13 | return vec![]; 14 | } 15 | 16 | match ty { 17 | // Handle references &T 18 | syn::Type::Reference(type_ref) => { 19 | match type_ref.elem.as_ref() { 20 | // Special case: &str should be returned as-is 21 | syn::Type::Path(path) 22 | if path.path.segments.len() == 1 && path.path.segments[0].ident == "str" => 23 | { 24 | vec![ty] 25 | } 26 | // Handle slice &[T] - extract T 27 | syn::Type::Slice(slice) => extract_types_for_assertion(&slice.elem), 28 | // For other references, recurse into T 29 | _ => extract_types_for_assertion(&type_ref.elem), 30 | } 31 | } 32 | // Handle arrays [T; N] 33 | syn::Type::Array(array) => extract_types_for_assertion(&array.elem), 34 | // Handle slices [T] 35 | syn::Type::Slice(slice) => extract_types_for_assertion(&slice.elem), 36 | // Handle tuples (T1, T2, ...) 37 | syn::Type::Tuple(tuple) => tuple 38 | .elems 39 | .iter() 40 | .flat_map(extract_types_for_assertion) 41 | .collect(), 42 | // Handle path types (Vec, Option, Result, etc.) 43 | syn::Type::Path(type_path) => { 44 | if let Some(last_segment) = type_path.path.segments.last() { 45 | let ident = &last_segment.ident; 46 | 47 | // Check for standard library container types 48 | if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments { 49 | let ident_str = ident.to_string(); 50 | let is_container = matches!( 51 | ident_str.as_str(), 52 | "Vec" 53 | | "Option" 54 | | "Result" 55 | | "Box" 56 | | "Rc" 57 | | "Arc" 58 | | "Cow" 59 | | "HashMap" 60 | | "BTreeMap" 61 | | "HashSet" 62 | | "BTreeSet" 63 | | "VecDeque" 64 | | "BinaryHeap" 65 | | "LinkedList" 66 | | "PhantomData" 67 | | "LazyLock" 68 | | "LazyCell" 69 | ); 70 | 71 | if is_container { 72 | // Extract generic type arguments 73 | return args 74 | .args 75 | .iter() 76 | .filter_map(|arg| { 77 | if let syn::GenericArgument::Type(ty) = arg { 78 | Some(ty) 79 | } else { 80 | None 81 | } 82 | }) 83 | .flat_map(extract_types_for_assertion) 84 | .collect(); 85 | } 86 | } 87 | } 88 | 89 | // For non-container types, return the type itself 90 | vec![ty] 91 | } 92 | // For other types, return the type itself 93 | _ => vec![ty], 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use super::*; 100 | use pretty_assertions::assert_eq; 101 | use quote::quote; 102 | 103 | #[test] 104 | fn test_extract_vec() { 105 | let ty: syn::Type = syn::parse_quote!(Vec); 106 | let result = extract_types_for_assertion(&ty); 107 | assert_eq!(result.len(), 1); 108 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 109 | } 110 | 111 | #[test] 112 | fn test_extract_option() { 113 | let ty: syn::Type = syn::parse_quote!(Option); 114 | let result = extract_types_for_assertion(&ty); 115 | assert_eq!(result.len(), 1); 116 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 117 | } 118 | 119 | #[test] 120 | fn test_extract_result() { 121 | let ty: syn::Type = syn::parse_quote!(Result); 122 | let result = extract_types_for_assertion(&ty); 123 | assert_eq!(result.len(), 2); 124 | assert_eq!( 125 | quote!(#(#result)*).to_string(), 126 | quote!(UserId ErrorType).to_string() 127 | ); 128 | } 129 | 130 | #[test] 131 | fn test_extract_array() { 132 | let ty: syn::Type = syn::parse_quote!([UserId; 10]); 133 | let result = extract_types_for_assertion(&ty); 134 | assert_eq!(result.len(), 1); 135 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 136 | } 137 | 138 | #[test] 139 | fn test_extract_slice_reference() { 140 | let ty: syn::Type = syn::parse_quote!(&[UserId]); 141 | let result = extract_types_for_assertion(&ty); 142 | assert_eq!(result.len(), 1); 143 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 144 | } 145 | 146 | #[test] 147 | fn test_extract_tuple() { 148 | let ty: syn::Type = syn::parse_quote!((UserId, UserName)); 149 | let result = extract_types_for_assertion(&ty); 150 | assert_eq!(result.len(), 2); 151 | assert_eq!( 152 | quote!(#(#result)*).to_string(), 153 | quote!(UserId UserName).to_string() 154 | ); 155 | } 156 | 157 | #[test] 158 | fn test_extract_nested() { 159 | let ty: syn::Type = syn::parse_quote!(Vec>); 160 | let result = extract_types_for_assertion(&ty); 161 | assert_eq!(result.len(), 1); 162 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 163 | } 164 | 165 | #[test] 166 | fn test_extract_complex_nested() { 167 | let ty: syn::Type = syn::parse_quote!(Result, ErrorType>); 168 | let result = extract_types_for_assertion(&ty); 169 | assert_eq!(result.len(), 3); 170 | assert_eq!( 171 | quote!(#(#result)*).to_string(), 172 | quote!(UserId UserName ErrorType).to_string() 173 | ); 174 | } 175 | 176 | #[test] 177 | fn test_extract_str_reference() { 178 | let ty: syn::Type = syn::parse_quote!(&str); 179 | let result = extract_types_for_assertion(&ty); 180 | assert_eq!(result.len(), 1); 181 | assert_eq!(quote!(#(#result)*).to_string(), quote!(&str).to_string()); 182 | } 183 | 184 | #[test] 185 | fn test_extract_reference_to_option() { 186 | let ty: syn::Type = syn::parse_quote!(&Option); 187 | let result = extract_types_for_assertion(&ty); 188 | assert_eq!(result.len(), 1); 189 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 190 | } 191 | 192 | #[test] 193 | fn test_extract_reference_to_vec() { 194 | let ty: syn::Type = syn::parse_quote!(&Vec); 195 | let result = extract_types_for_assertion(&ty); 196 | assert_eq!(result.len(), 1); 197 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 198 | } 199 | 200 | #[test] 201 | fn test_extract_reference_to_custom_type() { 202 | let ty: syn::Type = syn::parse_quote!(&UserId); 203 | let result = extract_types_for_assertion(&ty); 204 | assert_eq!(result.len(), 1); 205 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 206 | } 207 | 208 | #[test] 209 | fn test_extract_self_type() { 210 | let ty: syn::Type = syn::parse_quote!(Self); 211 | let result = extract_types_for_assertion(&ty); 212 | assert_eq!(result.len(), 0); 213 | } 214 | 215 | #[test] 216 | fn test_extract_self_assoc_type() { 217 | let ty: syn::Type = syn::parse_quote!(Self::UserId); 218 | let result = extract_types_for_assertion(&ty); 219 | assert_eq!(result.len(), 0); 220 | } 221 | 222 | #[test] 223 | fn test_extract_self_in_vec() { 224 | let ty: syn::Type = syn::parse_quote!(Vec); 225 | let result = extract_types_for_assertion(&ty); 226 | assert_eq!(result.len(), 0); 227 | } 228 | 229 | #[test] 230 | fn test_extract_self_in_option() { 231 | let ty: syn::Type = syn::parse_quote!(Option); 232 | let result = extract_types_for_assertion(&ty); 233 | assert_eq!(result.len(), 0); 234 | } 235 | 236 | #[test] 237 | fn test_extract_self_in_result() { 238 | let ty: syn::Type = syn::parse_quote!(Result); 239 | let result = extract_types_for_assertion(&ty); 240 | assert_eq!(result.len(), 1); 241 | assert_eq!( 242 | quote!(#(#result)*).to_string(), 243 | quote!(ErrorType).to_string() 244 | ); 245 | } 246 | 247 | #[test] 248 | fn test_extract_self_in_tuple() { 249 | let ty: syn::Type = syn::parse_quote!((Self, UserId)); 250 | let result = extract_types_for_assertion(&ty); 251 | assert_eq!(result.len(), 1); 252 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 253 | } 254 | 255 | #[test] 256 | fn test_extract_box() { 257 | let ty: syn::Type = syn::parse_quote!(Box); 258 | let result = extract_types_for_assertion(&ty); 259 | assert_eq!(result.len(), 1); 260 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 261 | } 262 | 263 | #[test] 264 | fn test_extract_rc() { 265 | let ty: syn::Type = syn::parse_quote!(Rc); 266 | let result = extract_types_for_assertion(&ty); 267 | assert_eq!(result.len(), 1); 268 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 269 | } 270 | 271 | #[test] 272 | fn test_extract_arc() { 273 | let ty: syn::Type = syn::parse_quote!(Arc); 274 | let result = extract_types_for_assertion(&ty); 275 | assert_eq!(result.len(), 1); 276 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 277 | } 278 | 279 | #[test] 280 | fn test_extract_cow() { 281 | let ty: syn::Type = syn::parse_quote!(Cow); 282 | let result = extract_types_for_assertion(&ty); 283 | assert_eq!(result.len(), 1); 284 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 285 | } 286 | 287 | #[test] 288 | fn test_extract_hashmap() { 289 | let ty: syn::Type = syn::parse_quote!(HashMap); 290 | let result = extract_types_for_assertion(&ty); 291 | assert_eq!(result.len(), 2); 292 | assert_eq!( 293 | quote!(#(#result)*).to_string(), 294 | quote!(String UserId).to_string() 295 | ); 296 | } 297 | 298 | #[test] 299 | fn test_extract_btreemap() { 300 | let ty: syn::Type = syn::parse_quote!(BTreeMap); 301 | let result = extract_types_for_assertion(&ty); 302 | assert_eq!(result.len(), 2); 303 | assert_eq!( 304 | quote!(#(#result)*).to_string(), 305 | quote!(String UserId).to_string() 306 | ); 307 | } 308 | 309 | #[test] 310 | fn test_extract_hashset() { 311 | let ty: syn::Type = syn::parse_quote!(HashSet); 312 | let result = extract_types_for_assertion(&ty); 313 | assert_eq!(result.len(), 1); 314 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 315 | } 316 | 317 | #[test] 318 | fn test_extract_btreeset() { 319 | let ty: syn::Type = syn::parse_quote!(BTreeSet); 320 | let result = extract_types_for_assertion(&ty); 321 | assert_eq!(result.len(), 1); 322 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 323 | } 324 | 325 | #[test] 326 | fn test_extract_vecdeque() { 327 | let ty: syn::Type = syn::parse_quote!(VecDeque); 328 | let result = extract_types_for_assertion(&ty); 329 | assert_eq!(result.len(), 1); 330 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 331 | } 332 | 333 | #[test] 334 | fn test_extract_nested_containers() { 335 | let ty: syn::Type = syn::parse_quote!(Arc>>); 336 | let result = extract_types_for_assertion(&ty); 337 | assert_eq!(result.len(), 1); 338 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 339 | } 340 | 341 | #[test] 342 | fn test_extract_binaryheap() { 343 | let ty: syn::Type = syn::parse_quote!(BinaryHeap); 344 | let result = extract_types_for_assertion(&ty); 345 | assert_eq!(result.len(), 1); 346 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 347 | } 348 | 349 | #[test] 350 | fn test_extract_linkedlist() { 351 | let ty: syn::Type = syn::parse_quote!(LinkedList); 352 | let result = extract_types_for_assertion(&ty); 353 | assert_eq!(result.len(), 1); 354 | assert_eq!(quote!(#(#result)*).to_string(), quote!(UserId).to_string()); 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Narrative 2 | 3 | An immensely simple library for story-driven development 4 | 5 | ## Overview 6 | 7 | Narrative is a library dedicated to developing a whole or some part of software 8 | based on stories expressed in a Rust trait. Though its primary design is for 9 | end-to-end testing, its simplicity supports a variety of use cases. 10 | 11 | ## Goals 12 | 13 | - **Story-driven**: Code respects story, not the other way around 14 | - **Data-driven**: Enabling stories to include structured data 15 | - **No additional tooling**: Eliminating the need for extra installation and 16 | learning 17 | - **Leverage existing ecosystem**: Rich experience with less implementation 18 | - **Zero runtime cost**: Stories are processed at compile time 19 | 20 | ## Terminology 21 | 22 | Key terms in this library are: 23 | 24 | - **Story**: a sequence of steps, written as a trait 25 | - **Step**: a single action or assertion in a story 26 | - **Story Trait**: a macro-generated trait that represents a story, with a 27 | method for each step 28 | - **Story Context**: a struct that holds metadata about a story and provides 29 | methods to run it 30 | - **Story Env**: a struct that implements a story trait 31 | 32 | ## Usage 33 | 34 | 1. Add [narrative](https://crates.io/crates/narrative) to your cargo 35 | dependencies. 36 | 37 | 2. Write your first story as a trait. 38 | 39 | ```rust 40 | #[narrative::story("This is my first story")] 41 | trait MyFirstStory { 42 | #[step("Hi, I'm a user")] 43 | fn as_a_user(); 44 | #[step("I have an apple", count = 1)] 45 | fn have_one_apple(count: u32); 46 | #[step("I have {count} oranges", count = 2)] 47 | fn have_two_oranges(count: u32); 48 | #[step("I should have {total} fruits", total = 3)] 49 | fn should_have_three_fruits(total: u32); 50 | } 51 | ``` 52 | 53 | Wow, it's neat! 54 | 55 | 3. Implement the story in Rust. 56 | 57 | ```rust 58 | struct MyFirstStoryEnv { 59 | sum: u32, 60 | } 61 | 62 | impl MyFirstStory for MyFirstStoryEnv { 63 | type Error = std::convert::Infallible; 64 | 65 | fn as_a_user(&mut self) -> Result<(), Self::Error> { 66 | println!("Hi, I'm a user"); 67 | Ok(()) 68 | } 69 | 70 | fn have_one_apple(&mut self, count: u32) -> Result<(), Self::Error> { 71 | self.sum += count; 72 | Ok(()) 73 | } 74 | 75 | fn have_two_oranges(&mut self, count: u32) -> Result<(), Self::Error> { 76 | self.sum += count; 77 | Ok(()) 78 | } 79 | 80 | fn should_have_three_fruits(&mut self, total: u32) -> Result<(), Self::Error> { 81 | assert_eq!(self.sum, total); 82 | Ok(()) 83 | } 84 | } 85 | ``` 86 | 87 | You may notice that the signature of the trait methods is a bit different from 88 | the declaration, but it's fine. 89 | 90 | 4. Use the story in your code. 91 | 92 | ```rust 93 | use narrative::story::RunStory; 94 | 95 | #[test] 96 | fn test() { 97 | let mut env = MyFirstStoryEnv { sum: 0 }; 98 | MyFirstStoryContext.run_story(&mut env).unwrap(); 99 | } 100 | ``` 101 | 102 | The `#[narrative::story]` macro generates a `MyFirstStoryContext` struct that 103 | implements `StoryContext`, which provides methods to run the story and introspect 104 | its structure. 105 | 106 | ### Features 107 | 108 | #### Async support 109 | 110 | Both sync and async traits are defined automatically. Prefix the trait name with 111 | `Async` to implement the async version. 112 | 113 | ```rust 114 | impl AsyncMyFirstStory for MyFirstStoryEnv { 115 | type Error = std::convert::Infallible; 116 | 117 | async fn as_a_user(&mut self) -> Result<(), Self::Error> { 118 | // async implementation 119 | Ok(()) 120 | } 121 | // ... other async methods 122 | } 123 | 124 | #[test] 125 | fn test_async() { 126 | use narrative::story::RunStoryAsync; 127 | let mut env = MyFirstStoryEnv { sum: 0 }; 128 | futures::executor::block_on(MyFirstStoryContext.run_story_async(&mut env)).unwrap(); 129 | } 130 | ``` 131 | 132 | #### Constants 133 | 134 | Define constants in the story trait to use in step text and arguments. 135 | 136 | ```rust 137 | #[narrative::story("User Story")] 138 | trait UserStory { 139 | const NAME: &str = "Alice"; 140 | const ID: &str = "user123"; 141 | 142 | #[step("User: {NAME}")] 143 | fn greet_user(); 144 | 145 | #[step("Login as {name}", name = NAME)] 146 | fn login(name: &str); 147 | 148 | #[step("Visit profile", url = format!("https://example.com/{ID}"))] 149 | fn visit_profile(url: String); 150 | } 151 | ``` 152 | 153 | #### Custom data types 154 | 155 | Use `#[narrative::local_type_for]` to define custom types for step arguments. 156 | This keeps stories independent from implementation details while leveraging Rust's 157 | type system. 158 | 159 | ```rust 160 | #[narrative::story("User Management")] 161 | trait UserStory { 162 | #[step("User {id:?} logs in as {role:?}", id = UserId::new("user123"), role = UserRole::Admin)] 163 | fn user_logs_in(id: UserId, role: UserRole); 164 | } 165 | 166 | #[derive(Debug, Clone, serde::Serialize)] 167 | #[narrative::local_type_for(UserStory)] 168 | pub struct UserId(&'static str); 169 | 170 | impl UserId { 171 | pub const fn new(id: &'static str) -> Self { 172 | Self(id) 173 | } 174 | } 175 | 176 | #[derive(Debug, Clone, serde::Serialize)] 177 | #[narrative::local_type_for(UserStory)] 178 | pub enum UserRole { 179 | Admin, 180 | User, 181 | } 182 | ``` 183 | 184 | Types marked with `#[narrative::local_type_for]` can only be used in the specified 185 | story, preventing coupling. Standard library types and common third-party types 186 | (with `serde::Serialize`) like `uuid::Uuid`, `chrono::DateTime`, etc., can be used 187 | directly without this attribute. 188 | 189 | #### Sub stories 190 | 191 | Stories can be composed by nesting them as steps. The parent step returns the 192 | sub story implementation. 193 | 194 | ```rust 195 | #[narrative::story("Setup")] 196 | trait Setup { 197 | #[step("Initialize database")] 198 | fn init_db(); 199 | } 200 | 201 | #[narrative::story("User Test")] 202 | trait UserTest { 203 | #[step(story: Setup, "Run setup")] 204 | fn setup(); 205 | 206 | #[step("Test user creation")] 207 | fn test_user(); 208 | } 209 | 210 | impl UserTest for Env { 211 | type Error = std::convert::Infallible; 212 | 213 | fn setup(&mut self) -> Result, Self::Error> { 214 | Ok(SetupEnv { /* ... */ }) 215 | } 216 | 217 | fn test_user(&mut self) -> Result<(), Self::Error> { 218 | // test implementation 219 | Ok(()) 220 | } 221 | } 222 | ``` 223 | 224 | #### Custom runners 225 | 226 | Implement `StoryRunner` or `AsyncStoryRunner` to customize story execution, 227 | add logging, reporting, or other cross-cutting concerns. 228 | 229 | ```rust 230 | use narrative::runner::StoryRunner; 231 | 232 | struct LoggingRunner; 233 | 234 | impl StoryRunner for LoggingRunner { 235 | fn start_story(&mut self, story: impl narrative::story::StoryContext) -> Result<(), E> { 236 | println!("Starting: {}", story.story_title()); 237 | Ok(()) 238 | } 239 | 240 | fn run_step(&mut self, step: T, state: &mut S) -> Result<(), E> 241 | where 242 | T: narrative::step::Step + narrative::step::Run, 243 | { 244 | println!("Running: {}", step.step_text()); 245 | step.run_with_runner(state, self) 246 | } 247 | 248 | // ... other methods 249 | } 250 | 251 | #[test] 252 | fn test_with_runner() { 253 | let mut env = MyFirstStoryEnv { sum: 0 }; 254 | let mut runner = LoggingRunner; 255 | MyFirstStoryContext.run_story_with_runner(&mut env, &mut runner).unwrap(); 256 | } 257 | ``` 258 | 259 | ### Subtle but Important Points 260 | 261 | #### Implementation details are omitted in story definitions 262 | 263 | Stories should not be concerned with their implementation, so details like `async`, 264 | `&mut self`, and `-> Result<(), Self::Error>` are not required in the trait 265 | definition. The macro infers these from your implementation. Use rust-analyzer's 266 | "Implement missing members" feature to generate the correct signatures. 267 | 268 | ## Design Decisions 269 | 270 | These decisions highlight Narrative's unique aspects, especially in comparison 271 | to [Gauge](https://gauge.org/), a well-known end-to-end testing framework. 272 | 273 | ### Narrative supports multi-language step implementations 274 | 275 | Stories can be introspected at runtime, allowing step implementations in other 276 | languages. The story context provides all metadata needed to dispatch steps to 277 | external processes: 278 | 279 | ```rust 280 | use narrative::story::StoryContext; 281 | 282 | fn execute_story_externally(context: impl StoryContext) { 283 | for step in context.steps() { 284 | let args = step.args().map(|arg| { 285 | ExternalArg { 286 | name: arg.name(), 287 | ty: arg.ty(), 288 | debug: format!("{:?}", arg.value()), 289 | json: serde_json::to_value(arg.value()).unwrap(), 290 | } 291 | }).collect(); 292 | send_to_external_process(step.step_text(), args); 293 | } 294 | } 295 | ``` 296 | 297 | ### Narrative is a library, not a framework 298 | 299 | Narrative has no test runner, no plugin system, nor no dedicated language 300 | server. Instead of being a framework, Narrative is a library that provides just 301 | a single macro to implement stories. It's just a small tie between a story to a 302 | plain Rust code. So, users can compose their own test runners or async runtime 303 | with stories, and can use the full of rust-analyzer's functionality. 304 | 305 | Narrative itself doesn't provide any features other than the core functionality, 306 | declaring stories as traits and implementing them in Rust code. It lays the 307 | groundwork for the simplicity and extensibility of this library. 308 | 309 | The followings are the missing features in Narrative, and they never be 310 | implemented in this library. But don't forget that you can do them by leveraging 311 | the core features. 312 | 313 | - step grouping 314 | - story grouping 315 | - test preparation and cleanup 316 | - table-driven tests 317 | - tags 318 | - screenshoting 319 | - retrying 320 | - parallelization 321 | - error reporting 322 | 323 | ### Narrative uses a declaration of trait to write a story 324 | 325 | In other words, a story is an interface and step implementation depends on it. 326 | 327 | Gauge uses markdown, and it's a great format for writing specifications, 328 | documents, and stories while readable by non programmer. But, it's not the best 329 | format for expressing data in structured way. We think story is more like a data 330 | than a document, and it should be expressed in a structured way. With structured 331 | data, we can leverage the power of software in the processing of them. In 332 | Narrative, we use traits for expressing stories. 333 | 334 | Using markdown for stories has another benefit, that is, it avoids the tight 335 | coupling between stories and the implementation. If stories depends on specific 336 | implementation, the story is not pure, and we loose many benefits of 337 | story-driven development. One of the benefits is that we, including 338 | non-programmer, can write stories freely without regard to the implementation, 339 | and it gives us a kind of agility to the development. 340 | 341 | But, it's not the case in Narrative though it let you write stories in Rust. In 342 | Narrative, stories are written as traits, and it has no dependency to the 343 | implementation, and it's just a contract between the story and the 344 | implementation. Narrative would not loose the benefits of using markdown, on the 345 | contray, it would make the situation better. 346 | 347 | Narrative explicitly separates the story and the implementation, and it forces 348 | the direction of the dependency. With markdown, we know that a story is the core 349 | of the development, but occasionally we forget it or have a kind of cognitive 350 | dissonance. It appeared to us as obvious experiences in the development, like, 351 | "we need to know defined tags in the implementation to write a correct story", 352 | "we have errors on the story editor if no step implementation", or "we failed to 353 | write the correct story because the steps chosen from the editor's suggestion 354 | are not implemented as we expect". In narrative, anyone can write stories 355 | anytime, and stories written can exist as valid real properties with no error 356 | even if implementation are completely undone. 357 | 358 | The concept, a story is a contract to the implementation, makes the development 359 | process and logical dependency graph clean and simple, and although it requires 360 | a bit more effort to implement stories, it would give us a lot of benefits in 361 | the long run of the development. 362 | 363 | Someone might think that writing or reading Rust traits is impossible or 364 | impractical to non-programmer, but we think it more optimistically. We are in 365 | the era of many people can read and write code with the help of the great tools 366 | and AIs, and, Personally, I believes clear codes wins documentation both for 367 | programmers and non-programmers, and I don't think non-programmers cannot read 368 | and write codes. 369 | 370 | ### Narrative encourages no reusing of steps 371 | 372 | We encourage you to write your stories in fresh mind every time without reusing 373 | existing steps, because we think stories should be self-contained. Being the 374 | situation comes with big wins described below. 375 | 376 | #### Accessibility for Newcomers 377 | 378 | It empowers story writers that are not familiar with the existing codebase. They 379 | don't need to know what steps already exist, to struggle with what steps to use, 380 | and to worry about whether the chosen step is implemented as they expect. 381 | 382 | #### Contextual Clarity 383 | 384 | Copying steps from other stories often leads to a mix-up of contexts, and making 385 | it not easy to decipher the key point of a story (without attaching proper 386 | aliases to common steps). While we tend to have many story have the same steps 387 | that shares the same context and implementation, it's challenging to maintain 388 | the coherency of sharing the same logic while we add, remove, modify the 389 | stories. 390 | 391 | One downside of this approach is that stories could have inconsistency in the 392 | writing style among them, but it can be mitigated by organizing stories in the 393 | near each other with have the same contexts. It nudges writers to write stories 394 | in a consistent way. 395 | 396 | #### Simplicity 397 | 398 | Reusing steps or group of steps could be a source of complexity. It's nightmare 399 | to modify a step that is used by many stories without breaking them. 400 | 401 | #### Fine-Grained Abstraction 402 | 403 | A step is relatively large a unit for reuse or abstraction. Instead of sharing 404 | the whole a step, we should share code between stories. But it should be done by 405 | extracting common, story-agnostic, and atomic unit of logic. A step 406 | implementation should be a composition of such units, and it should not leak the 407 | story's context in the abstraction. For instance, if a step is about clicking a 408 | submit button, it might be implemented as a composition of atomic logic like 409 | `find_element_by(id)`, `click(element)`, and `wait_for_page_load()`, and not to 410 | leak the context like `click_submit_button()` or `click_button("#submit")`. 411 | 412 | ## License 413 | 414 | Licensed under either of: 415 | 416 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 417 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 418 | 419 | at your option. 420 | --------------------------------------------------------------------------------