diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e73f59c..497914c 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 standardrb diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..7f64b26 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,2 @@ +[tools] +ruby = "3.4.7" 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/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 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 901d5d0..67f678b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,78 +3,119 @@ PATH specs: storyteller (0.4.4) activesupport + ostruct smart_init 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) - ast (2.4.2) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + ast (2.4.3) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.1.0) bump (0.10.0) - concurrent-ruby (1.2.2) - diff-lcs (1.5.0) - i18n (1.12.0) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + diff-lcs (1.6.2) + drb (2.2.3) + i18n (1.14.8) concurrent-ruby (~> 1.0) - json (2.6.2) - minitest (5.18.0) - parallel (1.22.1) - parser (3.1.2.0) + 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.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) - smart_init (5.0.2) + 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.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 arm64-darwin-21 + arm64-darwin-24 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 - 2.3.17 + 4.0.6 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/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/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..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 @@ -50,7 +51,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 +69,7 @@ def self.prepare(arg = nil, &) end def self.prepares_with(arg = nil, &block) - prepare(arg, block) + prepare(arg, &block) end # @@ -114,7 +115,7 @@ def self.verify(arg, &) end def self.done_criteria(arg = nil, &block) - verify(arg, block) + verify(arg, &block) end def success? @@ -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 318a4e9..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) @@ -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 diff --git a/storyteller.gemspec b/storyteller.gemspec index 4793840..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.1.0' + 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,17 +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 '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