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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 762938fa7f8db22e7ea14f8bd054fa80ec846b727b59840ca37d705ca8e70ebc
4
- data.tar.gz: 197db7e710f326106b6e651b428ecb9de2209b6d26c8fbd6bea85dbf39b2fe96
3
+ metadata.gz: 1b8372e74de2567e52902b236b08aa68779df450995d8a7d83f1d4241d37ede3
4
+ data.tar.gz: 13677405fdba528931c0f1b745d4507ac745192b218b363dbcc8a027f93b514c
5
5
  SHA512:
6
- metadata.gz: 8d1da22eef9b8f6561b82b2a60277fe05cb6fa4b7dbff602b3202d29041eb4b0805ba6dc84ab4e627bfa4e2b429f63538e21acdbbb0ee057dd1a242c467e8d5c
7
- data.tar.gz: 347083758996404aa4aecfb77b94acc064b7a9b85f3ba1ed5413fcfadf145a05e8b386a83b7079fdf7cc19e83896fa6bc5375db50c1cb4e88b36948d07e3b0a2
6
+ metadata.gz: 8f6a20d6b9de2c3eb81d18c845c34a073848dcf31e401fbe263674dbf0bd4e2a2960c21820948f3ece3cfb65d22b10ba564c53d30cb0e82315a8b2ebce423c7f
7
+ data.tar.gz: 6e750cba64ddb92652af80f1364b33c2c53b5c3ee02407203de45fd39f842056ecb4bb0d327a671c98afaf52712fb23afdeba000ae4de55b4303827b35031db7
data/.gitignore CHANGED
@@ -7,6 +7,7 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
  .DS_Store
10
+ *.gem
10
11
 
11
12
  # rspec failure tracking
12
13
  .rspec_status
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
@@ -2,4 +2,6 @@ sudo: false
2
2
  language: ruby
3
3
  rvm:
4
4
  - 2.5.0
5
- before_install: gem install bundler -v 1.16.1
5
+ before_install:
6
+ - gem update --system
7
+ - gem install bundler -v 1.16.1
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.1.0)
4
+ ruby_dci (0.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- byebug (10.0.2)
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
- byebug
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
- # RubyDci
1
+ [![Build Status](https://travis-ci.org/egze/ruby_dci.svg?branch=master)](https://travis-ci.org/egze/ruby_dci)
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/ruby_dci`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ # RubyDci
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
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
- TODO: Write usage instructions here
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
 
@@ -1,11 +1,17 @@
1
1
  module DCI
2
2
  class Configuration
3
- attr_accessor :transaction_class, :event_routes, :route_methods
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 = NullTransaction
7
- @event_routes = Hash.new([])
8
- @route_methods = nil
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
@@ -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.method(callback).call(event)
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 => e
22
- Rails.logger.error "Failed to dispatch event (transaction was still commited). #{ e }"
23
- raise e unless Rails.env.production?
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
@@ -1,3 +1,3 @@
1
1
  module DCI
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
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.1.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-17 00:00:00.000000000 Z
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: