actionable-rb 1.9.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/.claude-plugin/marketplace.json +15 -0
- data/.claude-plugin/plugin.json +9 -0
- data/.rspec +3 -0
- data/AGENTS.md +150 -0
- data/CHANGELOG.md +766 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +555 -0
- data/UPGRADING.md +957 -0
- data/USAGE.md +1291 -0
- data/docs/getting_started.md +611 -0
- data/lib/actionable/action.rb +1220 -0
- data/lib/actionable/batch_result.rb +92 -0
- data/lib/actionable/errors.rb +36 -0
- data/lib/actionable/history.rb +91 -0
- data/lib/actionable/input_dispatch.rb +94 -0
- data/lib/actionable/job.rb +157 -0
- data/lib/actionable/measurement.rb +64 -0
- data/lib/actionable/rails/after_commit.rb +142 -0
- data/lib/actionable/rails/proxy_validator.rb +59 -0
- data/lib/actionable/rails/transactions.rb +95 -0
- data/lib/actionable/rails.rb +24 -0
- data/lib/actionable/railtie.rb +10 -0
- data/lib/actionable/rbs.rb +502 -0
- data/lib/actionable/registry.rb +64 -0
- data/lib/actionable/result.rb +166 -0
- data/lib/actionable/results/failure.rb +43 -0
- data/lib/actionable/results/skipped.rb +30 -0
- data/lib/actionable/results/success.rb +14 -0
- data/lib/actionable/rspec/coverage/report.rb +264 -0
- data/lib/actionable/rspec/coverage/tracker.rb +330 -0
- data/lib/actionable/rspec/coverage.rb +45 -0
- data/lib/actionable/rspec/matchers.rb +266 -0
- data/lib/actionable/rspec/stubs.rb +250 -0
- data/lib/actionable/rspec.rb +40 -0
- data/lib/actionable/rubocop/config.yml +8 -0
- data/lib/actionable/rubocop/cop/actionable/verb_not_last.rb +62 -0
- data/lib/actionable/rubocop/plugin.rb +31 -0
- data/lib/actionable/rubocop.rb +25 -0
- data/lib/actionable/runner.rb +300 -0
- data/lib/actionable/steps/action.rb +76 -0
- data/lib/actionable/steps/base.rb +107 -0
- data/lib/actionable/steps/case.rb +124 -0
- data/lib/actionable/steps/method.rb +39 -0
- data/lib/actionable/steps.rb +27 -0
- data/lib/actionable/value_match.rb +22 -0
- data/lib/actionable/verb_position.rb +574 -0
- data/lib/actionable/version.rb +6 -0
- data/lib/actionable-rb.rb +6 -0
- data/lib/actionable.rb +59 -0
- data/sig/actionable/dsl.rbs +42 -0
- data/sig/actionable/rails.rbs +16 -0
- data/sig/actionable.rbs +2593 -0
- data/skills/actionable/SKILL.md +214 -0
- metadata +132 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Actionable
|
|
4
|
+
# ActiveModel validations applied to a separate delegate object (decision
|
|
5
|
+
# D7). Subclass it, declare rules with the ActiveModel +validates+ DSL, and
|
|
6
|
+
# construct it with any object — a plain PORO or an ActiveRecord model — whose
|
|
7
|
+
# attributes the rules read. This keeps validation rules separate from the
|
|
8
|
+
# data they check.
|
|
9
|
+
#
|
|
10
|
+
# class InvoiceRules < Actionable::ProxyValidator
|
|
11
|
+
# validates :amount, presence: true, numericality: { greater_than: 0 }
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# rules = InvoiceRules.new(invoice)
|
|
15
|
+
# fail(:invalid, **rules.formatted_errors) unless rules.valid?
|
|
16
|
+
#
|
|
17
|
+
# Part of the optional Rails adapter — available only after
|
|
18
|
+
# +require 'actionable/rails'+.
|
|
19
|
+
class ProxyValidator
|
|
20
|
+
include ActiveModel::Validations
|
|
21
|
+
|
|
22
|
+
# ActiveModel needs a model name to build i18n error messages; supply a
|
|
23
|
+
# fallback so anonymous subclasses (and the base) still produce messages.
|
|
24
|
+
#
|
|
25
|
+
# @return [ActiveModel::Name]
|
|
26
|
+
def self.model_name
|
|
27
|
+
@model_name ||= ActiveModel::Name.new(self, nil, name || 'ProxyValidator')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Object] the object whose attributes the rules validate
|
|
31
|
+
attr_reader :delegate
|
|
32
|
+
|
|
33
|
+
# @param delegate [Object] the object to validate
|
|
34
|
+
def initialize(delegate)
|
|
35
|
+
@delegate = delegate
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Hash{Symbol=>Array<String>}] validation errors keyed by
|
|
39
|
+
# attribute, after running the rules — ready to splat into +fail+
|
|
40
|
+
def formatted_errors
|
|
41
|
+
valid?
|
|
42
|
+
errors.to_hash
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Read attribute values from the delegate so the ActiveModel validators see
|
|
48
|
+
# the delegate's data.
|
|
49
|
+
def method_missing(name, *args, &block)
|
|
50
|
+
return @delegate.public_send(name, *args, &block) if @delegate.respond_to?(name)
|
|
51
|
+
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def respond_to_missing?(name, include_private = false)
|
|
56
|
+
@delegate.respond_to?(name, include_private) || super
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Actionable
|
|
4
|
+
module Rails
|
|
5
|
+
# Internal signal raised inside a transaction block to trigger a rollback
|
|
6
|
+
# without surfacing as a genuine error. The runner wrapper swallows it; a
|
|
7
|
+
# real ActiveRecord transaction rolls back and re-raises it (as it does for
|
|
8
|
+
# any exception), and the wrapper catches it outside the block.
|
|
9
|
+
class Rollback < StandardError; end
|
|
10
|
+
|
|
11
|
+
# The +transactional+ class macro (decision D9). Extended onto
|
|
12
|
+
# {Actionable::Action} by +actionable/rails+.
|
|
13
|
+
module Transactions
|
|
14
|
+
# Wrap this action's run in +model.transaction(**options)+.
|
|
15
|
+
#
|
|
16
|
+
# transactional model: :invoice
|
|
17
|
+
# transactional model: Invoice, requires_new: false # join an outer one
|
|
18
|
+
#
|
|
19
|
+
# Run inside a transaction that is already open, the action gets its own
|
|
20
|
+
# savepoint (+requires_new: true+) unless +requires_new:+ is given, so a
|
|
21
|
+
# recorded +Failure+ rolls back its own writes rather than leaving them for
|
|
22
|
+
# the outer transaction to commit.
|
|
23
|
+
#
|
|
24
|
+
# @param model [Symbol, Class] the model whose +transaction+ is used; a
|
|
25
|
+
# Symbol is resolved to a constant (+:invoice+ → +Invoice+)
|
|
26
|
+
# @param options [Object] options forwarded to +transaction+
|
|
27
|
+
# @return [void]
|
|
28
|
+
def transactional(model:, **options)
|
|
29
|
+
@transaction_config = {model: model, options: options}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The transaction config for this action, inherited from ancestors when
|
|
33
|
+
# not set locally (decision D9: inherited, overridable).
|
|
34
|
+
#
|
|
35
|
+
# @return [Hash{Symbol=>Object}, nil] +{model:, options:}+, or +nil+ when
|
|
36
|
+
# neither this action nor an ancestor is +transactional+
|
|
37
|
+
def transaction_config
|
|
38
|
+
return @transaction_config if defined?(@transaction_config) && @transaction_config
|
|
39
|
+
return superclass.transaction_config if superclass.respond_to?(:transaction_config)
|
|
40
|
+
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Prepended onto {Actionable::Runner} so a +transactional+ action runs its
|
|
46
|
+
# whole pipeline inside a transaction. A raised exception or a recorded
|
|
47
|
+
# +Failure+ rolls everything back; a +Success+ commits (decision D9).
|
|
48
|
+
module RunnerTransaction
|
|
49
|
+
# @return [Result]
|
|
50
|
+
def run
|
|
51
|
+
config = transaction_config
|
|
52
|
+
return super unless config
|
|
53
|
+
|
|
54
|
+
result = nil
|
|
55
|
+
model = resolve_model(config[:model])
|
|
56
|
+
model.transaction(**transaction_options(model, config[:options])) do
|
|
57
|
+
result = super
|
|
58
|
+
raise Rollback if result.failure?
|
|
59
|
+
end
|
|
60
|
+
result
|
|
61
|
+
rescue Rollback
|
|
62
|
+
result
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# @return [Hash{Symbol=>Object}, nil] the running action's transaction config
|
|
68
|
+
def transaction_config
|
|
69
|
+
klass = @instance.class
|
|
70
|
+
klass.respond_to?(:transaction_config) ? klass.transaction_config : nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The configured options, plus +requires_new: true+ when a transaction is
|
|
74
|
+
# already open and the action did not say otherwise. Joining the outer
|
|
75
|
+
# transaction would swallow this run's rollback, so a recorded failure's
|
|
76
|
+
# writes would commit with it (decision D9).
|
|
77
|
+
#
|
|
78
|
+
# @param model [Class]
|
|
79
|
+
# @param options [Hash{Symbol=>Object}]
|
|
80
|
+
# @return [Hash{Symbol=>Object}]
|
|
81
|
+
def transaction_options(model, options)
|
|
82
|
+
return options if options.key?(:requires_new)
|
|
83
|
+
return options unless model.respond_to?(:connection) && model.connection.transaction_open?
|
|
84
|
+
|
|
85
|
+
options.merge(requires_new: true)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param model [Symbol, Class]
|
|
89
|
+
# @return [Class] the resolved model class
|
|
90
|
+
def resolve_model(model)
|
|
91
|
+
model.is_a?(Class) ? model : model.to_s.camelize.constantize
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Optional Rails adapter (decisions D8/D9). This file is NEVER required by the
|
|
4
|
+
# core entry point — load it explicitly with `require 'actionable/rails'` (in a
|
|
5
|
+
# Rails app, the Railtie does so during boot). It is the only place that pulls
|
|
6
|
+
# in active_*.
|
|
7
|
+
require_relative '../actionable' # ensure the core is loaded (self-contained entry)
|
|
8
|
+
require 'active_model'
|
|
9
|
+
require 'active_support/core_ext/string/inflections' # camelize/constantize
|
|
10
|
+
|
|
11
|
+
require_relative 'rails/transactions'
|
|
12
|
+
require_relative 'rails/proxy_validator'
|
|
13
|
+
require_relative 'rails/after_commit'
|
|
14
|
+
|
|
15
|
+
# Wire the adapter into the core: the transactional and after_commit macros
|
|
16
|
+
# onto Action, and their behavior onto the Runner. RunnerAfterCommit is
|
|
17
|
+
# prepended last so it wraps RunnerTransaction: a run registers its
|
|
18
|
+
# after_commit hooks only once its own transaction is over.
|
|
19
|
+
Actionable::Action.extend(Actionable::Rails::Transactions)
|
|
20
|
+
Actionable::Action.singleton_class.prepend(Actionable::Rails::AfterCommit)
|
|
21
|
+
Actionable::Runner.prepend(Actionable::Rails::RunnerTransaction)
|
|
22
|
+
Actionable::Runner.prepend(Actionable::Rails::RunnerAfterCommit)
|
|
23
|
+
|
|
24
|
+
require_relative 'railtie' if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Actionable
|
|
4
|
+
# Integrates the optional Rails adapter with the host application's boot
|
|
5
|
+
# process (decision D8). Loaded by +actionable/rails+ only when running inside
|
|
6
|
+
# a Rails application; it is the hook point for future Rails-specific wiring
|
|
7
|
+
# (generators, initializers). The core gem never references Rails.
|
|
8
|
+
class Railtie < ::Rails::Railtie
|
|
9
|
+
end
|
|
10
|
+
end
|