diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..b65122c --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,24 @@ +# We want Exclude directives from different +# config files to get merged, not overwritten +inherit_mode: + merge: + - Exclude + +require: + # Standard's config uses custom cops, + # so it must be loaded along with custom Standard gems + - standard + - standard-custom + - standard-performance + # rubocop-performance is required when using Performance cops + - rubocop-performance + +inherit_gem: + standard: config/base.yml + standard-performance: config/base.yml + standard-custom: config/base.yml + +# Global options, like Ruby version +AllCops: + SuggestExtensions: false + TargetRubyVersion: 3.2 diff --git a/Gemfile b/Gemfile index 2e3ce4f..a8610f0 100644 --- a/Gemfile +++ b/Gemfile @@ -9,7 +9,6 @@ gem "rake", "~> 13.0" gem "rspec", "~> 3.0" gem "standard", "~> 1.3" gem "guard" -gem "guard-standardrb" gem "guard-rspec" gem "guard-bundler" gem "yard" diff --git a/Gemfile.lock b/Gemfile.lock index b2615fe..4b49875 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,9 +3,9 @@ PATH specs: workflows (0.1.0) dry-configurable - dry-events dry-struct faker + standard-procedure-plumbing GEM remote: https://rubygems.org/ @@ -22,9 +22,6 @@ GEM dry-core (1.0.1) concurrent-ruby (~> 1.0) zeitwerk (~> 2.6) - dry-events (1.0.1) - concurrent-ruby (~> 1.0) - dry-core (~> 1.0, < 2) dry-inflector (1.0.0) dry-logic (1.5.0) concurrent-ruby (~> 1.0) @@ -44,17 +41,7 @@ GEM zeitwerk (~> 2.6) faker (3.4.1) i18n (>= 1.8.11, < 2) - ffi (1.17.0) - ffi (1.17.0-aarch64-linux-gnu) - ffi (1.17.0-aarch64-linux-musl) - ffi (1.17.0-arm-linux-gnu) - ffi (1.17.0-arm-linux-musl) - ffi (1.17.0-arm64-darwin) - ffi (1.17.0-x86-linux-gnu) - ffi (1.17.0-x86-linux-musl) - ffi (1.17.0-x86_64-darwin) - ffi (1.17.0-x86_64-linux-gnu) - ffi (1.17.0-x86_64-linux-musl) + ffi (1.16.3) formatador (1.1.0) guard (2.18.1) formatador (>= 0.2.4) @@ -74,10 +61,6 @@ GEM guard (~> 2.1) guard-compat (~> 1.1) rspec (>= 2.99.0, < 4.0) - guard-standardrb (0.2.3) - guard (>= 2.0.0) - guard-compat (~> 1.0) - standard i18n (1.14.5) concurrent-ruby (~> 1.0) ice_nine (0.11.2) @@ -88,7 +71,7 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) lumberjack (1.2.10) - method_source (1.1.0) + method_source (1.0.0) nenv (0.3.0) notiffany (0.1.3) nenv (~> 0.1) @@ -104,7 +87,7 @@ GEM rainbow (3.1.1) rake (13.2.1) rb-fsevent (0.11.2) - rb-inotify (0.11.1) + rb-inotify (0.10.1) ffi (~> 1.0) regexp_parser (2.9.2) rexml (3.2.8) @@ -158,6 +141,9 @@ GEM standard-performance (1.4.0) lint_roller (~> 1.1) rubocop-performance (~> 1.21.0) + standard-procedure-plumbing (0.1.0) + dry-struct + dry-types strscan (3.1.0) thor (1.3.1) unicode-display_width (2.5.0) @@ -181,7 +167,6 @@ DEPENDENCIES guard guard-bundler guard-rspec - guard-standardrb rake (~> 13.0) rspec (~> 3.0) simplecov diff --git a/Guardfile b/Guardfile index 6bf7ea9..b7b8176 100644 --- a/Guardfile +++ b/Guardfile @@ -1,16 +1,5 @@ ignore(/bin/, /log/, /public/, /storage/, /tmp/) -group :formatting do - guard :standardrb, fix: true, all_on_start: true, progress: true do - watch(/.+\.rb$/) - watch(/.+\.thor$/) - watch(/.+\.rake$/) - watch(/Guardfile$/) - watch(/Rakefile$/) - watch(/Gemfile$/) - end -end - group :development do guard :rspec, cmd: "bundle exec rspec" do watch("spec/.+_helper.rb") { "spec" } diff --git a/lib/workflows.rb b/lib/workflows.rb index ec11028..5b302ff 100644 --- a/lib/workflows.rb +++ b/lib/workflows.rb @@ -2,6 +2,7 @@ require "dry/types" require "dry/configurable" +require "plumbing" module Workflows extend Dry::Configurable @@ -13,13 +14,13 @@ module Types Name = Strict::String Card = Types.Interface(:id, :name, :state) - Storage = Types.Interface(:create, :find, :where, :update, :delete) State = Types.Interface(:name, :perform_action) States = Types::Array.of(Types::State) Action = Types.Interface(:destination, :outputs) Actions = Types::Hash.map(Types::Coercible::String, Types::Action) - Services = Types.Interface(:[], :resolve) - Messages = Types.Interface(:publish, :subscribe) + Operation = Types.Interface(:call) + Services = Types::Hash.map(Types::Coercible::String, Types::Operation) + Messages = Types.Interface(:<<, :add_observer, :remove_observer) Workflow = Types.Interface(:states, :services, :messages) end @@ -32,5 +33,8 @@ class Error < StandardError; end require_relative "workflows/workflow" require_relative "workflows/card" - setting :cards, default: InMemoryCards, reader: true + # @return [Plumbing::Pipe] message queue for broadcasting events + setting :messages, default: Plumbing::Pipe.start, reader: true + # @return [Hash] a set of services available within this service + setting :services, default: {}, reader: true end diff --git a/lib/workflows/action.rb b/lib/workflows/action.rb index b87ffc5..939514c 100644 --- a/lib/workflows/action.rb +++ b/lib/workflows/action.rb @@ -3,9 +3,12 @@ require "dry/struct" module Workflows + # An action that can be performed on a {Card} to change its {State} class Action < Dry::Struct include NameToS + # @return [Workflows::State] attribute :destination, Types::State + # @return [Array(String)] attribute :outputs, Types::Array.of(Types::Coercible::String) end end diff --git a/lib/workflows/name_to_s.rb b/lib/workflows/name_to_s.rb index 3d66b97..159b780 100644 --- a/lib/workflows/name_to_s.rb +++ b/lib/workflows/name_to_s.rb @@ -1,5 +1,7 @@ module Workflows + # Convenience mix-in to return the included class's `name` as `to_s` module NameToS + # @return [String] def to_s name end diff --git a/lib/workflows/state.rb b/lib/workflows/state.rb index 329542f..9ff6b27 100644 --- a/lib/workflows/state.rb +++ b/lib/workflows/state.rb @@ -4,18 +4,28 @@ require_relative "action" module Workflows + # The state in a {Workflows::Workflow} state-machine class State < Dry::Struct include NameToS + # @return [String] attribute :name, Types::Name + # @return [Array(Workflows::Action)] attribute :actions, Types::Actions + # @param action_name [String] the name of the action to perform + # @param card [Workflows::Card] the card that will be acted upon + # @raise [Workflows::State::InvalidAction] if the action_name is invalid def perform_action action_name, card: card = Types::Card[card] action = actions[action_name] raise InvalidAction.new(action) if action.nil? - card = Workflows.cards.update card, state: action.destination + + updater = Types::Operation[Workflows.services["workflows.cards.storage.update.state"]] + card = updater.call card, state: action.destination + + messages = Types::Messages[Workflows.messages] action.outputs.each do |output| - card.emit output + messages.notify output, card: card end end diff --git a/lib/workflows/workflow.rb b/lib/workflows/workflow.rb index 57e0a0c..1a303a8 100644 --- a/lib/workflows/workflow.rb +++ b/lib/workflows/workflow.rb @@ -4,10 +4,13 @@ require_relative "state" module Workflows + # Workflow represents the structure of a finite state machine that {Workflows::Card}s move through. class Workflow < Dry::Struct include NameToS + # @return [String] attribute :name, Types::Name + # @return [Array(Workflows::State)] attribute :states, Types::States end end diff --git a/spec/workflows/card_spec.rb b/spec/workflows/card_spec.rb index 3be6b5f..daeb0ad 100644 --- a/spec/workflows/card_spec.rb +++ b/spec/workflows/card_spec.rb @@ -9,6 +9,4 @@ @card.perform("some_action") end - - it "emits an event" end diff --git a/spec/workflows/state_spec.rb b/spec/workflows/state_spec.rb index 9b2494e..e21f1d0 100644 --- a/spec/workflows/state_spec.rb +++ b/spec/workflows/state_spec.rb @@ -3,25 +3,29 @@ RSpec.describe Workflows::State do context "performing an action" do it "updates the card's state and tells it to emit some outputs" do + Workflows.services["workflows.cards.storage.update.state"] = ->(card, state:) do + expect(state).to eq @dispatched + card + end @dispatched = Workflows::State.new(name: "Dispatched", actions: {}) @dispatch = Workflows::Action.new(destination: @dispatched, outputs: ["card.dispatched"]) @initial = Workflows::State.new(name: "Iniital", actions: {dispatch: @dispatch}) @workflow = Workflows::Workflow.new(name: "Order processing", states: [@initial, @dispatched]) + @pipe = Workflows.messages @card = Workflows::Card.new(id: "123", name: "New order", state: @initial) - expect(Workflows.cards).to receive(:update).with(@card, state: @dispatched).and_return(@card) - expect(@card).to receive(:emit).with("card.dispatched") + expect(@pipe).to receive(:notify).with("card.dispatched", card: @card) @initial.perform_action "dispatch", card: @card end - end - it "raises an InvalidAction exception if the action is not valid" do - @initial = Workflows::State.new(name: "Iniital", actions: {}) + it "raises an InvalidAction exception if the action is not valid" do + @initial = Workflows::State.new(name: "Iniital", actions: {}) - @card = Workflows::Card.new(id: "123", name: "New order", state: @initial) + @card = Workflows::Card.new(id: "123", name: "New order", state: @initial) - expect { @initial.perform_action "dispatch", card: @card }.to raise_exception(Workflows::State::InvalidAction) + expect { @initial.perform_action "dispatch", card: @card }.to raise_exception(Workflows::State::InvalidAction) + end end end diff --git a/workflows.gemspec b/workflows.gemspec index 4327927..0480105 100644 --- a/workflows.gemspec +++ b/workflows.gemspec @@ -32,7 +32,7 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_dependency "dry-configurable" - spec.add_dependency "dry-events" + spec.add_dependency "standard-procedure-plumbing" spec.add_dependency "dry-struct" spec.add_dependency "faker" end