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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,32 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
ruby: ["2.7", "3.0", "3.1", "3.2", "jruby"]
ruby: ["3.2", "3.3", "3.4", "4.0"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- run: bundle exec rspec spec

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: "4.0"
bundler-cache: true
- run: bundle exec rubocop

audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: "4.0"
bundler-cache: true
- run: bundle exec bundler-audit check --update
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
*.gem
*.rbc
.ruby-version
.bundle
.config
.yardoc
Expand Down
95 changes: 95 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
plugins:
- rubocop-performance
- rubocop-rspec

AllCops:
NewCops: enable
TargetRubyVersion: 3.2
SuggestExtensions: false
Exclude:
- 'bin/**/*'
- 'vendor/**/*'
- 'coverage/**/*'

# Wisper is intentionally a small, single-purpose library; classes and
# methods are kept short by design, so class/method length limits add
# noise rather than value.
Metrics:
Enabled: false

Style/Documentation:
Enabled: true
Exclude:
- 'spec/**/*'

Style/StringLiterals:
EnforcedStyle: single_quotes

Style/FrozenStringLiteralComment:
Enabled: true

Layout/LineLength:
Max: 130
Exclude:
- 'spec/**/*'

# The specs use a mix of `key: value` and legacy `:key => value` hash
# syntax on purpose, to demonstrate/exercise both call styles that
# Wisper's public API supports (see publisher_spec.rb). Enforcing one
# style repo-wide would obscure that.
Style/HashSyntax:
Exclude:
- 'spec/**/*'

RSpec/ExampleLength:
Enabled: false

RSpec/MultipleExpectations:
Enabled: false

RSpec/NestedGroups:
Max: 6

RSpec/DescribeClass:
Exclude:
- 'spec/lib/simple_example_spec.rb'
- 'spec/lib/integration_spec.rb'

RSpec/MultipleMemoizedHelpers:
Enabled: false

RSpec/ContextWording:
Enabled: false

RSpec/NamedSubject:
Enabled: false

RSpec/MessageSpies:
Enabled: false

RSpec/StubbedMock:
Enabled: false

RSpec/VerifiedDoubles:
Enabled: false

# `listener_1`/`listener_2`, `publisher_klass_1`/`publisher_klass_2` etc.
# are the clearest names for these examples; a semantic rename would be
# more indirect for no real benefit.
Naming/VariableNumber:
Exclude:
- 'spec/**/*'

RSpec/IndexedLet:
Enabled: false

# The spec file layout mirrors the `lib/wisper/**` structure by intent,
# not the `describe`d class/module name.
RSpec/SpecFilePathFormat:
Enabled: false

# Several specs intentionally assert that a chainable method still
# returns `self` when given an empty block.
Lint/EmptyBlock:
Exclude:
- 'spec/**/*'
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4.0.6
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
## HEAD (unreleased)

* Adds: support for and CI coverage of Ruby 3.2 - 4.0
* Removes: support for Ruby < 3.2 [breaking change]
* fix: `Wisper::Broadcasters::LoggerBroadcaster` keyword-argument logging on
Ruby 3.4+, where `Hash#inspect` changed its output format for symbol keys
* chore: replace the abandoned `coveralls` gem (pinned to ancient
faraday/thor/tins releases that no longer install on current Ruby) with
`simplecov`; the suite now enforces 100% line and branch coverage
* chore: add RuboCop (with rubocop-performance, rubocop-rspec) and a
`.rubocop.yml`; codebase is fully lint-clean and uniformly formatted
* chore: add `bundler-audit` to CI to catch dependency vulnerabilities
* docs: add YARD documentation to all public classes and methods

## 3.0.0 (23rd May 2024)

Authors: maboelnour, kianmeng
Expand Down
21 changes: 19 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
# frozen_string_literal: true

source 'https://rubygems.org'

gemspec

gem 'rake'
gem 'rspec'
gem 'coveralls', require: false

group :test do
# local, dependency-free coverage reporting (replaces the abandoned
# `coveralls` gem, which pins ancient faraday/thor/tins releases that
# no longer install on current Ruby)
gem 'simplecov', require: false
end

group :development do
# static analysis
gem 'rubocop', require: false
gem 'rubocop-performance', require: false
gem 'rubocop-rspec', require: false

# dependency vulnerability scanning
gem 'bundler-audit', require: false
end

group :extras do
gem 'flay'
gem 'pry'
gem 'yard'
end
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
[![Gem Version](https://badge.fury.io/rb/wisper.svg)](http://badge.fury.io/rb/wisper)
[![Code Climate](https://codeclimate.com/github/krisleech/wisper.svg)](https://codeclimate.com/github/krisleech/wisper)
[![Build Status](https://github.com/krisleech/wisper/actions/workflows/test.yml/badge.svg)](https://github.com/krisleech/wisper/actions)
[![Coverage Status](https://coveralls.io/repos/krisleech/wisper/badge.svg?branch=master)](https://coveralls.io/r/krisleech/wisper?branch=master)

* Decouple core business logic from external concerns in Hexagonal style architectures
* Use as an alternative to ActiveRecord callbacks and Observers in Rails apps
Expand All @@ -27,6 +26,9 @@ Add this line to your application's Gemfile:
gem 'wisper', '~> 3.0'
```

Wisper requires Ruby 3.2 or newer, and is tested against 3.2, 3.3, 3.4 and
4.0 (see the [build status](https://github.com/krisleech/wisper/actions)).

## Usage

Any class with the `Wisper::Publisher` module included can broadcast events
Expand Down Expand Up @@ -339,12 +341,21 @@ See the [build status](https://github.com/krisleech/wisper/actions) for details.
bundle exec rspec
```

The suite enforces 100% line and branch coverage via [SimpleCov](https://github.com/simplecov-ruby/simplecov);
a report is written to `coverage/index.html`.

To run the specs on code changes try [entr](http://entrproject.org/):

```
ls **/*.rb | entr bundle exec rspec
```

## Linting

```
bundle exec rubocop
```

## Contributing

Please read the [Contributing Guidelines](https://github.com/krisleech/wisper/blob/master/CONTRIBUTING.md).
Expand All @@ -354,6 +365,13 @@ Please read the [Contributing Guidelines](https://github.com/krisleech/wisper/bl
* gem releases are [signed](http://guides.rubygems.org/security/) ([public key](https://github.com/krisleech/wisper/blob/master/gem-public_cert.pem))
* commits are GPG signed ([public key](https://pgp.mit.edu/pks/lookup?op=get&search=0x3ABC74851F7CCC88))
* My [Keybase.io profile](https://keybase.io/krisleech)
* publishing to RubyGems requires MFA (`rubygems_mfa_required` in the gemspec)
* dependencies are scanned for known vulnerabilities with
[bundler-audit](https://github.com/rubysec/bundler-audit):

```
bundle exec bundler-audit check --update
```

## License

Expand Down
22 changes: 17 additions & 5 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
require "bundler/gem_tasks"
# frozen_string_literal: true

require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
require 'coveralls/rake/task'

RSpec::Core::RakeTask.new
Coveralls::RakeTask.new

task :test_with_coveralls => [:spec, 'coveralls:push']
begin
require 'rubocop/rake_task'
RuboCop::RakeTask.new
rescue LoadError
# rubocop is only available in the :development group
end

begin
require 'bundler/audit/task'
Bundler::Audit::Task.new
rescue LoadError
# bundler-audit is only available in the :development group
end

task :default => :spec
task default: :spec
65 changes: 60 additions & 5 deletions lib/wisper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'set'
# frozen_string_literal: true

require 'wisper/version'
require 'wisper/configuration'
require 'wisper/publisher'
Expand All @@ -12,49 +13,103 @@
require 'wisper/broadcasters/send_broadcaster'
require 'wisper/broadcasters/logger_broadcaster'

# Wisper provides Ruby objects with Publish-Subscribe capabilities.
#
# Include {Wisper::Publisher} in any class to let its instances broadcast
# events to listeners which subscribe to them, either per-instance
# ({Wisper::Publisher#subscribe}) or globally (+Wisper.subscribe+).
#
# @see https://github.com/krisleech/wisper/wiki the wiki for further guides
# and examples
module Wisper
# Examples:
# Subscribes one or more listeners.
#
# With no block, the listeners are subscribed globally (equivalent to
# {GlobalListeners.subscribe}) until explicitly {unsubscribe}d.
#
# With a block, the listeners are subscribed globally only for the
# duration of the block, on the current thread (equivalent to
# {TemporaryListeners.subscribe}).
#
# @param listeners [Array<Object>] one or more listener objects
# @param options [Hash] see {ObjectRegistration#initialize} for the
# full set of supported options (`:on`, `:with`, `:prefix`, `:scope`,
# `:broadcaster`/`:async`)
#
# @example subscribe a single listener globally
# Wisper.subscribe(AuditRecorder.new)
#
# @example subscribe multiple listeners globally
# Wisper.subscribe(AuditRecorder.new, StatsRecorder.new)
#
# @example subscribe to a single event only
# Wisper.subscribe(AuditRecorder.new, on: 'order_created')
#
# @example subscribe scoped to a publisher class
# Wisper.subscribe(AuditRecorder.new, scope: 'MyPublisher')
#
# @example subscribe for the duration of a block only
# Wisper.subscribe(AuditRecorder.new, StatsRecorder.new) do
# # ..
# end
#
def self.subscribe(*args, **kwargs, &block)
# @return [Object] the return value of {TemporaryListeners.subscribe} or
# {GlobalListeners.subscribe}
def self.subscribe(*, **, &)
if block_given?
TemporaryListeners.subscribe(*args, **kwargs, &block)
TemporaryListeners.subscribe(*, **, &)
else
GlobalListeners.subscribe(*args, **kwargs)
GlobalListeners.subscribe(*, **)
end
end

# Removes one or more listeners from the global listeners.
#
# @param listeners [Array<Object>] the listener(s) previously passed to
# {subscribe}
#
# @return [GlobalListeners]
def self.unsubscribe(*listeners)
GlobalListeners.unsubscribe(*listeners)
end

# @return [Module] the {Publisher} module, so it can be included via
# `include Wisper.publisher` as an alternative to `include
# Wisper::Publisher`
def self.publisher
Publisher
end

# Removes all global listeners.
#
# @return [GlobalListeners]
def self.clear
GlobalListeners.clear
end

# Yields the global {Configuration} instance for modification.
#
# @example
# Wisper.configure do |config|
# config.broadcaster(:async, MyAsyncBroadcaster.new)
# end
#
# @yieldparam configuration [Configuration]
# @return [void]
def self.configure
yield(configuration)
end

# @return [Configuration] the memoized global configuration
def self.configuration
@configuration ||= Configuration.new
end

# Restores the default configuration, registering
# {Broadcasters::SendBroadcaster} as the `:default` broadcaster.
#
# @api private
# @return [void]
def self.setup
configure do |config|
config.broadcaster(:default, Broadcasters::SendBroadcaster.new)
Expand Down
Loading