legatus 0.1.0 → 0.1.1

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: fd9586b50f1209e599a2c6da395c11a0be0926bee165f0d0a1046330cf844ed1
4
- data.tar.gz: ad18c8452d8f52170fc04880f6d0370e6af3f956faa49bb1ba6021d13f502d83
3
+ metadata.gz: 54c62e8d695d9c99e975d2170a5355fbe6f4ba9da844375ca7d485456db900db
4
+ data.tar.gz: b9656081c257e65998f8eff41592fea7a13c56293b62c1f4f2089f468926e71e
5
5
  SHA512:
6
- metadata.gz: 226fa1d9f2d5f3168bbe4cdd3420513ceeeac36434490166555990eac2a44bed98dab8e81075fbdb85bb10f31cf90743abe9761432665261d4f0c6f57c590aa4
7
- data.tar.gz: 89eef7866335b9ef6dcaea661f85643087f430e81cb8fd2a28b3bd73897080cfd34bbbe50210581696a05b922df15e4f45f145d81e3ec997d18222e4cca2aa26
6
+ metadata.gz: f675a9c344bd4908d683ee8a07bcaaa1dbaa9c238b704677ed5c21b6f1ad4c6fcb71047941ded5090971966bc050efd7253c0cc98c58f9720e519ee9b464b0ab
7
+ data.tar.gz: b46a115930c206c238147ce8309f46dc330343db1ed6ffd5aa9ea2c88de9b090f2822d80d39b288a6723181ab58087f71d986feec30887f760e678fd21b72148
data/README.md CHANGED
@@ -1,8 +1,272 @@
1
1
  # Legatus
2
- Short description and motivation.
2
+ Build business directives in Rails. A `Legatus::Directive` has the following properties:
3
+
4
+ 1. `params` - The raw parameters from a controller.
5
+ 2. `props` - The filtered out from params. In traditional Rails apps, these are usually declared in the controller (e.g. for a scaffolded `BookController`, there will be a `book_params` method which filters the raw parameters).
6
+ 2. `errors` - Errors encountered during the directive's execution.
7
+
8
+ A `Legatus::Directive` also has the following default lifecycles called in sequence in the directive's `execute` (apart from initialize which is called on creation of the directive) method:
9
+
10
+ 1. `initialize` - Accepts raw parameters and prepares `props`.
11
+ 2. `clean` - Validate the extracted `props` for any missing or wrongly formatted input.
12
+ 3. `load` - Load models from the cleaned `props`.
13
+ 4. `validate` - Validate the loaded models (e.g. by default, done by calling `valid?` on them)
14
+ 5. `persist` - Persist the changes onto the database.
15
+
16
+ When calling `execute`, each of the lifecyle methods is expected to return a value that is truthy or falsy. If the return value is falsy, the execution stops (e.g., if `clean` returns false, `load`, `validate`, and `persist` will no longer be called). A directive can also have before and after callbacks for each of the lifecycle methods.
17
+
18
+ A directive can be defined in two ways:
19
+ 1. Overriding the lifecycle methods.
20
+ 2. Specifying meta information which will be used by the superclass' default methods.
3
21
 
4
22
  ## Usage
5
- How to use my plugin.
23
+
24
+ To create a directive, the class `Legatus::Directive` should be extended and the models handled by the directive should be declared using `attr_accessor`:
25
+ ```ruby
26
+ class Product::Item::Save < Legatus::Directive
27
+ attr_accessor :item
28
+ end
29
+ ```
30
+
31
+ For this example, we will be creating a directive for saving a Product which is an ActiveRecord object, wherein a Product can have many UnitPrices.
32
+
33
+ ### Initialize
34
+
35
+ The first step when dealing with directives is converting params from controllers into properties. In traditional Rails controllers, we would usually find:
36
+
37
+ ```ruby
38
+ protected
39
+ def order_params
40
+ params[:item].permit(:id, :name, :description, :merchant_id, :status)
41
+ end
42
+
43
+ def line_item_params
44
+ params[:item].permit(unit_prices: [:price, :effective_date, :_destroy])
45
+ end
46
+ ```
47
+
48
+ In our directive, the above would look like:
49
+
50
+ ```ruby
51
+ class Product::Item::Save < Legatus::Directive
52
+ attr_accessor :item
53
+
54
+ def initialize(params)
55
+ @props = {
56
+ order: params[:item].permit(:id, :name, :description, :merchant_id, :status),
57
+ unit_prices: params[:item].permit(unit_prices: [:price, :effective_date, :_destroy])[:unit_prices]
58
+ }
59
+ end
60
+ end
61
+ ```
62
+
63
+ Alternatively, if you don't want to override the constructor:
64
+
65
+ ```ruby
66
+ class Product::Item::Save < Legatus::Directive
67
+ attr_accessor :item
68
+
69
+ props do
70
+ {
71
+ item: { dig: [:item], permit: [:id, :name, :description, :partner_id, :status] },
72
+ unit_prices: { dig: [:item, :unit_prices], map: permit([:price, :effective_date, :_destroy]) }
73
+ }
74
+ end
75
+ end
76
+ ```
77
+
78
+ Wherein the value describes a series of method calls to be performed in sequence, that is:
79
+
80
+ ```ruby
81
+ # item: { dig: [:item], permit: [:id, :name, :description, :partner_id, :status] }
82
+ # is equivalent to:
83
+
84
+ @props[:item] = params[:item].dig(:item).permit(:id, :name, :description, :partner_id, :status)
85
+
86
+ # The method `permit` for `unit_prices` in the above example actually returns a lambda function which will be pased to map.
87
+ # unit_prices: { dig: [:item, :unit_prices], map: permit([:price, :effective_date, :_destroy]) }
88
+ # is equivalent to:
89
+
90
+ @props[:unit_prices] = params.dig(:item, :unit_prices).map &permit([:price, :effective_date, :_destroy])
91
+
92
+ ```
93
+
94
+ The method `permit` can also handle permitting nested values, for example:
95
+
96
+ ```ruby
97
+ props do
98
+ {
99
+ line_items: {
100
+ dig: [:order, :line_items],
101
+ map: permit(
102
+ [:id, :item_id, :price, :quantity, :payments, :added_at, :start_date, :end_date],
103
+ payments: [:id, :amount, :paid_at, :status]
104
+ )
105
+ }
106
+ }
107
+ end
108
+
109
+ # The above is equivalent to:
110
+ @props[:line_items] = params.dig(:order, :line_items).map do |li|
111
+ li.permit(:id, :item_id, :price, :quantity, :payments, :added_at, :start_date, :end_date).tap do |whitelisted|
112
+ whitelisted[:payments_attributes] = li.permit(payments: [:id, :amount, :paid_at, :status])[:payments]
113
+ end
114
+ end if params[:order][:line_items].present?
115
+ ```
116
+
117
+ The main advantage of using the class-level ``props`` declaration is that it will stop the chain of method invocations once the return value of one of the invocations returns nil (which is the case when the user leaves certain parameters blank). It uses ``Legatus::Chain`` to perform the method invocations.
118
+
119
+ ### Clean
120
+
121
+ The second step is "cleaning" the extracted properties of the directive. This may include setting default or derived values as well as validations before attempting to retrieve or create `ActiveRecord` models. In `Legatus::Directive` the clean method is defined as:
122
+
123
+ ```ruby
124
+ def clean
125
+ self.reqs(self.props, self.props.keys)
126
+ end
127
+ ```
128
+
129
+ Which simply means all properties declared in the previous step is required (i.e., the values must not return true when `.blank?` is called on them). To add a custom error, simply set a value using `@errors`:
130
+
131
+ ```ruby
132
+ def clean
133
+ @errors[:message] = 'Not authorized' if @user.is_guest?
134
+ end
135
+ ```
136
+
137
+ Take note that adding a value to `@error` will cause `valid?` of the directive to return false. Which will halt the execution chain if `execute` is used in the directive because `execute` will call `valid?` before proceeding to the next step.
138
+
139
+ ### Load
140
+
141
+ The third step is loading or initializing models or services that will be used to persist the changes for the directive. We can override it like so:
142
+
143
+ ```ruby
144
+ def load
145
+ @item = Product::Item.find_and_init(
146
+ @props[:item].slice(:id),
147
+ @props[:item].merge(unit_prices_attributes: @props[:unit_prices])
148
+ )
149
+ end
150
+ ```
151
+
152
+ In the above example, the method `find_and_init` is defined in `Legatus::Repository`. It simply uses find_by on the first parameter, instantiates a new one if none is found, and then sets the attributes of that model using the second attribute.
153
+
154
+ Alternatively, models can be declared using:
155
+
156
+ ```ruby
157
+ class Product::Item::Save < Legatus::Directive
158
+ attr_accessor :item
159
+
160
+ model(:item) do |props|
161
+ Product::Item.find_and_init(
162
+ props[:item].slice(:id),
163
+ props[:item].merge(unit_prices_attributes: props[:unit_prices])
164
+ )
165
+ end
166
+ end
167
+ ```
168
+
169
+ Attributes declared using `attr_accessor` can be injected onto the lambda function passed to `model` so long as the parameter name in the lambda function is the same as the attribute. For example, using a more complex directive:
170
+
171
+ ```ruby
172
+ class School::Student::Registration < Legatus::Directive
173
+
174
+ attr_accessor :user, :university,
175
+ :graduate, :student, :enrollment
176
+
177
+ props do |params|
178
+ #...
179
+ end
180
+
181
+ model(:user) do |props|
182
+ #...
183
+ end
184
+
185
+ model(:university) do |props|
186
+ #...
187
+ end
188
+
189
+ # The attributes user and university is passed onto the lambda
190
+ model(:graduate) do |props, user, university|
191
+ Credential::Graduate.find_and_init(
192
+ props[:graduate].merge(
193
+ user: user,
194
+ university: university
195
+ )
196
+ )
197
+ end
198
+ end
199
+ ```
200
+
201
+ This is achived using the flexcon gem.
202
+
203
+ ### Validate
204
+
205
+ The fourth step is the validation of the models. If you defined the models at the class level (e.g. `model(:item) { ... }`), by default, all models registered that way will be validated because the metadata on which attributes of the directive are models is available. On the other hand, if a custom load model was defined, you can also define a custom validate model:
206
+
207
+ ```ruby
208
+ def validate
209
+ if @item.invalid?
210
+ @errors[key] ||= {}
211
+ @errors[key].merge!(@item.errors)
212
+ end
213
+ end
214
+ ```
215
+
216
+ ### Persist
217
+
218
+ The fifth and final step is persisting the changes to the database. You can define a custom `persist` method:
219
+
220
+ ```ruby
221
+ def persist
222
+ @item.save
223
+ end
224
+ ```
225
+
226
+ Or define it at the class level:
227
+
228
+ ```ruby
229
+ class Product::Item::Save < Legatus::Directive
230
+
231
+ attr_accessor :item
232
+
233
+ transaction do |uow, operation|
234
+ uow.save operation.item
235
+ end
236
+ end
237
+ ```
238
+
239
+ The uow above is a ```Legatus::UnitOfWork``` which is useful for directives that persist multiple models. A unit of work will store all save operations as lambda functions and will only start persisting them when `commit` is called. This is useful for when there are additional logic that needs to be performed in between saving models. Such that when `commit` is called, only persistence operations are performed. When a transaction is defined at the class level, the `commit` automatically after calling the block.
240
+
241
+ ### All Together Now
242
+
243
+ The save order directive, using class-level definitions, would then look like:
244
+
245
+ ```ruby
246
+ class Product::Item::Save < Legatus::Directive
247
+
248
+ attr_accessor :item
249
+
250
+ props do |params|
251
+ {
252
+ item: { dig: [:item], permit: [:id, :name, :description, :partner_id, :status] },
253
+ unit_prices: { dig: [:item, :unit_prices], map: permit([:price, :effective_date, :_destroy]) }
254
+ }
255
+ end
256
+
257
+ model(:item) do |props|
258
+ Product::Item.find_and_init(
259
+ props[:item].slice(:id),
260
+ props[:item].merge(unit_prices_attributes: props[:unit_prices])
261
+ )
262
+ end
263
+
264
+ transaction do |uow, operation|
265
+ uow.save operation.item
266
+ end
267
+ end
268
+ ```
269
+
6
270
 
7
271
  ## Installation
8
272
  Add this line to your application's Gemfile:
@@ -21,17 +21,25 @@ module Legatus
21
21
  end
22
22
  end
23
23
 
24
- def permit(parent, attributes, subschema=nil)
25
- result = parent.permit(attributes)
26
- result.tap do |whitelisted|
27
- subschema.each do |key, allowed|
28
- child = parent[key]
29
- next if child.nil?
30
-
31
- if child.is_a?(Array)
32
- whitelisted[key] = child.map { |c| c.permit(allowed) }
33
- else
34
- whitelisted[key] = child.permit(allowed)
24
+ def chain(obj, invocations)
25
+ return Chain.new(invocations).apply(obj)
26
+ end
27
+
28
+ def permit(attributes, subschema=nil)
29
+ lambda do |parent|
30
+ result = parent.permit(attributes)
31
+ return result if subschema.nil?
32
+
33
+ result.tap do |whitelisted|
34
+ subschema.each do |key, allowed|
35
+ child = parent[key]
36
+ next if child.nil?
37
+
38
+ if child.is_a?(Array)
39
+ whitelisted[:"#{key}_attributes"] = child.map { |c| c.permit(allowed) }
40
+ else
41
+ whitelisted[:"#{key}_attributes"] = child.permit(allowed)
42
+ end
35
43
  end
36
44
  end
37
45
  end
@@ -93,38 +101,39 @@ module Legatus
93
101
  end
94
102
 
95
103
  def validate
96
- self.valid? and self.class.validations.each do |mname|
97
- self.check(mname => self.send(mname))
98
- end if self.class.validations.present?
104
+ return (
105
+ self.valid? and self.class.validations.each do |mname|
106
+ self.check(mname => self.send(mname))
107
+ end
108
+ ) if self.class.validations.present?
99
109
 
100
- self.valid? and self.class.models.each do |mname, loader|
101
- self.check(mname => self.send(mname))
102
- end if self.class.models.present?
110
+ return (
111
+ self.valid? and self.class.models.each do |mname, loader|
112
+ self.check(mname => self.send(mname))
113
+ end
114
+ ) if self.class.models.present?
103
115
  end
104
116
 
105
117
  def persist
106
- self.valid? and UnitOfWork.transaction do |uow|
107
- self.class.transactions.each do |handler|
108
- handler.call(uow, self)
109
- end
110
- end
118
+ return nil if self.invalid?
119
+
120
+ self.class.transactions.each do |handler|
121
+ uow = UnitOfWork.new
122
+ handler.call(uow, self)
123
+ uow.commit
124
+ end if self.class.transactions.present?
111
125
  end
112
126
 
113
127
  def execute
114
128
  return (
115
- self.valid? and
116
- self.executed?(:clean) and
117
- self.executed?(:load) and
118
- self.executed?(:validate) and
119
- self.executed?(:persist)
129
+ self.valid? and self.executed?(:clean) and
130
+ self.valid? and self.executed?(:load) and
131
+ self.valid? and self.executed?(:validate) and
132
+ self.valid? and self.executed?(:persist)
120
133
  )
121
134
  end
122
135
 
123
136
  protected
124
- def chain(obj, invocations)
125
- return Chain.new(invocations).apply(obj)
126
- end
127
-
128
137
  def reqs(source, attributes)
129
138
  attributes.each do |attribute|
130
139
  next if not source[attribute].blank?
@@ -1,5 +1,5 @@
1
1
  class Hash
2
2
  def permit(*keys)
3
- self.slice(keys)
3
+ self.slice(*keys)
4
4
  end
5
5
  end
@@ -1,25 +1,30 @@
1
1
  module Legatus
2
2
  module Repository
3
3
  extend ActiveSupport::Concern
4
-
5
4
  class_methods do
6
5
  def find_and_init(filters, attributes=nil)
7
- instance = self.find_by(filters)
6
+ instance = self.find_one(filters)
8
7
  instance ||= self.new
9
8
 
10
- attributes ||= filters
9
+ attributes = filters if attributes.blank?
11
10
  instance.assign_attributes(attributes)
12
11
 
13
12
  return instance
14
13
  end
15
14
 
16
15
  def find_or_init(filters, attributes)
17
- instance = self.find_by(filters)
18
- return if instance.present?
16
+ instance = self.find_one(filters)
17
+ return instance if instance.present?
19
18
 
20
19
  instance = self.new
21
20
  instance.assign_attributes(attributes)
22
21
  end
22
+
23
+ def find_one(filters)
24
+ instance = filters.find { |f| self.find_by(f) } if filters.is_a?(Array)
25
+ instance ||= self.find_by(filters)
26
+ return instance
27
+ end
23
28
  end
24
29
  end
25
30
  end
@@ -1,36 +1,74 @@
1
1
  module Legatus
2
2
  class UnitOfWork
3
3
 
4
- def self.transaction
5
- ActiveRecord::Base.connection.transaction do
6
- yield(UnitOfWork.new)
7
- end
4
+ def initialize
5
+ @steps = []
8
6
  end
9
7
 
10
8
  def save(*models)
11
- models.all? { |model| model.save }
9
+ @steps << lambda do
10
+ models.all? { |model| self.execute(:save, model) }
11
+ end
12
12
  end
13
13
 
14
14
  def update(*models)
15
- models.all? { |model| model.update }
15
+ @steps << lambda do
16
+ models.all? { |model| self.execute(:update, model) }
17
+ end
16
18
  end
17
19
 
18
20
  def create(*models)
19
- models.all? { |model| model.create }
21
+ @steps << lambda do
22
+ models.all? { |model| self.execute(:create, model) }
23
+ end
20
24
  end
21
25
 
22
26
  def destroy(*models)
23
- models.all? { |model| model.destroy }
27
+ @steps << lambda do
28
+ models.all? { |model| self.execute(:destroy, model) }
29
+ end
24
30
  end
25
31
 
26
32
  def persist(*models)
27
- models.all? do |model|
28
- if model.marked_for_destruction?
29
- model.destroy
30
- else
31
- model.save
33
+ @steps << lambda do
34
+ models.all? do |model|
35
+ if model.is_a?(Array)
36
+ model.all? { |elem| self.save_or_destroy(elem) }
37
+ else
38
+ self.save_or_destroy(model)
39
+ end
32
40
  end
33
41
  end
34
42
  end
43
+
44
+ def denormalize(model, schema)
45
+ @steps << lambda do
46
+ schema.each do |field, subschema|
47
+ assoc = subschema.keys[0]
48
+ aggre = subschema.values[0].keys[0]
49
+ subfield = subschema.values[0].values[0]
50
+
51
+ model[field] = model.send(subschema.keys[0]).send(aggre, subfield)
52
+ end
53
+ model.save
54
+ end
55
+ end
56
+
57
+ def commit
58
+ ActiveRecord::Base.connection.transaction do
59
+ @steps.all? { |step| step.call }
60
+ end
61
+ end
62
+
63
+ protected
64
+ def execute(methodname, model)
65
+ return model.all? { |elem| elem.send(methodname) } if model.is_a?(Array)
66
+ return model.send(methodname)
67
+ end
68
+
69
+ def save_or_destroy(model)
70
+ return model.destroy if model.marked_for_destruction?
71
+ return model.save
72
+ end
35
73
  end
36
74
  end
@@ -1,3 +1,3 @@
1
1
  module Legatus
2
- VERSION = '0.1.0'
2
+ VERSION = '0.1.1'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legatus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - rcpedro
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-09-21 00:00:00.000000000 Z
11
+ date: 2018-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails