Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[tools]
ruby = "3.1.6"
39 changes: 39 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ GEM

PLATFORMS
arm64-darwin-21
arm64-darwin-24
x86_64-linux

DEPENDENCIES
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
67 changes: 67 additions & 0 deletions docs/DEVELOPERS.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 3 additions & 3 deletions lib/storyteller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -68,7 +68,7 @@ def self.prepare(arg = nil, &)
end

def self.prepares_with(arg = nil, &block)
prepare(arg, block)
prepare(arg, &block)
end

#
Expand Down Expand Up @@ -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?
Expand Down
82 changes: 82 additions & 0 deletions spec/storyteller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading