bizness 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 52657da3321f8216c3aed22df7aa598b0ddce78d
4
- data.tar.gz: 351634c989bf984d60e09d97f625b12176f2c5e9
3
+ metadata.gz: fdcd210e4eec5f7e5f9d8b1945b83f4b400eb5c2
4
+ data.tar.gz: 501a8155d69ae354e60d6452af7b4861268f6071
5
5
  SHA512:
6
- metadata.gz: 944b4590e34e31ce2d4c127ed3e05e95712327b4077e2aa004e0c38566c3399b4ab8d154178e541ffab617c7b0cce242f31111ae7d720c92cca7442bbc2bc980
7
- data.tar.gz: 3e8b318b87f9243688fa97bde6d296fd5187184c5ce18d79974fe0f750924c6575123782bd37a95dfa24299d43e5bda45db0c67e8bda3dc569280511e82e90a7
6
+ metadata.gz: 0ce4d947499f155a46e627ac620b3fafa6d4c2da916c7da6867110b9793c8299a2798cb107b221f39cf97ee4c5f6089c07f88cf2cdd3b80e3005fb9d15f2fcd4
7
+ data.tar.gz: c020189fa5e2d77db8939a151021bcb568a5e7010d2d3b97506b3207a6df211e78462a6c01feb008be397526a52d102456bc38dccde5b1e97c7b5aaf542395fe
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Bizness
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/bizness`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ Get your bizness right and organize your business logic into operations.
6
4
 
7
5
  ## Installation
8
6
 
@@ -22,13 +20,119 @@ Or install it yourself as:
22
20
 
23
21
  ## Usage
24
22
 
25
- TODO: Write usage instructions here
23
+ ## Why use this?
24
+
25
+ First of all, you need to buy into the idea that ActiveRecord is an egregious violator of the Single Responsibility Principle.
26
+ Breaking out its responsibilities into separate objects, such as Query, Policy, Form and Service objects, has been a trend for some time now. These objects allow you to better organize and isolate your business logic, leading to faster design and better testing. You spend less time wondering how a feature's classes should be organized and more time addressing the needs of your domain.
27
+
28
+ ShippingEasy adopted this strategy, but we opted to use the term Operation rather than the nebulous term "Service Object" (or Interactor for that matter).
29
+
30
+ As we wrote more and more operations, some insights emerged:
31
+
32
+ 1. We always ran an operation in a transaction
33
+ 2. An operation nearly always corresponded to an important system event ("Complete Registration")
34
+ 3. Logging these events into a dashboard like Kibana could provide us with valuable metrics and insight into how our system was performing at a business logic level
35
+
36
+ Bizness, therefore, allows you to create PORO operation objects and easily augment their business logic execution via a series of filters. Out of the box there are filters to wrap every operation in a transaction as well as automatically broadcasting every operation as a series of events.
37
+ A filter is just a SimpleDelegator, so it's easy to create your own. Moving these responsbilities out of your operation classes keeps them lean and focused, and in the case of the event filters, automatically instruments every important business operation in your codebase (depending on how widely you adopt them).
38
+
39
+ ### Writing your own operation
40
+
41
+ 1. Ensure your class responds to the instance method `call`.
42
+
43
+ 2. If you are using the `EventFilter` and want to customize the message that is published when your operation is run, write a `to_h` instance method that returns a hash.
44
+
45
+ 3. You should raise all errors as runtime exceptions if your operation fails so the filters can rollback and react accordingly.
46
+
47
+ ### Running an operation
48
+
49
+ The pipeline can be executed from `Bizness.run`. You can either pass in an object, or execute a block.
50
+
51
+ ```ruby
52
+ Bizness.run(operation)
53
+ ```
54
+
55
+ ```ruby
56
+ Bizness.run do
57
+ puts “foo”
58
+ end
59
+ ```
60
+
61
+ By default, all filters that are globally configured on Bizness are run, but you may optionally pass in overrides as well:
62
+
63
+ ```ruby
64
+ Bizness.run(filters: Bizness::Filters::EventFilter) do
65
+ puts “foo”
66
+ end
67
+ ```
68
+
69
+ ### The Operation module
70
+
71
+ Though you don't need to use the `Bizness::Operation` module to define an operation, one is provided as a convenience. This module has several methods that make calling operations and running them through the pipeline more intuitive.
72
+
73
+ For example:
26
74
 
27
- ## Development
75
+ ```ruby
76
+ op = CompleteRegistrationOperation.new(customer: customer)
77
+
78
+ op.call! # submits itself through the filter pipeline
79
+
80
+ op.error # Exceptions are caught and the message is set on error
81
+
82
+ op.successful? # True if error is nil
83
+
84
+ op.to_h
85
+ ```
86
+
87
+ ### Pubsub
88
+
89
+ #### Publishing
90
+
91
+ Automatically publishing operations as events via the `EventFilter` is one of the primary reasons we developed Bizness. The `EventFilter` will always publish two events. For example, if your operation class is named `CompleteRegistrationOperation`, these events will be published:
92
+
93
+ * "operations:complete_registration:executed" - This event includes the start time, end time and duration of the operation
94
+ * "operations:complete_registration:succeeded" or "operations:complete_registration:failed" depending on if an exception was raised or not
95
+
96
+ #### Subscribing
97
+
98
+ Once an operation is publishing events, we wanted to make it easy for other operations to subscribe to those events. To wire up a class to easily subscribe to these events, simply extend the `Bizness::Subscriber` module. That gives your class access to the `subscribe` method.
99
+
100
+ You can use the `subscribe` method in one of two ways: with or without a block. You can use the block as a builder for your operation, translating the values coming in from the message into the arguments necessary to initialize your operation. You should always return an initialized operation from this block.
28
101
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
102
+ For example, if your operation requires a model to initialize:
30
103
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
104
+ ```ruby
105
+ class SendWelcomeEmailOperation
106
+ extend Bizness::Subscriber
107
+
108
+ subscribe(“operations:registration_complete:succeeded”) do |event_data|
109
+ customer = Customer.find(event_data[:customer_id])
110
+ new(customer: customer)
111
+ end
112
+
113
+ def initialize(customer:)
114
+ @customer = customer
115
+ end
116
+
117
+ # …. omitted for brevity
118
+ end
119
+ ```
120
+
121
+ If no block is passed, the attributes from the event payload will be used directly to initialize and execute your operations:
122
+
123
+ ```ruby
124
+ class SendWelcomeEmailOperation
125
+ extend Bizness::Subscriber
126
+
127
+ subscribe(“operations:registration_complete:succeeded”)
128
+
129
+ def initialize(customer_id:)
130
+ @customer = Customer.find(customer_id)
131
+ end
132
+
133
+ # …. omitted for brevity
134
+ end
135
+ ```
32
136
 
33
137
  ## Contributing
34
138
 
@@ -3,7 +3,6 @@ module Bizness::Filters
3
3
  def call
4
4
  ActiveRecord::Base.transaction(requires_new: true) do
5
5
  filtered_operation.call
6
- raise ActiveRecord::Rollback unless successful?
7
6
  end
8
7
  end
9
8
  end
@@ -5,10 +5,6 @@ module Bizness::Filters
5
5
  super(operation)
6
6
  end
7
7
 
8
- def self.wrap(collection)
9
- collection.map { |object| new(object) }
10
- end
11
-
12
8
  private
13
9
 
14
10
  attr_reader :__original_operation__
@@ -1,7 +1,7 @@
1
1
  module Bizness::Filters
2
2
  class EventFilter < Bizness::Filters::BaseFilter
3
3
  def call
4
- Hey.publish!("#{event_name}:executed", context.to_h) do
4
+ Hey.publish!("#{event_name}:executed", payload) do
5
5
  evented_call
6
6
  end
7
7
  end
@@ -9,16 +9,25 @@ module Bizness::Filters
9
9
  private
10
10
 
11
11
  def evented_call
12
- filtered_operation.call
13
- Hey.publish!("#{event_name}:#{successful? ? "succeeded" : "failed"}", context.to_h)
12
+ result = filtered_operation.call
13
+ Hey.publish!("#{event_name}:#{successful? ? "succeeded" : "failed"}", payload(result))
14
14
  rescue => e
15
- context.error = e.message
16
- Hey.publish!("#{event_name}:failed", context.to_h)
15
+ Hey.publish!("#{event_name}:failed", payload.merge(error: e.message))
17
16
  raise e
18
17
  end
19
18
 
20
19
  def event_name
21
20
  __original_operation__.class.name.underscore.gsub("/", ":")
22
21
  end
22
+
23
+ def payload(result = nil)
24
+ if self.respond_to?(:to_h)
25
+ to_h
26
+ elsif result.is_a?(Hash)
27
+ result
28
+ else
29
+ Hash.new
30
+ end
31
+ end
23
32
  end
24
33
  end
@@ -1,46 +1,32 @@
1
- class Bizness::Operation
2
- extend Forwardable
1
+ module Bizness::Operation
2
+ attr_reader :error
3
3
 
4
- def_delegators :context, :error, :successful?
5
-
6
- attr_reader :context
7
-
8
- def initialize(context = {})
9
- @context = Bizness::Context.new(context.to_h)
4
+ def call!
5
+ run
6
+ successful?
10
7
  end
11
8
 
12
- def self.filters
13
- Bizness.filters
9
+ def call
10
+ raise NotImplementedError
14
11
  end
15
12
 
16
- def self.call!(context = {})
17
- new(context).call!
13
+ def successful?
14
+ error.nil?
18
15
  end
19
16
 
20
- def call!
21
- execute_filtered_operation!
22
- context
17
+ def to_h
18
+ {}
23
19
  end
24
20
 
25
- def call
26
- raise NotImplementedError
27
- end
21
+ protected
28
22
 
29
- def fail!(error:)
30
- context.error = error
31
- end
23
+ attr_writer :error
32
24
 
33
25
  private
34
26
 
35
- def execute_filtered_operation!
36
- filtered_operation.call
37
- raise Bizness::Failure, context.error unless successful?
27
+ def run
28
+ Bizness.run(self)
38
29
  rescue => e
39
- context.error = e.message
40
- end
41
-
42
- def filtered_operation
43
- return self if self.class.filters.empty?
44
- self.class.filters.reduce(self) { |filtered_op, filter| filter.new(filtered_op) }
30
+ self.error = e.message
45
31
  end
46
32
  end
@@ -1,8 +1,9 @@
1
1
  module Bizness::Subscriber
2
- def subscribe(*event_names)
2
+ def subscribe(*event_names, &block)
3
3
  event_names.each do |event_name|
4
4
  Hey.subscribe!(event_name) do |event|
5
- call!(event[:metadata])
5
+ instance = block_given? ? block.call(event[:metadata]) : new(event[:metadata])
6
+ instance.call!
6
7
  end
7
8
  end
8
9
  end
@@ -1,3 +1,3 @@
1
1
  module Bizness
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/bizness.rb CHANGED
@@ -5,7 +5,6 @@ require "hey"
5
5
 
6
6
  module Bizness
7
7
  def self.configure
8
- Hey.configuration.namespace = "operations"
9
8
  yield(configuration)
10
9
  end
11
10
 
@@ -16,12 +15,15 @@ module Bizness
16
15
  def self.filters
17
16
  configuration.filters
18
17
  end
18
+
19
+ def self.run(operation = nil, filters: self.filters, &block)
20
+ operation = block if block_given?
21
+ filters.reduce(operation) { |filtered_op, filter| filter.new(filtered_op) }.call
22
+ end
19
23
  end
20
24
 
21
25
  require "bizness/configuration"
22
- require "bizness/context"
23
26
  require "bizness/operation"
24
- require "bizness/failure"
25
27
  require "bizness/subscriber"
26
28
  require "bizness/filters/base_filter"
27
29
  require "bizness/filters/active_record_transaction_filter"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bizness
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ShippingEasy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-08-09 00:00:00.000000000 Z
11
+ date: 2015-08-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -113,8 +113,6 @@ files:
113
113
  - bizness.gemspec
114
114
  - lib/bizness.rb
115
115
  - lib/bizness/configuration.rb
116
- - lib/bizness/context.rb
117
- - lib/bizness/failure.rb
118
116
  - lib/bizness/filters/active_record_transaction_filter.rb
119
117
  - lib/bizness/filters/base_filter.rb
120
118
  - lib/bizness/filters/event_filter.rb
@@ -1,18 +0,0 @@
1
- require "ostruct"
2
-
3
- class Bizness::Context < OpenStruct
4
- attr_accessor :error
5
-
6
- def successful?
7
- error.nil?
8
- end
9
-
10
- # If a value responds to #id, automatically set an equivalent "_id" key/value pair. For example, if an instance
11
- # of an ActiveRecord class Widget is set on a context object of :widget, set another attribute called :widget_id with
12
- # the value of the object's ID. This helps ensure the context's values can be sent as a message across application
13
- # boundries.
14
- def to_h
15
- super.each { |k, v| send("#{k}_id=", v.id) if v.respond_to?(:id) && send("#{k}_id").nil? }
16
- super
17
- end
18
- end
@@ -1,6 +0,0 @@
1
- class Bizness::Failure < StandardError
2
- def initialize(context = nil)
3
- @context = context
4
- super
5
- end
6
- end