relevium 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +55 -0
- data/LICENSE +21 -0
- data/README.md +356 -0
- data/Rakefile +10 -0
- data/bitbucket-pipelines.yml +16 -0
- data/lib/relevium.rb +4 -0
- data/lib/relevium/background_service.rb +56 -0
- data/lib/relevium/form.rb +140 -0
- data/lib/relevium/service.rb +103 -0
- data/lib/relevium/version.rb +5 -0
- data/relevium.gemspec +33 -0
- metadata +126 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 44b99ef9b55ed25a5a759e50028e9a8277d07885c3a6326d44c07c39a93cadd3
|
4
|
+
data.tar.gz: 9890b23b1a7eb169031fa80c9dae4263a77caa84d3f26d95353e0bfedebf9c0f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ec1b431da475adea43c8ead05e35dda1134803666480b75ffcdc9744d4be9ef75418392550353074995f34eb51e9578bfa8f47d74184a0904f98c24598bb769a
|
7
|
+
data.tar.gz: 7c2f1f38cdea797feb9e9ea122dd955ff52986a96d0a1ffd891239806353a0355714afc0161cfc7aa17450362a959d3bffe88f7d5db47cfe922ece6db653189c
|
data/.gitignore
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
2
|
+
#
|
3
|
+
# If you find yourself ignoring temporary files generated by your text editor
|
4
|
+
# or operating system, you probably want to add a global ignore instead:
|
5
|
+
# git config --global core.excludesfile '~/.gitignore_global'
|
6
|
+
|
7
|
+
# Ignore bundler config.
|
8
|
+
/.bundle
|
9
|
+
|
10
|
+
# Ignore debugger history
|
11
|
+
.byebug_history
|
12
|
+
|
13
|
+
# Ignore IDE files
|
14
|
+
.idea
|
15
|
+
|
16
|
+
# Ignore local gems
|
17
|
+
*.gem
|
18
|
+
|
19
|
+
.DS_Store
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
relevium (0.0.1)
|
5
|
+
activemodel (>= 4.2.6)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activemodel (4.2.11.3)
|
11
|
+
activesupport (= 4.2.11.3)
|
12
|
+
builder (~> 3.1)
|
13
|
+
activesupport (4.2.11.3)
|
14
|
+
i18n (~> 0.7)
|
15
|
+
minitest (~> 5.1)
|
16
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
17
|
+
tzinfo (~> 1.1)
|
18
|
+
builder (3.2.4)
|
19
|
+
byebug (11.0.1)
|
20
|
+
concurrent-ruby (1.1.7)
|
21
|
+
diff-lcs (1.4.4)
|
22
|
+
i18n (0.9.5)
|
23
|
+
concurrent-ruby (~> 1.0)
|
24
|
+
minitest (5.14.1)
|
25
|
+
rake (10.5.0)
|
26
|
+
rspec (3.5.0)
|
27
|
+
rspec-core (~> 3.5.0)
|
28
|
+
rspec-expectations (~> 3.5.0)
|
29
|
+
rspec-mocks (~> 3.5.0)
|
30
|
+
rspec-core (3.5.4)
|
31
|
+
rspec-support (~> 3.5.0)
|
32
|
+
rspec-expectations (3.5.0)
|
33
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
34
|
+
rspec-support (~> 3.5.0)
|
35
|
+
rspec-mocks (3.5.0)
|
36
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
37
|
+
rspec-support (~> 3.5.0)
|
38
|
+
rspec-support (3.5.0)
|
39
|
+
thread_safe (0.3.6)
|
40
|
+
tzinfo (1.2.7)
|
41
|
+
thread_safe (~> 0.1)
|
42
|
+
|
43
|
+
PLATFORMS
|
44
|
+
ruby
|
45
|
+
|
46
|
+
DEPENDENCIES
|
47
|
+
activemodel (>= 4.2.6)
|
48
|
+
bundler (>= 1.15)
|
49
|
+
byebug (>= 11.0.1)
|
50
|
+
rake (>= 10.0)
|
51
|
+
relevium!
|
52
|
+
rspec (>= 3.5.0)
|
53
|
+
|
54
|
+
BUNDLED WITH
|
55
|
+
1.17.3
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2020 Kirill Bazeltsev
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,356 @@
|
|
1
|
+
# Relevium
|
2
|
+

|
3
|
+
|
4
|
+
#### Simple ruby gem for rails projects with some useful patterns that would help you build more scalable apps
|
5
|
+
|
6
|
+
## Table of contents <a name='table-of-contents'></a>
|
7
|
+
1. [Service object](#service-object)
|
8
|
+
1. [Listeners](#listeners)
|
9
|
+
2. [Background service](#background-service)
|
10
|
+
3. [Form object](#form)
|
11
|
+
1. [Serialization with forms](#form)
|
12
|
+
|
13
|
+
|
14
|
+
## Service object <a name='service-object'></a>
|
15
|
+
[To the start](#table-of-contents)
|
16
|
+
|
17
|
+
Interface for running services - a place to move your business logic.
|
18
|
+
|
19
|
+
Example:
|
20
|
+
```ruby
|
21
|
+
class TestService < Relevium::Service
|
22
|
+
attr_reader :user_id
|
23
|
+
|
24
|
+
def initialize(user_id)
|
25
|
+
@user_id = user_id
|
26
|
+
end
|
27
|
+
|
28
|
+
def call
|
29
|
+
return broadcast(:fail) unless user_valid?
|
30
|
+
|
31
|
+
do_some_stuff
|
32
|
+
book = user.books.last
|
33
|
+
user.delete
|
34
|
+
broadcast(:ok, book)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def user_valid?
|
40
|
+
user.name == 'Kyle'
|
41
|
+
end
|
42
|
+
|
43
|
+
def do_some_stuff
|
44
|
+
#..more_code_here..
|
45
|
+
end
|
46
|
+
|
47
|
+
def user
|
48
|
+
@user ||= User.find_by(id: user_id)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
Results:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
@user = User.last # Name is 'Kyle'
|
57
|
+
|
58
|
+
TestService.call(@user.id)
|
59
|
+
# Will delete user and broadcast method won't take affect
|
60
|
+
|
61
|
+
TestService.call(@user.id) do |obj|
|
62
|
+
obj.on(:fail) { raise 'Some error' } # Listener for fail broadcast
|
63
|
+
obj.on(:ok) { |book| @book = book } # Listener for ok broadcast
|
64
|
+
end
|
65
|
+
# User will be deleted and @book variable will be assigned
|
66
|
+
|
67
|
+
@user.update(name: 'Stan')
|
68
|
+
TestService.call(@user.id) do |obj|
|
69
|
+
obj.on(:fail) { raise 'Some error' }
|
70
|
+
obj.on(:ok) { |book| @book = book }
|
71
|
+
end
|
72
|
+
# User won't be deleted and Some error' will be raised
|
73
|
+
```
|
74
|
+
|
75
|
+
### Adding listeners <a name='listeners'></a>
|
76
|
+
[To the start](#table-of-contents)
|
77
|
+
|
78
|
+
Example:
|
79
|
+
```ruby
|
80
|
+
class TestListener < Relevium::Service
|
81
|
+
attr_reader :email
|
82
|
+
|
83
|
+
def initialize(email)
|
84
|
+
@email = email
|
85
|
+
end
|
86
|
+
|
87
|
+
def call
|
88
|
+
unsubscribe_from_sendgrid
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def unsubscribe_from_sendgrid
|
94
|
+
# your code
|
95
|
+
end
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
class TestService < Relevium::Service
|
101
|
+
set_listener TestListener, :ok
|
102
|
+
attr_reader :user_id
|
103
|
+
|
104
|
+
def initialize(user_id)
|
105
|
+
@user_id = user_id
|
106
|
+
end
|
107
|
+
|
108
|
+
def call
|
109
|
+
do_some_stuff
|
110
|
+
user.delete
|
111
|
+
user.persisted? ? broadcast(:ok, user.email) : broadcast(:fail)
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def do_some_stuff
|
117
|
+
#..more_code_here..
|
118
|
+
end
|
119
|
+
|
120
|
+
def user
|
121
|
+
@user ||= User.find_by(id: user_id)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
```
|
125
|
+
|
126
|
+
Results:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
@user = User.last
|
130
|
+
|
131
|
+
TestService.call(@user.id)
|
132
|
+
# Will delete user and broadcast method won't take affect.
|
133
|
+
# However listener's `call` function will be called
|
134
|
+
# with `email` passed as the argument to initialize function.
|
135
|
+
```
|
136
|
+
---
|
137
|
+
Specify which function from listener to call:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
class TestListener < Relevium::Service
|
141
|
+
def initialize(arg)
|
142
|
+
@arg = arg
|
143
|
+
end
|
144
|
+
|
145
|
+
def on_ok
|
146
|
+
# ok code
|
147
|
+
end
|
148
|
+
|
149
|
+
def on_fail
|
150
|
+
# fail code
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
set_listener TestListener, :ok, function: :on_ok
|
155
|
+
set_listener TestListener, :fail, function: :on_fail
|
156
|
+
```
|
157
|
+
---
|
158
|
+
Specify arguments that should be passed to the listener:
|
159
|
+
```ruby
|
160
|
+
class TestListener < Relevium::Service
|
161
|
+
attr_reader :arg
|
162
|
+
|
163
|
+
def initialize(arg)
|
164
|
+
@arg = arg
|
165
|
+
end
|
166
|
+
|
167
|
+
def call
|
168
|
+
puts arg
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
class TestService < Relevium::Service
|
173
|
+
set_listener TestListener, :ok, args: :user_id
|
174
|
+
|
175
|
+
def initialize(user_id)
|
176
|
+
@user_id = user_id
|
177
|
+
end
|
178
|
+
|
179
|
+
def call
|
180
|
+
# some code
|
181
|
+
broadcast(:ok, 'test')
|
182
|
+
end
|
183
|
+
end
|
184
|
+
```
|
185
|
+
|
186
|
+
Result:
|
187
|
+
```ruby
|
188
|
+
TestService.call(1) do |service|
|
189
|
+
service.on(:ok) { |argument| puts argument }
|
190
|
+
end
|
191
|
+
|
192
|
+
# Output:
|
193
|
+
# test
|
194
|
+
# 1
|
195
|
+
```
|
196
|
+
---
|
197
|
+
Set up condition to call listener:
|
198
|
+
```ruby
|
199
|
+
class TestService < Relevium::Service
|
200
|
+
set_listener TestListener, :ok, if: Proc.new { |service| !service.user.persisted? }
|
201
|
+
|
202
|
+
def initialize(user_id)
|
203
|
+
@user = User.find(user_id)
|
204
|
+
end
|
205
|
+
|
206
|
+
def call
|
207
|
+
@user.delete
|
208
|
+
broadcast(:ok)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
```
|
212
|
+
|
213
|
+
Results:
|
214
|
+
```ruby
|
215
|
+
TestService.call(User.last.id)
|
216
|
+
# Listener would be trigger only if user was deleted.
|
217
|
+
```
|
218
|
+
|
219
|
+
## Background service <a name='background-service'></a>
|
220
|
+
[To the start](#table-of-contents)
|
221
|
+
|
222
|
+
Simple interface for running background jobs as services.
|
223
|
+
Meant to be inherited like this:
|
224
|
+
```ruby
|
225
|
+
class SidekiqService < Relevium::BackgroundService
|
226
|
+
def initialize(options)
|
227
|
+
super(options, ServiceObjectWorker)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
```
|
231
|
+
Needs a sidekiq worker in order to work, that should look like this:
|
232
|
+
```ruby
|
233
|
+
class ServiceObjectWorker
|
234
|
+
include Sidekiq::Worker
|
235
|
+
sidekiq_options queue: :default, retry: true
|
236
|
+
|
237
|
+
def perform(klass, *args)
|
238
|
+
klass.constantize.call(args)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
```
|
243
|
+
|
244
|
+
Now you can create a service like this:
|
245
|
+
```ruby
|
246
|
+
class SimpleService < SidekiqService
|
247
|
+
attr_reader :foo
|
248
|
+
|
249
|
+
def initialize(foo, options = {})
|
250
|
+
super(options)
|
251
|
+
@foo = foo
|
252
|
+
end
|
253
|
+
|
254
|
+
private
|
255
|
+
|
256
|
+
def perform
|
257
|
+
puts foo
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
```
|
262
|
+
|
263
|
+
And invoke it like this:
|
264
|
+
```ruby
|
265
|
+
SimpleService.call('foo', background: true, perform_in: 15.minutes)
|
266
|
+
# Will set sidekiq worker to be performd in 15 minutes,
|
267
|
+
# that will print 'foo' in sidekiq console
|
268
|
+
```
|
269
|
+
Note that listeners won't work when service is being called as background job.
|
270
|
+
|
271
|
+
## Form object <a name='form'></a>
|
272
|
+
[To the start](#table-of-contents)
|
273
|
+
|
274
|
+
Form object is used to move validations from models. Usually used when similar model needs different validations on
|
275
|
+
different forms. Can be used to build attributes for model to save.
|
276
|
+
|
277
|
+
Example:
|
278
|
+
```ruby
|
279
|
+
class UserForm < Relevium::Form
|
280
|
+
|
281
|
+
attribute :user_id, Integer
|
282
|
+
attribute :user_name, String
|
283
|
+
attribute :sibling_name, String, remove_from_hash: true
|
284
|
+
|
285
|
+
include_in_hash :sibling_id
|
286
|
+
|
287
|
+
validates :user_id, :user_name, presence: true
|
288
|
+
validate :is_kyle
|
289
|
+
|
290
|
+
private
|
291
|
+
|
292
|
+
def is_kyle
|
293
|
+
return true if user_name == 'Kyle'
|
294
|
+
|
295
|
+
errors.add(:user_name, 'should be Kyle')
|
296
|
+
end
|
297
|
+
|
298
|
+
def sibling_id
|
299
|
+
@sibling_id ||= User.find_by(name: sibling_name)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
```
|
303
|
+
Results:
|
304
|
+
```ruby
|
305
|
+
user_params = {user_id: 1, user_name: 'Kyle', sibling_name: 'John'}
|
306
|
+
form = UserForm.new(user_params)
|
307
|
+
form.valid? # true
|
308
|
+
form = UserForm.new(user_params.merge(user_name: 'Steve'))
|
309
|
+
form.valid? # false
|
310
|
+
form.errors.full_messages # ["User name should be Kyle"]
|
311
|
+
form = UserForm.new(user_params.merge(user_id: 'test'))
|
312
|
+
form.valid? # false
|
313
|
+
form.errors.full_messages # ["User can't be blank"]
|
314
|
+
form = UserForm.new(user_params.merge(user_id: '1')) # Will convert user_id into Integer
|
315
|
+
form.to_h # { user_id: 1, user_name: 'Kyle', sibling_id: 12 }
|
316
|
+
|
317
|
+
form.set(user_name, 'Stan')
|
318
|
+
form.set_attributes(user_id: 2, sibling_name: 'Ken')
|
319
|
+
form.to_h # { user_id: 2, user_name: 'Stan', sibling_name: 'Ken' }
|
320
|
+
```
|
321
|
+
---
|
322
|
+
### Serialization with forms <a name='serialization'></a>
|
323
|
+
[To the start](#table-of-contents)
|
324
|
+
|
325
|
+
```ruby
|
326
|
+
class UserForm < Relevium::Form
|
327
|
+
attribute :available_cash, Float
|
328
|
+
|
329
|
+
serialize_attributes :user_id, :sibling_id, :available_cash, :user_full_name
|
330
|
+
|
331
|
+
def user_full_name
|
332
|
+
first_name + ' ' + last_name
|
333
|
+
end
|
334
|
+
end
|
335
|
+
```
|
336
|
+
|
337
|
+
Now you can use this form to serialize active records to hash:
|
338
|
+
```ruby
|
339
|
+
ap User
|
340
|
+
# User < ActiveRecord::Base {
|
341
|
+
# :user_id => :integer,
|
342
|
+
# :available_cash => :string,
|
343
|
+
# :sibling_id => :integer,
|
344
|
+
# :first_name => :string,
|
345
|
+
# :last_name => :string
|
346
|
+
# }
|
347
|
+
UserForm.from_model(User.last).serialize
|
348
|
+
# Output:
|
349
|
+
# { user_id: 1, sibling_id: 2, available_cash: 123.45, user_full_name: 'Ken Stevenson' }
|
350
|
+
```
|
351
|
+
Also you can serialize active record collection or array of active records:
|
352
|
+
```ruby
|
353
|
+
UserForm.serialize_relation(User.where(id: (1..15)))
|
354
|
+
UserForm.serialize_relation(User.last(3).to_a)
|
355
|
+
```
|
356
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# Template Ruby build
|
2
|
+
|
3
|
+
# This template allows you to validate your Ruby code.
|
4
|
+
# The workflow allows running tests and code linting on the default branch.
|
5
|
+
|
6
|
+
image: ruby:2.7
|
7
|
+
|
8
|
+
pipelines:
|
9
|
+
default:
|
10
|
+
- parallel:
|
11
|
+
- step:
|
12
|
+
name: Run Rspec
|
13
|
+
script:
|
14
|
+
- gem install bundler -v 1.17.2
|
15
|
+
- bundle install
|
16
|
+
- bundle exec rspec
|
data/lib/relevium.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'relevium/service'
|
4
|
+
|
5
|
+
module Relevium
|
6
|
+
class BackgroundService < Service
|
7
|
+
attr_reader :options, :worker_class
|
8
|
+
|
9
|
+
def initialize(options, worker_class)
|
10
|
+
@options = options
|
11
|
+
@worker_class = worker_class
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
return broadcast(:fail) unless valid?
|
16
|
+
|
17
|
+
background? ? setup_worker : perform
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def perform
|
23
|
+
NoMethodError
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.worker_attributes(*attributes)
|
27
|
+
@worker_attributes_array = attributes
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.worker_attributes_array
|
31
|
+
@worker_attributes_array ||= []
|
32
|
+
end
|
33
|
+
|
34
|
+
def fetch_worker_attributes
|
35
|
+
self.class.worker_attributes_array.map do |worker_attribute|
|
36
|
+
instance_variable_get("@#{worker_attribute}")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def setup_worker
|
41
|
+
worker_class.perform_in(delay, self.class, *fetch_worker_attributes)
|
42
|
+
end
|
43
|
+
|
44
|
+
def delay
|
45
|
+
options[:perform_in] || 5.minutes
|
46
|
+
end
|
47
|
+
|
48
|
+
def background?
|
49
|
+
!options[:background].nil? && options[:background]
|
50
|
+
end
|
51
|
+
|
52
|
+
def valid?
|
53
|
+
true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require 'active_model'
|
5
|
+
|
6
|
+
module Relevium
|
7
|
+
class Form
|
8
|
+
include ActiveModel::Model
|
9
|
+
|
10
|
+
def initialize(hash)
|
11
|
+
set_attributes(hash)
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.from_model(model)
|
16
|
+
new(model.attributes)
|
17
|
+
rescue StandardError => _e
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_h
|
22
|
+
hash = {}
|
23
|
+
self.instance_variables.each do |var|
|
24
|
+
var_name = var.to_s.delete('@')
|
25
|
+
attribute = self.class.attributes.find { |attr| attr.attribute_name == var_name.to_sym }
|
26
|
+
hash[var_name] = instance_variable_get(var) if attribute&.remove_from_hash == false
|
27
|
+
end
|
28
|
+
self.class.methods_for_hash.each do |method|
|
29
|
+
hash[method] = self.send(method)
|
30
|
+
end
|
31
|
+
hash.default_proc = proc { |h, k| h.key?(k.to_s) ? h[k.to_s] : nil }
|
32
|
+
hash
|
33
|
+
end
|
34
|
+
|
35
|
+
def serialize
|
36
|
+
hash = {}
|
37
|
+
self.class.attributes_to_serialize.each do |attribute|
|
38
|
+
hash[attribute] = self.send(attribute) rescue instance_variable_get("@#{attribute}")
|
39
|
+
end
|
40
|
+
hash.default_proc = proc { |h, k| h.key?(k.to_s) ? h[k.to_s] : nil }
|
41
|
+
hash
|
42
|
+
end
|
43
|
+
|
44
|
+
def set(name, value)
|
45
|
+
attribute = self.class.attributes.find { |attr| attr.attribute_name.to_s == name.to_s }
|
46
|
+
if attribute.present?
|
47
|
+
set_attribute(attribute, value)
|
48
|
+
else
|
49
|
+
instance_variable_set("@#{name}", value)
|
50
|
+
end
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def set_attributes(hash)
|
55
|
+
hash.to_h.to_a.each do |attr_array|
|
56
|
+
set(attr_array[0], attr_array[1])
|
57
|
+
end
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
def errors_to_string
|
62
|
+
errors.full_messages.to_sentence
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.i18n_scope
|
66
|
+
:activerecord
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def set_attribute(attribute, value)
|
72
|
+
instance_variable_set(attribute.name_to_instance_variable, attribute.normalized_value(value))
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.attribute(attribute, type = nil, remove_from_hash: false)
|
76
|
+
attributes << Attribute.new(attribute, type, remove_from_hash)
|
77
|
+
self.class_eval { attr_reader attribute }
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.attributes
|
81
|
+
@attributes ||= Set.new
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.include_in_hash(*methods)
|
85
|
+
@methods_for_hash = methods
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.methods_for_hash
|
89
|
+
@methods_for_hash ||= []
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.serialize_attributes(*attributes)
|
93
|
+
@attributes_to_serialize = attributes
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.attributes_to_serialize
|
97
|
+
@attributes_to_serialize ||= []
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.serialize_relation(relation)
|
101
|
+
relation.map { |model| from_model(model).serialize }
|
102
|
+
end
|
103
|
+
|
104
|
+
class Boolean; end
|
105
|
+
end
|
106
|
+
|
107
|
+
class Attribute
|
108
|
+
DATE_TYPES = [Date, Time, DateTime].freeze
|
109
|
+
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].freeze
|
110
|
+
|
111
|
+
attr_reader :attribute_name, :type, :remove_from_hash
|
112
|
+
|
113
|
+
def initialize(attribute_name, type = nil, remove_from_hash = false)
|
114
|
+
@attribute_name = attribute_name
|
115
|
+
@type = type
|
116
|
+
@remove_from_hash = remove_from_hash
|
117
|
+
end
|
118
|
+
|
119
|
+
def normalized_value(value)
|
120
|
+
return nil if value.nil?
|
121
|
+
return value unless type
|
122
|
+
return value if value.is_a?(type)
|
123
|
+
|
124
|
+
cast_value_to_type(value)
|
125
|
+
rescue ArgumentError => _e
|
126
|
+
nil
|
127
|
+
end
|
128
|
+
|
129
|
+
def cast_value_to_type(value)
|
130
|
+
return TRUE_VALUES.include?(value) if type == ::Relevium::Form::Boolean
|
131
|
+
return method(type.to_s).call(value) unless DATE_TYPES.include?(type)
|
132
|
+
|
133
|
+
value.send("to_#{type.to_s.underscore}")
|
134
|
+
end
|
135
|
+
|
136
|
+
def name_to_instance_variable
|
137
|
+
"@#{attribute_name}"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'byebug'
|
4
|
+
|
5
|
+
module Relevium
|
6
|
+
class Service
|
7
|
+
def self.call(*args)
|
8
|
+
obj = new(*args)
|
9
|
+
yield obj if block_given?
|
10
|
+
obj.call
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(*args); end
|
14
|
+
|
15
|
+
def on(name, &block)
|
16
|
+
local_registrations << BlockRegistration.new(name, block)
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def broadcast(name, *args)
|
23
|
+
set_off_local_listeners(name, *args)
|
24
|
+
set_off_global_listeners(name, *args)
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_off_local_listeners(name, *args)
|
29
|
+
local_registrations.select { |registration| registration.listener == name }.first&.broadcast(*args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def set_off_global_listeners(name, *args)
|
33
|
+
select_proc = Proc.new { |registration| registration.message == name }
|
34
|
+
registrations = self.class.global_registrations.select(&select_proc)
|
35
|
+
registrations += self.class.superclass&.global_registrations&.select(&select_proc)
|
36
|
+
registrations.each do |registration|
|
37
|
+
next unless registration.condition.nil? || registration.condition.call(self)
|
38
|
+
|
39
|
+
registration.args.nil? ? registration.broadcast(*args) : registration.broadcast(*get_args(*registration.args))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_args(*args)
|
44
|
+
args.map { |arg| instance_variable_get("@#{arg}") }
|
45
|
+
end
|
46
|
+
|
47
|
+
def transaction(&block)
|
48
|
+
ActiveRecord::Base.transaction(&block) if block_given?
|
49
|
+
end
|
50
|
+
|
51
|
+
def local_registrations
|
52
|
+
@local_registrations ||= Set.new
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.set_listener(klass, message, options = {})
|
56
|
+
global_registrations << GlobalRegistration.new(klass, message, options, self)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.global_registrations
|
60
|
+
@global_registrations ||= Set.new
|
61
|
+
end
|
62
|
+
|
63
|
+
def call
|
64
|
+
raise NoMethodError
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class BlockRegistration
|
69
|
+
attr_reader :listener, :message
|
70
|
+
|
71
|
+
def initialize(listener, message)
|
72
|
+
@listener = listener
|
73
|
+
@message = message
|
74
|
+
end
|
75
|
+
|
76
|
+
def broadcast(*args)
|
77
|
+
message.call(*args)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class GlobalRegistration < BlockRegistration
|
82
|
+
attr_reader :function, :condition, :args, :whisperer
|
83
|
+
|
84
|
+
def initialize(listener, message, options = {}, whisperer = nil)
|
85
|
+
super(listener, message)
|
86
|
+
@function = options[:function] || 'call'
|
87
|
+
@condition = options[:if]
|
88
|
+
@args = options[:args]
|
89
|
+
@whisperer = whisperer
|
90
|
+
end
|
91
|
+
|
92
|
+
def broadcast(*args)
|
93
|
+
listener_obj = listener.new(*args)
|
94
|
+
listener_obj.instance_variable_set('@whisperer', whisperer)
|
95
|
+
listener_obj.send(function)
|
96
|
+
end
|
97
|
+
|
98
|
+
def validate!
|
99
|
+
raise 'Invalid function name for listener' unless [String, Symbol].include?(function.class)
|
100
|
+
raise 'Invalid condition for listener' unless condition.is_a?(Proc)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/relevium.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "relevium/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "relevium"
|
8
|
+
spec.version = Relevium::VERSION
|
9
|
+
spec.authors = ['Bazeltsev Kirill']
|
10
|
+
spec.email = ['kirill.bazeltsev@flender.ie']
|
11
|
+
|
12
|
+
spec.summary = 'A Ruby gem, that helps keep models and controllers thin'
|
13
|
+
spec.description = ''
|
14
|
+
spec.homepage = ''
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.add_dependency 'activemodel', '>= 4.2.6'
|
18
|
+
|
19
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
20
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
21
|
+
|
22
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
23
|
+
f.match(%r{^(test|spec|features)/})
|
24
|
+
end
|
25
|
+
spec.bindir = "exe"
|
26
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
|
+
spec.require_paths = ["lib"]
|
28
|
+
|
29
|
+
spec.add_development_dependency "bundler", ">= 1.15"
|
30
|
+
spec.add_development_dependency "rake", ">= 10.0"
|
31
|
+
spec.add_development_dependency "rspec", ">= 3.5.0"
|
32
|
+
spec.add_development_dependency "byebug", '>= 11.0.1'
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: relevium
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bazeltsev Kirill
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-02-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activemodel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.2.6
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.2.6
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.15'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.15'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.5.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 3.5.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 11.0.1
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 11.0.1
|
83
|
+
description: ''
|
84
|
+
email:
|
85
|
+
- kirill.bazeltsev@flender.ie
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- Gemfile
|
92
|
+
- Gemfile.lock
|
93
|
+
- LICENSE
|
94
|
+
- README.md
|
95
|
+
- Rakefile
|
96
|
+
- bitbucket-pipelines.yml
|
97
|
+
- lib/relevium.rb
|
98
|
+
- lib/relevium/background_service.rb
|
99
|
+
- lib/relevium/form.rb
|
100
|
+
- lib/relevium/service.rb
|
101
|
+
- lib/relevium/version.rb
|
102
|
+
- relevium.gemspec
|
103
|
+
homepage: ''
|
104
|
+
licenses:
|
105
|
+
- MIT
|
106
|
+
metadata: {}
|
107
|
+
post_install_message:
|
108
|
+
rdoc_options: []
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
requirements: []
|
122
|
+
rubygems_version: 3.0.8
|
123
|
+
signing_key:
|
124
|
+
specification_version: 4
|
125
|
+
summary: A Ruby gem, that helps keep models and controllers thin
|
126
|
+
test_files: []
|