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 +4 -4
- data/README.md +266 -2
- data/lib/legatus/directive.rb +40 -31
- data/lib/legatus/extensions/hash.rb +1 -1
- data/lib/legatus/repository.rb +10 -5
- data/lib/legatus/unit_of_work.rb +51 -13
- data/lib/legatus/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 54c62e8d695d9c99e975d2170a5355fbe6f4ba9da844375ca7d485456db900db
|
4
|
+
data.tar.gz: b9656081c257e65998f8eff41592fea7a13c56293b62c1f4f2089f468926e71e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f675a9c344bd4908d683ee8a07bcaaa1dbaa9c238b704677ed5c21b6f1ad4c6fcb71047941ded5090971966bc050efd7253c0cc98c58f9720e519ee9b464b0ab
|
7
|
+
data.tar.gz: b46a115930c206c238147ce8309f46dc330343db1ed6ffd5aa9ea2c88de9b090f2822d80d39b288a6723181ab58087f71d986feec30887f760e678fd21b72148
|
data/README.md
CHANGED
@@ -1,8 +1,272 @@
|
|
1
1
|
# Legatus
|
2
|
-
|
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
|
-
|
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:
|
data/lib/legatus/directive.rb
CHANGED
@@ -21,17 +21,25 @@ module Legatus
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
def
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
97
|
-
self.
|
98
|
-
|
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
|
-
|
101
|
-
self.
|
102
|
-
|
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.
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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?(:
|
117
|
-
self.executed?(:
|
118
|
-
self.executed?(:
|
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?
|
data/lib/legatus/repository.rb
CHANGED
@@ -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.
|
6
|
+
instance = self.find_one(filters)
|
8
7
|
instance ||= self.new
|
9
8
|
|
10
|
-
attributes
|
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.
|
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
|
data/lib/legatus/unit_of_work.rb
CHANGED
@@ -1,36 +1,74 @@
|
|
1
1
|
module Legatus
|
2
2
|
class UnitOfWork
|
3
3
|
|
4
|
-
def
|
5
|
-
|
6
|
-
yield(UnitOfWork.new)
|
7
|
-
end
|
4
|
+
def initialize
|
5
|
+
@steps = []
|
8
6
|
end
|
9
7
|
|
10
8
|
def save(*models)
|
11
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
28
|
-
|
29
|
-
model.
|
30
|
-
|
31
|
-
|
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
|
data/lib/legatus/version.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2018-10-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|