ruby_dci 0.1.0 → 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/.gitignore +1 -0
- data/.rubocop.yml +0 -20
- data/.travis.yml +3 -1
- data/CHANGELOG.md +18 -0
- data/Gemfile.lock +7 -3
- data/README.md +139 -4
- data/lib/dci/configuration.rb +10 -4
- data/lib/dci/context.rb +6 -1
- data/lib/dci/event_router.rb +6 -5
- data/lib/dci/version.rb +1 -1
- data/ruby_dci.gemspec +1 -8
- metadata +18 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1b8372e74de2567e52902b236b08aa68779df450995d8a7d83f1d4241d37ede3
|
4
|
+
data.tar.gz: 13677405fdba528931c0f1b745d4507ac745192b218b363dbcc8a027f93b514c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8f6a20d6b9de2c3eb81d18c845c34a073848dcf31e401fbe263674dbf0bd4e2a2960c21820948f3ece3cfb65d22b10ba564c53d30cb0e82315a8b2ebce423c7f
|
7
|
+
data.tar.gz: 6e750cba64ddb92652af80f1364b33c2c53b5c3ee02407203de45fd39f842056ecb4bb0d327a671c98afaf52712fb23afdeba000ae4de55b4303827b35031db7
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -21,8 +21,6 @@ AllCops:
|
|
21
21
|
|
22
22
|
TargetRubyVersion: 2.5
|
23
23
|
|
24
|
-
TargetRailsVersion: 5.2
|
25
|
-
|
26
24
|
DefaultFormatter: simple
|
27
25
|
|
28
26
|
DisabledByDefault: true
|
@@ -96,24 +94,6 @@ Performance/RedundantMatch:
|
|
96
94
|
Performance/TimesMap:
|
97
95
|
Enabled: true
|
98
96
|
|
99
|
-
Rails/ActionFilter:
|
100
|
-
Enabled: true
|
101
|
-
|
102
|
-
Rails/Delegate:
|
103
|
-
Enabled: true
|
104
|
-
|
105
|
-
Rails/FindBy:
|
106
|
-
Enabled: true
|
107
|
-
|
108
|
-
Rails/Output:
|
109
|
-
Enabled: true
|
110
|
-
|
111
|
-
Rails/PluralizationGrammar:
|
112
|
-
Enabled: true
|
113
|
-
|
114
|
-
Rails/Validation:
|
115
|
-
Enabled: true
|
116
|
-
|
117
97
|
RSpec/DescribeMethod:
|
118
98
|
Enabled: true
|
119
99
|
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
## 0.2.0 (2018-05-19)
|
2
|
+
|
3
|
+
Features:
|
4
|
+
|
5
|
+
- `DCI::Configuration` now takes a `on_exception_in_router` handler, instead of requiring a `logger` instance.
|
6
|
+
|
7
|
+
Bugfixes:
|
8
|
+
|
9
|
+
- Added specs
|
10
|
+
- Added `README.md`
|
11
|
+
- Added `CHANGELOG.md`
|
12
|
+
|
13
|
+
## 0.1.0 (2018-05-18)
|
14
|
+
|
15
|
+
Features:
|
16
|
+
|
17
|
+
- `DCI::Context` with basic functionality
|
18
|
+
- `DCI::Role` with basic functionality
|
data/Gemfile.lock
CHANGED
@@ -1,13 +1,17 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
ruby_dci (0.
|
4
|
+
ruby_dci (0.2.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
-
|
9
|
+
coderay (1.1.2)
|
10
10
|
diff-lcs (1.3)
|
11
|
+
method_source (0.9.0)
|
12
|
+
pry (0.11.3)
|
13
|
+
coderay (~> 1.1.0)
|
14
|
+
method_source (~> 0.9.0)
|
11
15
|
rake (10.5.0)
|
12
16
|
rspec (3.7.0)
|
13
17
|
rspec-core (~> 3.7.0)
|
@@ -28,7 +32,7 @@ PLATFORMS
|
|
28
32
|
|
29
33
|
DEPENDENCIES
|
30
34
|
bundler (~> 1.16)
|
31
|
-
|
35
|
+
pry (~> 0.11.3)
|
32
36
|
rake (~> 10.0)
|
33
37
|
rspec (~> 3.0)
|
34
38
|
ruby_dci!
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
|
1
|
+
[![Build Status](https://travis-ci.org/egze/ruby_dci.svg?branch=master)](https://travis-ci.org/egze/ruby_dci)
|
2
2
|
|
3
|
-
|
3
|
+
# RubyDci
|
4
4
|
|
5
|
-
|
5
|
+
A classic DCI implementation for ruby with some extra sugar. I've been using DCI in my Rails projects and I extracted some common patterns into this gem.
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
@@ -20,9 +20,144 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
$ gem install ruby_dci
|
22
22
|
|
23
|
+
## Before you begin
|
24
|
+
|
25
|
+
First of all, make yourself familiar with [DCI](http://dci-in-ruby.info/) :
|
26
|
+
|
27
|
+
> DCI (Data Context Interaction) is a new way to look at object-oriented programming. Instead of focusing on individual objects, the DCI paradigm focuses on communication between objects and makes it explicit. It improves the readability of the code, which helps programmers to reason about their programs.
|
28
|
+
|
29
|
+
With the theory out of the way, let's see what this gem will give you. You will get:
|
30
|
+
|
31
|
+
* `DCI::Context` module to include in your contexts or use cases.
|
32
|
+
* `DCI::Role` module to include in your roles.
|
33
|
+
* Transaction support for the code executed in the context.
|
34
|
+
* Event routing and processing which should run after the transaction is commited.
|
35
|
+
|
36
|
+
This is a common pattern with DCI. You run your use case, code is executed in a transaction, and then later you want to publish the result to the message broker for example.
|
37
|
+
|
23
38
|
## Usage
|
24
39
|
|
25
|
-
|
40
|
+
### Configuration
|
41
|
+
|
42
|
+
I configure the gem in `config/initializers/dci_configuration.rb`.
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
DCI.configure do |config|
|
46
|
+
config.event_routes = Hash.new([])
|
47
|
+
config.route_methods = EventRouteStore.new
|
48
|
+
config.transaction_class = ApplicationRecord
|
49
|
+
config.raise_in_event_router = !Rails.env.production?
|
50
|
+
config.on_exception_in_router = -> (exception) {}
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
### config.transaction_class
|
55
|
+
|
56
|
+
Usually you want your code to run in a transaction. Either everything runs fine, or nothing is saved. This is done by wrapping the executed code in a transaction block. I use ActiveRecord, but you can use whatever you want. Your class just needs to implement a `transaction` method that takes a block. If you don't want any transactions, you can either skip `config.transaction_class` completely, or set it to `DCI::NullTransaction`.
|
57
|
+
|
58
|
+
### config.event_routes
|
59
|
+
|
60
|
+
This is your mapping of events that may happen in the context. Key is a class name, and the value is an array of method names. Example:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
{
|
64
|
+
DomainEvents::ProductAddedToCart => [ :send_product_added_notification ]
|
65
|
+
}
|
66
|
+
```
|
67
|
+
|
68
|
+
The system will know that it needs to execute `send_product_added_notification` from `config.route_methods` for every event of class `DomainEvents::ProductAddedToCart`. If you don't have any actions that you need to perform after a transaction, the just skip `config.event_routes` completely or set it to `Hash.new([])`.
|
69
|
+
|
70
|
+
I implement events as plain ruby Structs. Example:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
module DomainEvents
|
74
|
+
|
75
|
+
ProductAddedToCart = Struct.new(:product)
|
76
|
+
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
Why do I do it like this? It makes it easier to add other callbacks later. I can do it in one place instead of searching through hundreds of files. Also makes testing easier.
|
81
|
+
|
82
|
+
### config.route_methods
|
83
|
+
|
84
|
+
This is a class that implements the methods for the `config.event_routes` mapping. Example:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
class EventRouteStore
|
88
|
+
|
89
|
+
def send_product_added_notification(event)
|
90
|
+
AddedToCartNotificationJob.perform_later(id: event.product.id)
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
### config.raise_in_event_router
|
97
|
+
|
98
|
+
When your transaction is commited, you don't want to raise an exception during event processing. You can turn it off in production environment, but still raise when you are developing.
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
config.raise_in_event_router = !Rails.env.production?
|
102
|
+
```
|
103
|
+
|
104
|
+
### config.on_exception_in_router
|
105
|
+
|
106
|
+
In case there is an exception in the event router, you can provide a handler for the exception. It should be a lambda that receives an exception as a parameter. You can use it to log the exception. If you don't need any logging, just skip `config.on_exception_in_router` completely, or assign an empty lambda.
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
config.logger = -> (exception) { Rails.logger.error(exception) }
|
110
|
+
```
|
111
|
+
|
112
|
+
|
113
|
+
### Context
|
114
|
+
|
115
|
+
In a Rails app I put my contexts in `app/contexts`. You define a context by including `DCI::Context`.
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
class AddProductToCart
|
119
|
+
|
120
|
+
include DCI::Context
|
121
|
+
|
122
|
+
attr_accessor :customer, :product
|
123
|
+
def initialize(user:, product:)
|
124
|
+
@customer = user.extend(Customer)
|
125
|
+
@product = product
|
126
|
+
end
|
127
|
+
|
128
|
+
def call
|
129
|
+
customer.add_to_cart!(product: product)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
Somewhere else in code, for example in a controller, you call it like this:
|
135
|
+
```ruby
|
136
|
+
AddProductToCart.call(user: current_user, product: @product)
|
137
|
+
```
|
138
|
+
|
139
|
+
Couple of thigs to keep in mind:
|
140
|
+
* The context will either succeed or raise an exception. I prefer to rescue exceptions instead of checking for result. This has a benefit of keeping my code linear, instead of `if .. else` nesting.
|
141
|
+
* There is usually no result from the `.call`. For example, if you want to create a `User` record, don't pass request params to your context, then create the object somewhere in a role and somehow try to return this object back. Instead build the User object already in the controller and pass the instance to your `.call`. Will save you a lot of headache.
|
142
|
+
|
143
|
+
### Role
|
144
|
+
|
145
|
+
In a Rails app I put my roles in `app/roles`. Roles are plain ruby modules. You define a role by including `DCI::Role`.
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
module Customer
|
149
|
+
|
150
|
+
include DCI::Role
|
151
|
+
|
152
|
+
def add_to_cart!(product:)
|
153
|
+
# do your thing
|
154
|
+
|
155
|
+
# add event to the context
|
156
|
+
context.events << DomainEvents::ProductAddedToCart.new(product)
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
```
|
26
161
|
|
27
162
|
## Development
|
28
163
|
|
data/lib/dci/configuration.rb
CHANGED
@@ -1,11 +1,17 @@
|
|
1
1
|
module DCI
|
2
2
|
class Configuration
|
3
|
-
attr_accessor :transaction_class,
|
3
|
+
attr_accessor :transaction_class,
|
4
|
+
:event_routes,
|
5
|
+
:route_methods,
|
6
|
+
:raise_in_event_router,
|
7
|
+
:on_exception_in_router
|
4
8
|
|
5
9
|
def initialize
|
6
|
-
@transaction_class
|
7
|
-
@event_routes
|
8
|
-
@route_methods
|
10
|
+
@transaction_class = NullTransaction
|
11
|
+
@event_routes = Hash.new([])
|
12
|
+
@route_methods = nil
|
13
|
+
@raise_in_event_router = false
|
14
|
+
@on_exception_in_router = -> (exception) { }
|
9
15
|
end
|
10
16
|
end
|
11
17
|
end
|
data/lib/dci/context.rb
CHANGED
@@ -16,7 +16,7 @@ module DCI
|
|
16
16
|
|
17
17
|
def perform_in_transaction
|
18
18
|
old_context = context
|
19
|
-
@events =
|
19
|
+
@events = init_context_events
|
20
20
|
self.context = self
|
21
21
|
|
22
22
|
res = nil
|
@@ -39,6 +39,11 @@ module DCI
|
|
39
39
|
def call
|
40
40
|
raise NotImplementedError.new("implement me")
|
41
41
|
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def init_context_events
|
45
|
+
[]
|
46
|
+
end
|
42
47
|
end
|
43
48
|
|
44
49
|
module ClassMethods
|
data/lib/dci/event_router.rb
CHANGED
@@ -2,15 +2,15 @@ module DCI
|
|
2
2
|
|
3
3
|
module EventRouter
|
4
4
|
|
5
|
-
private
|
6
5
|
def route_events!(events)
|
7
6
|
Array(events).each(&method(:route_event!))
|
8
7
|
end
|
9
8
|
|
9
|
+
private
|
10
10
|
def route_event!(event)
|
11
11
|
DCI.configuration.event_routes[event.class].each do |callback|
|
12
12
|
dispatch_catching_standard_errors do
|
13
|
-
DCI.configuration.route_methods.
|
13
|
+
DCI.configuration.route_methods.send(callback, event)
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
@@ -18,9 +18,10 @@ module DCI
|
|
18
18
|
def dispatch_catching_standard_errors(&block)
|
19
19
|
begin
|
20
20
|
block.call
|
21
|
-
rescue StandardError =>
|
22
|
-
|
23
|
-
|
21
|
+
rescue StandardError => exception
|
22
|
+
DCI.configuration.on_exception_in_router.call(exception) rescue nil
|
23
|
+
|
24
|
+
raise exception if DCI.configuration.raise_in_event_router
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
data/lib/dci/version.rb
CHANGED
data/ruby_dci.gemspec
CHANGED
@@ -13,14 +13,6 @@ Gem::Specification.new do |spec|
|
|
13
13
|
spec.homepage = "https://github.com/egze/ruby_dci"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
|
-
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
17
|
-
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
18
|
-
if spec.respond_to?(:metadata)
|
19
|
-
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
20
|
-
else
|
21
|
-
raise "RubyGems 2.0 or newer is required to protect against " \
|
22
|
-
"public gem pushes."
|
23
|
-
end
|
24
16
|
|
25
17
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
26
18
|
f.match(%r{^(test|spec|features)/})
|
@@ -31,4 +23,5 @@ Gem::Specification.new do |spec|
|
|
31
23
|
spec.add_development_dependency "bundler", "~> 1.16"
|
32
24
|
spec.add_development_dependency "rake", "~> 10.0"
|
33
25
|
spec.add_development_dependency "rspec", "~> 3.0"
|
26
|
+
spec.add_development_dependency "pry", "~> 0.11.3"
|
34
27
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_dci
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aleksandr Lossenko
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-05-
|
11
|
+
date: 2018-05-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.11.3
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.11.3
|
55
69
|
description: Provides base modules for DCI contexts, roles and event processing that
|
56
70
|
should happen outside of the context transaction.
|
57
71
|
email:
|
@@ -64,6 +78,7 @@ files:
|
|
64
78
|
- ".rspec"
|
65
79
|
- ".rubocop.yml"
|
66
80
|
- ".travis.yml"
|
81
|
+
- CHANGELOG.md
|
67
82
|
- Gemfile
|
68
83
|
- Gemfile.lock
|
69
84
|
- LICENSE.txt
|
@@ -83,8 +98,7 @@ files:
|
|
83
98
|
homepage: https://github.com/egze/ruby_dci
|
84
99
|
licenses:
|
85
100
|
- MIT
|
86
|
-
metadata:
|
87
|
-
allowed_push_host: https://rubygems.org
|
101
|
+
metadata: {}
|
88
102
|
post_install_message:
|
89
103
|
rdoc_options: []
|
90
104
|
require_paths:
|