runger_actions 0.19.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.
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'runger_actions'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/guard ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'guard' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', Pathname.new(__FILE__).realpath)
13
+
14
+ bundle_binstub = File.expand_path('bundle', __dir__)
15
+
16
+ if File.file?(bundle_binstub)
17
+ if File.read(bundle_binstub, 300).include?('This file was generated by Bundler')
18
+ load(bundle_binstub)
19
+ else
20
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
21
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
22
+ end
23
+ end
24
+
25
+ require 'rubygems'
26
+ require 'bundler/setup'
27
+
28
+ load Gem.bin_path('guard', 'guard')
data/bin/release ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'release' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', Pathname.new(__FILE__).realpath)
13
+
14
+ bundle_binstub = File.expand_path('bundle', __dir__)
15
+
16
+ if File.file?(bundle_binstub)
17
+ if File.read(bundle_binstub, 300).include?('This file was generated by Bundler')
18
+ load(bundle_binstub)
19
+ else
20
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
21
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
22
+ end
23
+ end
24
+
25
+ require 'rubygems'
26
+ require 'bundler/setup'
27
+
28
+ load Gem.bin_path('release_assistant', 'release')
data/bin/rspec ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', Pathname.new(__FILE__).realpath)
13
+
14
+ bundle_binstub = File.expand_path('bundle', __dir__)
15
+
16
+ if File.file?(bundle_binstub)
17
+ if File.read(bundle_binstub, 300).include?('This file was generated by Bundler')
18
+ load(bundle_binstub)
19
+ else
20
+ abort(<<~ERROR)
21
+ Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.
23
+ ERROR
24
+ end
25
+ end
26
+
27
+ require 'rubygems'
28
+ require 'bundler/setup'
29
+
30
+ load Gem.bin_path('rspec-core', 'rspec')
data/bin/rubocop ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', Pathname.new(__FILE__).realpath)
13
+
14
+ bundle_binstub = File.expand_path('bundle', __dir__)
15
+
16
+ if File.file?(bundle_binstub)
17
+ if File.read(bundle_binstub, 300).include?('This file was generated by Bundler')
18
+ load(bundle_binstub)
19
+ else
20
+ abort(<<~ERROR)
21
+ Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.
23
+ ERROR
24
+ end
25
+ end
26
+
27
+ require 'rubygems'
28
+ require 'bundler/setup'
29
+
30
+ load Gem.bin_path('rubocop', 'rubocop')
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates an action with the given name.
3
+
4
+ Example:
5
+ bin/rails generate runger_actions:action Users::Create
6
+
7
+ This will create:
8
+ app/actions/users/create.rb
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module RungerActions::Generators ; end
6
+
7
+ class RungerActions::Generators::ActionGenerator < Rails::Generators::NamedBase
8
+ source_root File.expand_path('templates', __dir__)
9
+
10
+ def create_policy
11
+ template('action.rb', File.join('app/actions', class_path, "#{file_name}.rb"))
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name %> < ApplicationAction
4
+ # requires :email, String
5
+
6
+ # returns :user, User
7
+
8
+ def execute
9
+ # result.user = User.create!(email: email)
10
+ # NewUserMailer.user_created(result.user.id).deliver_later
11
+ end
12
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RungerActions::Base
4
+ extend Memoist
5
+
6
+ class << self
7
+ extend Memoist
8
+
9
+ def run!(params)
10
+ new!(params).run!
11
+ end
12
+
13
+ def new!(params)
14
+ action = new(params)
15
+ if action.valid?
16
+ action
17
+ else
18
+ raise(RungerActions::InvalidParam, action.errors.full_messages.join(', '))
19
+ end
20
+ end
21
+
22
+ def requires(param_name, *shape_descriptions, &blk)
23
+ required_params[param_name] = Shaped::Shape(*shape_descriptions)
24
+
25
+ shape_description = shape_descriptions.first if shape_descriptions.size == 1
26
+ if (
27
+ shape_description.is_a?(Class) && (shape_description < ActiveRecord::Base) && blk.present?
28
+ )
29
+ register_validator_klass(param_name, shape_description, blk)
30
+ end
31
+
32
+ define_reader_method(param_name)
33
+ end
34
+
35
+ def define_reader_method(param_name)
36
+ define_method(param_name) do
37
+ @params[param_name]
38
+ end
39
+ end
40
+
41
+ def register_validator_klass(param_name, param_klass, blk)
42
+ validator_klass = const_set("#{param_name.to_s.camelize}Validator", Class.new)
43
+ validator_klass.include(ActiveModel::Model)
44
+ validator_klass.attr_accessor(*param_klass.column_names)
45
+ validator_klass.class_eval(&blk)
46
+ validators[param_name] = validator_klass
47
+ end
48
+
49
+ def returns(param_name, *shape_descriptions)
50
+ shape = Shaped::Shape(*shape_descriptions)
51
+ promised_values[param_name] = shape
52
+ result_klass.class_eval do
53
+ define_method(param_name) do
54
+ @return_values[param_name]
55
+ end
56
+
57
+ define_method("#{param_name}=") do |value|
58
+ if locked?
59
+ raise(RungerActions::MutatingLockedResult, <<~ERROR.squish)
60
+ You are attempting to assign a value to an instance of #{self.class} outside of the
61
+ #{self.class.module_parent}#execute method. This is not allowed; you may only assign
62
+ values to the `result` within the #execute method.
63
+ ERROR
64
+ end
65
+
66
+ if !shape.matched_by?(value)
67
+ raise(RungerActions::TypeMismatch, <<~ERROR.squish)
68
+ Attemted to assign an invalid value for `result.#{param_name}` ; expected an object
69
+ shaped like #{shape} but got #{value.inspect}
70
+ ERROR
71
+ end
72
+
73
+ @return_values[param_name] = value
74
+ end
75
+ end
76
+ end
77
+
78
+ def fails_with(error_type)
79
+ result_klass.class_eval do
80
+ define_method("#{error_type}!") do |error_message = nil|
81
+ @failure = error_type
82
+ @error_message = error_message
83
+ if @action.raise_on_failure?
84
+ raise(
85
+ RungerActions::RuntimeFailure,
86
+ "#{@action.class.name} action failed with `#{error_type}`",
87
+ )
88
+ end
89
+ end
90
+
91
+ define_method("#{error_type}?") do
92
+ @failure == error_type
93
+ end
94
+ end
95
+ end
96
+
97
+ memoize \
98
+ def result_klass
99
+ const_set(:Result, Class.new(RungerActions::Result))
100
+ end
101
+
102
+ memoize \
103
+ def required_params
104
+ {}
105
+ end
106
+
107
+ memoize \
108
+ def promised_values
109
+ {}
110
+ end
111
+
112
+ memoize \
113
+ def validators
114
+ {}
115
+ end
116
+ end
117
+
118
+ attr_reader :errors
119
+
120
+ # We can't specify keyword arguments for this method because we don't know which keywords/params
121
+ # the method will need to accept; that's defined by the user.
122
+ #
123
+ # rubocop:disable Style/OptionHash
124
+ def initialize(params = {})
125
+ @params = params
126
+ @errors = ActiveModel::Errors.new(self)
127
+ validate_required_params!
128
+ end
129
+ # rubocop:enable Style/OptionHash
130
+
131
+ def run(raise_on_failure: false)
132
+ @raise_on_failure = raise_on_failure
133
+ if !respond_to?(:execute)
134
+ raise(RungerActions::ExecuteNotImplemented, <<~ERROR.squish)
135
+ All RungerActions classes must implement an #execute instance method, but #{self.class}
136
+ fails to do so.
137
+ ERROR
138
+ end
139
+
140
+ execute
141
+ result.lock!
142
+ verify_promised_return_values! if result.success?
143
+ result
144
+ end
145
+
146
+ def run!
147
+ if valid?
148
+ run(raise_on_failure: true)
149
+ else
150
+ raise(RungerActions::InvalidParam, @errors.full_messages.join(', '))
151
+ end
152
+ end
153
+
154
+ def valid?
155
+ run_custom_validations
156
+ @errors.blank?
157
+ end
158
+
159
+ def raise_on_failure?
160
+ !!@raise_on_failure
161
+ end
162
+
163
+ memoize \
164
+ def result
165
+ self.class.result_klass.new(action: self)
166
+ end
167
+
168
+ private
169
+
170
+ def verify_promised_return_values!
171
+ missing_return_values = self.class.promised_values.keys - result.return_values.keys
172
+ if missing_return_values.any?
173
+ violation_messages =
174
+ missing_return_values.map do |missing_return_value|
175
+ expected_shape = self.class.promised_values[missing_return_value]
176
+ "`#{missing_return_value}` (should be shaped like #{expected_shape})"
177
+ end
178
+
179
+ raise(RungerActions::MissingResultValue, <<~ERROR.squish)
180
+ #{self.class.name} failed to set all promised return values on its `result`. The
181
+ following were missing on the `result`: #{violation_messages.join(', ')}.
182
+ ERROR
183
+ end
184
+ end
185
+
186
+ def run_custom_validations
187
+ self.class.required_params.each_key do |param_name|
188
+ validator_klass = self.class.validators[param_name]
189
+ next if validator_klass.nil?
190
+
191
+ model_instance = @params[param_name]
192
+ validator_instance = validator_klass.new(model_instance.attributes)
193
+ if !validator_instance.valid?
194
+ @errors = validator_instance.errors
195
+ end
196
+ end
197
+ end
198
+
199
+ def validate_required_params!
200
+ missing_params = self.class.required_params.keys - @params.keys
201
+ if missing_params.any?
202
+ raise(RungerActions::MissingParam, <<~ERROR.squish)
203
+ Required param(s) #{missing_params.map { "`#{_1}`" }.join(', ')} were not provided to
204
+ the #{self.class} action.
205
+ ERROR
206
+ end
207
+
208
+ type_mismatches = []
209
+ self.class.required_params.each do |param_name, shape|
210
+ value = @params[param_name]
211
+ if !shape.matched_by?(value)
212
+ type_mismatches << [param_name, shape, value]
213
+ end
214
+ end
215
+
216
+ if type_mismatches.any?
217
+ messages =
218
+ type_mismatches.map do |param_name, shape, value|
219
+ <<~MESSAGE.squish
220
+ `#{param_name}` is expected to be shaped like #{shape}, but was
221
+ `#{value.is_a?(String) ? value.inspect : value}`
222
+ MESSAGE
223
+ end
224
+ raise(RungerActions::TypeMismatch, <<~ERROR.squish)
225
+ One or more required params are of the wrong type: #{messages.join(' ; ')}.
226
+ ERROR
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RungerActions::Error < StandardError ; end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RungerActions::Result
4
+ attr_reader :error_message, :return_values
5
+
6
+ def initialize(action:)
7
+ @action = action
8
+ @return_values = {}
9
+ @failure = nil
10
+ end
11
+
12
+ def lock!
13
+ @locked = true
14
+ end
15
+
16
+ def locked?
17
+ @locked == true
18
+ end
19
+
20
+ def success?
21
+ @failure.nil?
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RungerActions
4
+ VERSION = '0.19.0'
5
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RungerActions ; end
4
+
5
+ require 'active_model'
6
+ require 'active_record'
7
+ require 'active_support/all'
8
+ require 'memoist'
9
+ require 'shaped'
10
+ Dir["#{File.dirname(__FILE__)}/runger_actions/**/*.rb"].each { |file| require file }
11
+
12
+ class RungerActions::ExecuteNotImplemented < RungerActions::Error ; end
13
+ class RungerActions::InvalidParam < RungerActions::Error ; end
14
+ class RungerActions::MissingParam < RungerActions::Error ; end
15
+ class RungerActions::MissingResultValue < RungerActions::Error ; end
16
+ class RungerActions::MutatingLockedResult < RungerActions::Error ; end
17
+ class RungerActions::RuntimeFailure < RungerActions::Error ; end
18
+ class RungerActions::TypeMismatch < RungerActions::Error ; end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/runger_actions/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'runger_actions'
7
+ spec.version = RungerActions::VERSION
8
+ spec.authors = ['David Runger']
9
+ spec.email = ['davidjrunger@gmail.com']
10
+
11
+ spec.summary = 'Organize (and validate) the business logic of your Rails application.'
12
+ spec.description = 'Organize (and validate) the business logic of your Rails application.'
13
+ spec.homepage = 'https://github.com/davidrunger/runger_actions'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.2.0')
16
+
17
+ spec.metadata['rubygems_mfa_required'] = 'true'
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/davidrunger/runger_actions'
20
+ spec.metadata['changelog_uri'] =
21
+ 'https://github.com/davidrunger/runger_actions/blob/master/CHANGELOG.md'
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files =
26
+ Dir.chdir(File.expand_path(__dir__)) do
27
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
28
+ end
29
+ spec.require_paths = ['lib']
30
+
31
+ spec.add_runtime_dependency('memoist', '~> 0.16')
32
+ spec.add_runtime_dependency('rails', '>= 6', '< 8')
33
+ spec.add_runtime_dependency('shaped', '>= 0.9', '< 0.11')
34
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: runger_actions
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.19.0
5
+ platform: ruby
6
+ authors:
7
+ - David Runger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-05-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: memoist
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.16'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '6'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '8'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '6'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '8'
47
+ - !ruby/object:Gem::Dependency
48
+ name: shaped
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0.9'
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '0.11'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0.9'
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '0.11'
67
+ description: Organize (and validate) the business logic of your Rails application.
68
+ email:
69
+ - davidjrunger@gmail.com
70
+ executables: []
71
+ extensions: []
72
+ extra_rdoc_files: []
73
+ files:
74
+ - ".github/dependabot.yml"
75
+ - ".github/workflows/ruby.yml"
76
+ - ".gitignore"
77
+ - ".release_assistant.yml"
78
+ - ".rspec"
79
+ - ".rubocop.yml"
80
+ - ".ruby-version"
81
+ - CHANGELOG.md
82
+ - Gemfile
83
+ - Gemfile.lock
84
+ - LICENSE.txt
85
+ - README.md
86
+ - RELEASING.md
87
+ - Rakefile
88
+ - bin/_guard-core
89
+ - bin/console
90
+ - bin/guard
91
+ - bin/release
92
+ - bin/rspec
93
+ - bin/rubocop
94
+ - bin/setup
95
+ - lib/generators/runger_actions/action/USAGE
96
+ - lib/generators/runger_actions/action/action_generator.rb
97
+ - lib/generators/runger_actions/action/templates/action.rb
98
+ - lib/runger_actions.rb
99
+ - lib/runger_actions/base.rb
100
+ - lib/runger_actions/error.rb
101
+ - lib/runger_actions/result.rb
102
+ - lib/runger_actions/version.rb
103
+ - runger_actions.gemspec
104
+ homepage: https://github.com/davidrunger/runger_actions
105
+ licenses:
106
+ - MIT
107
+ metadata:
108
+ rubygems_mfa_required: 'true'
109
+ homepage_uri: https://github.com/davidrunger/runger_actions
110
+ source_code_uri: https://github.com/davidrunger/runger_actions
111
+ changelog_uri: https://github.com/davidrunger/runger_actions/blob/master/CHANGELOG.md
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: 3.2.0
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubygems_version: 3.4.4
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Organize (and validate) the business logic of your Rails application.
131
+ test_files: []