diff --git a/.rubocop.yml b/.rubocop.yml index 0da64c4..4e55d29 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -7,6 +7,9 @@ AllCops: - "Rakefile" SuggestExtensions: false +Lint/EmptyBlock: + Enabled: false + Metrics/BlockLength: Exclude: - "spec/**/*" diff --git a/CHANGELOG.md b/CHANGELOG.md index d18353e..a61d157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.5.0 + +### New features + +- Add `EventSource` mixin. + ## 0.4.0 ### New features diff --git a/Gemfile.lock b/Gemfile.lock index b5a27c4..1d6bcc7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - stimpack (0.4.0) + stimpack (0.5.0) activesupport (~> 6.1) GEM diff --git a/README.md b/README.md index 50435a6..2184ac4 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,47 @@ and behaviour. ## Table of Contents +- [EventSource](#eventsource) - [FunctionalObject](#functionalobject) - [OptionsDeclaration](#optionsdeclaration) - [ResultMonad](#resultmonad) +## EventSource + +A mixin that turns the class into an event emitter with which others can +register listeners. The class can then use `#emit` to broadcast events to any +listensers. + +**Example:** + +Given the following event source: + +```ruby +class Foo + include Stimpack::EventSource + + def bar + emit(:bar, { message: "Hello, world!" }) + end +end +``` + +we can register a callback to listen for events from another part of our +application, and we will receive an event object when the event is emitted: + +```ruby +Foo.on(:bar) do |event| + puts event.message +end + +Foo.new.bar +#=> "Hello, world!" +``` + +*Note: Callbacks are invoked synchronously in the same thread, so don't use +this to perform long-running tasks. You can use the event listener to schedule +a background job, though!* + ## FunctionalObject A simple mixin that provides a shorthand notation for instantiating and diff --git a/lib/stimpack/event_source.rb b/lib/stimpack/event_source.rb new file mode 100644 index 0000000..2c1e369 --- /dev/null +++ b/lib/stimpack/event_source.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +# TODO: Remove dependency on ActiveSupport. +# +require "active_support/core_ext/class/attribute" + +module Stimpack + module EventSource + class Event + def initialize(name, data = {}) + @name = name + @data = data + end + + attr_reader :name, :data + + def respond_to_missing?(method) + data.key?(method) || super + end + + def method_missing(method, *arguments, &block) + if data.key?(method) + data[method] + else + super + end + end + end + + module ClassMethods + def self.extended(klass) + klass.class_eval do + # TODO: Remove dependency on ActiveSupport. + # + class_attribute :event_listeners, + instance_accessor: false, + default: Hash.new { |h, k| h[k] = [] } + end + end + + def on(event_name, &block) + event_listeners["#{self}.#{event_name}"] << block + end + end + + def self.included(klass) + klass.extend(ClassMethods) + end + + def emit(event_name, data) + event_name = "#{self.class}.#{event_name}" + + event = Event.new(event_name, data) + + self.class.event_listeners[event_name].each { |l| l.(event) } + end + end +end diff --git a/lib/stimpack/version.rb b/lib/stimpack/version.rb index 14d90c5..2d4e001 100644 --- a/lib/stimpack/version.rb +++ b/lib/stimpack/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Stimpack - VERSION = "0.4.0" + VERSION = "0.5.0" end diff --git a/spec/stimpack/event_source/event_spec.rb b/spec/stimpack/event_source/event_spec.rb new file mode 100644 index 0000000..4ff3cc0 --- /dev/null +++ b/spec/stimpack/event_source/event_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "stimpack/event_source" + +RSpec.describe Stimpack::EventSource::Event do + subject(:event) { described_class.new(name, data) } + + describe "#name" do + let(:name) { "foo.bar.baz" } + let(:data) {} + + it { expect(event.name).to eq("foo.bar.baz") } + end + + describe "#method_missing" do + let(:name) { "foo.bar.baz" } + + context "when field is found in data" do + let(:data) do + { + foo: "bar" + } + end + + it { expect(event.foo).to eq("bar") } + end + + context "when field is not found in data" do + let(:data) do + {} + end + + it { expect { event.foo }.to raise_error(NoMethodError) } + end + end +end diff --git a/spec/stimpack/event_source_spec.rb b/spec/stimpack/event_source_spec.rb new file mode 100644 index 0000000..bd4d260 --- /dev/null +++ b/spec/stimpack/event_source_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "stimpack/event_source" + +RSpec.describe Stimpack::EventSource do + subject(:service) { klass } + + let(:klass) do + Class.new do + include Stimpack::EventSource + + def self.to_s + "Foo" + end + end + end + + describe ".on" do + it { expect { service.on(:foo) {} }.to change { klass.event_listeners["Foo.foo"].size }.by(1) } + end + + describe "#emit" do + let(:receiver) { spy } + + before do + allow(receiver).to receive(:qux) + + service.on(:qux) { |d| receiver.baz(d) } + + service.new.emit(:qux, { quux: 1 }) + end + + it { expect(receiver).to have_received(:baz).with(Stimpack::EventSource::Event) } + end +end