readymade 0.4.2 → 0.4.4
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 +4 -4
- data/.gitignore +3 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile.lock +10 -10
- data/README.md +69 -3
- data/lib/generators/readymade/install_generator.rb +14 -0
- data/lib/generators/readymade/templates/config/initializers/readymade.rb +23 -0
- data/lib/readymade/background_bang_job.rb +7 -3
- data/lib/readymade/background_job.rb +42 -21
- data/lib/readymade/version.rb +1 -1
- data/lib/readymade.rb +52 -1
- metadata +4 -3
- data/lib/.DS_Store +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b3d5fedc87f2028115618494f4b1e4af9cb79fe9f82f7059bb9bb4d1705b3da
|
|
4
|
+
data.tar.gz: 841cbdc4364c34a52706011db59158a041a5edd575db5083133b1eb2faffb6ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6251e1a6304385b8bdd5d421d300c4d92ff24d21d374b18430938f6ebbf65c59cef58d3709da984b3f3e71945cb13fc6e6cedd0fa7235083d70e20dfb6cef095
|
|
7
|
+
data.tar.gz: 2bbdb741c5b1bef74ad7e56efb946cf24675b25a17490fcee416ec7f49bd8ee4fdf3f601f9d44d345f1ad55f452f929114ff2df2d80aa34cfe299e162dab6cdb
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
All notable changes to this project will be documented in this file.
|
|
3
3
|
|
|
4
|
+
|
|
5
|
+
## [0.4.4] - 2025-09-22
|
|
6
|
+
|
|
7
|
+
* Add initializer generator
|
|
8
|
+
* Add `ActiveJob::Uniqueness` support
|
|
9
|
+
|
|
10
|
+
## [0.4.3] - 2025-07-31
|
|
11
|
+
|
|
12
|
+
* Remove job_options.discard_on` argument to `Readymade::BackgroundJob` to discard job on specific exceptions due to incorrect behaviour on different Rails versions
|
|
13
|
+
|
|
4
14
|
## [0.4.2] - 2025-05-26
|
|
5
15
|
|
|
6
16
|
* Add `job_options.discard_on` argument to `Readymade::BackgroundJob` to discard job on specific exceptions
|
data/Gemfile.lock
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
readymade (0.4.
|
|
4
|
+
readymade (0.4.4)
|
|
5
5
|
activejob
|
|
6
6
|
activemodel
|
|
7
7
|
|
|
8
8
|
GEM
|
|
9
9
|
remote: https://rubygems.org/
|
|
10
10
|
specs:
|
|
11
|
-
activejob (8.0.2)
|
|
12
|
-
activesupport (= 8.0.2)
|
|
11
|
+
activejob (8.0.2.1)
|
|
12
|
+
activesupport (= 8.0.2.1)
|
|
13
13
|
globalid (>= 0.3.6)
|
|
14
|
-
activemodel (8.0.2)
|
|
15
|
-
activesupport (= 8.0.2)
|
|
16
|
-
activesupport (8.0.2)
|
|
14
|
+
activemodel (8.0.2.1)
|
|
15
|
+
activesupport (= 8.0.2.1)
|
|
16
|
+
activesupport (8.0.2.1)
|
|
17
17
|
base64
|
|
18
18
|
benchmark (>= 0.3)
|
|
19
19
|
bigdecimal
|
|
@@ -26,12 +26,12 @@ GEM
|
|
|
26
26
|
securerandom (>= 0.3)
|
|
27
27
|
tzinfo (~> 2.0, >= 2.0.5)
|
|
28
28
|
uri (>= 0.13.1)
|
|
29
|
-
base64 (0.
|
|
30
|
-
benchmark (0.4.
|
|
31
|
-
bigdecimal (3.
|
|
29
|
+
base64 (0.3.0)
|
|
30
|
+
benchmark (0.4.1)
|
|
31
|
+
bigdecimal (3.2.3)
|
|
32
32
|
byebug (11.1.3)
|
|
33
33
|
concurrent-ruby (1.3.5)
|
|
34
|
-
connection_pool (2.5.
|
|
34
|
+
connection_pool (2.5.4)
|
|
35
35
|
diff-lcs (1.4.4)
|
|
36
36
|
drb (2.2.3)
|
|
37
37
|
globalid (1.2.1)
|
data/README.md
CHANGED
|
@@ -6,6 +6,7 @@ This gems contains basic components to follow [ABDI architecture](https://github
|
|
|
6
6
|
|
|
7
7
|
### Tested with ruby:
|
|
8
8
|
|
|
9
|
+
- 3.3
|
|
9
10
|
- 3.1
|
|
10
11
|
- 3.0
|
|
11
12
|
- 2.7
|
|
@@ -35,6 +36,8 @@ Inherit your components from:
|
|
|
35
36
|
* `Readymade::Action`
|
|
36
37
|
* `Readymade::Operation`
|
|
37
38
|
|
|
39
|
+
---
|
|
40
|
+
|
|
38
41
|
### Readymade::Response
|
|
39
42
|
|
|
40
43
|
```ruby
|
|
@@ -47,6 +50,8 @@ response.status # 'fail'
|
|
|
47
50
|
response.data # { errors: { some: 'errors' } }
|
|
48
51
|
```
|
|
49
52
|
|
|
53
|
+
---
|
|
54
|
+
|
|
50
55
|
### Readymade::Form
|
|
51
56
|
|
|
52
57
|
Check more form features examples in `lib/readymade/form.rb`
|
|
@@ -73,7 +78,7 @@ order_form.valid? # true
|
|
|
73
78
|
class MyForm < Readymade::Form
|
|
74
79
|
PERMITTED_ATTRIBUTES = %i[email name category country]
|
|
75
80
|
REQUIRED_ATTRIBUTES = %i[email]
|
|
76
|
-
|
|
81
|
+
|
|
77
82
|
def form_options
|
|
78
83
|
{
|
|
79
84
|
categories: args[:company].categories,
|
|
@@ -100,6 +105,8 @@ end
|
|
|
100
105
|
= f.text_field :name, required: @form.required?(:name) # false
|
|
101
106
|
```
|
|
102
107
|
|
|
108
|
+
---
|
|
109
|
+
|
|
103
110
|
### Readymade::InstantForm
|
|
104
111
|
|
|
105
112
|
Permit params and validates presence inline
|
|
@@ -108,6 +115,8 @@ Permit params and validates presence inline
|
|
|
108
115
|
Readymade::InstantForm.new(my_params, permitted: %i[name phone], required: %i[email]) # permits: name, phone, email; validates on presence: email
|
|
109
116
|
```
|
|
110
117
|
|
|
118
|
+
---
|
|
119
|
+
|
|
111
120
|
### Readymade::Action
|
|
112
121
|
|
|
113
122
|
```ruby
|
|
@@ -146,7 +155,7 @@ class Orders::Actions::SendNotifications < Readymade::Action
|
|
|
146
155
|
end
|
|
147
156
|
|
|
148
157
|
Orders::Actions::SendNotifications.call_async(order: order)
|
|
149
|
-
Orders::Actions::SendNotifications.call_async!(order: order, job_options: { queue_as: :my_queue
|
|
158
|
+
Orders::Actions::SendNotifications.call_async!(order: order, job_options: { queue_as: :my_queue })
|
|
150
159
|
```
|
|
151
160
|
|
|
152
161
|
```ruby
|
|
@@ -210,6 +219,53 @@ Orders::Actions::SendNotifications.call_async!(order: order) # job will be faile
|
|
|
210
219
|
|
|
211
220
|
```
|
|
212
221
|
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### Locking Duplicate Jobs (triggered by `call_async`)
|
|
226
|
+
|
|
227
|
+
To enable job locking, first generate the initializer:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
rails generate readymade:install
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Add `gem 'activejob-uniqueness', '~> 0.4.0'` to your Gemfile. (not included by default to reduce dependencies)
|
|
234
|
+
|
|
235
|
+
Then follow the instructions inside the generated file.
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
#### Default behavior
|
|
240
|
+
|
|
241
|
+
* All `call_async` actions run in the `:default` queue.
|
|
242
|
+
* By default, you can configure Readymade to **lock all jobs in `:default`** so that duplicate jobs won’t be enqueued.
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
#### Customizing which jobs are locked
|
|
246
|
+
|
|
247
|
+
You have two main strategies:
|
|
248
|
+
|
|
249
|
+
1. **Use `job_options` in your call**
|
|
250
|
+
If you don’t want a job to be locked, run it in a different queue:
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
MyAction.call_async(user_id: 42, job_options: { queue_as: :critical })
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
2. **Configure specific queues to lock**
|
|
257
|
+
In your initializer, define which queues should enforce locking:
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
Readymade.configure do |config|
|
|
261
|
+
config.lock_jobs = true
|
|
262
|
+
config.locked_queues = [:mailers, :anatytics] # only these queues will lock duplicates
|
|
263
|
+
end
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
|
|
213
269
|
### Readymade::Operation
|
|
214
270
|
|
|
215
271
|
Provides set of help methods like: `build_form`, `form_valid?`, `validation_fail`, `save_record`, etc.
|
|
@@ -230,6 +286,8 @@ class Orders::Operations::Create < Readymade::Operation
|
|
|
230
286
|
end
|
|
231
287
|
```
|
|
232
288
|
|
|
289
|
+
---
|
|
290
|
+
|
|
233
291
|
### Readymade::Controller::Serialization
|
|
234
292
|
|
|
235
293
|
```ruby
|
|
@@ -244,6 +302,8 @@ Dependencies that must be installed on your own:
|
|
|
244
302
|
- [pagy](https://rubygems.org/gems/pagy)
|
|
245
303
|
- [api-pagination](https://rubygems.org/gems/api-pagination)
|
|
246
304
|
|
|
305
|
+
---
|
|
306
|
+
|
|
247
307
|
### Readymade::Model::ApiAttachable
|
|
248
308
|
|
|
249
309
|
Add base64 attachments format for your models
|
|
@@ -274,12 +334,14 @@ let(:avatar) { Rack::Test::UploadedFile.new(Rails.root.join('spec/support/assets
|
|
|
274
334
|
let(:params) { { user: attributes_for(:user).merge!(avatar: to_api_file(avatar)) } }
|
|
275
335
|
```
|
|
276
336
|
|
|
337
|
+
---
|
|
338
|
+
|
|
277
339
|
### Readymade::Model::Filterable
|
|
278
340
|
|
|
279
341
|
```ruby
|
|
280
342
|
class User < ApplicationRecord
|
|
281
343
|
include Readyamde::Model::Filterable
|
|
282
|
-
|
|
344
|
+
|
|
283
345
|
scope :by_status, ->(status) { where(status: status) }
|
|
284
346
|
scope :by_role, ->(role) { where(role: role) }
|
|
285
347
|
end
|
|
@@ -290,6 +352,8 @@ User.all.filter_collection({ by_status: 'active', by_role: 'manager' })
|
|
|
290
352
|
User.all.filter_collection({ by_status: 'active', by_role: 'manager' }, chain_with: :or) # active OR manager
|
|
291
353
|
```
|
|
292
354
|
|
|
355
|
+
---
|
|
356
|
+
|
|
293
357
|
### Readymade::Model::ValidatableEnum
|
|
294
358
|
|
|
295
359
|
Instead of raised error when enum value is not valid, it adds error to the record
|
|
@@ -310,6 +374,8 @@ user.validate # false
|
|
|
310
374
|
user.errors.full_messages # ["Role 'superadmin' is not a valid role", "Status 'archived' is not a valid status"]
|
|
311
375
|
```
|
|
312
376
|
|
|
377
|
+
---
|
|
378
|
+
|
|
313
379
|
## Development
|
|
314
380
|
|
|
315
381
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Readymade
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
desc 'Copy Readymade default files'
|
|
7
|
+
source_root File.expand_path('templates', __dir__)
|
|
8
|
+
|
|
9
|
+
def copy_config
|
|
10
|
+
template 'config/initializers/readymade.rb'
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Readymade.configure do |config|
|
|
4
|
+
# Change to true to enable job locking
|
|
5
|
+
config.lock_jobs = false
|
|
6
|
+
|
|
7
|
+
# Define lock strategy
|
|
8
|
+
# Strategy The job is locked The job is unlocked
|
|
9
|
+
#
|
|
10
|
+
# until_executing when pushed to the queue when processing starts
|
|
11
|
+
# until_executed when pushed to the queue when the job is processed successfully
|
|
12
|
+
# until_expired when pushed to the queue when the lock is expired
|
|
13
|
+
# until_and_while_executing when pushed to the queue when processing starts a runtime lock is acquired to prevent simultaneous jobs
|
|
14
|
+
# while_executing when processing starts when the job is processed with any result including an error
|
|
15
|
+
|
|
16
|
+
# config.lock_type = :until_executed
|
|
17
|
+
|
|
18
|
+
# new jobs with the same args will be logged within 1.day or until existing one is being executing
|
|
19
|
+
# config.lock_ttl = 1.day
|
|
20
|
+
|
|
21
|
+
# Array of queues to apply job locking to
|
|
22
|
+
# config.locked_queues = [:default]
|
|
23
|
+
end
|
|
@@ -4,8 +4,12 @@ require 'active_job' unless defined?(::ActiveJob)
|
|
|
4
4
|
|
|
5
5
|
module Readymade
|
|
6
6
|
class BackgroundBangJob < ::Readymade::BackgroundJob
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
queue_as { self.arguments[0].dig(:queue_as) || self.arguments[0].dig(:job_options, :queue_as) || :default }
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def callable_method
|
|
12
|
+
:call!
|
|
9
13
|
end
|
|
10
14
|
end
|
|
11
|
-
end
|
|
15
|
+
end
|
|
@@ -4,39 +4,60 @@ require 'active_job' unless defined?(::ActiveJob)
|
|
|
4
4
|
|
|
5
5
|
module Readymade
|
|
6
6
|
class BackgroundJob < ::ActiveJob::Base
|
|
7
|
-
queue_as
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
queue_as { self.arguments[0].dig(:queue_as) || self.arguments[0].dig(:job_options, :queue_as) || :default }
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def apply_uniqueness!
|
|
11
|
+
return unless Readymade.config&.lock_jobs?
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
require "active_job/uniqueness"
|
|
15
|
+
|
|
16
|
+
unique Readymade.config.lock_type,
|
|
17
|
+
lock_ttl: Readymade.config.lock_ttl,
|
|
18
|
+
on_conflict: ->(job) { handle_duplication(job) }
|
|
19
|
+
rescue LoadError
|
|
20
|
+
warn uniqueness_not_loaded_warning
|
|
21
|
+
end
|
|
10
22
|
end
|
|
11
23
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
rescue_from StandardError, with: :handle_rescue_from
|
|
24
|
+
def handle_duplication(job)
|
|
25
|
+
return if Readymade.config.locked_queues.include?(job.queue_name.to_sym)
|
|
15
26
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
end
|
|
27
|
+
ActiveJob::Uniqueness.unlock!(job_class_name: job.class.name)
|
|
28
|
+
end
|
|
19
29
|
|
|
20
|
-
|
|
30
|
+
def uniqueness_not_loaded_warning
|
|
31
|
+
<<~MSG
|
|
32
|
+
|
|
33
|
+
======== READYMADE WARNING ========
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
The `activejob-uniqueness` gem is not installed, but `lock_jobs` is enabled.
|
|
36
|
+
Please add the following to your Gemfile:
|
|
24
37
|
|
|
25
|
-
|
|
38
|
+
gem "activejob-uniqueness", "~> 0.4.0"
|
|
26
39
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
else
|
|
31
|
-
logger.error "Job failed with deserialization error: #{exception.message}"
|
|
32
|
-
raise exception
|
|
40
|
+
===================================
|
|
41
|
+
|
|
42
|
+
MSG
|
|
33
43
|
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def perform(**args)
|
|
48
|
+
args.delete(:class_name).to_s.constantize.send(callable_method, **args)
|
|
34
49
|
end
|
|
35
50
|
|
|
36
51
|
private
|
|
37
52
|
|
|
38
|
-
def
|
|
39
|
-
|
|
53
|
+
def callable_method
|
|
54
|
+
:call
|
|
40
55
|
end
|
|
41
56
|
end
|
|
42
57
|
end
|
|
58
|
+
|
|
59
|
+
if defined?(::ActiveSupport)
|
|
60
|
+
ActiveSupport.on_load(:after_initialize) do
|
|
61
|
+
Readymade::BackgroundJob.apply_uniqueness!
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/readymade/version.rb
CHANGED
data/lib/readymade.rb
CHANGED
|
@@ -14,5 +14,56 @@ require 'readymade/version'
|
|
|
14
14
|
|
|
15
15
|
module Readymade
|
|
16
16
|
class Error < StandardError; end
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
attr_accessor :config
|
|
20
|
+
|
|
21
|
+
def configure
|
|
22
|
+
self.config ||= Config.new
|
|
23
|
+
yield(config)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Config
|
|
28
|
+
attr_reader :lock_jobs, :lock_type, :lock_ttl, :locked_queues
|
|
29
|
+
|
|
30
|
+
ALLOWED_LOCK_TYPES = %i[until_executing until_executed until_expired until_and_while_executing while_executing].freeze
|
|
31
|
+
|
|
32
|
+
def initialize
|
|
33
|
+
@lock_jobs = false
|
|
34
|
+
@lock_type = :until_executed
|
|
35
|
+
@lock_ttl = 1.days
|
|
36
|
+
@locked_queues = [:default]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def lock_jobs=(bool)
|
|
40
|
+
raise ArgumentError, 'Lock jobs must be a boolean' unless [TrueClass, FalseClass, NilClass].include?(bool.class)
|
|
41
|
+
|
|
42
|
+
@lock_jobs = bool
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def lock_type=(lock_type)
|
|
46
|
+
raise ArgumentError, 'Lock type must be a symbol' unless lock_type.is_a?(Symbol)
|
|
47
|
+
raise ArgumentError, "Lock type must be one of: #{ALLOWED_LOCK_TYPES}" unless ALLOWED_LOCK_TYPES.include?(lock_type)
|
|
48
|
+
|
|
49
|
+
@lock_type = lock_type
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def lock_ttl=(ttl)
|
|
53
|
+
raise ArgumentError, 'Lock ttl must be an integer' unless ttl.is_a?(Integer)
|
|
54
|
+
|
|
55
|
+
@lock_ttl = ttl
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def locked_queues=(queues)
|
|
59
|
+
raise ArgumentError, 'Locked queues must be an array' unless queues.is_a?(Array)
|
|
60
|
+
|
|
61
|
+
@locked_queues = queues
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def lock_jobs?
|
|
65
|
+
lock_jobs
|
|
66
|
+
end
|
|
67
|
+
end
|
|
18
68
|
end
|
|
69
|
+
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: readymade
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- OrestF
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-10-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: byebug
|
|
@@ -99,7 +99,8 @@ files:
|
|
|
99
99
|
- Rakefile
|
|
100
100
|
- bin/console
|
|
101
101
|
- bin/setup
|
|
102
|
-
- lib
|
|
102
|
+
- lib/generators/readymade/install_generator.rb
|
|
103
|
+
- lib/generators/readymade/templates/config/initializers/readymade.rb
|
|
103
104
|
- lib/readymade.rb
|
|
104
105
|
- lib/readymade/action.rb
|
|
105
106
|
- lib/readymade/background_bang_job.rb
|
data/lib/.DS_Store
DELETED
|
Binary file
|