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 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,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -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
@@ -0,0 +1,3 @@
1
+ module RailsAsyncMethods
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,9 @@
1
+ require "rails_async_methods/version"
2
+
3
+ module RailsAsyncMethods
4
+ autoload :AsyncMethod, 'rails_async_methods/async_method'
5
+ end
6
+
7
+ ActiveSupport.on_load(:active_record) do
8
+ include RailsAsyncMethods::AsyncMethod
9
+ 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: []