runger_actions 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
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: []