diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..3bc88e6 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,2 @@ +[tools] +ruby = "3.1.6" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a5c69a6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,39 @@ +# AGENTS + +This file provides project-specific guidance for AI agents working in this repository. + +## Project Summary +Storyteller is a small Ruby gem that provides a DSL for executing user stories through a staged lifecycle using `ActiveSupport::Callbacks` and `SmartInit`. + +## Repository Map +- `lib/storyteller.rb`: Core DSL and lifecycle logic (`Storyteller::Story`). +- `lib/storyteller/logger.rb`: Custom logger (silent mode warnings). +- `lib/storyteller/version.rb`: Version constant. +- `spec/storyteller_spec.rb`: Main RSpec coverage. +- `bin/setup`: Dependency installation. +- `bin/console`: Interactive console. +- `docs/`: Public documentation (site content). + +## Commands +- Setup: `bin/setup` +- Tests: `bundle exec rake spec` +- Lint: `bundle exec rubocop` +- Default (tests + lint): `bundle exec rake` + +## Development Conventions +- Keep the lifecycle order intact: init → preparation → validation → run → verification → after_run. +- A story must define at least one `step`; validation should fail otherwise. +- Respect `silent_story: true` by skipping `after_run` callbacks and logging a warning. +- Prefer adding or updating tests in `spec/storyteller_spec.rb` when changing behavior. +- Update `CHANGELOG.md` for user-visible behavior changes. + +## When Editing +- Prefer minimal, focused changes that preserve the DSL surface. +- Avoid breaking compatibility with existing callback names (`requisite`, `validate`, `verify`, `done_criteria`). + +## Release Notes +- Version lives in `lib/storyteller/version.rb`. +- Releases are handled by `bundle exec rake release`. + +## If Unsure +- Read `README.md` and `docs/DEVELOPERS.md` before making structural changes. diff --git a/Gemfile.lock b/Gemfile.lock index 901d5d0..899e831 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,6 +65,7 @@ GEM PLATFORMS arm64-darwin-21 + arm64-darwin-24 x86_64-linux DEPENDENCIES diff --git a/README.md b/README.md index 166ba8f..f621112 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,8 @@ To install this gem onto your local machine, run `bundle exec rake install`. To You can learn more about the making process by visiting [AvispaTech's development blog on the subject](https://blog.avispa.tech/2022/08/01/storyteller-1.html). +See `docs/DEVELOPERS.md` for a focused developer guide. + ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/avispatech/storyteller. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/storyteller/blob/main/CODE_OF_CONDUCT.md). diff --git a/docs/DEVELOPERS.md b/docs/DEVELOPERS.md new file mode 100644 index 0000000..a807ffb --- /dev/null +++ b/docs/DEVELOPERS.md @@ -0,0 +1,67 @@ +# Developer Guide + +## Overview +Storyteller is a small Ruby gem that provides a DSL for running user stories via a callback-driven lifecycle. The core implementation lives in `lib/storyteller.rb`, with a custom logger in `lib/storyteller/logger.rb` and the gem version in `lib/storyteller/version.rb`. + +Lifecycle stages and callbacks are implemented with `ActiveSupport::Callbacks`, and initialization is handled by `SmartInit`. + +## Requirements +- Ruby >= 3.1 +- Bundler + +## Setup +```bash +bin/setup +``` + +## Running Tests +```bash +bundle exec rake spec +``` + +## Linting +```bash +bundle exec rubocop +``` + +## Default Task (Tests + Lint) +```bash +bundle exec rake +``` + +## Console +```bash +bin/console +``` + +## Project Layout +- `lib/storyteller.rb`: Main DSL and lifecycle implementation (`Storyteller::Story`). +- `lib/storyteller/logger.rb`: Custom logger used for silent mode warnings. +- `lib/storyteller/version.rb`: Gem version constant. +- `spec/`: RSpec tests. +- `bin/`: Developer scripts (`setup`, `console`). +- `docs/`: Public docs and website content. + +## Key Behaviors +- Stories must define at least one `step` or validation will fail. +- `initialize_with` always injects `silent_story: false` by default. +- `execute` runs lifecycle callbacks in order: init, preparation, validation, run, verification, after_run. +- When `silent_story: true`, `after_run` callbacks are skipped and a warning is logged. + +## Adding or Changing Features +1. Update the DSL or lifecycle logic in `lib/storyteller.rb`. +2. Add/adjust tests in `spec/storyteller_spec.rb`. +3. Run `bundle exec rake`. +4. Update `CHANGELOG.md` if behavior changes. + +## Release Process +1. Update version in `lib/storyteller/version.rb`. +2. Update `CHANGELOG.md`. +3. Run: +```bash +bundle exec rake release +``` + +## Notes +- `requisite` is the canonical validation hook (aliases exist for compatibility). +- `verify` / `done_criteria` is used for post-execution success checks. diff --git a/lib/storyteller.rb b/lib/storyteller.rb index ec101f4..d7cde04 100644 --- a/lib/storyteller.rb +++ b/lib/storyteller.rb @@ -50,7 +50,7 @@ def self.requisite(arg = nil, &block) end def self.validates_with(arg = nil, &block) - requisite(arg, block) + requisite(arg, &block) end set_callback :preparation, :after do @@ -68,7 +68,7 @@ def self.prepare(arg = nil, &) end def self.prepares_with(arg = nil, &block) - prepare(arg, block) + prepare(arg, &block) end # @@ -114,7 +114,7 @@ def self.verify(arg, &) end def self.done_criteria(arg = nil, &block) - verify(arg, block) + verify(arg, &block) end def success? diff --git a/spec/storyteller_spec.rb b/spec/storyteller_spec.rb index 318a4e9..7413c81 100644 --- a/spec/storyteller_spec.rb +++ b/spec/storyteller_spec.rb @@ -297,4 +297,86 @@ def call_spy end end end + + describe 'aliases and callbacks' do + it 'supports validates_with as an alias of requisite' do + class ValidatesWithAliasStory < NonEmptyStepStory + initialize_with :spy + validates_with :check_spy + + def check_spy + error(:spy, :invalid) unless spy.valid? + end + end + + spy = object_double('Spy', valid?: true) + expect(ValidatesWithAliasStory.new(spy:)).to be_valid + end + + it 'supports prepares_with as an alias of prepare' do + class PreparesWithAliasStory < NonEmptyStepStory + initialize_with :spy + prepares_with :load_spy + requisite :spy_loaded? + + def load_spy + @loaded = true + end + + def spy_loaded? + error(:spy, :missing) unless @loaded + end + end + + spy = object_double('Spy') + expect(PreparesWithAliasStory.execute(spy:)).to be_success + end + + it 'supports done_criteria as an alias of verify' do + class DoneCriteriaAliasStory < Storyteller::Story + initialize_with :spy + step -> {} + done_criteria :check_spy + + def check_spy + error(:spy, :invalid) unless spy.valid? + end + end + + spy = object_double('Spy', valid?: true) + expect(DoneCriteriaAliasStory.execute(spy:)).to be_success + end + + it 'runs after_init callbacks once during execution' do + class AfterInitStory < Storyteller::Story + initialize_with :spy + step -> {} + after_init :mark_init + + def mark_init + spy.call + end + end + + spy = spy('Thing') # rubocop:disable RSpec/VerifiedDoubles + AfterInitStory.execute(spy:) + expect(spy).to have_received(:call).at_most(1) + end + + it 'supports check with multiple callbacks' do + class CheckCallbacksStory < Storyteller::Story + initialize_with :spy1, :spy2 + check [:first_check, :second_check] + + def first_check = spy1.call + def second_check = spy2.call + end + + spy1 = spy('Thing') # rubocop:disable RSpec/VerifiedDoubles + spy2 = spy('Thing') # rubocop:disable RSpec/VerifiedDoubles + CheckCallbacksStory.execute(spy1:, spy2:) + expect(spy1).to have_received(:call) + expect(spy2).to have_received(:call) + end + end end