From cee5425cc1ad699185625b2872ffd65f6460c293 Mon Sep 17 00:00:00 2001 From: Leonardo Luarte G Date: Tue, 3 Feb 2026 19:18:35 -0300 Subject: [PATCH 1/7] docs: add developer guide and agents file --- AGENTS.md | 39 +++++++++++++++++++++++++++ README.md | 2 ++ docs/DEVELOPERS.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 AGENTS.md create mode 100644 docs/DEVELOPERS.md 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/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. From f0624a0b6ccb364dc51e8d1a8730ad3197160907 Mon Sep 17 00:00:00 2001 From: Leonardo Luarte G Date: Tue, 3 Feb 2026 19:18:43 -0300 Subject: [PATCH 2/7] fix: forward blocks in callback aliases --- lib/storyteller.rb | 6 +-- spec/storyteller_spec.rb | 82 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) 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 From 10f7ba1261539d3bac83be1069a15496db752c40 Mon Sep 17 00:00:00 2001 From: Leonardo Luarte G Date: Tue, 3 Feb 2026 19:18:48 -0300 Subject: [PATCH 3/7] chore: add mise config and update lockfile --- .mise.toml | 2 ++ Gemfile.lock | 1 + 2 files changed, 3 insertions(+) create mode 100644 .mise.toml 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/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 From 37b428a9376ae265edf3f34eec394cc94a06929a Mon Sep 17 00:00:00 2001 From: Leonardo Luarte G Date: Sat, 28 Mar 2026 00:04:57 -0300 Subject: [PATCH 4/7] chore: upgrade Ruby support to 3.3.11+ and update CI - Bump required_ruby_version to >= 3.3.11 - Update mise config to Ruby 3.4.7 - Overhaul CI workflow: target master branch, matrix over 3.3/3.4/4.0, run rspec and rubocop separately Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/main.yml | 26 +++++++++++--------------- .mise.toml | 2 +- storyteller.gemspec | 2 +- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e73f59c..f7c3f86 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,27 +1,23 @@ -name: Ruby +name: CI on: push: - branches: - - main - + branches: [master] pull_request: jobs: - build: + test: runs-on: ubuntu-latest name: Ruby ${{ matrix.ruby }} strategy: matrix: - ruby: - - '3.1.1' + ruby: ["3.3", "3.4", "4.0"] steps: - - uses: actions/checkout@v3 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.ruby }} - bundler-cache: true - - name: Run the default task - run: bundle exec rake + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - run: bundle exec rspec + - run: bundle exec rubocop diff --git a/.mise.toml b/.mise.toml index 3bc88e6..7f64b26 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,2 +1,2 @@ [tools] -ruby = "3.1.6" +ruby = "3.4.7" diff --git a/storyteller.gemspec b/storyteller.gemspec index 4793840..1435372 100644 --- a/storyteller.gemspec +++ b/storyteller.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |spec| like a recipe, to increase the understanding of the problem' spec.homepage = 'https://blog.avispa.tech/storyteller/' spec.license = 'MIT' - spec.required_ruby_version = '>= 3.1.0' + spec.required_ruby_version = '>= 3.3.11' # spec.metadata['allowed_push_host'] = "TODO: Set to your gem server 'https://example.com'" From 22f7327b706e75092c67e9845ad9c0a5c5715080 Mon Sep 17 00:00:00 2001 From: Leonardo Luarte G Date: Sat, 28 Mar 2026 00:10:29 -0300 Subject: [PATCH 5/7] chore: update lockfile for Ruby 3.4+ compatibility Bump minitest to 6.0.2 (removes ruby < 4.0 constraint) and activesupport to 8.1.3 (declares bigdecimal explicitly, required since Ruby 3.4 removed it from default gems) Co-Authored-By: Claude Sonnet 4.6 --- Gemfile.lock | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 899e831..1e63484 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,22 +8,38 @@ PATH GEM remote: https://rubygems.org/ specs: - activesupport (7.0.4.3) - concurrent-ruby (~> 1.0, >= 1.0.2) + activesupport (8.1.3) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) minitest (>= 5.1) - tzinfo (~> 2.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) ast (2.4.2) + base64 (0.3.0) + bigdecimal (4.1.0) bump (0.10.0) - concurrent-ruby (1.2.2) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) diff-lcs (1.5.0) - i18n (1.12.0) + drb (2.2.3) + i18n (1.14.8) concurrent-ruby (~> 1.0) json (2.6.2) - minitest (5.18.0) + logger (1.7.0) + minitest (6.0.2) + drb (~> 2.0) + prism (~> 1.5) parallel (1.22.1) parser (3.1.2.0) ast (~> 2.4.1) + prism (1.9.0) rainbow (3.1.1) rake (13.0.6) regexp_parser (2.5.0) @@ -58,10 +74,12 @@ GEM rubocop-rspec (2.12.1) rubocop (~> 1.31) ruby-progressbar (1.11.0) + securerandom (0.4.1) smart_init (5.0.2) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.2.0) + uri (1.1.1) PLATFORMS arm64-darwin-21 @@ -78,4 +96,4 @@ DEPENDENCIES storyteller! BUNDLED WITH - 2.3.17 + 4.0.6 From dbd69093bd2b3ae39b5656b0924378f7bea3b01a Mon Sep 17 00:00:00 2001 From: Leonardo Luarte G Date: Sat, 28 Mar 2026 00:12:43 -0300 Subject: [PATCH 6/7] chore: add ostruct as explicit dependency for Ruby 4 compatibility Ruby 4.0 removed ostruct from default gems; declare it explicitly since activesupport requires it at runtime. Co-Authored-By: Claude Sonnet 4.6 --- Gemfile.lock | 2 ++ storyteller.gemspec | 1 + 2 files changed, 3 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index 1e63484..d6d6372 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,6 +3,7 @@ PATH specs: storyteller (0.4.4) activesupport + ostruct smart_init GEM @@ -36,6 +37,7 @@ GEM minitest (6.0.2) drb (~> 2.0) prism (~> 1.5) + ostruct (0.6.3) parallel (1.22.1) parser (3.1.2.0) ast (~> 2.4.1) diff --git a/storyteller.gemspec b/storyteller.gemspec index 1435372..1f28dd9 100644 --- a/storyteller.gemspec +++ b/storyteller.gemspec @@ -35,6 +35,7 @@ Gem::Specification.new do |spec| # Uncomment to register a new dependency of your gem # spec.add_dependency "example-gem", "~> 1.0" spec.add_dependency 'activesupport' + spec.add_dependency 'ostruct' spec.add_dependency 'smart_init' # For more information and examples about making a new gem, check out our From da7edb0575a40c9219785adeba92fdf566dc32ad Mon Sep 17 00:00:00 2001 From: Leonardo Luarte G Date: Sat, 28 Mar 2026 00:26:55 -0300 Subject: [PATCH 7/7] =?UTF-8?q?chore:=20switch=20to=20StandardRB=20and=20f?= =?UTF-8?q?ix=20Ruby=203.3=E2=80=934.0=20compatibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace rubocop/rubocop-rspec/rubocop-rake with standard (>= 1.35.1) - Add .standard.yml disabling Lint/ConstantDefinitionInBlock in specs - Remove .rubocop.yml; update CI and bin/ci to use standardrb - Add benchmark and racc as explicit dev deps for Ruby 4 compatibility - Fix copy-paste bug in specs using wrong class name (SingleInvalidCriteriaClass) - Apply standard style fixes across lib and spec files Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/main.yml | 2 +- .rubocop.yml | 54 ---------------- .standard.yml | 3 + Gemfile | 14 ++--- Gemfile.lock | 94 +++++++++++++++++----------- bin/ci | 30 +++++++++ lib/storyteller.rb | 13 ++-- lib/storyteller/logger.rb | 2 +- lib/storyteller/version.rb | 2 +- lint-standard.sh | 21 +++++++ settings.json | 17 ++++++ spec/spec_helper.rb | 4 +- spec/storyteller_spec.rb | 122 ++++++++++++++++++------------------- storyteller.gemspec | 34 +++++------ 14 files changed, 225 insertions(+), 187 deletions(-) delete mode 100644 .rubocop.yml create mode 100644 .standard.yml create mode 100755 bin/ci create mode 100755 lint-standard.sh create mode 100644 settings.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f7c3f86..497914c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,4 +20,4 @@ jobs: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bundle exec rspec - - run: bundle exec rubocop + - run: bundle exec standardrb diff --git a/.rubocop.yml b/.rubocop.yml deleted file mode 100644 index db58b94..0000000 --- a/.rubocop.yml +++ /dev/null @@ -1,54 +0,0 @@ -require: - - rubocop-rspec - -AllCops: - NewCops: enable - TargetRubyVersion: 3.1.2 - Exclude: - - vendor/bundle/**/* - - '**/db/schema.rb' - - '**/db/**/*' - - 'config/**/*' - - 'bin/*' - - 'config.ru' - - 'Rakefile' - -Metrics/ClassLength: - Enabled: false - -Style/Documentation: - Enabled: false - -Style/ClassAndModuleChildren: - Enabled: false - -Style/EmptyMethod: - Enabled: false - -Bundler/OrderedGems: - Enabled: false - -Lint/UnusedMethodArgument: - Enabled: false - -Style/FrozenStringLiteralComment: - Enabled: false - -# This exclusions are meant for RSpec -Lint/ConstantDefinitionInBlock: - Enabled: false - -RSpec/LeakyConstantDeclaration: - Enabled: false - -RSpec/MultipleExpectations: - Enabled: false - -RSpec/ExampleLength: - Enabled: false - -RSpec/VerifiedDoubleReference: - EnforcedStyle: string - -RSpec/NestedGroups: - Max: 5 \ No newline at end of file diff --git a/.standard.yml b/.standard.yml new file mode 100644 index 0000000..49460ad --- /dev/null +++ b/.standard.yml @@ -0,0 +1,3 @@ +ignore: + - "spec/**/*": + - Lint/ConstantDefinitionInBlock diff --git a/Gemfile b/Gemfile index d552d9e..a311478 100644 --- a/Gemfile +++ b/Gemfile @@ -1,16 +1,16 @@ # frozen_string_literal: true -source 'https://rubygems.org' +source "https://rubygems.org" # Specify your gem's dependencies in storyteller.gemspec gemspec -gem 'rake', '~> 13.0' +gem "rake", "~> 13.0" group :development, :test do - gem 'bump' - gem 'rubocop-rake' - gem 'rubocop-rspec' - gem 'rspec', '~> 3.2' - gem 'rubocop', '~> 1.21' + gem "benchmark" + gem "bump" + gem "racc" + gem "rspec", "~> 3.2" + gem "standard", ">= 1.35.1" end diff --git a/Gemfile.lock b/Gemfile.lock index d6d6372..67f678b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -22,65 +22,85 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) - ast (2.4.2) + ast (2.4.3) base64 (0.3.0) + benchmark (0.5.0) bigdecimal (4.1.0) bump (0.10.0) concurrent-ruby (1.3.6) connection_pool (3.0.2) - diff-lcs (1.5.0) + diff-lcs (1.6.2) drb (2.2.3) i18n (1.14.8) concurrent-ruby (~> 1.0) - json (2.6.2) + json (2.19.3) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) logger (1.7.0) minitest (6.0.2) drb (~> 2.0) prism (~> 1.5) ostruct (0.6.3) - parallel (1.22.1) - parser (3.1.2.0) + parallel (1.27.0) + parser (3.3.11.1) ast (~> 2.4.1) + racc prism (1.9.0) + racc (1.8.1) rainbow (3.1.1) - rake (13.0.6) - regexp_parser (2.5.0) - rexml (3.2.5) - rspec (3.11.0) - rspec-core (~> 3.11.0) - rspec-expectations (~> 3.11.0) - rspec-mocks (~> 3.11.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.0) + rake (13.3.1) + regexp_parser (2.11.3) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.1) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-support (3.11.0) - rubocop (1.32.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.7) + rubocop (1.84.2) json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) parallel (~> 1.10) - parser (>= 3.1.0.0) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.19.1, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.19.1) - parser (>= 3.1.1.0) - rubocop-rake (0.6.0) - rubocop (~> 1.0) - rubocop-rspec (2.12.1) - rubocop (~> 1.31) - ruby-progressbar (1.11.0) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.1) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + ruby-progressbar (1.13.0) securerandom (0.4.1) - smart_init (5.0.2) + smart_init (5.1.0) + standard (1.54.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.84.0) + standard-custom (~> 1.0.0) + standard-performance (~> 1.8) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.9.0) + lint_roller (~> 1.1) + rubocop-performance (~> 1.26.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.2.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) uri (1.1.1) PLATFORMS @@ -89,12 +109,12 @@ PLATFORMS x86_64-linux DEPENDENCIES + benchmark bump + racc rake (~> 13.0) rspec (~> 3.2) - rubocop (~> 1.21) - rubocop-rake - rubocop-rspec + standard (>= 1.35.1) storyteller! BUNDLED WITH diff --git a/bin/ci b/bin/ci new file mode 100755 index 0000000..d4dcae2 --- /dev/null +++ b/bin/ci @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -e + +RUBIES=("3.3.11" "3.4.7" "4.0.1") +PASS=() +FAIL=() + +for version in "${RUBIES[@]}"; do + echo "" + echo "========================================" + echo "Ruby $version" + echo "========================================" + + if mise exec ruby@$version -- bundle install --quiet 2>&1 && \ + mise exec ruby@$version -- bundle exec rspec && \ + mise exec ruby@$version -- bundle exec standardrb; then + PASS+=("$version") + else + FAIL+=("$version") + fi +done + +echo "" +echo "========================================" +echo "Results" +echo "========================================" +for v in "${PASS[@]}"; do echo " PASS Ruby $v"; done +for v in "${FAIL[@]}"; do echo " FAIL Ruby $v"; done + +[ ${#FAIL[@]} -eq 0 ] diff --git a/lib/storyteller.rb b/lib/storyteller.rb index d7cde04..a9ecc7d 100644 --- a/lib/storyteller.rb +++ b/lib/storyteller.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require 'smart_init' -require 'active_support/all' -require_relative 'storyteller/version' -require_relative 'storyteller/logger' +require "smart_init" +require "active_support/all" +require_relative "storyteller/version" +require_relative "storyteller/logger" module Storyteller LOGGER = CustomLogger.new @@ -14,12 +14,13 @@ class Story extend SmartInit def self.initialize_with(*params, **harams) - new_harams = harams.merge({ silent_story: false }) + new_harams = harams.merge({silent_story: false}) super(*params, **new_harams) end is_callable method_name: :execute include ActiveSupport::Callbacks + attr_reader :errors, :result define_callbacks :init, :validation, :preparation, :run, :after_run, :verification @@ -131,7 +132,7 @@ def valid? end def error(element, kind) - @errors << { element:, kind: } + @errors << {element:, kind:} end def initialized? = @stage != :initializing diff --git a/lib/storyteller/logger.rb b/lib/storyteller/logger.rb index e38bba3..3487854 100644 --- a/lib/storyteller/logger.rb +++ b/lib/storyteller/logger.rb @@ -1,4 +1,4 @@ -require 'logger' +require "logger" module Storyteller class CustomLogger < Logger diff --git a/lib/storyteller/version.rb b/lib/storyteller/version.rb index f13244b..9f8c396 100644 --- a/lib/storyteller/version.rb +++ b/lib/storyteller/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Storyteller - VERSION = '0.4.4' + VERSION = "0.4.4" end diff --git a/lint-standard.sh b/lint-standard.sh new file mode 100755 index 0000000..db830b8 --- /dev/null +++ b/lint-standard.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Read Claude's tool input +input=$(cat) +file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty') + +# Only run for Ruby files +if [[ ! "$file_path" =~ \.(rb|rake)$ ]]; then + exit 0 +fi + +# Run StandardRB on the file +# --format simple keeps the output clean for the AI +lint_output=$(bundle exec standardrb --format simple "$file_path" 2>&1) + +if [[ $? -ne 0 ]]; then + # Block the change and provide the linting errors as the reason + echo "{\"decision\": \"block\", \"reason\": \"Standard linting errors:\n$lint_output\"}" +else + # Allow the change to proceed + echo "{\"decision\": \"allow\"}" +fi diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..e1633a3 --- /dev/null +++ b/settings.json @@ -0,0 +1,17 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/lint-standard.sh", + "timeout": 30 + } + ] + } + ] + } +} + diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e5b0704..ccf7c93 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require 'storyteller' +require "storyteller" RSpec.configure do |config| # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = '.rspec_status' + config.example_status_persistence_file_path = ".rspec_status" # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! diff --git a/spec/storyteller_spec.rb b/spec/storyteller_spec.rb index 7413c81..35315f3 100644 --- a/spec/storyteller_spec.rb +++ b/spec/storyteller_spec.rb @@ -10,12 +10,12 @@ def call end RSpec.describe Storyteller do - it 'has a version number' do + it "has a version number" do expect(Storyteller::VERSION).not_to be_nil end - describe '#valid?' do - context 'when no steps are given' do + describe "#valid?" do + context "when no steps are given" do it do class NoStepClass < Storyteller::Story end @@ -24,7 +24,7 @@ class NoStepClass < Storyteller::Story end end - context 'when no validation is added' do + context "when no validation is added" do it do class NoValidationClass < NonEmptyStepStory end @@ -32,18 +32,18 @@ class NoValidationClass < NonEmptyStepStory end end - context 'when single validation is added' do - it 'validates using lambdas' do + context "when single validation is added" do + it "validates using lambdas" do class SingleValidationUsingBlockClass < NonEmptyStepStory initialize_with :a requisite -> { error(:obj_a, :invalid) unless a.valid? } end - obj_d = object_double('User', valid?: true) + obj_d = object_double("User", valid?: true) expect(SingleValidationUsingBlockClass.new(a: obj_d)).to be_valid expect(obj_d).to(have_received(:valid?).at_least(1)) end - it 'validates using symbols' do + it "validates using symbols" do class SingleValidationUsingSymbolClass < NonEmptyStepStory initialize_with :a requisite :check_a @@ -52,28 +52,28 @@ def check_a error(:obj_a, :invalid) unless a.valid? end end - obj_d = instance_double('User', valid?: true) + obj_d = instance_double("User", valid?: true) expect(SingleValidationUsingSymbolClass.new(a: obj_d)).to be_valid expect(obj_d).to(have_received(:valid?).at_least(1)) end end - context 'when multiple validation are added' do - it 'validates using lambdas' do + context "when multiple validation are added" do + it "validates using lambdas" do class MultipleValidationUsingBlockClass < NonEmptyStepStory initialize_with :a, :b requisite -> { error(:obj_a, :invalid) unless a.valid? } requisite -> { error(:obj_b, :invalid) unless b.valid? } end - a = object_double('User', valid?: true) - b = object_double('User', valid?: true) + a = object_double("User", valid?: true) + b = object_double("User", valid?: true) expect(MultipleValidationUsingBlockClass.new(a:, b:)).to be_valid expect(a).to have_received(:valid?).at_least(1) expect(b).to have_received(:valid?).at_least(1) end - it 'validates using symbols' do + it "validates using symbols" do class MultipleValidationUsingSymbolClass < NonEmptyStepStory initialize_with :a, :b requisite :check_a @@ -86,16 +86,16 @@ def check_b end end - a = object_double('User', valid?: true) - b = object_double('User', valid?: true) + a = object_double("User", valid?: true) + b = object_double("User", valid?: true) expect(MultipleValidationUsingSymbolClass.new(a:, b:)).to be_valid expect(a).to have_received(:valid?).at_least(1) expect(b).to have_received(:valid?).at_least(1) end end - context 'when validation criteria is invalid' do - context 'when there is one criteria' do + context "when validation criteria is invalid" do + context "when there is one criteria" do it do class SingleInvalidCriteriaClass < NonEmptyStepStory initialize_with :a @@ -105,12 +105,12 @@ def check_a error(:obj_a, :invalid) unless a.valid? end end - obj_d = object_double('User', valid?: false) + obj_d = object_double("User", valid?: false) expect(SingleInvalidCriteriaClass.new(a: obj_d)).not_to be_valid end end - context 'when some criteria is invalid' do + context "when some criteria is invalid" do it do class PartiallyInvalidCriteriaClass < NonEmptyStepStory initialize_with :a, :b @@ -125,13 +125,13 @@ def check_b error(:obj_b, :invalid) unless b.valid? end end - obj_a = object_double('User', valid?: false) - obj_b = object_double('User', valid?: true) - expect(SingleInvalidCriteriaClass.new(a: obj_a, b: obj_b)).not_to be_valid + obj_a = object_double("User", valid?: false) + obj_b = object_double("User", valid?: true) + expect(PartiallyInvalidCriteriaClass.new(a: obj_a, b: obj_b)).not_to be_valid end end - context 'when all criteria is invalid' do + context "when all criteria is invalid" do it do class AllInvalidCriteriaClass < NonEmptyStepStory initialize_with :a, :b @@ -146,16 +146,16 @@ def check_b error(:obj_b, :invalid) unless b.valid? end end - obj_a = object_double('User', valid?: false) - obj_b = object_double('User', valid?: false) - expect(SingleInvalidCriteriaClass.new(a: obj_a, b: obj_b)).not_to be_valid + obj_a = object_double("User", valid?: false) + obj_b = object_double("User", valid?: false) + expect(AllInvalidCriteriaClass.new(a: obj_a, b: obj_b)).not_to be_valid end end end end - describe '#execute' do - context 'when it has one step' do + describe "#execute" do + context "when it has one step" do it do class OneStepStory < Storyteller::Story initialize_with :spy @@ -165,13 +165,13 @@ def single_step spy.call end end - spy = spy('Thing') # rubocop:disable RSpec/VerifiedDoubles) + spy = spy("Thing") # standard:disable RSpec/VerifiedDoubles OneStepStory.execute(spy:) expect(spy).to have_received(:call) end end - context 'when it has multiple steps' do + context "when it has multiple steps" do it do class MultipleStepStory < Storyteller::Story initialize_with :spy1, :spy2 @@ -182,15 +182,15 @@ def first_step = spy1.call def second_step = spy2.call end - spy1 = spy('Thing') # rubocop:disable RSpec/VerifiedDoubles) - spy2 = spy('Thing') # rubocop:disable RSpec/VerifiedDoubles) + spy1 = spy("Thing") # standard:disable RSpec/VerifiedDoubles + spy2 = spy("Thing") # standard:disable RSpec/VerifiedDoubles MultipleStepStory.execute(spy1:, spy2:) expect(spy1).to have_received(:call) expect(spy2).to have_received(:call) end end - context 'when it has repeated steps' do + context "when it has repeated steps" do it do class RepeatedStepsStory < Storyteller::Story initialize_with :spy @@ -199,20 +199,20 @@ class RepeatedStepsStory < Storyteller::Story def first_step = spy.call end - spy = spy('Thing') # rubocop:disable RSpec/VerifiedDoubles) + spy = spy("Thing") # standard:disable RSpec/VerifiedDoubles RepeatedStepsStory.execute(spy:) expect(spy).to have_received(:call).at_most(1) end end end - describe '#success?' do - context 'when there is no error on any steps' do + describe "#success?" do + context "when there is no error on any steps" do it do expect(NonEmptyStepStory.execute).to be_success end - context 'when there is done criteria' do + context "when there is done criteria" do let(:klass) do class NonEmptyStepWithCriteriaStory < Storyteller::Story initialize_with :spy @@ -227,23 +227,23 @@ def check_spy NonEmptyStepWithCriteriaStory end - context 'when criteria is valid' do + context "when criteria is valid" do it do - spy = object_double('Spy', valid?: true) + spy = object_double("Spy", valid?: true) expect(klass.execute(spy:)).to be_success end end - context 'when criteria is invalid' do + context "when criteria is invalid" do it do - spy = object_double('Spy', valid?: false) + spy = object_double("Spy", valid?: false) expect(klass.execute(spy:)).not_to be_success end end end end - context 'when there is an error on any step' do + context "when there is an error on any step" do it do class FailedStepStory < Storyteller::Story step -> { error(:step, :failure) } @@ -252,8 +252,8 @@ class FailedStepStory < Storyteller::Story expect(FailedStepStory.execute).not_to be_success end - context 'when there is done criteria' do - it 'doesnt call the done criterias' do + context "when there is done criteria" do + it "doesnt call the done criterias" do class FailedStepWithDoneCriteriaStory < Storyteller::Story initialize_with :spy step -> { error(:step, :failure) } @@ -263,7 +263,7 @@ class FailedStepWithDoneCriteriaStory < Storyteller::Story def check = spy.call end - spy = spy('Thing') # rubocop:disable RSpec/VerifiedDoubles + spy = spy("Thing") # rubocop:disable RSpec/VerifiedDoubles expect(FailedStepWithDoneCriteriaStory.execute(spy:)).not_to be_success expect(spy).not_to have_received(:call) end @@ -271,8 +271,8 @@ def check = spy.call end end - describe '#after_run' do - context 'when there silent_story is active' do + describe "#after_run" do + context "when there silent_story is active" do it do class SilentStory < Storyteller::Story initialize_with :spy, captcha: false, returns: :blank @@ -286,7 +286,7 @@ def call_spy end end - spy = spy('Thing') # rubocop:disable RSpec/VerifiedDoubles + spy = spy("Thing") # rubocop:disable RSpec/VerifiedDoubles expect(SilentStory.execute(spy:, silent_story: true)).to be_success expect(spy).not_to have_received(:call) ss = SilentStory.new(spy:, silent_story: true) @@ -298,8 +298,8 @@ def call_spy end end - describe 'aliases and callbacks' do - it 'supports validates_with as an alias of requisite' do + 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 @@ -309,11 +309,11 @@ def check_spy end end - spy = object_double('Spy', valid?: true) + spy = object_double("Spy", valid?: true) expect(ValidatesWithAliasStory.new(spy:)).to be_valid end - it 'supports prepares_with as an alias of prepare' do + it "supports prepares_with as an alias of prepare" do class PreparesWithAliasStory < NonEmptyStepStory initialize_with :spy prepares_with :load_spy @@ -328,11 +328,11 @@ def spy_loaded? end end - spy = object_double('Spy') + spy = object_double("Spy") expect(PreparesWithAliasStory.execute(spy:)).to be_success end - it 'supports done_criteria as an alias of verify' do + it "supports done_criteria as an alias of verify" do class DoneCriteriaAliasStory < Storyteller::Story initialize_with :spy step -> {} @@ -343,11 +343,11 @@ def check_spy end end - spy = object_double('Spy', valid?: true) + spy = object_double("Spy", valid?: true) expect(DoneCriteriaAliasStory.execute(spy:)).to be_success end - it 'runs after_init callbacks once during execution' do + it "runs after_init callbacks once during execution" do class AfterInitStory < Storyteller::Story initialize_with :spy step -> {} @@ -358,12 +358,12 @@ def mark_init end end - spy = spy('Thing') # rubocop:disable RSpec/VerifiedDoubles + 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 + it "supports check with multiple callbacks" do class CheckCallbacksStory < Storyteller::Story initialize_with :spy1, :spy2 check [:first_check, :second_check] @@ -372,8 +372,8 @@ 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 + 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) diff --git a/storyteller.gemspec b/storyteller.gemspec index 1f28dd9..558df11 100644 --- a/storyteller.gemspec +++ b/storyteller.gemspec @@ -1,25 +1,25 @@ # frozen_string_literal: true -require_relative 'lib/storyteller/version' +require_relative "lib/storyteller/version" Gem::Specification.new do |spec| - spec.name = 'storyteller' + spec.name = "storyteller" spec.version = Storyteller::VERSION - spec.authors = ['Leonardo Luarte G'] - spec.email = ['leonardo@luarte.net'] + spec.authors = ["Leonardo Luarte G"] + spec.email = ["leonardo@luarte.net"] - spec.summary = 'Run user stories based on a simple DSL' + spec.summary = "Run user stories based on a simple DSL" spec.description = 'User stories or Use Cases can be written in a procedural way, like a recipe, to increase the understanding of the problem' - spec.homepage = 'https://blog.avispa.tech/storyteller/' - spec.license = 'MIT' - spec.required_ruby_version = '>= 3.3.11' + spec.homepage = "https://blog.avispa.tech/storyteller/" + spec.license = "MIT" + spec.required_ruby_version = ">= 3.3.11" # spec.metadata['allowed_push_host'] = "TODO: Set to your gem server 'https://example.com'" - spec.metadata['homepage_uri'] = spec.homepage - spec.metadata['source_code_uri'] = 'https://github.com/avispatech/storyteller' - spec.metadata['changelog_uri'] = 'https://github.com/avispatech/storyteller/changelog' + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "https://github.com/avispatech/storyteller" + spec.metadata["changelog_uri"] = "https://github.com/avispatech/storyteller/changelog" # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. @@ -28,18 +28,18 @@ Gem::Specification.new do |spec| (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) end end - spec.bindir = 'exe' + spec.bindir = "exe" spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } - spec.require_paths = ['lib'] + spec.require_paths = ["lib"] # Uncomment to register a new dependency of your gem # spec.add_dependency "example-gem", "~> 1.0" - spec.add_dependency 'activesupport' - spec.add_dependency 'ostruct' - spec.add_dependency 'smart_init' + spec.add_dependency "activesupport" + spec.add_dependency "ostruct" + spec.add_dependency "smart_init" # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html # Bundler.require(:default, :development) - spec.metadata['rubygems_mfa_required'] = 'true' + spec.metadata["rubygems_mfa_required"] = "true" end