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 +4 -4
- data/README.md +111 -7
- data/lib/bizness/filters/active_record_transaction_filter.rb +0 -1
- data/lib/bizness/filters/base_filter.rb +0 -4
- data/lib/bizness/filters/event_filter.rb +14 -5
- data/lib/bizness/operation.rb +16 -30
- data/lib/bizness/subscriber.rb +3 -2
- data/lib/bizness/version.rb +1 -1
- data/lib/bizness.rb +5 -3
- metadata +2 -4
- data/lib/bizness/context.rb +0 -18
- data/lib/bizness/failure.rb +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fdcd210e4eec5f7e5f9d8b1945b83f4b400eb5c2
|
4
|
+
data.tar.gz: 501a8155d69ae354e60d6452af7b4861268f6071
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ce4d947499f155a46e627ac620b3fafa6d4c2da916c7da6867110b9793c8299a2798cb107b221f39cf97ee4c5f6089c07f88cf2cdd3b80e3005fb9d15f2fcd4
|
7
|
+
data.tar.gz: c020189fa5e2d77db8939a151021bcb568a5e7010d2d3b97506b3207a6df211e78462a6c01feb008be397526a52d102456bc38dccde5b1e97c7b5aaf542395fe
|
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# Bizness
|
2
2
|
|
3
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
102
|
+
For example, if your operation requires a model to initialize:
|
30
103
|
|
31
|
-
|
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
|
|
@@ -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",
|
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"}",
|
12
|
+
result = filtered_operation.call
|
13
|
+
Hey.publish!("#{event_name}:#{successful? ? "succeeded" : "failed"}", payload(result))
|
14
14
|
rescue => e
|
15
|
-
|
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
|
data/lib/bizness/operation.rb
CHANGED
@@ -1,46 +1,32 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module Bizness::Operation
|
2
|
+
attr_reader :error
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
13
|
-
|
9
|
+
def call
|
10
|
+
raise NotImplementedError
|
14
11
|
end
|
15
12
|
|
16
|
-
def
|
17
|
-
|
13
|
+
def successful?
|
14
|
+
error.nil?
|
18
15
|
end
|
19
16
|
|
20
|
-
def
|
21
|
-
|
22
|
-
context
|
17
|
+
def to_h
|
18
|
+
{}
|
23
19
|
end
|
24
20
|
|
25
|
-
|
26
|
-
raise NotImplementedError
|
27
|
-
end
|
21
|
+
protected
|
28
22
|
|
29
|
-
|
30
|
-
context.error = error
|
31
|
-
end
|
23
|
+
attr_writer :error
|
32
24
|
|
33
25
|
private
|
34
26
|
|
35
|
-
def
|
36
|
-
|
37
|
-
raise Bizness::Failure, context.error unless successful?
|
27
|
+
def run
|
28
|
+
Bizness.run(self)
|
38
29
|
rescue => e
|
39
|
-
|
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
|
data/lib/bizness/subscriber.rb
CHANGED
@@ -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
|
5
|
+
instance = block_given? ? block.call(event[:metadata]) : new(event[:metadata])
|
6
|
+
instance.call!
|
6
7
|
end
|
7
8
|
end
|
8
9
|
end
|
data/lib/bizness/version.rb
CHANGED
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.
|
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-
|
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
|
data/lib/bizness/context.rb
DELETED
@@ -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
|