rails_async_methods 0.1.0
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/MIT-LICENSE +20 -0
- data/README.md +107 -0
- data/Rakefile +3 -0
- data/lib/generators/rails_async_methods_generator.rb +18 -0
- data/lib/rails_async_methods/active_job_options_parser.rb +45 -0
- data/lib/rails_async_methods/async_method.rb +46 -0
- data/lib/rails_async_methods/parameter_parser.rb +64 -0
- data/lib/rails_async_methods/version.rb +3 -0
- data/lib/rails_async_methods.rb +9 -0
- metadata +140 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 672c0a204c6d32b0c4b33f9da9b1c9964afc45d9626128ddccf52f77a12bd8c1
|
4
|
+
data.tar.gz: a4cf18a167be4b327aaab34b49875280444fb7f6f52743a85aa1564f2cba2f33
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 650de1333dc34b7d8307508d12f4d49eda5ab5e249342727500b7431a4fd81d6ae66cd8d9ad3d5a28a06c2000d24dff7dd7ed625cad55bdb8dfb755a6c8cbd61
|
7
|
+
data.tar.gz: 1e17bb9130bbfef087ebc8cfe86051284c44a12c0234287373dd63536157b47a2bbc31f320a129827c38a0950266b792ebe77bb746360466e8884ea92fb3913e
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2022 benngarcia
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# RailsAsyncMethods
|
2
|
+
Rails Async Methods is an opinionated gem meant to remove boilerplate while creating asynchronous rails jobs. It provides a declaritive interface for converting any model method into an asychronous method by provided an abstracted wrapper around rails ActiveJob API.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
```ruby
|
6
|
+
class User
|
7
|
+
def example_method
|
8
|
+
# logic...
|
9
|
+
end
|
10
|
+
end
|
11
|
+
async :example_method
|
12
|
+
```
|
13
|
+
This will give you access to ```user_instance.async_example_method```, which when called will use ActiveJob's API to create an ActiveJob with your backend of choice and call the example_method when the job is ran.
|
14
|
+
|
15
|
+
One important distinction is that for a method call like
|
16
|
+
```ruby
|
17
|
+
def example_method_with_args(a:, b:)
|
18
|
+
#logic
|
19
|
+
end
|
20
|
+
async :example_method_with_args
|
21
|
+
```
|
22
|
+
the ```async_example_method_with_args``` method will have a signature that matches the original method. This makes testing and debugging during development faster, as both sync and async method calls will fail when called instead of silently failing as an active job.
|
23
|
+
|
24
|
+
### Options
|
25
|
+
|
26
|
+
You can pass the following options to your async declaration.
|
27
|
+
|
28
|
+
- prefix: specifies a prefix other than ```async_```, i.e.
|
29
|
+
```ruby
|
30
|
+
async :example_method, prefix: :asynchrounous_
|
31
|
+
|
32
|
+
user_instance.asynchronous_example_method
|
33
|
+
```
|
34
|
+
|
35
|
+
- job: use a custom job other than the generated ```RailsAsyncMethods::AbstractJob``` - see section on Custom Jobs below i.e.
|
36
|
+
```ruby
|
37
|
+
async :example_method, job: CustomExampleMethodJob
|
38
|
+
```
|
39
|
+
|
40
|
+
- ActiveJob configurations
|
41
|
+
- queue: specify a custom queue
|
42
|
+
- wait_until: specify a date for the job to run at
|
43
|
+
- wait: give an amount of time for the job to wait until executing
|
44
|
+
- priority: delayed job priority
|
45
|
+
```ruby
|
46
|
+
async :example_method, queue: :fast, wait_until: 1.week.from_now, wait: 1.week, priority: 1
|
47
|
+
```
|
48
|
+
|
49
|
+
## Installation
|
50
|
+
Add this line to your application's Gemfile:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
gem "rails_async_methods"
|
54
|
+
```
|
55
|
+
|
56
|
+
And then execute:
|
57
|
+
```bash
|
58
|
+
$ bundle
|
59
|
+
```
|
60
|
+
|
61
|
+
Then, run the generator
|
62
|
+
```bash
|
63
|
+
$ rails generate rails_async_methods
|
64
|
+
```
|
65
|
+
|
66
|
+
Or install it yourself as:
|
67
|
+
```bash
|
68
|
+
$ gem install rails_async_methods
|
69
|
+
```
|
70
|
+
|
71
|
+
## Extras
|
72
|
+
|
73
|
+
### Custom Jobs
|
74
|
+
While you can implement any custom job, and have it implement the perform method with this signature.
|
75
|
+
```ruby
|
76
|
+
def perform(receiver, method, *args, **kwargs)
|
77
|
+
receiver.send(method, *args, **kwargs)
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
I would instead recommend inherting from the generated ActiveJob, i.e.
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
class RailsAsyncMethods::CustomJob < RailsAsyncMethods::AbstractJob
|
85
|
+
around_perform :do_special_thing
|
86
|
+
|
87
|
+
private
|
88
|
+
def do_special_thing
|
89
|
+
# Special Things
|
90
|
+
end
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
## Contributing
|
95
|
+
Contributions Welcome! Please create an issue first, and then fork and create a PR.
|
96
|
+
|
97
|
+
## License
|
98
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
99
|
+
|
100
|
+
## Why?
|
101
|
+
You don't need to move 1-2 LOC out of your model. If it's larger, use a service object. Either way, your controllers and requests should be interacting with resources typically. They should not need to know about Jobs/Service/Other Design Pattern implementations of interacting with your resources. In this line of thinking, most calls that interact with a resource should be called on that resource's model. Thus, Job's that wrap 1-2 LOC or a call to a Service object are unneccesary boilerplate.
|
102
|
+
|
103
|
+
So, either write your minimal logic in the model, or use whatever refactoring you deem necessary, but leave the model as the entry point for interacting with the resource. Then, declare that method as Async when you need fit.
|
104
|
+
|
105
|
+
I told you this gem is opinionated.
|
106
|
+
|
107
|
+
Also, the existing alternatives override the method's name, leading to confusion as what appears to be synchronous was made asyncronous.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
class RailsAsyncMethodsGenerator < Rails::Generators::Base
|
2
|
+
desc "This generator creates the base abstract job for the rails_async_methods gem"
|
3
|
+
def create_abstract_job
|
4
|
+
file_path = "app/jobs/rails_async_methods/abstract_job.rb"
|
5
|
+
return if File.exist? file_path
|
6
|
+
|
7
|
+
create_file file_path,
|
8
|
+
<<~FILE
|
9
|
+
class RailsAsyncMethods::AbstractJob < ApplicationJob
|
10
|
+
queue_as :default
|
11
|
+
|
12
|
+
def perform(receiver, method, *args, **kwargs)
|
13
|
+
receiver.send(method, *args, **kwargs)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
FILE
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module RailsAsyncMethods
|
2
|
+
class ActiveJobOptionsParser
|
3
|
+
SET_OPTIONS = %i[queue wait_until wait priority].freeze
|
4
|
+
|
5
|
+
attr_reader :prefix, :queue, :wait_until, :wait, :priority, :job
|
6
|
+
STRING_ARG_SEPERATOR = [':', ','].freeze
|
7
|
+
|
8
|
+
def initialize(opts={})
|
9
|
+
@prefix = method_prefix(opts[:prefix])
|
10
|
+
@queue = opts[:queue]
|
11
|
+
@wait_until = opts[:wait_until].to_f if opts[:wait_until]
|
12
|
+
@wait = opts[:wait].seconds.from_now.to_f if opts[:wait]
|
13
|
+
@priority = opts[:priority].to_i if opts[:priority]
|
14
|
+
@job = get_job_obj(opts[:job])
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_h
|
18
|
+
valid_instance_values
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
return '' if valid_instance_values.empty?
|
23
|
+
|
24
|
+
valid_instance_values.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def valid_instance_values
|
30
|
+
Hash[SET_OPTIONS.filter_map { |name| [name, send(name)] unless send(name).nil? }]
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_prefix(prefix)
|
34
|
+
return 'async_' if prefix.nil? || prefix.empty?
|
35
|
+
|
36
|
+
(prefix.is_a? Symbol) ? prefix.to_s : prefix
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_job_obj(job)
|
40
|
+
return RailsAsyncMethods::AbstractJob if job.nil?
|
41
|
+
|
42
|
+
(job.is_a? Symbol) ? Object.const_get(job) : job
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rails_async_methods/parameter_parser'
|
2
|
+
require 'rails_async_methods/active_job_options_parser'
|
3
|
+
|
4
|
+
module RailsAsyncMethods
|
5
|
+
module AsyncMethod
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class NotPersistedError < StandardError; end
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
# Supported Options
|
12
|
+
# wait: Pass Date
|
13
|
+
def async(method_name, opts={})
|
14
|
+
raise NoMethodError unless method_defined?(method_name)
|
15
|
+
unbound_method = instance_method(method_name.to_sym)
|
16
|
+
|
17
|
+
parsed_method_arguments = RailsAsyncMethods::ParameterParser.new(unbound_method.parameters)
|
18
|
+
active_job_arguments = RailsAsyncMethods::ActiveJobOptionsParser.new(opts)
|
19
|
+
|
20
|
+
if parsed_method_arguments.empty?
|
21
|
+
define_method "#{active_job_arguments.prefix.concat(method_name.to_s)}" do
|
22
|
+
raise NotPersistedError, 'Instance must be persisted to run asynchronously' unless persisted?
|
23
|
+
raise TypeError, 'Cannot pass a block to an asynchronous method' if block_given?
|
24
|
+
|
25
|
+
active_job_arguments.job.set(**active_job_arguments.to_h).perform_later(self, method_name)
|
26
|
+
end
|
27
|
+
else
|
28
|
+
stringified_method_body = begin
|
29
|
+
<<~RUBY
|
30
|
+
define_method "#{active_job_arguments.prefix.concat(method_name.to_s)}" do |#{parsed_method_arguments.as_argument_string}|
|
31
|
+
raise NotPersistedError, "Instance must be persisted to run asynchronously" unless persisted?
|
32
|
+
raise TypeError, "Cannot pass a block to an asynchronous methods" if block_given?
|
33
|
+
|
34
|
+
positional_args, keyword_args = parsed_method_arguments.arg_values_for_job { |argument| binding.local_variable_get(argument) }
|
35
|
+
#{active_job_arguments.job}
|
36
|
+
.set(#{active_job_arguments})
|
37
|
+
.perform_later(self, method_name, *positional_args, **keyword_args)
|
38
|
+
end
|
39
|
+
RUBY
|
40
|
+
end
|
41
|
+
module_eval stringified_method_body, *unbound_method.source_location
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module RailsAsyncMethods
|
2
|
+
class ParameterParser
|
3
|
+
REQ_POS_DELIMETER = ','.freeze
|
4
|
+
OPT_POS_DELIMETER = '=nil,'.freeze
|
5
|
+
REST_POS_DELIMETER = ['*', ','].freeze
|
6
|
+
REQ_KEY_DELIMETER = ':,'.freeze
|
7
|
+
OPT_KEY_DELIMETER = ':nil,'.freeze
|
8
|
+
REST_KEY_DELIMETER = ['**', ','].freeze
|
9
|
+
BLOCK_DELIMETER = '&'.freeze
|
10
|
+
attr_reader :parameters
|
11
|
+
|
12
|
+
def initialize(parameters)
|
13
|
+
@parameters = parameters
|
14
|
+
end
|
15
|
+
|
16
|
+
def empty?
|
17
|
+
parameters.empty?
|
18
|
+
end
|
19
|
+
|
20
|
+
def as_argument_string
|
21
|
+
return '' if @parameters.empty?
|
22
|
+
|
23
|
+
parameters.map { |type, name| to_argument_string(type, name)}.join.chomp(',')
|
24
|
+
end
|
25
|
+
|
26
|
+
def arg_values_for_job(&block)
|
27
|
+
final_arg_values = [[], {}]
|
28
|
+
@parameters.each do |type, name|
|
29
|
+
case type
|
30
|
+
when :req, :opt
|
31
|
+
final_arg_values[0].append(block.call(name))
|
32
|
+
when :rest
|
33
|
+
final_arg_values[0].append(*block.call(name))
|
34
|
+
when :keyreq, :key
|
35
|
+
final_arg_values[1].merge!(name => block.call(name))
|
36
|
+
when :keyrest
|
37
|
+
final_arg_values[1].merge!(block.call(name))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
final_arg_values
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def to_argument_string(type, name)
|
46
|
+
case type
|
47
|
+
when :req
|
48
|
+
name.to_s.concat(REQ_POS_DELIMETER)
|
49
|
+
when :opt
|
50
|
+
name.to_s.concat(OPT_POS_DELIMETER)
|
51
|
+
when :keyreq
|
52
|
+
name.to_s.concat(REQ_KEY_DELIMETER)
|
53
|
+
when :key
|
54
|
+
name.to_s.concat(OPT_KEY_DELIMETER)
|
55
|
+
when :rest
|
56
|
+
name.to_s.prepend(REST_POS_DELIMETER[0]).concat(REST_POS_DELIMETER[1])
|
57
|
+
when :keyrest
|
58
|
+
name.to_s.prepend(REST_KEY_DELIMETER[0]).concat(REST_KEY_DELIMETER[1])
|
59
|
+
when :block
|
60
|
+
name.to_s.prepend(BLOCK_DELIMETER)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
metadata
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rails_async_methods
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- benngarcia
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-05-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 7.0.2.4
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 7.0.2.4
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pg
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sidekiq
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Utilizes the ActiveJob api to provide async callers and receivers without
|
98
|
+
duplicating code.
|
99
|
+
email:
|
100
|
+
- beng4606@gmail.com
|
101
|
+
executables: []
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- MIT-LICENSE
|
106
|
+
- README.md
|
107
|
+
- Rakefile
|
108
|
+
- lib/generators/rails_async_methods_generator.rb
|
109
|
+
- lib/rails_async_methods.rb
|
110
|
+
- lib/rails_async_methods/active_job_options_parser.rb
|
111
|
+
- lib/rails_async_methods/async_method.rb
|
112
|
+
- lib/rails_async_methods/parameter_parser.rb
|
113
|
+
- lib/rails_async_methods/version.rb
|
114
|
+
homepage: https://github.com/benngarcia/rails_async_methods
|
115
|
+
licenses:
|
116
|
+
- MIT
|
117
|
+
metadata:
|
118
|
+
homepage_uri: https://github.com/benngarcia/rails_async_methods
|
119
|
+
changelog_uri: https://github.com/benngarcia/rails_async_methods/blob/master/CHANGELOG.md
|
120
|
+
post_install_message:
|
121
|
+
rdoc_options: []
|
122
|
+
require_paths:
|
123
|
+
- lib
|
124
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
requirements: []
|
135
|
+
rubygems_version: 3.3.3
|
136
|
+
signing_key:
|
137
|
+
specification_version: 4
|
138
|
+
summary: Quickly, create async callers and receivers for your rails methods, because
|
139
|
+
y'know DRY.
|
140
|
+
test_files: []
|