├── .github └── workflows │ ├── actor_like.yml │ ├── aggregate_root.yml │ ├── decider.yml │ ├── document_way.yml │ ├── duck_typing.yml │ ├── extracted_state.yml │ ├── functional.yml │ ├── polymorphic.yml │ ├── poro.yml │ ├── query_based.yml │ ├── rails_way.yml │ ├── repository.yml │ ├── roles.yml │ └── yield_based.yml ├── .ruby-version ├── Makefile ├── README.md ├── examples ├── actor_like │ ├── .mutant.yml │ ├── Gemfile │ ├── Gemfile.lock │ ├── Makefile │ ├── lib │ │ ├── aggregate_repository.rb │ │ ├── aggregate_root.rb │ │ ├── aggregate_state.rb │ │ ├── project_management.rb │ │ └── project_management │ │ │ ├── handler.rb │ │ │ ├── issue.rb │ │ │ └── issue_state.rb │ └── test │ │ └── issue_test.rb ├── aggregate_root │ ├── .mutant.yml │ ├── Gemfile │ ├── Gemfile.lock │ ├── Makefile │ ├── lib │ │ ├── project_management.rb │ │ └── project_management │ │ │ ├── handler.rb │ │ │ ├── issue.rb │ │ │ └── repository.rb │ └── test │ │ └── issue_test.rb ├── decider │ ├── .mutant.yml │ ├── Gemfile │ ├── Gemfile.lock │ ├── Makefile │ ├── lib │ │ ├── project_management.rb │ │ └── project_management │ │ │ ├── handler.rb │ │ │ ├── issue.rb │ │ │ └── repository.rb │ └── test │ │ └── issue_test.rb ├── document_way │ ├── .mutant.yml │ ├── Gemfile │ ├── Gemfile.lock │ ├── Makefile │ ├── lib │ │ ├── project_management.rb │ │ └── project_management │ │ │ ├── handler.rb │ │ │ ├── issue.rb │ │ │ └── repository.rb │ └── test │ │ └── issue_test.rb ├── duck_typing │ ├── .mutant.yml │ ├── Gemfile │ ├── Gemfile.lock │ ├── Makefile │ ├── lib │ │ ├── project_management.rb │ │ └── project_management │ │ │ ├── handler.rb │ │ │ ├── issue.rb │ │ │ ├── repository.rb │ │ │ └── ui.rb │ └── test │ │ └── issue_test.rb ├── extracted_state │ ├── .mutant.yml │ ├── Gemfile │ ├── Gemfile.lock │ ├── Makefile │ ├── lib │ │ ├── project_management.rb │ │ └── project_management │ │ │ ├── handler.rb │ │ │ ├── issue.rb │ │ │ ├── issue_state.rb │ │ │ └── repository.rb │ └── test │ │ └── issue_test.rb ├── functional │ ├── .mutant.yml │ ├── Gemfile │ ├── Gemfile.lock │ ├── Makefile │ ├── lib │ │ ├── project_management.rb │ │ └── project_management │ │ │ ├── handler.rb │ │ │ ├── issue.rb │ │ │ ├── issue_state.rb │ │ │ └── repository.rb │ └── test │ │ └── issue_test.rb ├── polymorphic │ ├── .mutant.yml │ ├── Gemfile │ ├── Gemfile.lock │ ├── Makefile │ ├── lib │ │ ├── project_management.rb │ │ └── project_management │ │ │ ├── handler.rb │ │ │ └── issue.rb │ └── test │ │ └── issue_test.rb ├── poro │ ├── .mutant.yml │ ├── Gemfile │ ├── Gemfile.lock │ ├── Makefile │ ├── lib │ │ ├── project_management.rb │ │ └── project_management │ │ │ ├── handler.rb │ │ │ ├── issue.rb │ │ │ └── issue_projection.rb │ └── test │ │ └── issue_test.rb ├── query_based │ ├── .mutant.yml │ ├── Gemfile │ ├── Gemfile.lock │ ├── Makefile │ ├── lib │ │ ├── project_management.rb │ │ └── project_management │ │ │ ├── handler.rb │ │ │ ├── issue.rb │ │ │ ├── issue_projection.rb │ │ │ └── repository.rb │ └── test │ │ └── issue_test.rb ├── rails_way │ ├── .mutant.yml │ ├── Gemfile │ ├── Gemfile.lock │ ├── Makefile │ ├── lib │ │ ├── project_management.rb │ │ └── project_management │ │ │ ├── handler.rb │ │ │ └── issue.rb │ └── test │ │ └── issue_test.rb ├── repository │ ├── .mutant.yml │ ├── Gemfile │ ├── Gemfile.lock │ ├── Makefile │ ├── lib │ │ ├── project_management.rb │ │ └── project_management │ │ │ ├── handler.rb │ │ │ ├── issue.rb │ │ │ └── repository.rb │ └── test │ │ └── issue_test.rb ├── roles │ ├── .mutant.yml │ ├── Gemfile │ ├── Gemfile.lock │ ├── Makefile │ ├── lib │ │ ├── project_management.rb │ │ └── project_management │ │ │ ├── handler.rb │ │ │ └── issue.rb │ └── test │ │ └── issue_test.rb └── yield_based │ ├── .mutant.yml │ ├── Gemfile │ ├── Gemfile.lock │ ├── Makefile │ ├── lib │ ├── aggregate_repository.rb │ ├── aggregate_root.rb │ ├── project_management.rb │ └── project_management │ │ ├── handler.rb │ │ └── issue.rb │ └── test │ └── issue_test.rb └── shared └── lib ├── project_management.rb └── project_management ├── commands.rb ├── errors.rb ├── events.rb └── test.rb /.github/workflows/actor_like.yml: -------------------------------------------------------------------------------- 1 | name: actor_like 2 | on: 3 | workflow_dispatch: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | jobs: 10 | test: 11 | env: 12 | WORKING_DIRECTORY: examples/actor_like 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: test -e Gemfile.lock 18 | working-directory: ${{ env.WORKING_DIRECTORY }} 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: 3.3.6 22 | bundler-cache: true 23 | working-directory: ${{ env.WORKING_DIRECTORY }} 24 | - run: make test 25 | working-directory: ${{ env.WORKING_DIRECTORY }} 26 | mutate: 27 | env: 28 | WORKING_DIRECTORY: examples/actor_like 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 5 31 | steps: 32 | - uses: actions/checkout@v4 33 | - run: test -e Gemfile.lock 34 | working-directory: ${{ env.WORKING_DIRECTORY }} 35 | - uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: 3.3.6 38 | bundler-cache: true 39 | working-directory: ${{ env.WORKING_DIRECTORY }} 40 | - run: make mutate 41 | working-directory: ${{ env.WORKING_DIRECTORY }} 42 | -------------------------------------------------------------------------------- /.github/workflows/aggregate_root.yml: -------------------------------------------------------------------------------- 1 | name: aggregate_root 2 | on: 3 | workflow_dispatch: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | jobs: 10 | test: 11 | env: 12 | WORKING_DIRECTORY: examples/aggregate_root 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: test -e Gemfile.lock 18 | working-directory: ${{ env.WORKING_DIRECTORY }} 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: 3.3.6 22 | bundler-cache: true 23 | working-directory: ${{ env.WORKING_DIRECTORY }} 24 | - run: make test 25 | working-directory: ${{ env.WORKING_DIRECTORY }} 26 | mutate: 27 | env: 28 | WORKING_DIRECTORY: examples/aggregate_root 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 5 31 | steps: 32 | - uses: actions/checkout@v4 33 | - run: test -e Gemfile.lock 34 | working-directory: ${{ env.WORKING_DIRECTORY }} 35 | - uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: 3.3.6 38 | bundler-cache: true 39 | working-directory: ${{ env.WORKING_DIRECTORY }} 40 | - run: make mutate 41 | working-directory: ${{ env.WORKING_DIRECTORY }} -------------------------------------------------------------------------------- /.github/workflows/decider.yml: -------------------------------------------------------------------------------- 1 | name: decider 2 | on: 3 | workflow_dispatch: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | jobs: 10 | test: 11 | env: 12 | WORKING_DIRECTORY: examples/decider 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: test -e Gemfile.lock 18 | working-directory: ${{ env.WORKING_DIRECTORY }} 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: 3.3.6 22 | bundler-cache: true 23 | working-directory: ${{ env.WORKING_DIRECTORY }} 24 | - run: make test 25 | working-directory: ${{ env.WORKING_DIRECTORY }} 26 | mutate: 27 | env: 28 | WORKING_DIRECTORY: examples/decider 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 5 31 | steps: 32 | - uses: actions/checkout@v4 33 | - run: test -e Gemfile.lock 34 | working-directory: ${{ env.WORKING_DIRECTORY }} 35 | - uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: 3.3.6 38 | bundler-cache: true 39 | working-directory: ${{ env.WORKING_DIRECTORY }} 40 | - run: make mutate 41 | working-directory: ${{ env.WORKING_DIRECTORY }} -------------------------------------------------------------------------------- /.github/workflows/document_way.yml: -------------------------------------------------------------------------------- 1 | name: document_way 2 | on: 3 | workflow_dispatch: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | jobs: 10 | test: 11 | env: 12 | WORKING_DIRECTORY: examples/document_way 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: test -e Gemfile.lock 18 | working-directory: ${{ env.WORKING_DIRECTORY }} 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: 3.3.6 22 | bundler-cache: true 23 | working-directory: ${{ env.WORKING_DIRECTORY }} 24 | - run: make test 25 | working-directory: ${{ env.WORKING_DIRECTORY }} 26 | mutate: 27 | env: 28 | WORKING_DIRECTORY: examples/document_way 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 5 31 | steps: 32 | - uses: actions/checkout@v4 33 | - run: test -e Gemfile.lock 34 | working-directory: ${{ env.WORKING_DIRECTORY }} 35 | - uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: 3.3.6 38 | bundler-cache: true 39 | working-directory: ${{ env.WORKING_DIRECTORY }} 40 | - run: make mutate 41 | working-directory: ${{ env.WORKING_DIRECTORY }} -------------------------------------------------------------------------------- /.github/workflows/duck_typing.yml: -------------------------------------------------------------------------------- 1 | name: duck_typing 2 | on: 3 | workflow_dispatch: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | jobs: 10 | test: 11 | env: 12 | WORKING_DIRECTORY: examples/duck_typing 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: test -e Gemfile.lock 18 | working-directory: ${{ env.WORKING_DIRECTORY }} 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: 3.3.6 22 | bundler-cache: true 23 | working-directory: ${{ env.WORKING_DIRECTORY }} 24 | - run: make test 25 | working-directory: ${{ env.WORKING_DIRECTORY }} 26 | mutate: 27 | env: 28 | WORKING_DIRECTORY: examples/duck_typing 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 5 31 | steps: 32 | - uses: actions/checkout@v4 33 | - run: test -e Gemfile.lock 34 | working-directory: ${{ env.WORKING_DIRECTORY }} 35 | - uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: 3.3.6 38 | bundler-cache: true 39 | working-directory: ${{ env.WORKING_DIRECTORY }} 40 | - run: make mutate 41 | working-directory: ${{ env.WORKING_DIRECTORY }} -------------------------------------------------------------------------------- /.github/workflows/extracted_state.yml: -------------------------------------------------------------------------------- 1 | name: extracted_state 2 | on: 3 | workflow_dispatch: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | jobs: 10 | test: 11 | env: 12 | WORKING_DIRECTORY: examples/extracted_state 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: test -e Gemfile.lock 18 | working-directory: ${{ env.WORKING_DIRECTORY }} 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: 3.3.6 22 | bundler-cache: true 23 | working-directory: ${{ env.WORKING_DIRECTORY }} 24 | - run: make test 25 | working-directory: ${{ env.WORKING_DIRECTORY }} 26 | mutate: 27 | env: 28 | WORKING_DIRECTORY: examples/extracted_state 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 5 31 | steps: 32 | - uses: actions/checkout@v4 33 | - run: test -e Gemfile.lock 34 | working-directory: ${{ env.WORKING_DIRECTORY }} 35 | - uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: 3.3.6 38 | bundler-cache: true 39 | working-directory: ${{ env.WORKING_DIRECTORY }} 40 | - run: make mutate 41 | working-directory: ${{ env.WORKING_DIRECTORY }} 42 | -------------------------------------------------------------------------------- /.github/workflows/functional.yml: -------------------------------------------------------------------------------- 1 | name: functional 2 | on: 3 | workflow_dispatch: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | jobs: 10 | test: 11 | env: 12 | WORKING_DIRECTORY: examples/functional 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: test -e Gemfile.lock 18 | working-directory: ${{ env.WORKING_DIRECTORY }} 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: 3.3.6 22 | bundler-cache: true 23 | working-directory: ${{ env.WORKING_DIRECTORY }} 24 | - run: make test 25 | working-directory: ${{ env.WORKING_DIRECTORY }} 26 | mutate: 27 | env: 28 | WORKING_DIRECTORY: examples/functional 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 5 31 | steps: 32 | - uses: actions/checkout@v4 33 | - run: test -e Gemfile.lock 34 | working-directory: ${{ env.WORKING_DIRECTORY }} 35 | - uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: 3.3.6 38 | bundler-cache: true 39 | working-directory: ${{ env.WORKING_DIRECTORY }} 40 | - run: make mutate 41 | working-directory: ${{ env.WORKING_DIRECTORY }} 42 | -------------------------------------------------------------------------------- /.github/workflows/polymorphic.yml: -------------------------------------------------------------------------------- 1 | name: polymorphic 2 | on: 3 | workflow_dispatch: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | jobs: 10 | test: 11 | env: 12 | WORKING_DIRECTORY: examples/polymorphic 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: test -e Gemfile.lock 18 | working-directory: ${{ env.WORKING_DIRECTORY }} 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: 3.3.6 22 | bundler-cache: true 23 | working-directory: ${{ env.WORKING_DIRECTORY }} 24 | - run: make test 25 | working-directory: ${{ env.WORKING_DIRECTORY }} 26 | mutate: 27 | env: 28 | WORKING_DIRECTORY: examples/polymorphic 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 5 31 | steps: 32 | - uses: actions/checkout@v4 33 | - run: test -e Gemfile.lock 34 | working-directory: ${{ env.WORKING_DIRECTORY }} 35 | - uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: 3.3.6 38 | bundler-cache: true 39 | working-directory: ${{ env.WORKING_DIRECTORY }} 40 | - run: make mutate 41 | working-directory: ${{ env.WORKING_DIRECTORY }} -------------------------------------------------------------------------------- /.github/workflows/poro.yml: -------------------------------------------------------------------------------- 1 | name: poro 2 | on: 3 | workflow_dispatch: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | jobs: 10 | test: 11 | env: 12 | WORKING_DIRECTORY: examples/poro 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: test -e Gemfile.lock 18 | working-directory: ${{ env.WORKING_DIRECTORY }} 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: 3.3.6 22 | bundler-cache: true 23 | working-directory: ${{ env.WORKING_DIRECTORY }} 24 | - run: make test 25 | working-directory: ${{ env.WORKING_DIRECTORY }} 26 | mutate: 27 | env: 28 | WORKING_DIRECTORY: examples/poro 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 5 31 | steps: 32 | - uses: actions/checkout@v4 33 | - run: test -e Gemfile.lock 34 | working-directory: ${{ env.WORKING_DIRECTORY }} 35 | - uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: 3.3.6 38 | bundler-cache: true 39 | working-directory: ${{ env.WORKING_DIRECTORY }} 40 | - run: make mutate 41 | working-directory: ${{ env.WORKING_DIRECTORY }} -------------------------------------------------------------------------------- /.github/workflows/query_based.yml: -------------------------------------------------------------------------------- 1 | name: query_based 2 | on: 3 | workflow_dispatch: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | jobs: 10 | test: 11 | env: 12 | WORKING_DIRECTORY: examples/query_based 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: test -e Gemfile.lock 18 | working-directory: ${{ env.WORKING_DIRECTORY }} 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: 3.3.6 22 | bundler-cache: true 23 | working-directory: ${{ env.WORKING_DIRECTORY }} 24 | - run: make test 25 | working-directory: ${{ env.WORKING_DIRECTORY }} 26 | mutate: 27 | env: 28 | WORKING_DIRECTORY: examples/query_based 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 5 31 | steps: 32 | - uses: actions/checkout@v4 33 | - run: test -e Gemfile.lock 34 | working-directory: ${{ env.WORKING_DIRECTORY }} 35 | - uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: 3.3.6 38 | bundler-cache: true 39 | working-directory: ${{ env.WORKING_DIRECTORY }} 40 | - run: make mutate 41 | working-directory: ${{ env.WORKING_DIRECTORY }} -------------------------------------------------------------------------------- /.github/workflows/rails_way.yml: -------------------------------------------------------------------------------- 1 | name: rails_way 2 | on: 3 | workflow_dispatch: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | jobs: 10 | test: 11 | env: 12 | WORKING_DIRECTORY: examples/rails_way 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: test -e Gemfile.lock 18 | working-directory: ${{ env.WORKING_DIRECTORY }} 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: 3.3.6 22 | bundler-cache: true 23 | working-directory: ${{ env.WORKING_DIRECTORY }} 24 | - run: make test 25 | working-directory: ${{ env.WORKING_DIRECTORY }} 26 | mutate: 27 | env: 28 | WORKING_DIRECTORY: examples/rails_way 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 5 31 | steps: 32 | - uses: actions/checkout@v4 33 | - run: test -e Gemfile.lock 34 | working-directory: ${{ env.WORKING_DIRECTORY }} 35 | - uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: 3.3.6 38 | bundler-cache: true 39 | working-directory: ${{ env.WORKING_DIRECTORY }} 40 | - run: make mutate 41 | working-directory: ${{ env.WORKING_DIRECTORY }} -------------------------------------------------------------------------------- /.github/workflows/repository.yml: -------------------------------------------------------------------------------- 1 | name: repository 2 | on: 3 | workflow_dispatch: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | jobs: 10 | test: 11 | env: 12 | WORKING_DIRECTORY: examples/repository 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: test -e Gemfile.lock 18 | working-directory: ${{ env.WORKING_DIRECTORY }} 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: 3.3.6 22 | bundler-cache: true 23 | working-directory: ${{ env.WORKING_DIRECTORY }} 24 | - run: make test 25 | working-directory: ${{ env.WORKING_DIRECTORY }} 26 | mutate: 27 | env: 28 | WORKING_DIRECTORY: examples/repository 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 5 31 | steps: 32 | - uses: actions/checkout@v4 33 | - run: test -e Gemfile.lock 34 | working-directory: ${{ env.WORKING_DIRECTORY }} 35 | - uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: 3.3.6 38 | bundler-cache: true 39 | working-directory: ${{ env.WORKING_DIRECTORY }} 40 | - run: make mutate 41 | working-directory: ${{ env.WORKING_DIRECTORY }} -------------------------------------------------------------------------------- /.github/workflows/roles.yml: -------------------------------------------------------------------------------- 1 | name: roles 2 | on: 3 | workflow_dispatch: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | jobs: 10 | test: 11 | env: 12 | WORKING_DIRECTORY: examples/roles 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: test -e Gemfile.lock 18 | working-directory: ${{ env.WORKING_DIRECTORY }} 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: 3.3.6 22 | bundler-cache: true 23 | working-directory: ${{ env.WORKING_DIRECTORY }} 24 | - run: make test 25 | working-directory: ${{ env.WORKING_DIRECTORY }} 26 | mutate: 27 | env: 28 | WORKING_DIRECTORY: examples/roles 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 5 31 | steps: 32 | - uses: actions/checkout@v4 33 | - run: test -e Gemfile.lock 34 | working-directory: ${{ env.WORKING_DIRECTORY }} 35 | - uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: 3.3.6 38 | bundler-cache: true 39 | working-directory: ${{ env.WORKING_DIRECTORY }} 40 | - run: make mutate 41 | working-directory: ${{ env.WORKING_DIRECTORY }} -------------------------------------------------------------------------------- /.github/workflows/yield_based.yml: -------------------------------------------------------------------------------- 1 | name: yield_based 2 | on: 3 | workflow_dispatch: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | jobs: 10 | test: 11 | env: 12 | WORKING_DIRECTORY: examples/yield_based 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: test -e Gemfile.lock 18 | working-directory: ${{ env.WORKING_DIRECTORY }} 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: 3.3.6 22 | bundler-cache: true 23 | working-directory: ${{ env.WORKING_DIRECTORY }} 24 | - run: make test 25 | working-directory: ${{ env.WORKING_DIRECTORY }} 26 | mutate: 27 | env: 28 | WORKING_DIRECTORY: examples/yield_based 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 5 31 | steps: 32 | - uses: actions/checkout@v4 33 | - run: test -e Gemfile.lock 34 | working-directory: ${{ env.WORKING_DIRECTORY }} 35 | - uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: 3.3.6 38 | bundler-cache: true 39 | working-directory: ${{ env.WORKING_DIRECTORY }} 40 | - run: make mutate 41 | working-directory: ${{ env.WORKING_DIRECTORY }} -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.2 -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EXAMPLES = actor_like \ 2 | aggregate_root \ 3 | decider \ 4 | document_way \ 5 | duck_typing \ 6 | extracted_state \ 7 | functional \ 8 | polymorphic \ 9 | poro \ 10 | query_based \ 11 | rails_way \ 12 | repository \ 13 | roles \ 14 | yield_based 15 | 16 | $(addprefix test_, $(EXAMPLES)): 17 | @make -C examples/$(subst test_,,$@) test 18 | 19 | $(addprefix mutate_, $(EXAMPLES)): 20 | @make -C examples/$(subst mutate_,,$@) mutate 21 | 22 | $(addprefix install_, $(EXAMPLES)): 23 | @make -C examples/$(subst install_,,$@) install 24 | 25 | install: $(addprefix install_, $(EXAMPLES)) 26 | test: $(addprefix test_, $(EXAMPLES)) 27 | mutate: $(addprefix mutate_, $(EXAMPLES)) 28 | 29 | .PHONY: install test mutate 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aggregates 2 | 3 | An experiment of different aggregate implementations. All implementations must pass same test suite: arranged with commands, asserted with events. 4 | 5 | ## Experiment subject 6 | 7 | Quite typical workflow of an issue in a popular task tracking software (Jira). 8 | 9 | ![workflow](https://confluence.atlassian.com/adminjiraserver072/files/828787890/828787899/1/1456788407758/JIRA+Workflow.png) 10 | 11 | ## Existing experiments 12 | 13 | ### Classical example 14 | 15 | [source](examples/aggregate_root) 16 | 17 | - probably most recognized implementation (appeared in [Greg Young's CQRS example](https://github.com/gregoryyoung/m-r/blob/31d315faf272182d7567a038bbe832a73b879737/SimpleCQRS/Domain.cs#L63-L96)) 18 | - does not expose its internal state via reader methods 19 | - testability without persistence (just check if operation yields correct event) 20 | 21 | Module source: https://github.com/RailsEventStore/rails_event_store/tree/5378b343dbf427f5ea68f7ddfc66d6a449a6ff82/aggregate_root/lib 22 | 23 | ### Aggregate with exposed queries 24 | 25 | [source](examples/query_based) 26 | 27 | - clear separation of state sourcing (with projection) 28 | - aggregate not aware of events 29 | - handler queries aggregate whether particular action is possible 30 | 31 | ### Aggregate with extracted state 32 | 33 | [source](examples/extracted_state) 34 | 35 | - aggregate initialized with already sourced state 36 | 37 | ### Functional aggregate 38 | 39 | [source](examples/functional) 40 | 41 | - no single aggregate, just functions that take state and return events 42 | 43 | ### Polymorphic 44 | 45 | [source](examples/polymorphic) 46 | 47 | - domain classes per each state 48 | - no messaging in domain classes 49 | - no id in domain class 50 | - invalid state transition cared by raising exception 51 | 52 | More: https://blog.arkency.com/make-your-ruby-code-more-modular-and-functional-with-polymorphic-aggregate-classes/ 53 | 54 | ### Duck typing 55 | 56 | [source](examples/duck_typing) 57 | 58 | - domain classes per each state 59 | - no messaging in domain classes 60 | - no id in domain class 61 | - invalid state transition cared by not having such methods on objects (duck) 62 | 63 | ### Aggregate with yield 64 | 65 | [source](examples/yield_based) 66 | 67 | - yield is used to publish events (no unpublished_events in aggregate) 68 | - aggregate repository separated from logic 69 | 70 | ### Aggregate with repository 71 | 72 | [source](examples/repository) 73 | 74 | - aggregate is unware of infrastructure 75 | - aggregate can built itself from events (but it could be recreated in any way) 76 | - aggregate keeps the state in PORO way 77 | - aggregate registers events aka changes 78 | - aggregate provides API to read registered events 79 | - Infrastructure (through repository in this case) is responsible for building/saving the aggregate so it could be done in any way - Event Sourcing, serialization etc 80 | 81 | ### Roles/DCI 82 | 83 | [source](examples/roles) 84 | - better mental model by not having separate classes per state 85 | - one object which changes roles 86 | - `extend(Role.clone)` is used as Ruby ignores subsequent extend with the same module 87 | 88 | ### PORO with attributes 89 | 90 | [source](examples/poro) 91 | 92 | - clear separation of state sourcing (with projection) 93 | - aggregate not aware of events 94 | - aggregate object is still responsible for holding invariants 95 | - no id in domain class 96 | -------------------------------------------------------------------------------- /examples/actor_like/.mutant.yml: -------------------------------------------------------------------------------- 1 | integration: 2 | name: minitest 3 | includes: 4 | - lib 5 | requires: 6 | - project_management 7 | matcher: 8 | subjects: 9 | - ProjectManagement* 10 | ignore: 11 | - ProjectManagement::Test* 12 | coverage_criteria: 13 | process_abort: true 14 | usage: opensource -------------------------------------------------------------------------------- /examples/actor_like/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ruby_event_store" 6 | gem "minitest" 7 | gem "mutant" 8 | gem "mutant-minitest" 9 | -------------------------------------------------------------------------------- /examples/actor_like/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | concurrent-ruby (1.3.4) 6 | diff-lcs (1.5.1) 7 | minitest (5.25.1) 8 | mutant (0.12.4) 9 | diff-lcs (~> 1.3) 10 | parser (~> 3.3.0) 11 | regexp_parser (~> 2.9.0) 12 | sorbet-runtime (~> 0.5.0) 13 | unparser (~> 0.6.14) 14 | mutant-minitest (0.12.4) 15 | minitest (~> 5.11) 16 | mutant (= 0.12.4) 17 | parser (3.3.6.0) 18 | ast (~> 2.4.1) 19 | racc 20 | racc (1.8.1) 21 | regexp_parser (2.9.2) 22 | ruby_event_store (2.15.0) 23 | concurrent-ruby (~> 1.0, >= 1.1.6) 24 | sorbet-runtime (0.5.11647) 25 | unparser (0.6.15) 26 | diff-lcs (~> 1.3) 27 | parser (>= 3.3.0) 28 | 29 | PLATFORMS 30 | arm64-darwin 31 | x86_64-linux 32 | 33 | DEPENDENCIES 34 | minitest 35 | mutant 36 | mutant-minitest 37 | ruby_event_store 38 | 39 | BUNDLED WITH 40 | 2.5.23 41 | -------------------------------------------------------------------------------- /examples/actor_like/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | @bundle install 3 | 4 | test: 5 | @bundle exec ruby -Ilib -rproject_management test/issue_test.rb 6 | 7 | mutate: 8 | @bundle exec mutant run 9 | 10 | .PHONY: install test mutate 11 | -------------------------------------------------------------------------------- /examples/actor_like/lib/aggregate_repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AggregateRepository 4 | def initialize(event_store) 5 | @event_store = event_store 6 | end 7 | 8 | def with_state(state, stream) 9 | @event_store.read.stream(stream).each { |event| state.call(event) } 10 | 11 | store = ->(event) do 12 | @event_store.append(event, stream_name: stream) 13 | true 14 | end 15 | yield state, store 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /examples/actor_like/lib/aggregate_root.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AggregateRoot 4 | module ClassMethods 5 | def on(*event_klasses, &block) 6 | event_klasses.each do |event_klass| 7 | name = 8 | event_klass.name || 9 | raise(ArgumentError, "Anonymous class is missing name") 10 | handler_name = "on_#{name}" 11 | define_method(handler_name, &block) 12 | @on_methods ||= {} 13 | @on_methods[event_klass] = handler_name 14 | private(handler_name) 15 | end 16 | end 17 | 18 | def on_methods 19 | ancestors 20 | .select { |k| k.instance_variables.include?(:@on_methods) } 21 | .map { |k| k.instance_variable_get(:@on_methods) } 22 | .inject({}, &:merge) 23 | end 24 | end 25 | 26 | def self.included(host_class) 27 | host_class.extend(ClassMethods) 28 | end 29 | 30 | def initialize(state) 31 | @state = state 32 | end 33 | 34 | def link(supervisor) 35 | @supervisor = supervisor 36 | self 37 | end 38 | 39 | def apply(event) 40 | supervisor.call(event) if supervisor 41 | state.call(event) 42 | self 43 | end 44 | 45 | private 46 | 47 | attr_reader :supervisor, :state 48 | end 49 | -------------------------------------------------------------------------------- /examples/actor_like/lib/aggregate_state.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AggregateState 4 | module ClassMethods 5 | def on(*event_klasses, &block) 6 | event_klasses.each do |event_klass| 7 | name = 8 | event_klass.name || 9 | raise(ArgumentError, "Anonymous class is missing name") 10 | handler_name = "on_#{name}" 11 | define_method(handler_name, &block) 12 | @on_methods ||= {} 13 | @on_methods[event_klass] = handler_name 14 | private(handler_name) 15 | end 16 | end 17 | 18 | def on_methods 19 | ancestors 20 | .select { |k| k.instance_variables.include?(:@on_methods) } 21 | .map { |k| k.instance_variable_get(:@on_methods) } 22 | .inject({}, &:merge) 23 | end 24 | end 25 | 26 | def self.included(host_class) 27 | host_class.extend(ClassMethods) 28 | end 29 | 30 | def call(event) 31 | name = self.class.on_methods.fetch(event.class) 32 | self.method(name).call(event) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /examples/actor_like/lib/project_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../../shared/lib/project_management" 4 | require_relative "aggregate_state" 5 | require_relative "aggregate_root" 6 | require_relative "aggregate_repository" 7 | require_relative "project_management/handler" 8 | require_relative "project_management/issue_state" 9 | require_relative "project_management/issue" 10 | -------------------------------------------------------------------------------- /examples/actor_like/lib/project_management/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Handler 5 | def initialize(event_store) 6 | @repository = AggregateRepository.new(event_store) 7 | end 8 | 9 | def call(cmd) 10 | case cmd 11 | in CreateIssue[id:] 12 | create(id) 13 | in ResolveIssue[id:] 14 | resolve(id) 15 | in CloseIssue[id:] 16 | close(id) 17 | in ReopenIssue[id:] 18 | reopen(id) 19 | in StartIssueProgress[id:] 20 | start(id) 21 | in StopIssueProgress[id:] 22 | stop(id) 23 | end 24 | rescue Issue::InvalidTransition 25 | raise Error 26 | end 27 | 28 | def create(id) 29 | with_issue(id) { |issue| issue.create(id) } 30 | end 31 | 32 | def resolve(id) 33 | with_issue(id) { |issue| issue.resolve } 34 | end 35 | 36 | def close(id) 37 | with_issue(id) { |issue| issue.close } 38 | end 39 | 40 | def reopen(id) 41 | with_issue(id) { |issue| issue.reopen } 42 | end 43 | 44 | def start(id) 45 | with_issue(id) { |issue| issue.start } 46 | end 47 | 48 | def stop(id) 49 | with_issue(id) { |issue| issue.stop } 50 | end 51 | 52 | private 53 | 54 | def stream_name(id) = "Issue$#{id}" 55 | 56 | def with_issue(id) 57 | @repository.with_state(IssueState.new, stream_name(id)) do |state, store| 58 | yield Issue.new(state).link(store) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /examples/actor_like/lib/project_management/issue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Issue 5 | include AggregateRoot 6 | InvalidTransition = Class.new(StandardError) 7 | 8 | def create(id) 9 | invalid_transition unless can_create? 10 | apply(IssueOpened.new(data: { issue_id: id })) 11 | end 12 | 13 | def resolve 14 | invalid_transition unless can_resolve? 15 | apply(IssueResolved.new(data: { issue_id: state.id })) 16 | end 17 | 18 | def close 19 | invalid_transition unless can_close? 20 | apply(IssueClosed.new(data: { issue_id: state.id })) 21 | end 22 | 23 | def reopen 24 | invalid_transition unless can_reopen? 25 | apply(IssueReopened.new(data: { issue_id: state.id })) 26 | end 27 | 28 | def start 29 | invalid_transition unless can_start? 30 | apply(IssueProgressStarted.new(data: { issue_id: state.id })) 31 | end 32 | 33 | def stop 34 | invalid_transition unless can_stop? 35 | apply(IssueProgressStopped.new(data: { issue_id: state.id })) 36 | end 37 | 38 | private 39 | 40 | def invalid_transition 41 | raise InvalidTransition 42 | end 43 | 44 | def open? 45 | state.status == :open 46 | end 47 | 48 | def closed? 49 | state.status == :closed 50 | end 51 | 52 | def in_progress? 53 | state.status == :in_progress 54 | end 55 | 56 | def reopened? 57 | state.status == :reopened 58 | end 59 | 60 | def resolved? 61 | state.status == :resolved 62 | end 63 | 64 | def can_reopen? 65 | closed? || resolved? 66 | end 67 | 68 | def can_start? 69 | open? || reopened? 70 | end 71 | 72 | def can_stop? 73 | in_progress? 74 | end 75 | 76 | def can_close? 77 | open? || in_progress? || reopened? || resolved? 78 | end 79 | 80 | def can_resolve? 81 | open? || reopened? || in_progress? 82 | end 83 | 84 | def can_create? 85 | state.status.nil? 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /examples/actor_like/lib/project_management/issue_state.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class IssueState < Struct.new(:id, :status) 5 | include AggregateState 6 | 7 | on IssueOpened do |ev| 8 | self.id = ev.data.fetch(:issue_id) 9 | self.status = :open 10 | end 11 | 12 | on IssueResolved do |_ev| 13 | self.status = :resolved 14 | end 15 | 16 | on IssueClosed do |_ev| 17 | self.status = :closed 18 | end 19 | 20 | on IssueReopened do |_ev| 21 | self.status = :reopened 22 | end 23 | 24 | on IssueProgressStarted do |_ev| 25 | self.status = :in_progress 26 | end 27 | 28 | on IssueProgressStopped do |_ev| 29 | self.status = :open 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /examples/actor_like/test/issue_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require "minitest/mock" 5 | require "mutant/minitest/coverage" 6 | require "ruby_event_store" 7 | 8 | require_relative "../lib/project_management" 9 | 10 | module ProjectManagement 11 | class IssueTest < Minitest::Test 12 | include Test.with( 13 | handler: ->(event_store) { Handler.new(event_store) }, 14 | event_store: -> { RubyEventStore::Client.new } 15 | ) 16 | 17 | cover "ProjectManagement::Issue*" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/aggregate_root/.mutant.yml: -------------------------------------------------------------------------------- 1 | integration: 2 | name: minitest 3 | includes: 4 | - lib 5 | requires: 6 | - project_management 7 | matcher: 8 | subjects: 9 | - ProjectManagement* 10 | ignore: 11 | - ProjectManagement::Test* 12 | coverage_criteria: 13 | process_abort: true 14 | usage: opensource -------------------------------------------------------------------------------- /examples/aggregate_root/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ruby_event_store" 6 | gem "minitest" 7 | gem "mutant" 8 | gem "mutant-minitest" 9 | gem "aggregate_root" 10 | -------------------------------------------------------------------------------- /examples/aggregate_root/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | aggregate_root (2.15.0) 5 | ruby_event_store (= 2.15.0) 6 | ast (2.4.2) 7 | concurrent-ruby (1.3.4) 8 | diff-lcs (1.5.1) 9 | minitest (5.25.1) 10 | mutant (0.12.4) 11 | diff-lcs (~> 1.3) 12 | parser (~> 3.3.0) 13 | regexp_parser (~> 2.9.0) 14 | sorbet-runtime (~> 0.5.0) 15 | unparser (~> 0.6.14) 16 | mutant-minitest (0.12.4) 17 | minitest (~> 5.11) 18 | mutant (= 0.12.4) 19 | parser (3.3.6.0) 20 | ast (~> 2.4.1) 21 | racc 22 | racc (1.8.1) 23 | regexp_parser (2.9.2) 24 | ruby_event_store (2.15.0) 25 | concurrent-ruby (~> 1.0, >= 1.1.6) 26 | sorbet-runtime (0.5.11647) 27 | unparser (0.6.15) 28 | diff-lcs (~> 1.3) 29 | parser (>= 3.3.0) 30 | 31 | PLATFORMS 32 | arm64-darwin 33 | x86_64-linux 34 | 35 | DEPENDENCIES 36 | aggregate_root 37 | minitest 38 | mutant 39 | mutant-minitest 40 | ruby_event_store 41 | 42 | BUNDLED WITH 43 | 2.5.23 44 | -------------------------------------------------------------------------------- /examples/aggregate_root/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | @bundle install 3 | 4 | test: 5 | @bundle exec ruby -Ilib -rproject_management test/issue_test.rb 6 | 7 | mutate: 8 | @bundle exec mutant run 9 | 10 | .PHONY: install test mutate 11 | -------------------------------------------------------------------------------- /examples/aggregate_root/lib/project_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "aggregate_root" 4 | 5 | require_relative "../../../shared/lib/project_management" 6 | require_relative "project_management/handler" 7 | require_relative "project_management/repository" 8 | require_relative "project_management/issue" 9 | -------------------------------------------------------------------------------- /examples/aggregate_root/lib/project_management/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Handler 5 | def initialize(event_store) 6 | @repository = Repository.new(event_store) 7 | end 8 | 9 | def call(cmd) 10 | case cmd 11 | in CreateIssue[id:] 12 | create(id) 13 | in ResolveIssue[id:] 14 | resolve(id) 15 | in CloseIssue[id:] 16 | close(id) 17 | in ReopenIssue[id:] 18 | reopen(id) 19 | in StartIssueProgress[id:] 20 | start(id) 21 | in StopIssueProgress[id:] 22 | stop(id) 23 | end 24 | rescue Issue::InvalidTransition 25 | raise Error 26 | end 27 | 28 | private 29 | 30 | def create(id) 31 | issue, stream_name = Issue.new(id), stream_name(id) 32 | @repository.load(issue, stream_name) 33 | issue.open 34 | @repository.store(issue, stream_name) 35 | end 36 | 37 | def resolve(id) 38 | issue, stream_name = Issue.new(id), stream_name(id) 39 | @repository.load(issue, stream_name) 40 | issue.resolve 41 | @repository.store(issue, stream_name) 42 | end 43 | 44 | def close(id) 45 | issue, stream_name = Issue.new(id), stream_name(id) 46 | @repository.load(issue, stream_name) 47 | issue.close 48 | @repository.store(issue, stream_name) 49 | end 50 | 51 | def reopen(id) 52 | issue, stream_name = Issue.new(id), stream_name(id) 53 | @repository.load(issue, stream_name) 54 | issue.reopen 55 | @repository.store(issue, stream_name) 56 | end 57 | 58 | def start(id) 59 | issue, stream_name = Issue.new(id), stream_name(id) 60 | @repository.load(issue, stream_name) 61 | issue.start 62 | @repository.store(issue, stream_name) 63 | end 64 | 65 | def stop(id) 66 | issue, stream_name = Issue.new(id), stream_name(id) 67 | @repository.load(issue, stream_name) 68 | issue.stop 69 | @repository.store(issue, stream_name) 70 | end 71 | 72 | def stream_name(id) = "Issue$#{id}" 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /examples/aggregate_root/lib/project_management/issue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Issue 5 | include AggregateRoot 6 | 7 | InvalidTransition = Class.new(StandardError) 8 | 9 | def initialize(id) 10 | @id = id 11 | end 12 | 13 | def open 14 | fail if @status 15 | 16 | apply(IssueOpened.new(data: { issue_id: @id })) 17 | end 18 | 19 | def resolve 20 | fail unless %i[open in_progress reopened].include? @status 21 | 22 | apply(IssueResolved.new(data: { issue_id: @id })) 23 | end 24 | 25 | def close 26 | fail unless %i[open in_progress resolved reopened].include? @status 27 | 28 | apply(IssueClosed.new(data: { issue_id: @id })) 29 | end 30 | 31 | def reopen 32 | fail unless %i[resolved closed].include? @status 33 | 34 | apply(IssueReopened.new(data: { issue_id: @id })) 35 | end 36 | 37 | def start 38 | fail unless %i[open reopened].include? @status 39 | 40 | apply(IssueProgressStarted.new(data: { issue_id: @id })) 41 | end 42 | 43 | def stop 44 | fail unless %i[in_progress].include? @status 45 | 46 | apply(IssueProgressStopped.new(data: { issue_id: @id })) 47 | end 48 | 49 | private 50 | 51 | def fail = raise InvalidTransition 52 | 53 | on(IssueOpened) { |_event| @status = :open } 54 | on(IssueResolved) { |_event| @status = :resolved } 55 | on(IssueClosed) { |_event| @status = :closed } 56 | on(IssueReopened) { |_event| @status = :reopened } 57 | on(IssueProgressStarted) { |_event| @status = :in_progress } 58 | on(IssueProgressStopped) { |_event| @status = :open } 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /examples/aggregate_root/lib/project_management/repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | Repository = AggregateRoot::Repository 5 | end 6 | -------------------------------------------------------------------------------- /examples/aggregate_root/test/issue_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require "minitest/mock" 5 | require "mutant/minitest/coverage" 6 | require "ruby_event_store" 7 | 8 | require_relative "../lib/project_management" 9 | 10 | module ProjectManagement 11 | class IssueTest < Minitest::Test 12 | include Test.with( 13 | handler: ->(event_store) { Handler.new(event_store) }, 14 | event_store: -> { RubyEventStore::Client.new } 15 | ) 16 | 17 | cover "ProjectManagement::Issue*" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/decider/.mutant.yml: -------------------------------------------------------------------------------- 1 | integration: 2 | name: minitest 3 | includes: 4 | - lib 5 | requires: 6 | - project_management 7 | matcher: 8 | subjects: 9 | - ProjectManagement* 10 | ignore: 11 | - ProjectManagement::Test* 12 | coverage_criteria: 13 | process_abort: true 14 | usage: opensource -------------------------------------------------------------------------------- /examples/decider/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ruby_event_store" 6 | gem "minitest" 7 | gem "mutant" 8 | gem "mutant-minitest" 9 | -------------------------------------------------------------------------------- /examples/decider/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | concurrent-ruby (1.3.4) 6 | diff-lcs (1.5.1) 7 | minitest (5.25.1) 8 | mutant (0.12.4) 9 | diff-lcs (~> 1.3) 10 | parser (~> 3.3.0) 11 | regexp_parser (~> 2.9.0) 12 | sorbet-runtime (~> 0.5.0) 13 | unparser (~> 0.6.14) 14 | mutant-minitest (0.12.4) 15 | minitest (~> 5.11) 16 | mutant (= 0.12.4) 17 | parser (3.3.6.0) 18 | ast (~> 2.4.1) 19 | racc 20 | racc (1.8.1) 21 | regexp_parser (2.9.2) 22 | ruby_event_store (2.15.0) 23 | concurrent-ruby (~> 1.0, >= 1.1.6) 24 | sorbet-runtime (0.5.11647) 25 | unparser (0.6.15) 26 | diff-lcs (~> 1.3) 27 | parser (>= 3.3.0) 28 | 29 | PLATFORMS 30 | arm64-darwin 31 | x86_64-linux 32 | 33 | DEPENDENCIES 34 | minitest 35 | mutant 36 | mutant-minitest 37 | ruby_event_store 38 | 39 | BUNDLED WITH 40 | 2.5.23 41 | -------------------------------------------------------------------------------- /examples/decider/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | @bundle install 3 | 4 | test: 5 | @bundle exec ruby -Ilib -rproject_management test/issue_test.rb 6 | 7 | mutate: 8 | @bundle exec mutant run 9 | 10 | .PHONY: install test mutate 11 | -------------------------------------------------------------------------------- /examples/decider/lib/project_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../../shared/lib/project_management" 4 | require_relative "project_management/handler" 5 | require_relative "project_management/issue" 6 | require_relative "project_management/repository" 7 | -------------------------------------------------------------------------------- /examples/decider/lib/project_management/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Handler 5 | def initialize(event_store) 6 | @decider = Issue 7 | @repository = Repository.new(event_store) 8 | end 9 | 10 | def call(cmd) 11 | state = @repository.load(cmd.id, @decider) 12 | 13 | case @decider.decide(cmd, state) 14 | in [StandardError] 15 | raise Error 16 | in [Event => event] 17 | @repository.store(cmd.id, event) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/decider/lib/project_management/issue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | module Issue 5 | InvalidTransition = Class.new(StandardError) 6 | State = Data.define(:id, :status) 7 | 8 | class << self 9 | def decide(command, state) 10 | case [command, state] 11 | in [CreateIssue, State(id:, status: nil)] 12 | [IssueOpened.new(data: { issue_id: id })] 13 | in [ResolveIssue, State(id:, status: :open | :in_progress | :reopened)] 14 | [IssueResolved.new(data: { issue_id: id })] 15 | in [CloseIssue, State(id:, status: :open | :in_progress | :resolved | :reopened)] 16 | [IssueClosed.new(data: { issue_id: id })] 17 | in [ReopenIssue, State(id:, status: :resolved | :closed)] 18 | [IssueReopened.new(data: { issue_id: id })] 19 | in [StartIssueProgress, State(id:, status: :open | :reopened)] 20 | [IssueProgressStarted.new(data: { issue_id: id })] 21 | in [StopIssueProgress, State(id:, status: :in_progress)] 22 | [IssueProgressStopped.new(data: { issue_id: id })] 23 | else 24 | [InvalidTransition.new] 25 | end 26 | end 27 | 28 | def evolve(state, event) 29 | case event 30 | when IssueOpened 31 | state.with(status: :open) 32 | when IssueResolved 33 | state.with(status: :resolved) 34 | when IssueClosed 35 | state.with(status: :closed) 36 | when IssueReopened 37 | state.with(status: :reopened) 38 | when IssueProgressStarted 39 | state.with(status: :in_progress) 40 | when IssueProgressStopped 41 | state.with(status: :open) 42 | end 43 | end 44 | 45 | def initial_state(id) 46 | State.new(id: id, status: nil) 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /examples/decider/lib/project_management/repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Repository 5 | def initialize(event_store) 6 | @event_store = event_store 7 | end 8 | 9 | def load(id, decider) 10 | @event_store 11 | .read 12 | .stream(stream_name(id)) 13 | .reduce(decider.initial_state(id)) do |state, event| 14 | decider.evolve(state, event) 15 | end 16 | end 17 | 18 | def store(id, events) 19 | @event_store.append(events, stream_name: stream_name(id)) 20 | end 21 | 22 | private 23 | 24 | def stream_name(id) = "Issue$#{id}" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /examples/decider/test/issue_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require "minitest/mock" 5 | require "mutant/minitest/coverage" 6 | require "ruby_event_store" 7 | 8 | require_relative "../lib/project_management" 9 | 10 | module ProjectManagement 11 | class IssueTest < Minitest::Test 12 | include Test.with( 13 | handler: ->(event_store) { Handler.new(event_store) }, 14 | event_store: -> { RubyEventStore::Client.new } 15 | ) 16 | 17 | cover "ProjectManagement::Issue*" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/document_way/.mutant.yml: -------------------------------------------------------------------------------- 1 | integration: 2 | name: minitest 3 | includes: 4 | - lib 5 | requires: 6 | - project_management 7 | matcher: 8 | subjects: 9 | - ProjectManagement* 10 | ignore: 11 | - ProjectManagement::Test* 12 | coverage_criteria: 13 | process_abort: true 14 | usage: opensource -------------------------------------------------------------------------------- /examples/document_way/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ruby_event_store" 6 | gem "minitest" 7 | gem "mutant" 8 | gem "mutant-minitest" 9 | gem "activerecord", ">= 8" 10 | gem "sqlite3", ">= 2" 11 | -------------------------------------------------------------------------------- /examples/document_way/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activemodel (8.0.0) 5 | activesupport (= 8.0.0) 6 | activerecord (8.0.0) 7 | activemodel (= 8.0.0) 8 | activesupport (= 8.0.0) 9 | timeout (>= 0.4.0) 10 | activesupport (8.0.0) 11 | base64 12 | benchmark (>= 0.3) 13 | bigdecimal 14 | concurrent-ruby (~> 1.0, >= 1.3.1) 15 | connection_pool (>= 2.2.5) 16 | drb 17 | i18n (>= 1.6, < 2) 18 | logger (>= 1.4.2) 19 | minitest (>= 5.1) 20 | securerandom (>= 0.3) 21 | tzinfo (~> 2.0, >= 2.0.5) 22 | uri (>= 0.13.1) 23 | ast (2.4.2) 24 | base64 (0.2.0) 25 | benchmark (0.4.0) 26 | bigdecimal (3.1.8) 27 | concurrent-ruby (1.3.4) 28 | connection_pool (2.4.1) 29 | diff-lcs (1.5.1) 30 | drb (2.2.1) 31 | i18n (1.14.6) 32 | concurrent-ruby (~> 1.0) 33 | logger (1.6.1) 34 | minitest (5.25.1) 35 | mutant (0.12.4) 36 | diff-lcs (~> 1.3) 37 | parser (~> 3.3.0) 38 | regexp_parser (~> 2.9.0) 39 | sorbet-runtime (~> 0.5.0) 40 | unparser (~> 0.6.14) 41 | mutant-minitest (0.12.4) 42 | minitest (~> 5.11) 43 | mutant (= 0.12.4) 44 | parser (3.3.6.0) 45 | ast (~> 2.4.1) 46 | racc 47 | racc (1.8.1) 48 | regexp_parser (2.9.2) 49 | ruby_event_store (2.15.0) 50 | concurrent-ruby (~> 1.0, >= 1.1.6) 51 | securerandom (0.3.2) 52 | sorbet-runtime (0.5.11647) 53 | sqlite3 (2.3.0-arm64-darwin) 54 | sqlite3 (2.3.0-x86_64-linux-gnu) 55 | timeout (0.4.2) 56 | tzinfo (2.0.6) 57 | concurrent-ruby (~> 1.0) 58 | unparser (0.6.15) 59 | diff-lcs (~> 1.3) 60 | parser (>= 3.3.0) 61 | uri (1.0.2) 62 | 63 | PLATFORMS 64 | arm64-darwin 65 | x86_64-linux 66 | 67 | DEPENDENCIES 68 | activerecord (>= 8) 69 | minitest 70 | mutant 71 | mutant-minitest 72 | ruby_event_store 73 | sqlite3 (>= 2) 74 | 75 | BUNDLED WITH 76 | 2.5.23 77 | -------------------------------------------------------------------------------- /examples/document_way/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | @bundle install 3 | 4 | test: 5 | @bundle exec ruby -Ilib -rproject_management test/issue_test.rb 6 | 7 | mutate: 8 | @bundle exec mutant run 9 | 10 | .PHONY: install test mutate 11 | -------------------------------------------------------------------------------- /examples/document_way/lib/project_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_record" 4 | 5 | require_relative "../../../shared/lib/project_management" 6 | require_relative "project_management/handler" 7 | require_relative "project_management/issue" 8 | require_relative "project_management/repository" 9 | -------------------------------------------------------------------------------- /examples/document_way/lib/project_management/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Handler 5 | def initialize(event_store) 6 | @event_store = event_store 7 | end 8 | 9 | def call(cmd) 10 | case cmd 11 | in CreateIssue[id:] 12 | create(id) 13 | in ResolveIssue[id:] 14 | resolve(id) 15 | in CloseIssue[id:] 16 | close(id) 17 | in ReopenIssue[id:] 18 | reopen(id) 19 | in StartIssueProgress[id:] 20 | start(id) 21 | in StopIssueProgress[id:] 22 | stop(id) 23 | end 24 | rescue Issue::InvalidTransition 25 | raise Error 26 | end 27 | 28 | def create(id) = with_aggregate(id) { |issue| issue.open } 29 | def resolve(id) = with_aggregate(id) { |issue| issue.resolve } 30 | def close(id) = with_aggregate(id) { |issue| issue.close } 31 | def reopen(id) = with_aggregate(id) { |issue| issue.reopen } 32 | def start(id) = with_aggregate(id) { |issue| issue.start } 33 | def stop(id) = with_aggregate(id) { |issue| issue.stop } 34 | 35 | private 36 | 37 | def stream_name(id) = "Issue$#{id}" 38 | 39 | def with_aggregate(id) 40 | repository = Repository.new(id) 41 | issue = Issue.new(repository.load) 42 | 43 | @event_store.append(yield(issue), stream_name: stream_name(id)) 44 | repository.store(issue.state) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /examples/document_way/lib/project_management/issue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Issue 5 | InvalidTransition = Class.new(StandardError) 6 | 7 | attr_reader :state 8 | 9 | def initialize(state) 10 | @state = state 11 | end 12 | 13 | def open 14 | fail if state.status 15 | 16 | state.status = "open" 17 | IssueOpened.new(data: { issue_id: state.id }) 18 | end 19 | 20 | def resolve 21 | fail unless %w[open in_progress reopened].include? state.status 22 | 23 | state.status = "resolved" 24 | IssueResolved.new(data: { issue_id: state.id }) 25 | end 26 | 27 | def close 28 | fail unless %w[open in_progress resolved reopened].include? state.status 29 | 30 | state.status = "closed" 31 | IssueClosed.new(data: { issue_id: state.id }) 32 | end 33 | 34 | def reopen 35 | fail unless %w[resolved closed].include? state.status 36 | 37 | state.status = "reopened" 38 | IssueReopened.new(data: { issue_id: state.id }) 39 | end 40 | 41 | def start 42 | fail unless %w[open reopened].include? state.status 43 | 44 | state.status = "in_progress" 45 | IssueProgressStarted.new(data: { issue_id: state.id }) 46 | end 47 | 48 | def stop 49 | fail unless %w[in_progress].include? state.status 50 | 51 | state.status = "open" 52 | IssueProgressStopped.new(data: { issue_id: state.id }) 53 | end 54 | 55 | def fail = raise InvalidTransition 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /examples/document_way/lib/project_management/repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Repository 5 | class Record < ActiveRecord::Base 6 | self.table_name = :issues 7 | end 8 | private_constant :Record 9 | 10 | State = Struct.new(:id, :status) 11 | 12 | def initialize(id) 13 | @id = id 14 | end 15 | 16 | def store(state) 17 | Record.where(uuid: @id).update_all(status: state.status) 18 | end 19 | 20 | def load 21 | record = Record.find_or_create_by(uuid: @id) 22 | State.new(record.uuid, record.status) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /examples/document_way/test/issue_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require "minitest/mock" 5 | require "mutant/minitest/coverage" 6 | require "ruby_event_store" 7 | 8 | require_relative "../lib/project_management" 9 | 10 | module ProjectManagement 11 | class IssueTest < Minitest::Test 12 | include Test.with( 13 | handler: ->(event_store) { Handler.new(event_store) }, 14 | event_store: -> { RubyEventStore::Client.new } 15 | ) 16 | 17 | cover "ProjectManagement::Issue*" 18 | 19 | def test_save_targets_record_of_expected_id 20 | assert_query( 21 | /UPDATE "issues" SET "status" = \? WHERE "issues"."uuid" = \?/ 22 | ) { create_issue } 23 | end 24 | 25 | def setup 26 | ActiveRecord::Base.establish_connection( 27 | adapter: "sqlite3", 28 | database: ":memory:" 29 | ) 30 | 31 | ActiveRecord::Schema.verbose = false 32 | ActiveRecord::Schema.define do 33 | create_table :issues, force: true do |t| 34 | t.string :uuid 35 | t.string :status 36 | end 37 | 38 | add_index :issues, :uuid, unique: true 39 | end 40 | end 41 | 42 | def assert_query(regex, &block) 43 | queries = [] 44 | callback = ->(_, _, _, _, payload) { queries << payload[:sql] } 45 | 46 | ActiveSupport::Notifications.subscribed( 47 | callback, 48 | "sql.active_record", 49 | &block 50 | ) 51 | assert queries.any? { |q| regex.match(q) }, <<~MSG 52 | Expected query matching #{regex.inspect} to be executed 53 | 54 | Recored queries: 55 | #{queries.grep_v(/transaction/).join("\n")} 56 | MSG 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /examples/duck_typing/.mutant.yml: -------------------------------------------------------------------------------- 1 | integration: 2 | name: minitest 3 | includes: 4 | - lib 5 | requires: 6 | - project_management 7 | matcher: 8 | subjects: 9 | - ProjectManagement* 10 | ignore: 11 | - ProjectManagement::Test* 12 | - ProjectManagement::UI* 13 | coverage_criteria: 14 | process_abort: true 15 | usage: opensource -------------------------------------------------------------------------------- /examples/duck_typing/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ruby_event_store" 6 | gem "minitest" 7 | gem "mutant" 8 | gem "mutant-minitest" 9 | gem "tty-prompt" 10 | -------------------------------------------------------------------------------- /examples/duck_typing/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | concurrent-ruby (1.3.4) 6 | diff-lcs (1.5.1) 7 | minitest (5.25.1) 8 | mutant (0.12.4) 9 | diff-lcs (~> 1.3) 10 | parser (~> 3.3.0) 11 | regexp_parser (~> 2.9.0) 12 | sorbet-runtime (~> 0.5.0) 13 | unparser (~> 0.6.14) 14 | mutant-minitest (0.12.4) 15 | minitest (~> 5.11) 16 | mutant (= 0.12.4) 17 | parser (3.3.6.0) 18 | ast (~> 2.4.1) 19 | racc 20 | pastel (0.8.0) 21 | tty-color (~> 0.5) 22 | racc (1.8.1) 23 | regexp_parser (2.9.2) 24 | ruby_event_store (2.15.0) 25 | concurrent-ruby (~> 1.0, >= 1.1.6) 26 | sorbet-runtime (0.5.11647) 27 | tty-color (0.6.0) 28 | tty-cursor (0.7.1) 29 | tty-prompt (0.23.1) 30 | pastel (~> 0.8) 31 | tty-reader (~> 0.8) 32 | tty-reader (0.9.0) 33 | tty-cursor (~> 0.7) 34 | tty-screen (~> 0.8) 35 | wisper (~> 2.0) 36 | tty-screen (0.8.2) 37 | unparser (0.6.15) 38 | diff-lcs (~> 1.3) 39 | parser (>= 3.3.0) 40 | wisper (2.0.1) 41 | 42 | PLATFORMS 43 | arm64-darwin 44 | x86_64-linux 45 | 46 | DEPENDENCIES 47 | minitest 48 | mutant 49 | mutant-minitest 50 | ruby_event_store 51 | tty-prompt 52 | 53 | BUNDLED WITH 54 | 2.5.23 55 | -------------------------------------------------------------------------------- /examples/duck_typing/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | @bundle install 3 | 4 | test: 5 | @bundle exec ruby -Ilib -rproject_management test/issue_test.rb 6 | 7 | mutate: 8 | @bundle exec mutant run 9 | 10 | .PHONY: install test mutate 11 | -------------------------------------------------------------------------------- /examples/duck_typing/lib/project_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "tty-prompt" 4 | 5 | require_relative "../../../shared/lib/project_management" 6 | require_relative "project_management/handler" 7 | require_relative "project_management/issue" 8 | require_relative "project_management/repository" 9 | require_relative "project_management/ui" 10 | -------------------------------------------------------------------------------- /examples/duck_typing/lib/project_management/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Handler 5 | def initialize(event_store) 6 | @repository = Repository.new(event_store) 7 | end 8 | 9 | def call(cmd) 10 | case cmd 11 | in CreateIssue[id:] 12 | create(id) 13 | in ResolveIssue[id:] 14 | resolve(id) 15 | in CloseIssue[id:] 16 | close(id) 17 | in ReopenIssue[id:] 18 | reopen(id) 19 | in StartIssueProgress[id:] 20 | start(id) 21 | in StopIssueProgress[id:] 22 | stop(id) 23 | end 24 | rescue NoMethodError 25 | raise Error 26 | end 27 | 28 | def create(id) 29 | load_issue(id) do |issue| 30 | issue.open 31 | IssueOpened.new(data: { issue_id: id }) 32 | end 33 | end 34 | 35 | def close(id) 36 | load_issue(id) do |issue| 37 | issue.close 38 | IssueClosed.new(data: { issue_id: id }) 39 | end 40 | end 41 | 42 | def start(id) 43 | load_issue(id) do |issue| 44 | issue.start 45 | IssueProgressStarted.new(data: { issue_id: id }) 46 | end 47 | end 48 | 49 | def stop(id) 50 | load_issue(id) do |issue| 51 | issue.stop 52 | IssueProgressStopped.new(data: { issue_id: id }) 53 | end 54 | end 55 | 56 | def reopen(id) 57 | load_issue(id) do |issue| 58 | issue.reopen 59 | IssueReopened.new(data: { issue_id: id }) 60 | end 61 | end 62 | 63 | def resolve(id) 64 | load_issue(id) do |issue| 65 | issue.resolve 66 | IssueResolved.new(data: { issue_id: id }) 67 | end 68 | end 69 | 70 | private 71 | 72 | def load_issue(id) 73 | issue = @repository.load(id, Issue.new) 74 | events = yield(issue) 75 | @repository.store(id, events) 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /examples/duck_typing/lib/project_management/issue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Issue 5 | def open = Open.new 6 | end 7 | 8 | class Open 9 | def start = InProgress.new 10 | def resolve = Resolved.new 11 | def close = Closed.new 12 | end 13 | 14 | class InProgress 15 | def stop = Open.new 16 | def close = Closed.new 17 | def resolve = Resolved.new 18 | end 19 | 20 | class Resolved 21 | def close = Closed.new 22 | def reopen = Open.new 23 | end 24 | 25 | class Closed 26 | def reopen = Open.new 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /examples/duck_typing/lib/project_management/repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Repository 5 | def initialize(event_store) 6 | @event_store = event_store 7 | end 8 | 9 | def load(id, initial_issue) 10 | @event_store 11 | .read 12 | .stream(stream_name(id)) 13 | .reduce(initial_issue) do |issue, event| 14 | case event 15 | when IssueOpened 16 | issue.open 17 | when IssueProgressStarted 18 | issue.start 19 | when IssueProgressStopped 20 | issue.stop 21 | when IssueResolved 22 | issue.resolve 23 | when IssueReopened 24 | issue.reopen 25 | when IssueClosed 26 | issue.close 27 | end 28 | end 29 | end 30 | 31 | def store(id, events) 32 | @event_store.append(events, stream_name: stream_name(id)) 33 | end 34 | 35 | private 36 | 37 | def stream_name(id) = "Issue$#{id}" 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /examples/duck_typing/lib/project_management/ui.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class UI 5 | def initialize 6 | @issue = Issue.new 7 | @prompt = TTY::Prompt.new 8 | end 9 | 10 | def show 11 | @issue = @issue.send(ask_which_method) while true 12 | end 13 | 14 | private 15 | 16 | def ask_which_method 17 | @prompt.select(message, @issue.public_methods(false)) 18 | end 19 | 20 | def message 21 | "What to do? State: #{@issue.class.name.split("::").last}." 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /examples/duck_typing/test/issue_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require "minitest/mock" 5 | require "mutant/minitest/coverage" 6 | require "ruby_event_store" 7 | 8 | require_relative "../lib/project_management" 9 | 10 | module ProjectManagement 11 | class IssueTest < Minitest::Test 12 | include Test.with( 13 | handler: ->(event_store) { Handler.new(event_store) }, 14 | event_store: -> { RubyEventStore::Client.new } 15 | ) 16 | 17 | cover "ProjectManagement::Issue*" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/extracted_state/.mutant.yml: -------------------------------------------------------------------------------- 1 | integration: 2 | name: minitest 3 | includes: 4 | - lib 5 | requires: 6 | - project_management 7 | matcher: 8 | subjects: 9 | - ProjectManagement* 10 | ignore: 11 | - ProjectManagement::Test* 12 | coverage_criteria: 13 | process_abort: true 14 | usage: opensource -------------------------------------------------------------------------------- /examples/extracted_state/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ruby_event_store" 6 | gem "minitest" 7 | gem "mutant" 8 | gem "mutant-minitest" 9 | -------------------------------------------------------------------------------- /examples/extracted_state/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | concurrent-ruby (1.3.4) 6 | diff-lcs (1.5.1) 7 | minitest (5.25.1) 8 | mutant (0.12.4) 9 | diff-lcs (~> 1.3) 10 | parser (~> 3.3.0) 11 | regexp_parser (~> 2.9.0) 12 | sorbet-runtime (~> 0.5.0) 13 | unparser (~> 0.6.14) 14 | mutant-minitest (0.12.4) 15 | minitest (~> 5.11) 16 | mutant (= 0.12.4) 17 | parser (3.3.6.0) 18 | ast (~> 2.4.1) 19 | racc 20 | racc (1.8.1) 21 | regexp_parser (2.9.2) 22 | ruby_event_store (2.15.0) 23 | concurrent-ruby (~> 1.0, >= 1.1.6) 24 | sorbet-runtime (0.5.11647) 25 | unparser (0.6.15) 26 | diff-lcs (~> 1.3) 27 | parser (>= 3.3.0) 28 | 29 | PLATFORMS 30 | arm64-darwin 31 | x86_64-linux 32 | 33 | DEPENDENCIES 34 | minitest 35 | mutant 36 | mutant-minitest 37 | ruby_event_store 38 | 39 | BUNDLED WITH 40 | 2.5.23 41 | -------------------------------------------------------------------------------- /examples/extracted_state/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | @bundle install 3 | 4 | test: 5 | @bundle exec ruby -Ilib -rproject_management test/issue_test.rb 6 | 7 | mutate: 8 | @bundle exec mutant run 9 | 10 | .PHONY: install test mutate 11 | -------------------------------------------------------------------------------- /examples/extracted_state/lib/project_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../../shared/lib/project_management" 4 | require_relative "project_management/handler" 5 | require_relative "project_management/issue" 6 | require_relative "project_management/repository" 7 | require_relative "project_management/issue_state" 8 | -------------------------------------------------------------------------------- /examples/extracted_state/lib/project_management/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Handler 5 | def initialize(event_store) 6 | @repository = Repository.new(event_store) 7 | end 8 | 9 | def call(cmd) 10 | case cmd 11 | in CreateIssue[id:] 12 | create(id) 13 | in ResolveIssue[id:] 14 | resolve(id) 15 | in CloseIssue[id:] 16 | close(id) 17 | in ReopenIssue[id:] 18 | reopen(id) 19 | in StartIssueProgress[id:] 20 | start(id) 21 | in StopIssueProgress[id:] 22 | stop(id) 23 | end 24 | rescue Issue::InvalidTransition 25 | raise Error 26 | end 27 | 28 | def create(id) = with_aggregate(id) { |issue| issue.open } 29 | def resolve(id) = with_aggregate(id) { |issue| issue.resolve } 30 | def close(id) = with_aggregate(id) { |issue| issue.close } 31 | def reopen(id) = with_aggregate(id) { |issue| issue.reopen } 32 | def start(id) = with_aggregate(id) { |issue| issue.start } 33 | def stop(id) = with_aggregate(id) { |issue| issue.stop } 34 | 35 | private 36 | 37 | def with_aggregate(id) 38 | state = @repository.load(id, IssueState.initial(id)) 39 | issue = Issue.new(state) 40 | yield issue 41 | 42 | @repository.store(id, issue.changes) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /examples/extracted_state/lib/project_management/issue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Issue 5 | InvalidTransition = Class.new(StandardError) 6 | 7 | attr_reader :changes 8 | 9 | def initialize(state) 10 | @changes = [] 11 | @state = state 12 | end 13 | 14 | def open 15 | fail if @state.status 16 | 17 | changes << IssueOpened.new(data: { issue_id: @state.id }) 18 | end 19 | 20 | def resolve 21 | fail unless %i[open in_progress reopened].include? @state.status 22 | 23 | changes << IssueResolved.new(data: { issue_id: @state.id }) 24 | end 25 | 26 | def close 27 | fail unless %i[open in_progress resolved reopened].include? @state.status 28 | 29 | changes << IssueClosed.new(data: { issue_id: @state.id }) 30 | end 31 | 32 | def reopen 33 | fail unless %i[resolved closed].include? @state.status 34 | 35 | changes << IssueReopened.new(data: { issue_id: @state.id }) 36 | end 37 | 38 | def start 39 | fail unless %i[open reopened].include? @state.status 40 | 41 | changes << IssueProgressStarted.new(data: { issue_id: @state.id }) 42 | end 43 | 44 | def stop 45 | fail unless %i[in_progress].include? @state.status 46 | 47 | changes << IssueProgressStopped.new(data: { issue_id: @state.id }) 48 | end 49 | 50 | def fail = raise InvalidTransition 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /examples/extracted_state/lib/project_management/issue_state.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | IssueState = 5 | Data.define(:id, :status) do 6 | def self.initial(id) 7 | new(id: id, status: nil) 8 | end 9 | 10 | def apply(event) 11 | case event 12 | when IssueOpened 13 | with(status: :open) 14 | when IssueResolved 15 | with(status: :resolved) 16 | when IssueClosed 17 | with(status: :closed) 18 | when IssueReopened 19 | with(status: :reopened) 20 | when IssueProgressStarted 21 | with(status: :in_progress) 22 | when IssueProgressStopped 23 | with(status: :open) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /examples/extracted_state/lib/project_management/repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Repository 5 | def initialize(event_store) 6 | @event_store = event_store 7 | end 8 | 9 | def load(id, initial_state) 10 | @event_store 11 | .read 12 | .stream(stream_name(id)) 13 | .reduce(initial_state) { |state, event| state.apply(event) } 14 | end 15 | 16 | def store(id, events) 17 | @event_store.append(events, stream_name: stream_name(id)) 18 | end 19 | 20 | private 21 | 22 | def stream_name(id) = "Issue$#{id}" 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /examples/extracted_state/test/issue_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require "minitest/mock" 5 | require "mutant/minitest/coverage" 6 | require "ruby_event_store" 7 | 8 | require_relative "../lib/project_management" 9 | 10 | module ProjectManagement 11 | class IssueTest < Minitest::Test 12 | include Test.with( 13 | handler: ->(event_store) { Handler.new(event_store) }, 14 | event_store: -> { RubyEventStore::Client.new } 15 | ) 16 | 17 | cover "ProjectManagement::Issue*" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/functional/.mutant.yml: -------------------------------------------------------------------------------- 1 | integration: 2 | name: minitest 3 | includes: 4 | - lib 5 | requires: 6 | - project_management 7 | matcher: 8 | subjects: 9 | - ProjectManagement* 10 | ignore: 11 | - ProjectManagement::Test* 12 | coverage_criteria: 13 | process_abort: true 14 | usage: opensource -------------------------------------------------------------------------------- /examples/functional/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ruby_event_store" 6 | gem "minitest" 7 | gem "mutant" 8 | gem "mutant-minitest" 9 | -------------------------------------------------------------------------------- /examples/functional/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | concurrent-ruby (1.3.4) 6 | diff-lcs (1.5.1) 7 | minitest (5.25.1) 8 | mutant (0.12.4) 9 | diff-lcs (~> 1.3) 10 | parser (~> 3.3.0) 11 | regexp_parser (~> 2.9.0) 12 | sorbet-runtime (~> 0.5.0) 13 | unparser (~> 0.6.14) 14 | mutant-minitest (0.12.4) 15 | minitest (~> 5.11) 16 | mutant (= 0.12.4) 17 | parser (3.3.6.0) 18 | ast (~> 2.4.1) 19 | racc 20 | racc (1.8.1) 21 | regexp_parser (2.9.2) 22 | ruby_event_store (2.15.0) 23 | concurrent-ruby (~> 1.0, >= 1.1.6) 24 | sorbet-runtime (0.5.11647) 25 | unparser (0.6.15) 26 | diff-lcs (~> 1.3) 27 | parser (>= 3.3.0) 28 | 29 | PLATFORMS 30 | arm64-darwin 31 | x86_64-linux 32 | 33 | DEPENDENCIES 34 | minitest 35 | mutant 36 | mutant-minitest 37 | ruby_event_store 38 | 39 | BUNDLED WITH 40 | 2.5.23 41 | -------------------------------------------------------------------------------- /examples/functional/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | @bundle install 3 | 4 | test: 5 | @bundle exec ruby -Ilib -rproject_management test/issue_test.rb 6 | 7 | mutate: 8 | @bundle exec mutant run 9 | 10 | .PHONY: install test mutate 11 | -------------------------------------------------------------------------------- /examples/functional/lib/project_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../../shared/lib/project_management" 4 | require_relative "project_management/handler" 5 | require_relative "project_management/issue" 6 | require_relative "project_management/repository" 7 | require_relative "project_management/issue_state" 8 | -------------------------------------------------------------------------------- /examples/functional/lib/project_management/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Handler 5 | def initialize(event_store) 6 | @repository = Repository.new(event_store) 7 | end 8 | 9 | def call(cmd) 10 | case cmd 11 | in CreateIssue[id:] 12 | create(id) 13 | in ResolveIssue[id:] 14 | resolve(id) 15 | in CloseIssue[id:] 16 | close(id) 17 | in ReopenIssue[id:] 18 | reopen(id) 19 | in StartIssueProgress[id:] 20 | start(id) 21 | in StopIssueProgress[id:] 22 | stop(id) 23 | end 24 | end 25 | 26 | def create(id) = with_state(id) { |state| Issue.open(state) } 27 | def resolve(id) = with_state(id) { |state| Issue.resolve(state) } 28 | def close(id) = with_state(id) { |state| Issue.close(state) } 29 | def reopen(id) = with_state(id) { |state| Issue.reopen(state) } 30 | def start(id) = with_state(id) { |state| Issue.start(state) } 31 | def stop(id) = with_state(id) { |state| Issue.stop(state) } 32 | 33 | private 34 | 35 | def with_state(id) 36 | state = @repository.load(id, IssueState.initial(id)) 37 | 38 | case yield(state) 39 | in StandardError 40 | raise Error 41 | in Event => event 42 | @repository.store(id, event) 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /examples/functional/lib/project_management/issue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | module Issue 5 | InvalidTransition = Class.new(StandardError) 6 | 7 | class << self 8 | def open(state) 9 | if state.status 10 | InvalidTransition.new 11 | else 12 | IssueOpened.new(data: { issue_id: state.id }) 13 | end 14 | end 15 | 16 | def resolve(state) 17 | if %i[open in_progress reopened].include? state.status 18 | IssueResolved.new(data: { issue_id: state.id }) 19 | else 20 | InvalidTransition.new 21 | end 22 | end 23 | 24 | def close(state) 25 | if %i[open in_progress resolved reopened].include? state.status 26 | IssueClosed.new(data: { issue_id: state.id }) 27 | else 28 | InvalidTransition.new 29 | end 30 | end 31 | 32 | def reopen(state) 33 | if %i[resolved closed].include? state.status 34 | IssueReopened.new(data: { issue_id: state.id }) 35 | else 36 | InvalidTransition.new 37 | end 38 | end 39 | 40 | def stop(state) 41 | if %i[in_progress].include? state.status 42 | IssueProgressStopped.new(data: { issue_id: state.id }) 43 | else 44 | InvalidTransition.new 45 | end 46 | end 47 | 48 | def start(state) 49 | if %i[open reopened].include? state.status 50 | IssueProgressStarted.new(data: { issue_id: state.id }) 51 | else 52 | InvalidTransition.new 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /examples/functional/lib/project_management/issue_state.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | IssueState = 5 | Data.define(:id, :status) do 6 | def self.initial(id) 7 | new(id: id, status: nil) 8 | end 9 | 10 | def apply(event) 11 | case event 12 | when IssueOpened 13 | with(status: :open) 14 | when IssueResolved 15 | with(status: :resolved) 16 | when IssueClosed 17 | with(status: :closed) 18 | when IssueReopened 19 | with(status: :reopened) 20 | when IssueProgressStarted 21 | with(status: :in_progress) 22 | when IssueProgressStopped 23 | with(status: :open) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /examples/functional/lib/project_management/repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Repository 5 | def initialize(event_store) 6 | @event_store = event_store 7 | end 8 | 9 | def load(id, initial_state) 10 | @event_store 11 | .read 12 | .stream(stream_name(id)) 13 | .reduce(initial_state) { |state, event| state.apply(event) } 14 | end 15 | 16 | def store(id, events) 17 | @event_store.append(events, stream_name: stream_name(id)) 18 | end 19 | 20 | private 21 | 22 | def stream_name(id) = "Issue$#{id}" 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /examples/functional/test/issue_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require "minitest/mock" 5 | require "mutant/minitest/coverage" 6 | require "ruby_event_store" 7 | 8 | require_relative "../lib/project_management" 9 | 10 | module ProjectManagement 11 | class IssueTest < Minitest::Test 12 | include Test.with( 13 | handler: ->(event_store) { Handler.new(event_store) }, 14 | event_store: -> { RubyEventStore::Client.new } 15 | ) 16 | 17 | cover "ProjectManagement::Issue*" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/polymorphic/.mutant.yml: -------------------------------------------------------------------------------- 1 | integration: 2 | name: minitest 3 | includes: 4 | - lib 5 | requires: 6 | - project_management 7 | matcher: 8 | subjects: 9 | - ProjectManagement* 10 | ignore: 11 | - ProjectManagement::Test* 12 | coverage_criteria: 13 | process_abort: true 14 | usage: opensource -------------------------------------------------------------------------------- /examples/polymorphic/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ruby_event_store" 6 | gem "minitest" 7 | gem "mutant" 8 | gem "mutant-minitest" 9 | -------------------------------------------------------------------------------- /examples/polymorphic/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | concurrent-ruby (1.3.4) 6 | diff-lcs (1.5.1) 7 | minitest (5.25.1) 8 | mutant (0.12.4) 9 | diff-lcs (~> 1.3) 10 | parser (~> 3.3.0) 11 | regexp_parser (~> 2.9.0) 12 | sorbet-runtime (~> 0.5.0) 13 | unparser (~> 0.6.14) 14 | mutant-minitest (0.12.4) 15 | minitest (~> 5.11) 16 | mutant (= 0.12.4) 17 | parser (3.3.6.0) 18 | ast (~> 2.4.1) 19 | racc 20 | racc (1.8.1) 21 | regexp_parser (2.9.2) 22 | ruby_event_store (2.15.0) 23 | concurrent-ruby (~> 1.0, >= 1.1.6) 24 | sorbet-runtime (0.5.11647) 25 | unparser (0.6.15) 26 | diff-lcs (~> 1.3) 27 | parser (>= 3.3.0) 28 | 29 | PLATFORMS 30 | arm64-darwin 31 | x86_64-linux 32 | 33 | DEPENDENCIES 34 | minitest 35 | mutant 36 | mutant-minitest 37 | ruby_event_store 38 | 39 | BUNDLED WITH 40 | 2.5.23 41 | -------------------------------------------------------------------------------- /examples/polymorphic/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | @bundle install 3 | 4 | test: 5 | @bundle exec ruby -Ilib -rproject_management test/issue_test.rb 6 | 7 | mutate: 8 | @bundle exec mutant run 9 | 10 | .PHONY: install test mutate 11 | -------------------------------------------------------------------------------- /examples/polymorphic/lib/project_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../../shared/lib/project_management" 4 | require_relative "project_management/handler" 5 | require_relative "project_management/issue" 6 | -------------------------------------------------------------------------------- /examples/polymorphic/lib/project_management/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Handler 5 | def initialize(event_store) 6 | @event_store = event_store 7 | end 8 | 9 | def call(cmd) 10 | case cmd 11 | in CreateIssue[id:] 12 | create(id) 13 | in ResolveIssue[id:] 14 | resolve(id) 15 | in CloseIssue[id:] 16 | close(id) 17 | in ReopenIssue[id:] 18 | reopen(id) 19 | in StartIssueProgress[id:] 20 | start(id) 21 | in StopIssueProgress[id:] 22 | stop(id) 23 | end 24 | rescue Issue::InvalidTransition 25 | raise Error 26 | end 27 | 28 | def create(id) 29 | load_issue(id) do |issue| 30 | issue.open 31 | IssueOpened.new(data: { issue_id: id }) 32 | end 33 | end 34 | 35 | def close(id) 36 | load_issue(id) do |issue| 37 | issue.close 38 | IssueClosed.new(data: { issue_id: id }) 39 | end 40 | end 41 | 42 | def start(id) 43 | load_issue(id) do |issue| 44 | issue.start 45 | IssueProgressStarted.new(data: { issue_id: id }) 46 | end 47 | end 48 | 49 | def stop(id) 50 | load_issue(id) do |issue| 51 | issue.stop 52 | IssueProgressStopped.new(data: { issue_id: id }) 53 | end 54 | end 55 | 56 | def reopen(id) 57 | load_issue(id) do |issue| 58 | issue.reopen 59 | IssueReopened.new(data: { issue_id: id }) 60 | end 61 | end 62 | 63 | def resolve(id) 64 | load_issue(id) do |issue| 65 | issue.resolve 66 | IssueResolved.new(data: { issue_id: id }) 67 | end 68 | end 69 | 70 | private 71 | 72 | def stream_name(id) 73 | "Issue$#{id}" 74 | end 75 | 76 | def load_issue(id) 77 | issue = Issue.new 78 | @event_store 79 | .read 80 | .stream(stream_name(id)) 81 | .each do |event| 82 | case event 83 | when IssueOpened 84 | issue = issue.open 85 | when IssueProgressStarted 86 | issue = issue.start 87 | when IssueProgressStopped 88 | issue = issue.stop 89 | when IssueResolved 90 | issue = issue.resolve 91 | when IssueReopened 92 | issue = issue.reopen 93 | when IssueClosed 94 | issue = issue.close 95 | end 96 | end 97 | events = yield issue 98 | @event_store.append(events, stream_name: stream_name(id)) 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /examples/polymorphic/lib/project_management/issue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Issue 5 | InvalidTransition = Class.new(StandardError) 6 | 7 | def open 8 | Open.new 9 | end 10 | 11 | def start 12 | raise InvalidTransition 13 | end 14 | 15 | def resolve 16 | raise InvalidTransition 17 | end 18 | 19 | def stop 20 | raise InvalidTransition 21 | end 22 | 23 | def reopen 24 | raise InvalidTransition 25 | end 26 | 27 | def close 28 | raise InvalidTransition 29 | end 30 | end 31 | 32 | class Open 33 | def open 34 | raise Issue::InvalidTransition 35 | end 36 | 37 | def start 38 | InProgress.new 39 | end 40 | 41 | def resolve 42 | Resolved.new 43 | end 44 | 45 | def stop 46 | raise Issue::InvalidTransition 47 | end 48 | 49 | def reopen 50 | raise Issue::InvalidTransition 51 | end 52 | 53 | def close 54 | Closed.new 55 | end 56 | end 57 | 58 | class InProgress 59 | def open 60 | raise Issue::InvalidTransition 61 | end 62 | 63 | def start 64 | raise Issue::InvalidTransition 65 | end 66 | 67 | def resolve 68 | Resolved.new 69 | end 70 | 71 | def stop 72 | Open.new 73 | end 74 | 75 | def reopen 76 | raise Issue::InvalidTransition 77 | end 78 | 79 | def close 80 | Closed.new 81 | end 82 | end 83 | 84 | class Resolved 85 | def open 86 | raise Issue::InvalidTransition 87 | end 88 | 89 | def start 90 | raise Issue::InvalidTransition 91 | end 92 | 93 | def resolve 94 | raise Issue::InvalidTransition 95 | end 96 | 97 | def stop 98 | raise Issue::InvalidTransition 99 | end 100 | 101 | def reopen 102 | Open.new 103 | end 104 | 105 | def close 106 | Closed.new 107 | end 108 | end 109 | 110 | class Closed 111 | def open 112 | raise Issue::InvalidTransition 113 | end 114 | 115 | def start 116 | raise Issue::InvalidTransition 117 | end 118 | 119 | def resolve 120 | raise Issue::InvalidTransition 121 | end 122 | 123 | def stop 124 | raise Issue::InvalidTransition 125 | end 126 | 127 | def reopen 128 | Open.new 129 | end 130 | 131 | def close 132 | raise Issue::InvalidTransition 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /examples/polymorphic/test/issue_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require "minitest/mock" 5 | require "mutant/minitest/coverage" 6 | require "ruby_event_store" 7 | 8 | require_relative "../lib/project_management" 9 | 10 | module ProjectManagement 11 | class IssueTest < Minitest::Test 12 | include Test.with( 13 | handler: ->(event_store) { Handler.new(event_store) }, 14 | event_store: -> { RubyEventStore::Client.new } 15 | ) 16 | 17 | cover "ProjectManagement::Issue*" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/poro/.mutant.yml: -------------------------------------------------------------------------------- 1 | integration: 2 | name: minitest 3 | includes: 4 | - lib 5 | requires: 6 | - project_management 7 | matcher: 8 | subjects: 9 | - ProjectManagement* 10 | ignore: 11 | - ProjectManagement::Test* 12 | coverage_criteria: 13 | process_abort: true 14 | usage: opensource -------------------------------------------------------------------------------- /examples/poro/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ruby_event_store" 6 | gem "minitest" 7 | gem "mutant" 8 | gem "mutant-minitest" 9 | -------------------------------------------------------------------------------- /examples/poro/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | concurrent-ruby (1.3.4) 6 | diff-lcs (1.5.1) 7 | minitest (5.25.1) 8 | mutant (0.12.4) 9 | diff-lcs (~> 1.3) 10 | parser (~> 3.3.0) 11 | regexp_parser (~> 2.9.0) 12 | sorbet-runtime (~> 0.5.0) 13 | unparser (~> 0.6.14) 14 | mutant-minitest (0.12.4) 15 | minitest (~> 5.11) 16 | mutant (= 0.12.4) 17 | parser (3.3.6.0) 18 | ast (~> 2.4.1) 19 | racc 20 | racc (1.8.1) 21 | regexp_parser (2.9.2) 22 | ruby_event_store (2.15.0) 23 | concurrent-ruby (~> 1.0, >= 1.1.6) 24 | sorbet-runtime (0.5.11647) 25 | unparser (0.6.15) 26 | diff-lcs (~> 1.3) 27 | parser (>= 3.3.0) 28 | 29 | PLATFORMS 30 | arm64-darwin 31 | x86_64-linux 32 | 33 | DEPENDENCIES 34 | minitest 35 | mutant 36 | mutant-minitest 37 | ruby_event_store 38 | 39 | BUNDLED WITH 40 | 2.5.23 41 | -------------------------------------------------------------------------------- /examples/poro/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | @bundle install 3 | 4 | test: 5 | @bundle exec ruby -Ilib -rproject_management test/issue_test.rb 6 | 7 | mutate: 8 | @bundle exec mutant run 9 | 10 | .PHONY: install test mutate 11 | -------------------------------------------------------------------------------- /examples/poro/lib/project_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../../shared/lib/project_management" 4 | require_relative "project_management/issue" 5 | require_relative "project_management/issue_projection" 6 | require_relative "project_management/handler" 7 | -------------------------------------------------------------------------------- /examples/poro/lib/project_management/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Handler 5 | def initialize(event_store) 6 | @event_store = event_store 7 | end 8 | 9 | def call(cmd) 10 | case cmd 11 | in CreateIssue[id:] 12 | create(id) 13 | in ResolveIssue[id:] 14 | resolve(id) 15 | in CloseIssue[id:] 16 | close(id) 17 | in ReopenIssue[id:] 18 | reopen(id) 19 | in StartIssueProgress[id:] 20 | start(id) 21 | in StopIssueProgress[id:] 22 | stop(id) 23 | end 24 | rescue Issue::InvalidTransition 25 | raise Error 26 | end 27 | 28 | def create(id) 29 | with_aggregate(id) do |issue| 30 | issue.create 31 | IssueOpened.new(data: { issue_id: id }) 32 | end 33 | end 34 | 35 | def resolve(id) 36 | with_aggregate(id) do |issue| 37 | issue.resolve 38 | IssueResolved.new(data: { issue_id: id }) 39 | end 40 | end 41 | 42 | def close(id) 43 | with_aggregate(id) do |issue| 44 | issue.close 45 | IssueClosed.new(data: { issue_id: id }) 46 | end 47 | end 48 | 49 | def reopen(id) 50 | with_aggregate(id) do |issue| 51 | issue.reopen 52 | IssueReopened.new(data: { issue_id: id }) 53 | end 54 | end 55 | 56 | def start(id) 57 | with_aggregate(id) do |issue| 58 | issue.start 59 | IssueProgressStarted.new(data: { issue_id: id }) 60 | end 61 | end 62 | 63 | def stop(id) 64 | with_aggregate(id) do |issue| 65 | issue.stop 66 | IssueProgressStopped.new(data: { issue_id: id }) 67 | end 68 | end 69 | 70 | private 71 | 72 | def stream_name(id) = "Issue$#{id}" 73 | 74 | def with_aggregate(id) 75 | state = IssueProjection.new(@event_store).call(stream_name(id)) 76 | event = yield Issue.new(state.status) 77 | @event_store.append(event, stream_name: stream_name(id)) 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /examples/poro/lib/project_management/issue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Issue 5 | InvalidTransition = Class.new(StandardError) 6 | 7 | attr_reader :status 8 | 9 | def initialize(status) 10 | @status = status 11 | end 12 | 13 | def create 14 | raise InvalidTransition unless status.nil? 15 | end 16 | 17 | def resolve 18 | raise InvalidTransition unless open? || reopened? || in_progress? 19 | end 20 | 21 | def close 22 | unless open? || in_progress? || reopened? || resolved? 23 | raise InvalidTransition 24 | end 25 | end 26 | 27 | def reopen 28 | raise InvalidTransition unless closed? || resolved? 29 | end 30 | 31 | def start 32 | raise InvalidTransition unless open? || reopened? 33 | end 34 | 35 | def stop 36 | raise InvalidTransition unless in_progress? 37 | end 38 | 39 | private 40 | 41 | attr_reader :status 42 | 43 | def open? 44 | status == :open 45 | end 46 | 47 | def closed? 48 | status == :closed 49 | end 50 | 51 | def in_progress? 52 | status == :in_progress 53 | end 54 | 55 | def reopened? 56 | status == :reopened 57 | end 58 | 59 | def resolved? 60 | status == :resolved 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /examples/poro/lib/project_management/issue_projection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class IssueProjection 5 | State = Struct.new(:status) 6 | 7 | def initialize(event_store) 8 | @event_store = event_store 9 | end 10 | 11 | def call(stream_name) 12 | RubyEventStore::Projection 13 | .from_stream(stream_name) 14 | .init(-> { State.new }) 15 | .when(IssueOpened, ->(state, _) { state.status = :open }) 16 | .when(IssueReopened, ->(state, _) { state.status = :reopened }) 17 | .when(IssueClosed, ->(state, _) { state.status = :closed }) 18 | .when(IssueResolved, ->(state, _) { state.status = :resolved }) 19 | .when( 20 | IssueProgressStarted, 21 | ->(state, _) { state.status = :in_progress } 22 | ) 23 | .when(IssueProgressStopped, ->(state, _) { state.status = :open }) 24 | .run(@event_store) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /examples/poro/test/issue_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require "minitest/mock" 5 | require "mutant/minitest/coverage" 6 | require "ruby_event_store" 7 | 8 | require_relative "../lib/project_management" 9 | 10 | module ProjectManagement 11 | class IssueTest < Minitest::Test 12 | include Test.with( 13 | handler: ->(event_store) { Handler.new(event_store) }, 14 | event_store: -> { RubyEventStore::Client.new } 15 | ) 16 | 17 | cover "ProjectManagement::Issue*" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/query_based/.mutant.yml: -------------------------------------------------------------------------------- 1 | integration: 2 | name: minitest 3 | includes: 4 | - lib 5 | requires: 6 | - project_management 7 | matcher: 8 | subjects: 9 | - ProjectManagement* 10 | ignore: 11 | - ProjectManagement::Test* 12 | coverage_criteria: 13 | process_abort: true 14 | usage: opensource 15 | -------------------------------------------------------------------------------- /examples/query_based/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ruby_event_store" 6 | gem "minitest" 7 | gem "mutant" 8 | gem "mutant-minitest" 9 | -------------------------------------------------------------------------------- /examples/query_based/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | concurrent-ruby (1.3.4) 6 | diff-lcs (1.5.1) 7 | minitest (5.25.1) 8 | mutant (0.12.4) 9 | diff-lcs (~> 1.3) 10 | parser (~> 3.3.0) 11 | regexp_parser (~> 2.9.0) 12 | sorbet-runtime (~> 0.5.0) 13 | unparser (~> 0.6.14) 14 | mutant-minitest (0.12.4) 15 | minitest (~> 5.11) 16 | mutant (= 0.12.4) 17 | parser (3.3.6.0) 18 | ast (~> 2.4.1) 19 | racc 20 | racc (1.8.1) 21 | regexp_parser (2.9.2) 22 | ruby_event_store (2.15.0) 23 | concurrent-ruby (~> 1.0, >= 1.1.6) 24 | sorbet-runtime (0.5.11647) 25 | unparser (0.6.15) 26 | diff-lcs (~> 1.3) 27 | parser (>= 3.3.0) 28 | 29 | PLATFORMS 30 | arm64-darwin 31 | x86_64-linux 32 | 33 | DEPENDENCIES 34 | minitest 35 | mutant 36 | mutant-minitest 37 | ruby_event_store 38 | 39 | BUNDLED WITH 40 | 2.5.23 41 | -------------------------------------------------------------------------------- /examples/query_based/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | @bundle install 3 | 4 | test: 5 | @bundle exec ruby -Ilib -rproject_management test/issue_test.rb 6 | 7 | mutate: 8 | @bundle exec mutant run 9 | 10 | .PHONY: install test mutate 11 | -------------------------------------------------------------------------------- /examples/query_based/lib/project_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../../shared/lib/project_management" 4 | require_relative "project_management/issue" 5 | require_relative "project_management/repository" 6 | require_relative "project_management/issue_projection" 7 | require_relative "project_management/handler" 8 | -------------------------------------------------------------------------------- /examples/query_based/lib/project_management/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Handler 5 | def initialize(event_store) 6 | @repository = Repository.new(event_store) 7 | end 8 | 9 | def call(cmd) 10 | case cmd 11 | in CreateIssue[id:] 12 | create(id) 13 | in ResolveIssue[id:] 14 | resolve(id) 15 | in CloseIssue[id:] 16 | close(id) 17 | in ReopenIssue[id:] 18 | reopen(id) 19 | in StartIssueProgress[id:] 20 | start(id) 21 | in StopIssueProgress[id:] 22 | stop(id) 23 | end 24 | end 25 | 26 | def create(id) 27 | with_aggregate(id) do |issue| 28 | raise Error unless issue.can_create? 29 | 30 | IssueOpened.new(data: { issue_id: id }) 31 | end 32 | end 33 | 34 | def resolve(id) 35 | with_aggregate(id) do |issue| 36 | raise Error unless issue.can_resolve? 37 | 38 | IssueResolved.new(data: { issue_id: id }) 39 | end 40 | end 41 | 42 | def close(id) 43 | with_aggregate(id) do |issue| 44 | raise Error unless issue.can_close? 45 | 46 | IssueClosed.new(data: { issue_id: id }) 47 | end 48 | end 49 | 50 | def reopen(id) 51 | with_aggregate(id) do |issue| 52 | raise Error unless issue.can_reopen? 53 | 54 | IssueReopened.new(data: { issue_id: id }) 55 | end 56 | end 57 | 58 | def start(id) 59 | with_aggregate(id) do |issue| 60 | raise Error unless issue.can_start? 61 | 62 | IssueProgressStarted.new(data: { issue_id: id }) 63 | end 64 | end 65 | 66 | def stop(id) 67 | with_aggregate(id) do |issue| 68 | raise Error unless issue.can_stop? 69 | 70 | IssueProgressStopped.new(data: { issue_id: id }) 71 | end 72 | end 73 | 74 | private 75 | 76 | def with_aggregate(id) 77 | issue = @repository.load(id, Issue.initial) 78 | events = yield(issue) 79 | @repository.store(id, events) 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /examples/query_based/lib/project_management/issue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | Issue = 5 | Data.define(:status) do 6 | def self.initial = new(status: nil) 7 | 8 | def open = with(status: :open) 9 | def resolve = with(status: :resolved) 10 | def close = with(status: :closed) 11 | def reopen = with(status: :reopened) 12 | def start = with(status: :in_progress) 13 | def stop = with(status: :open) 14 | 15 | def can_create? = status.nil? 16 | def can_reopen? = %i[closed resolved].include? status 17 | def can_start? = %i[open reopened].include? status 18 | def can_stop? = %i[in_progress].include? status 19 | def can_close? = %i[open in_progress reopened resolved].include? status 20 | def can_resolve? = %i[open reopened in_progress].include? status 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /examples/query_based/lib/project_management/issue_projection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class IssueProjection 5 | def self.call(query, initial_issue) 6 | query 7 | .reduce(initial_issue) do |issue, event| 8 | case event 9 | when IssueOpened 10 | issue.open 11 | when IssueReopened 12 | issue.reopen 13 | when IssueClosed 14 | issue.close 15 | when IssueResolved 16 | issue.resolve 17 | when IssueProgressStarted 18 | issue.start 19 | when IssueProgressStopped 20 | issue.stop 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /examples/query_based/lib/project_management/repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Repository 5 | def initialize(event_store) 6 | @event_store = event_store 7 | end 8 | 9 | def load(id, initial_state) 10 | query = 11 | @event_store 12 | .read 13 | .stream(stream_name(id)) 14 | IssueProjection.call(query, initial_state) 15 | end 16 | 17 | def store(id, events) 18 | @event_store.append(events, stream_name: stream_name(id)) 19 | end 20 | 21 | private 22 | 23 | def stream_name(id) = "Issue$#{id}" 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /examples/query_based/test/issue_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require "minitest/mock" 5 | require "mutant/minitest/coverage" 6 | require "ruby_event_store" 7 | 8 | require_relative "../lib/project_management" 9 | 10 | module ProjectManagement 11 | class IssueTest < Minitest::Test 12 | include Test.with( 13 | handler: ->(event_store) { Handler.new(event_store) }, 14 | event_store: -> { RubyEventStore::Client.new } 15 | ) 16 | 17 | cover "ProjectManagement::Issue*" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/rails_way/.mutant.yml: -------------------------------------------------------------------------------- 1 | integration: 2 | name: minitest 3 | includes: 4 | - lib 5 | requires: 6 | - project_management 7 | matcher: 8 | subjects: 9 | - ProjectManagement* 10 | ignore: 11 | - ProjectManagement::Test* 12 | coverage_criteria: 13 | process_abort: true 14 | usage: opensource -------------------------------------------------------------------------------- /examples/rails_way/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ruby_event_store" 6 | gem "minitest" 7 | gem "mutant" 8 | gem "mutant-minitest" 9 | gem "activerecord", ">= 8" 10 | gem "sqlite3", ">= 2" 11 | gem "aasm" 12 | -------------------------------------------------------------------------------- /examples/rails_way/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | aasm (5.5.0) 5 | concurrent-ruby (~> 1.0) 6 | activemodel (8.0.0) 7 | activesupport (= 8.0.0) 8 | activerecord (8.0.0) 9 | activemodel (= 8.0.0) 10 | activesupport (= 8.0.0) 11 | timeout (>= 0.4.0) 12 | activesupport (8.0.0) 13 | base64 14 | benchmark (>= 0.3) 15 | bigdecimal 16 | concurrent-ruby (~> 1.0, >= 1.3.1) 17 | connection_pool (>= 2.2.5) 18 | drb 19 | i18n (>= 1.6, < 2) 20 | logger (>= 1.4.2) 21 | minitest (>= 5.1) 22 | securerandom (>= 0.3) 23 | tzinfo (~> 2.0, >= 2.0.5) 24 | uri (>= 0.13.1) 25 | ast (2.4.2) 26 | base64 (0.2.0) 27 | benchmark (0.4.0) 28 | bigdecimal (3.1.8) 29 | concurrent-ruby (1.3.4) 30 | connection_pool (2.4.1) 31 | diff-lcs (1.5.1) 32 | drb (2.2.1) 33 | i18n (1.14.6) 34 | concurrent-ruby (~> 1.0) 35 | logger (1.6.1) 36 | minitest (5.25.1) 37 | mutant (0.12.4) 38 | diff-lcs (~> 1.3) 39 | parser (~> 3.3.0) 40 | regexp_parser (~> 2.9.0) 41 | sorbet-runtime (~> 0.5.0) 42 | unparser (~> 0.6.14) 43 | mutant-minitest (0.12.4) 44 | minitest (~> 5.11) 45 | mutant (= 0.12.4) 46 | parser (3.3.6.0) 47 | ast (~> 2.4.1) 48 | racc 49 | racc (1.8.1) 50 | regexp_parser (2.9.2) 51 | ruby_event_store (2.15.0) 52 | concurrent-ruby (~> 1.0, >= 1.1.6) 53 | securerandom (0.3.2) 54 | sorbet-runtime (0.5.11647) 55 | sqlite3 (2.3.0-arm64-darwin) 56 | sqlite3 (2.3.0-x86_64-linux-gnu) 57 | timeout (0.4.2) 58 | tzinfo (2.0.6) 59 | concurrent-ruby (~> 1.0) 60 | unparser (0.6.15) 61 | diff-lcs (~> 1.3) 62 | parser (>= 3.3.0) 63 | uri (1.0.2) 64 | 65 | PLATFORMS 66 | arm64-darwin 67 | x86_64-linux 68 | 69 | DEPENDENCIES 70 | aasm 71 | activerecord (>= 8) 72 | minitest 73 | mutant 74 | mutant-minitest 75 | ruby_event_store 76 | sqlite3 (>= 2) 77 | 78 | BUNDLED WITH 79 | 2.5.23 80 | -------------------------------------------------------------------------------- /examples/rails_way/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | @bundle install 3 | 4 | test: 5 | @bundle exec ruby -Ilib -rproject_management test/issue_test.rb 6 | 7 | mutate: 8 | @bundle exec mutant run 9 | 10 | .PHONY: install test mutate 11 | -------------------------------------------------------------------------------- /examples/rails_way/lib/project_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_record" 4 | require "aasm" 5 | 6 | require_relative "../../../shared/lib/project_management" 7 | require_relative "project_management/handler" 8 | require_relative "project_management/issue" 9 | -------------------------------------------------------------------------------- /examples/rails_way/lib/project_management/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Handler 5 | def initialize(event_store) 6 | @event_store = event_store 7 | end 8 | 9 | def call(cmd) 10 | case cmd 11 | in CreateIssue[id:] 12 | create(id) 13 | in ResolveIssue[id:] 14 | resolve(id) 15 | in CloseIssue[id:] 16 | close(id) 17 | in ReopenIssue[id:] 18 | reopen(id) 19 | in StartIssueProgress[id:] 20 | start(id) 21 | in StopIssueProgress[id:] 22 | stop(id) 23 | end 24 | rescue AASM::InvalidTransition, 25 | ActiveRecord::RecordNotFound, 26 | ActiveRecord::RecordNotUnique 27 | raise Error 28 | end 29 | 30 | def create(id) 31 | create_issue(id) { IssueOpened.new(data: { issue_id: id }) } 32 | end 33 | 34 | def close(id) 35 | load_issue(id) do |issue| 36 | issue.close 37 | IssueClosed.new(data: { issue_id: id }) 38 | end 39 | end 40 | 41 | def start(id) 42 | load_issue(id) do |issue| 43 | issue.start 44 | IssueProgressStarted.new(data: { issue_id: id }) 45 | end 46 | end 47 | 48 | def stop(id) 49 | load_issue(id) do |issue| 50 | issue.stop 51 | IssueProgressStopped.new(data: { issue_id: id }) 52 | end 53 | end 54 | 55 | def reopen(id) 56 | load_issue(id) do |issue| 57 | issue.reopen 58 | IssueReopened.new(data: { issue_id: id }) 59 | end 60 | end 61 | 62 | def resolve(id) 63 | load_issue(id) do |issue| 64 | issue.resolve 65 | IssueResolved.new(data: { issue_id: id }) 66 | end 67 | end 68 | 69 | private 70 | 71 | def stream_name(id) = "Issue$#{id}" 72 | 73 | def create_issue(id) 74 | Issue.create!(uuid: id) 75 | @event_store.append(yield, stream_name: stream_name(id)) 76 | end 77 | 78 | def load_issue(id) 79 | issue = Issue.find_by!(uuid: id) 80 | @event_store.append(yield(issue), stream_name: stream_name(id)) 81 | issue.save! 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /examples/rails_way/lib/project_management/issue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Issue < ActiveRecord::Base 5 | include AASM 6 | 7 | aasm column: :state do 8 | state :open, initial: true 9 | state :resolved 10 | state :closed 11 | state :in_progress 12 | state :reopened 13 | 14 | event :resolve do 15 | transitions from: %i[open in_progress reopened], to: :resolved 16 | end 17 | 18 | event :close do 19 | transitions from: %i[open in_progress reopened resolved], to: :closed 20 | end 21 | 22 | event :reopen do 23 | transitions from: %i[closed resolved], to: :reopened 24 | end 25 | 26 | event :start do 27 | transitions from: %i[open reopened], to: :in_progress 28 | end 29 | 30 | event :stop do 31 | transitions from: :in_progress, to: :open 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /examples/rails_way/test/issue_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require "minitest/mock" 5 | require "mutant/minitest/coverage" 6 | require "ruby_event_store" 7 | 8 | require_relative "../lib/project_management" 9 | 10 | module ProjectManagement 11 | class IssueTest < Minitest::Test 12 | include Test.with( 13 | handler: ->(event_store) { Handler.new(event_store) }, 14 | event_store: -> { RubyEventStore::Client.new } 15 | ) 16 | 17 | cover "ProjectManagement::Issue*" 18 | 19 | def setup 20 | ActiveRecord::Base.establish_connection( 21 | adapter: "sqlite3", 22 | database: ":memory:" 23 | ) 24 | 25 | ActiveRecord::Schema.verbose = false 26 | ActiveRecord::Schema.define do 27 | create_table :issues, force: true do |t| 28 | t.string :uuid 29 | t.string :state 30 | end 31 | 32 | add_index :issues, :uuid, unique: true 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /examples/repository/.mutant.yml: -------------------------------------------------------------------------------- 1 | integration: 2 | name: minitest 3 | includes: 4 | - lib 5 | requires: 6 | - project_management 7 | matcher: 8 | subjects: 9 | - ProjectManagement* 10 | ignore: 11 | - ProjectManagement::Test* 12 | coverage_criteria: 13 | process_abort: true 14 | usage: opensource -------------------------------------------------------------------------------- /examples/repository/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ruby_event_store" 6 | gem "minitest" 7 | gem "mutant" 8 | gem "mutant-minitest" 9 | -------------------------------------------------------------------------------- /examples/repository/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | concurrent-ruby (1.3.4) 6 | diff-lcs (1.5.1) 7 | minitest (5.25.1) 8 | mutant (0.12.4) 9 | diff-lcs (~> 1.3) 10 | parser (~> 3.3.0) 11 | regexp_parser (~> 2.9.0) 12 | sorbet-runtime (~> 0.5.0) 13 | unparser (~> 0.6.14) 14 | mutant-minitest (0.12.4) 15 | minitest (~> 5.11) 16 | mutant (= 0.12.4) 17 | parser (3.3.6.0) 18 | ast (~> 2.4.1) 19 | racc 20 | racc (1.8.1) 21 | regexp_parser (2.9.2) 22 | ruby_event_store (2.15.0) 23 | concurrent-ruby (~> 1.0, >= 1.1.6) 24 | sorbet-runtime (0.5.11647) 25 | unparser (0.6.15) 26 | diff-lcs (~> 1.3) 27 | parser (>= 3.3.0) 28 | 29 | PLATFORMS 30 | arm64-darwin 31 | x86_64-linux 32 | 33 | DEPENDENCIES 34 | minitest 35 | mutant 36 | mutant-minitest 37 | ruby_event_store 38 | 39 | BUNDLED WITH 40 | 2.5.23 41 | -------------------------------------------------------------------------------- /examples/repository/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | @bundle install 3 | 4 | test: 5 | @bundle exec ruby -Ilib -rproject_management test/issue_test.rb 6 | 7 | mutate: 8 | @bundle exec mutant run 9 | 10 | .PHONY: install test mutate 11 | -------------------------------------------------------------------------------- /examples/repository/lib/project_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../../shared/lib/project_management" 4 | require_relative "project_management/repository" 5 | require_relative "project_management/issue" 6 | require_relative "project_management/handler" 7 | -------------------------------------------------------------------------------- /examples/repository/lib/project_management/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Handler 5 | def initialize(event_store) 6 | @repository = Repository.new(event_store) 7 | end 8 | 9 | def call(cmd) 10 | case cmd 11 | in CreateIssue[id:] 12 | create(id) 13 | in ResolveIssue[id:] 14 | resolve(id) 15 | in CloseIssue[id:] 16 | close(id) 17 | in ReopenIssue[id:] 18 | reopen(id) 19 | in StartIssueProgress[id:] 20 | start(id) 21 | in StopIssueProgress[id:] 22 | stop(id) 23 | end 24 | rescue Issue::InvalidTransition 25 | raise Error 26 | end 27 | 28 | def create(id) 29 | with_aggregate(id, &:create) 30 | end 31 | 32 | def resolve(id) 33 | with_aggregate(id, &:resolve) 34 | end 35 | 36 | def close(id) 37 | with_aggregate(id, &:close) 38 | end 39 | 40 | def reopen(id) 41 | with_aggregate(id, &:reopen) 42 | end 43 | 44 | def start(id) 45 | with_aggregate(id, &:start) 46 | end 47 | 48 | def stop(id) 49 | with_aggregate(id, &:stop) 50 | end 51 | 52 | private 53 | 54 | def with_aggregate(id) 55 | events = @repository.load(id) 56 | issue = Issue.load(id, events) 57 | yield issue 58 | @repository.save(id, issue.changes) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /examples/repository/lib/project_management/issue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Issue 5 | InvalidTransition = Class.new(StandardError) 6 | 7 | attr_reader :changes 8 | 9 | def self.load(id, events) 10 | issue = new(id) 11 | issue.load(events) 12 | end 13 | 14 | def initialize(id) 15 | @id = id 16 | @changes = [] 17 | end 18 | 19 | def load(events) 20 | events.each { |ev| on(ev) } 21 | clear_changes 22 | 23 | self 24 | end 25 | 26 | def create 27 | invalid_transition unless can_create? 28 | 29 | @status = :open 30 | 31 | register_event(IssueOpened.new(data: { issue_id: @id })) 32 | end 33 | 34 | def resolve 35 | invalid_transition unless can_resolve? 36 | 37 | @status = :resolved 38 | 39 | register_event(IssueResolved.new(data: { issue_id: @id })) 40 | end 41 | 42 | def close 43 | invalid_transition unless can_close? 44 | 45 | @status = :closed 46 | 47 | register_event(IssueClosed.new(data: { issue_id: @id })) 48 | end 49 | 50 | def reopen 51 | invalid_transition unless can_reopen? 52 | 53 | @status = :reopened 54 | 55 | register_event(IssueReopened.new(data: { issue_id: @id })) 56 | end 57 | 58 | def start 59 | invalid_transition unless can_start? 60 | 61 | @status = :in_progress 62 | 63 | register_event(IssueProgressStarted.new(data: { issue_id: @id })) 64 | end 65 | 66 | def stop 67 | invalid_transition unless can_stop? 68 | 69 | @status = :open 70 | 71 | register_event(IssueProgressStopped.new(data: { issue_id: @id })) 72 | end 73 | 74 | private 75 | 76 | def register_event(event) 77 | changes << event 78 | end 79 | 80 | def invalid_transition 81 | raise InvalidTransition 82 | end 83 | 84 | def open? 85 | @status == :open 86 | end 87 | 88 | def closed? 89 | @status == :closed 90 | end 91 | 92 | def in_progress? 93 | @status == :in_progress 94 | end 95 | 96 | def reopened? 97 | @status == :reopened 98 | end 99 | 100 | def resolved? 101 | @status == :resolved 102 | end 103 | 104 | def can_reopen? 105 | closed? || resolved? 106 | end 107 | 108 | def can_start? 109 | open? || reopened? 110 | end 111 | 112 | def can_stop? 113 | in_progress? 114 | end 115 | 116 | def can_close? 117 | open? || in_progress? || reopened? || resolved? 118 | end 119 | 120 | def can_resolve? 121 | open? || reopened? || in_progress? 122 | end 123 | 124 | def can_create? 125 | @status.nil? 126 | end 127 | 128 | def clear_changes 129 | @changes = [] 130 | end 131 | 132 | def on(event) 133 | case event 134 | when IssueOpened 135 | create 136 | when IssueResolved 137 | resolve 138 | when IssueClosed 139 | close 140 | when IssueReopened 141 | reopen 142 | when IssueProgressStarted 143 | start 144 | when IssueProgressStopped 145 | stop 146 | end 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /examples/repository/lib/project_management/repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Repository 5 | def initialize(event_store) 6 | @event_store = event_store 7 | end 8 | 9 | def load(id) 10 | @event_store.read.stream(stream_name(id)) 11 | end 12 | 13 | def save(id, events) 14 | @event_store.append(events, stream_name: stream_name(id)) 15 | end 16 | 17 | private 18 | 19 | def stream_name(id) = "Issue$#{id}" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/repository/test/issue_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require "minitest/mock" 5 | require "mutant/minitest/coverage" 6 | require "ruby_event_store" 7 | 8 | require_relative "../lib/project_management" 9 | 10 | module ProjectManagement 11 | class IssueTest < Minitest::Test 12 | include Test.with( 13 | handler: ->(event_store) { Handler.new(event_store) }, 14 | event_store: -> { RubyEventStore::Client.new } 15 | ) 16 | 17 | cover "ProjectManagement::Issue*" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/roles/.mutant.yml: -------------------------------------------------------------------------------- 1 | integration: 2 | name: minitest 3 | includes: 4 | - lib 5 | requires: 6 | - project_management 7 | matcher: 8 | subjects: 9 | - ProjectManagement* 10 | ignore: 11 | - ProjectManagement::Test* 12 | - ProjectManagement::Issue#open 13 | - ProjectManagement::Open* 14 | - ProjectManagement::InProgress* 15 | - ProjectManagement::Resolved* 16 | - ProjectManagement::Closed* 17 | coverage_criteria: 18 | process_abort: true 19 | usage: opensource -------------------------------------------------------------------------------- /examples/roles/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ruby_event_store" 6 | gem "minitest" 7 | gem "mutant" 8 | gem "mutant-minitest" 9 | -------------------------------------------------------------------------------- /examples/roles/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | concurrent-ruby (1.3.4) 6 | diff-lcs (1.5.1) 7 | minitest (5.25.1) 8 | mutant (0.12.4) 9 | diff-lcs (~> 1.3) 10 | parser (~> 3.3.0) 11 | regexp_parser (~> 2.9.0) 12 | sorbet-runtime (~> 0.5.0) 13 | unparser (~> 0.6.14) 14 | mutant-minitest (0.12.4) 15 | minitest (~> 5.11) 16 | mutant (= 0.12.4) 17 | parser (3.3.6.0) 18 | ast (~> 2.4.1) 19 | racc 20 | racc (1.8.1) 21 | regexp_parser (2.9.2) 22 | ruby_event_store (2.15.0) 23 | concurrent-ruby (~> 1.0, >= 1.1.6) 24 | sorbet-runtime (0.5.11647) 25 | unparser (0.6.15) 26 | diff-lcs (~> 1.3) 27 | parser (>= 3.3.0) 28 | 29 | PLATFORMS 30 | arm64-darwin 31 | x86_64-linux 32 | 33 | DEPENDENCIES 34 | minitest 35 | mutant 36 | mutant-minitest 37 | ruby_event_store 38 | 39 | BUNDLED WITH 40 | 2.5.23 41 | -------------------------------------------------------------------------------- /examples/roles/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | @bundle install 3 | 4 | test: 5 | @bundle exec ruby -Ilib -rproject_management test/issue_test.rb 6 | 7 | mutate: 8 | @bundle exec mutant run 9 | 10 | .PHONY: install test mutate 11 | -------------------------------------------------------------------------------- /examples/roles/lib/project_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../../shared/lib/project_management" 4 | require_relative "project_management/handler" 5 | require_relative "project_management/issue" 6 | -------------------------------------------------------------------------------- /examples/roles/lib/project_management/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Handler 5 | def initialize(event_store) 6 | @event_store = event_store 7 | end 8 | 9 | def call(cmd) 10 | case cmd 11 | in CreateIssue[id:] 12 | create(id) 13 | in ResolveIssue[id:] 14 | resolve(id) 15 | in CloseIssue[id:] 16 | close(id) 17 | in ReopenIssue[id:] 18 | reopen(id) 19 | in StartIssueProgress[id:] 20 | start(id) 21 | in StopIssueProgress[id:] 22 | stop(id) 23 | end 24 | rescue Issue::InvalidTransition 25 | raise Error 26 | end 27 | 28 | def create(id) 29 | load_issue(id) do |issue| 30 | issue.open 31 | IssueOpened.new(data: { issue_id: id }) 32 | end 33 | end 34 | 35 | def close(id) 36 | load_issue(id) do |issue| 37 | issue.close 38 | IssueClosed.new(data: { issue_id: id }) 39 | end 40 | end 41 | 42 | def start(id) 43 | load_issue(id) do |issue| 44 | issue.start 45 | IssueProgressStarted.new(data: { issue_id: id }) 46 | end 47 | end 48 | 49 | def stop(id) 50 | load_issue(id) do |issue| 51 | issue.stop 52 | IssueProgressStopped.new(data: { issue_id: id }) 53 | end 54 | end 55 | 56 | def reopen(id) 57 | load_issue(id) do |issue| 58 | issue.reopen 59 | IssueReopened.new(data: { issue_id: id }) 60 | end 61 | end 62 | 63 | def resolve(id) 64 | load_issue(id) do |issue| 65 | issue.resolve 66 | IssueResolved.new(data: { issue_id: id }) 67 | end 68 | end 69 | 70 | private 71 | 72 | def stream_name(id) 73 | "Issue$#{id}" 74 | end 75 | 76 | def load_issue(id) 77 | issue = 78 | @event_store 79 | .read 80 | .stream(stream_name(id)) 81 | .reduce(Issue.new) do |issue, event| 82 | case event 83 | when IssueOpened 84 | issue.open 85 | when IssueProgressStarted 86 | issue.start 87 | when IssueProgressStopped 88 | issue.stop 89 | when IssueResolved 90 | issue.resolve 91 | when IssueReopened 92 | issue.reopen 93 | when IssueClosed 94 | issue.close 95 | end 96 | end 97 | 98 | @event_store.append(yield(issue), stream_name: stream_name(id)) 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /examples/roles/lib/project_management/issue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Issue 5 | InvalidTransition = Class.new(StandardError) 6 | 7 | def open = extend(Open.clone) 8 | def start = fail 9 | def resolve = fail 10 | def stop = fail 11 | def reopen = fail 12 | def close = fail 13 | 14 | private def fail = raise(InvalidTransition) 15 | end 16 | 17 | module Open 18 | def open = fail 19 | def start = extend(InProgress.clone) 20 | def resolve = extend(Resolved.clone) 21 | def stop = fail 22 | def reopen = fail 23 | def close = extend(Closed.clone) 24 | end 25 | 26 | module InProgress 27 | def open = fail 28 | def start = fail 29 | def resolve = extend(Resolved.clone) 30 | def stop = extend(Open.clone) 31 | def reopen = fail 32 | def close = extend(Closed.clone) 33 | end 34 | 35 | module Resolved 36 | def open = fail 37 | def start = fail 38 | def resolve = fail 39 | def stop = fail 40 | def reopen = extend(Open.clone) 41 | def close = extend(Closed.clone) 42 | end 43 | 44 | module Closed 45 | def open = fail 46 | def start = fail 47 | def resolve = fail 48 | def stop = fail 49 | def reopen = extend(Open.clone) 50 | def close = fail 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /examples/roles/test/issue_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require "minitest/mock" 5 | require "mutant/minitest/coverage" 6 | require "ruby_event_store" 7 | 8 | require_relative "../lib/project_management" 9 | 10 | module ProjectManagement 11 | class IssueTest < Minitest::Test 12 | include Test.with( 13 | handler: ->(event_store) { Handler.new(event_store) }, 14 | event_store: -> { RubyEventStore::Client.new } 15 | ) 16 | 17 | cover "ProjectManagement::Issue*" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/yield_based/.mutant.yml: -------------------------------------------------------------------------------- 1 | integration: 2 | name: minitest 3 | includes: 4 | - lib 5 | requires: 6 | - project_management 7 | matcher: 8 | subjects: 9 | - ProjectManagement* 10 | ignore: 11 | - ProjectManagement::Test* 12 | coverage_criteria: 13 | process_abort: true 14 | usage: opensource -------------------------------------------------------------------------------- /examples/yield_based/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ruby_event_store" 6 | gem "minitest" 7 | gem "mutant" 8 | gem "mutant-minitest" 9 | -------------------------------------------------------------------------------- /examples/yield_based/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | concurrent-ruby (1.3.4) 6 | diff-lcs (1.5.1) 7 | minitest (5.25.1) 8 | mutant (0.12.4) 9 | diff-lcs (~> 1.3) 10 | parser (~> 3.3.0) 11 | regexp_parser (~> 2.9.0) 12 | sorbet-runtime (~> 0.5.0) 13 | unparser (~> 0.6.14) 14 | mutant-minitest (0.12.4) 15 | minitest (~> 5.11) 16 | mutant (= 0.12.4) 17 | parser (3.3.6.0) 18 | ast (~> 2.4.1) 19 | racc 20 | racc (1.8.1) 21 | regexp_parser (2.9.2) 22 | ruby_event_store (2.15.0) 23 | concurrent-ruby (~> 1.0, >= 1.1.6) 24 | sorbet-runtime (0.5.11647) 25 | unparser (0.6.15) 26 | diff-lcs (~> 1.3) 27 | parser (>= 3.3.0) 28 | 29 | PLATFORMS 30 | arm64-darwin 31 | x86_64-linux 32 | 33 | DEPENDENCIES 34 | minitest 35 | mutant 36 | mutant-minitest 37 | ruby_event_store 38 | 39 | BUNDLED WITH 40 | 2.5.23 41 | -------------------------------------------------------------------------------- /examples/yield_based/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | @bundle install 3 | 4 | test: 5 | @bundle exec ruby -Ilib -rproject_management test/issue_test.rb 6 | 7 | mutate: 8 | @bundle exec mutant run 9 | 10 | .PHONY: install test mutate 11 | -------------------------------------------------------------------------------- /examples/yield_based/lib/aggregate_repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AggregateRepository 4 | def initialize(event_store) 5 | @event_store = event_store 6 | end 7 | 8 | def with_aggregate(aggregate, stream) 9 | @event_store.read.stream(stream).each { |event| aggregate.apply(event) } 10 | 11 | store = ->(event) { @event_store.append(event, stream_name: stream) } 12 | yield aggregate, store 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /examples/yield_based/lib/aggregate_root.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AggregateRoot 4 | module ClassMethods 5 | def on(*event_klasses, &block) 6 | event_klasses.each do |event_klass| 7 | name = 8 | event_klass.name || 9 | raise(ArgumentError, "Anonymous class is missing name") 10 | handler_name = "on_#{name}" 11 | define_method(handler_name, &block) 12 | @on_methods ||= {} 13 | @on_methods[event_klass] = handler_name 14 | private(handler_name) 15 | end 16 | end 17 | 18 | def on_methods 19 | ancestors 20 | .select { |k| k.instance_variables.include?(:@on_methods) } 21 | .map { |k| k.instance_variable_get(:@on_methods) } 22 | .inject({}, &:merge) 23 | end 24 | end 25 | 26 | def self.included(host_class) 27 | host_class.extend(ClassMethods) 28 | end 29 | 30 | def apply(event) 31 | yield event if block_given? 32 | name = self.class.on_methods.fetch(event.class) 33 | self.method(name).call(event) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /examples/yield_based/lib/project_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../../shared/lib/project_management" 4 | require_relative "aggregate_root" 5 | require_relative "aggregate_repository" 6 | require_relative "project_management/handler" 7 | require_relative "project_management/issue" 8 | -------------------------------------------------------------------------------- /examples/yield_based/lib/project_management/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Handler 5 | def initialize(event_store) 6 | @repository = AggregateRepository.new(event_store) 7 | end 8 | 9 | def call(cmd) 10 | case cmd 11 | in CreateIssue[id:] 12 | create(id) 13 | in ResolveIssue[id:] 14 | resolve(id) 15 | in CloseIssue[id:] 16 | close(id) 17 | in ReopenIssue[id:] 18 | reopen(id) 19 | in StartIssueProgress[id:] 20 | start(id) 21 | in StopIssueProgress[id:] 22 | stop(id) 23 | end 24 | rescue Issue::InvalidTransition 25 | raise Error 26 | end 27 | 28 | def create(id) 29 | with_issue(id) { |issue, store| issue.create(id) { |ev| store.call(ev) } } 30 | end 31 | 32 | def resolve(id) 33 | with_issue(id) { |issue, store| issue.resolve { |ev| store.call(ev) } } 34 | end 35 | 36 | def close(id) 37 | with_issue(id) { |issue, store| issue.close { |ev| store.call(ev) } } 38 | end 39 | 40 | def reopen(id) 41 | with_issue(id) { |issue, store| issue.reopen { |ev| store.call(ev) } } 42 | end 43 | 44 | def start(id) 45 | with_issue(id) { |issue, store| issue.start { |ev| store.call(ev) } } 46 | end 47 | 48 | def stop(id) 49 | with_issue(id) { |issue, store| issue.stop { |ev| store.call(ev) } } 50 | end 51 | 52 | private 53 | 54 | def stream_name(id) = "Issue$#{id}" 55 | 56 | def with_issue(id) 57 | @repository.with_aggregate(Issue.new, stream_name(id)) do |issue, store| 58 | yield issue, store 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /examples/yield_based/lib/project_management/issue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | class Issue 5 | include AggregateRoot 6 | InvalidTransition = Class.new(StandardError) 7 | 8 | def create(id) 9 | invalid_transition unless can_create? 10 | apply(IssueOpened.new(data: { issue_id: id })) { |ev| yield ev } 11 | end 12 | 13 | def resolve 14 | invalid_transition unless can_resolve? 15 | apply(IssueResolved.new(data: { issue_id: @id })) { |ev| yield ev } 16 | end 17 | 18 | def close 19 | invalid_transition unless can_close? 20 | apply(IssueClosed.new(data: { issue_id: @id })) { |ev| yield ev } 21 | end 22 | 23 | def reopen 24 | invalid_transition unless can_reopen? 25 | apply(IssueReopened.new(data: { issue_id: @id })) { |ev| yield ev } 26 | end 27 | 28 | def start 29 | invalid_transition unless can_start? 30 | apply(IssueProgressStarted.new(data: { issue_id: @id })) { |ev| yield ev } 31 | end 32 | 33 | def stop 34 | invalid_transition unless can_stop? 35 | apply(IssueProgressStopped.new(data: { issue_id: @id })) { |ev| yield ev } 36 | end 37 | 38 | private 39 | 40 | def invalid_transition 41 | raise InvalidTransition 42 | end 43 | 44 | def open? 45 | @status == :open 46 | end 47 | 48 | def closed? 49 | @status == :closed 50 | end 51 | 52 | def in_progress? 53 | @status == :in_progress 54 | end 55 | 56 | def reopened? 57 | @status == :reopened 58 | end 59 | 60 | def resolved? 61 | @status == :resolved 62 | end 63 | 64 | def can_reopen? 65 | closed? || resolved? 66 | end 67 | 68 | def can_start? 69 | open? || reopened? 70 | end 71 | 72 | def can_stop? 73 | in_progress? 74 | end 75 | 76 | def can_close? 77 | open? || in_progress? || reopened? || resolved? 78 | end 79 | 80 | def can_resolve? 81 | open? || reopened? || in_progress? 82 | end 83 | 84 | def can_create? 85 | @status.nil? 86 | end 87 | 88 | on IssueOpened do |ev| 89 | @id = ev.data.fetch(:issue_id) 90 | @status = :open 91 | end 92 | 93 | on IssueResolved do |_ev| 94 | @status = :resolved 95 | end 96 | 97 | on IssueClosed do |_ev| 98 | @status = :closed 99 | end 100 | 101 | on IssueReopened do |_ev| 102 | @status = :reopened 103 | end 104 | 105 | on IssueProgressStarted do |_ev| 106 | @status = :in_progress 107 | end 108 | 109 | on IssueProgressStopped do |_ev| 110 | @status = :open 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /examples/yield_based/test/issue_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require "minitest/mock" 5 | require "mutant/minitest/coverage" 6 | require "ruby_event_store" 7 | 8 | require_relative "../lib/project_management" 9 | 10 | module ProjectManagement 11 | class IssueTest < Minitest::Test 12 | include Test.with( 13 | handler: ->(event_store) { Handler.new(event_store) }, 14 | event_store: -> { RubyEventStore::Client.new } 15 | ) 16 | 17 | cover "ProjectManagement::Issue*" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /shared/lib/project_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "project_management/commands" 4 | require_relative "project_management/events" 5 | require_relative "project_management/errors" 6 | require_relative "project_management/test" 7 | 8 | PM = ProjectManagement 9 | -------------------------------------------------------------------------------- /shared/lib/project_management/commands.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | Command = Data.define(:id) 5 | 6 | CreateIssue = Class.new(Command) 7 | ResolveIssue = Class.new(Command) 8 | CloseIssue = Class.new(Command) 9 | ReopenIssue = Class.new(Command) 10 | StartIssueProgress = Class.new(Command) 11 | StopIssueProgress = Class.new(Command) 12 | end 13 | -------------------------------------------------------------------------------- /shared/lib/project_management/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | Error = Class.new(StandardError) 5 | end 6 | -------------------------------------------------------------------------------- /shared/lib/project_management/events.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | Event = 5 | Data.define(:event_id, :data, :metadata) do 6 | def initialize(event_id: SecureRandom.uuid, data: {}, metadata: {}) = 7 | super(event_id: event_id, data: data, metadata: metadata) 8 | 9 | def event_type = self.class.name 10 | end 11 | 12 | IssueOpened = Class.new(Event) 13 | IssueResolved = Class.new(Event) 14 | IssueClosed = Class.new(Event) 15 | IssueReopened = Class.new(Event) 16 | IssueProgressStarted = Class.new(Event) 17 | IssueProgressStopped = Class.new(Event) 18 | end 19 | -------------------------------------------------------------------------------- /shared/lib/project_management/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProjectManagement 4 | module Test 5 | def self.with(handler:, event_store:) 6 | Module.new do 7 | def test_impossible_initial_transitions 8 | assert_error { start_issue_progress } 9 | assert_error { stop_issue_progress } 10 | assert_error { close_issue } 11 | assert_error { reopen_issue } 12 | assert_error { resolve_issue } 13 | end 14 | 15 | def test_open_from_initial 16 | create_issue 17 | 18 | assert_events(issue_opened) 19 | end 20 | 21 | def test_open_from_in_progress 22 | create_issue 23 | start_issue_progress 24 | stop_issue_progress 25 | 26 | assert_events( 27 | issue_opened, 28 | issue_progress_started, 29 | issue_progress_stopped 30 | ) 31 | end 32 | 33 | def test_open_impossible_transitions 34 | create_issue 35 | 36 | assert_error { create_issue } 37 | assert_error { stop_issue_progress } 38 | assert_error { reopen_issue } 39 | end 40 | 41 | def test_resolved_from_open 42 | create_issue 43 | resolve_issue 44 | 45 | assert_events(issue_opened, issue_resolved) 46 | end 47 | 48 | def test_resolved_from_in_progress 49 | create_issue 50 | start_issue_progress 51 | resolve_issue 52 | 53 | assert_events(issue_opened, issue_progress_started, issue_resolved) 54 | end 55 | 56 | def test_resolved_from_reopened 57 | create_issue 58 | close_issue 59 | reopen_issue 60 | resolve_issue 61 | 62 | assert_events( 63 | issue_opened, 64 | issue_closed, 65 | issue_reopened, 66 | issue_resolved 67 | ) 68 | end 69 | 70 | def test_resolved_impossible_transitions 71 | create_issue 72 | resolve_issue 73 | 74 | assert_error { create_issue } 75 | assert_error { start_issue_progress } 76 | assert_error { stop_issue_progress } 77 | assert_error { resolve_issue } 78 | end 79 | 80 | def test_closed_from_open 81 | create_issue 82 | close_issue 83 | 84 | assert_events(issue_opened, issue_closed) 85 | end 86 | 87 | def test_closed_from_in_progress 88 | create_issue 89 | start_issue_progress 90 | close_issue 91 | 92 | assert_events(issue_opened, issue_progress_started, issue_closed) 93 | end 94 | 95 | def test_closed_from_resolved 96 | create_issue 97 | resolve_issue 98 | close_issue 99 | 100 | assert_events(issue_opened, issue_resolved, issue_closed) 101 | end 102 | 103 | def test_closed_from_reopened 104 | create_issue 105 | close_issue 106 | reopen_issue 107 | close_issue 108 | 109 | assert_events( 110 | issue_opened, 111 | issue_closed, 112 | issue_reopened, 113 | issue_closed 114 | ) 115 | end 116 | 117 | def test_closed_impossible_transitions 118 | create_issue 119 | close_issue 120 | 121 | assert_error { create_issue } 122 | assert_error { start_issue_progress } 123 | assert_error { stop_issue_progress } 124 | assert_error { close_issue } 125 | assert_error { resolve_issue } 126 | end 127 | 128 | def test_in_progress_from_open 129 | create_issue 130 | start_issue_progress 131 | 132 | assert_events(issue_opened, issue_progress_started) 133 | end 134 | 135 | def test_in_progress_from_reopened 136 | create_issue 137 | close_issue 138 | reopen_issue 139 | start_issue_progress 140 | 141 | assert_events( 142 | issue_opened, 143 | issue_closed, 144 | issue_reopened, 145 | issue_progress_started 146 | ) 147 | end 148 | 149 | def test_in_progress_impossible_transitions 150 | create_issue 151 | start_issue_progress 152 | 153 | assert_error { create_issue } 154 | assert_error { start_issue_progress } 155 | assert_error { reopen_issue } 156 | end 157 | 158 | def test_reopened_from_closed 159 | create_issue 160 | close_issue 161 | reopen_issue 162 | 163 | assert_events(issue_opened, issue_closed, issue_reopened) 164 | end 165 | 166 | def test_reopened_from_resolved 167 | create_issue 168 | resolve_issue 169 | reopen_issue 170 | 171 | assert_events(issue_opened, issue_resolved, issue_reopened) 172 | end 173 | 174 | def test_reopened_impossible_transitions 175 | create_issue 176 | close_issue 177 | reopen_issue 178 | 179 | assert_error { create_issue } 180 | assert_error { stop_issue_progress } 181 | assert_error { reopen_issue } 182 | end 183 | 184 | def test_in_progress_from_open_after_progress_stopped 185 | create_issue 186 | start_issue_progress 187 | stop_issue_progress 188 | start_issue_progress 189 | 190 | assert_events( 191 | issue_opened, 192 | issue_progress_started, 193 | issue_progress_stopped, 194 | issue_progress_started 195 | ) 196 | end 197 | 198 | def test_in_progress_from_open_after_resolved_and_reopened 199 | create_issue 200 | start_issue_progress 201 | resolve_issue 202 | reopen_issue 203 | start_issue_progress 204 | 205 | assert_events( 206 | issue_opened, 207 | issue_progress_started, 208 | issue_resolved, 209 | issue_reopened, 210 | issue_progress_started 211 | ) 212 | end 213 | 214 | def test_reopened_from_closed_after_progress_started 215 | create_issue 216 | start_issue_progress 217 | close_issue 218 | reopen_issue 219 | 220 | assert_events( 221 | issue_opened, 222 | issue_progress_started, 223 | issue_closed, 224 | issue_reopened 225 | ) 226 | end 227 | 228 | def test_reopened_from_closed_after_progress_started_and_resolved 229 | create_issue 230 | start_issue_progress 231 | resolve_issue 232 | close_issue 233 | reopen_issue 234 | 235 | assert_events( 236 | issue_opened, 237 | issue_progress_started, 238 | issue_resolved, 239 | issue_closed, 240 | issue_reopened 241 | ) 242 | end 243 | 244 | def test_closed_from_resolved_after_progress_started 245 | create_issue 246 | start_issue_progress 247 | resolve_issue 248 | close_issue 249 | 250 | assert_events( 251 | issue_opened, 252 | issue_progress_started, 253 | issue_resolved, 254 | issue_closed 255 | ) 256 | end 257 | 258 | def test_stream_isolation 259 | create_issue 260 | handler.call(CreateIssue.new(SecureRandom.uuid)) 261 | resolve_issue 262 | 263 | assert_events(issue_opened, issue_resolved) 264 | end 265 | 266 | private 267 | 268 | attr_reader :event_store, :handler 269 | 270 | define_method :before_setup do 271 | @event_store = event_store.call 272 | @handler = handler.call(@event_store) 273 | end 274 | 275 | def issue_id = "c97a6121-f933-4609-9e96-e77dc2f67a16" 276 | 277 | def issue_data = { issue_id: issue_id } 278 | 279 | def stream_name = "Issue$#{issue_id}" 280 | 281 | def create_issue = handler.call(CreateIssue.new(issue_id)) 282 | 283 | def reopen_issue = handler.call(ReopenIssue.new(issue_id)) 284 | 285 | def close_issue = handler.call(CloseIssue.new(issue_id)) 286 | 287 | def resolve_issue = handler.call(ResolveIssue.new(issue_id)) 288 | 289 | def start_issue_progress = 290 | handler.call(StartIssueProgress.new(issue_id)) 291 | 292 | def stop_issue_progress = handler.call(StopIssueProgress.new(issue_id)) 293 | 294 | def issue_opened = IssueOpened.new(data: issue_data) 295 | 296 | def issue_reopened = IssueReopened.new(data: issue_data) 297 | 298 | def issue_resolved = IssueResolved.new(data: issue_data) 299 | 300 | def issue_closed = IssueClosed.new(data: issue_data) 301 | 302 | def issue_progress_started = IssueProgressStarted.new(data: issue_data) 303 | 304 | def issue_progress_stopped = IssueProgressStopped.new(data: issue_data) 305 | 306 | def assert_error(&) = assert_raises(Error, &) 307 | 308 | def assert_events(*events, comparable: ->(e) { [e.event_type, e.data] }) 309 | assert_equal( 310 | events.map(&comparable), 311 | event_store.read.stream(stream_name).map(&comparable) 312 | ) 313 | end 314 | end 315 | end 316 | end 317 | end 318 | --------------------------------------------------------------------------------