Skip to content

Commit fd594c1

Browse files
committed
Add ActiveRecord plugin
1 parent 4e6dc49 commit fd594c1

4 files changed

Lines changed: 1167 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## [1.4.0] - 2026-04-01
2+
### Added
3+
- Add `:active_record` plugin
4+
15
## [1.3.0] - 2026-03-25
26
### Changed
37
- Removed support for `Ruby` versions older than 3.3
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# frozen_string_literal: true
2+
3+
require "active_record"
4+
5+
module Pathway
6+
module Plugins
7+
module ActiveRecord
8+
module DSLMethods
9+
def transaction(step_name = nil, if: nil, unless: nil, &steps)
10+
_with_db_steps(steps, step_name, *_opts_if_unless(binding)) do |runner|
11+
::ActiveRecord::Base.transaction(requires_new: true) do
12+
raise ::ActiveRecord::Rollback if runner.call.failure?
13+
end
14+
end
15+
end
16+
17+
def after_commit(step_name = nil, if: nil, unless: nil, &steps)
18+
_with_db_steps(steps, step_name, *_opts_if_unless(binding)) do |runner, state|
19+
dsl_copy = _dsl_for(state)
20+
::ActiveRecord.after_all_transactions_commit { runner.call(dsl_copy) }
21+
end
22+
end
23+
24+
def after_rollback(step_name = nil, if: nil, unless: nil, &steps)
25+
_with_db_steps(steps, step_name, *_opts_if_unless(binding)) do |runner, state|
26+
dsl_copy = _dsl_for(state)
27+
::ActiveRecord::Base.current_transaction.after_rollback { runner.call(dsl_copy) }
28+
end
29+
end
30+
31+
private
32+
33+
def _opts_if_unless(bg) = %i[if unless].map { bg.local_variable_get(_1) }
34+
35+
def _with_db_steps(steps, step_name = nil, if_cond = nil, unless_cond = nil, &db_logic)
36+
raise ArgumentError, "options :if and :unless are mutually exclusive" if if_cond && unless_cond
37+
raise ArgumentError, "must provide either a step or a block but not both" if !step_name.nil? == !steps.nil?
38+
39+
steps ||= proc { step step_name }
40+
41+
if if_cond
42+
if_true(if_cond) { _with_db_steps(steps, &db_logic) }
43+
elsif unless_cond
44+
if_false(unless_cond) { _with_db_steps(steps, &db_logic) }
45+
else
46+
around(db_logic, &steps)
47+
end
48+
end
49+
end
50+
51+
module ClassMethods
52+
attr_accessor :model_class, :search_field, :model_not_found
53+
54+
def model(model_class, search_by: model_class.primary_key, set_result_key: true, set_context_param: true, error_message: nil)
55+
self.model_class = model_class
56+
self.search_field = search_by
57+
self.result_key = Inflector.underscore(Inflector.demodulize(model_class.name)).to_sym if set_result_key
58+
self.model_not_found = error_message || "#{Inflector.humanize(Inflector.underscore(Inflector.demodulize(model_class.name)))} not found".freeze
59+
60+
self.context(result_key => Contextualizer::OPTIONAL) if set_result_key && set_context_param
61+
end
62+
63+
def inherited(subclass)
64+
super
65+
subclass.model_class = model_class
66+
subclass.search_field = search_field
67+
subclass.model_not_found = model_not_found
68+
end
69+
end
70+
71+
module InstanceMethods
72+
extend Forwardable
73+
74+
delegate %i[model_class search_field model_not_found] => "self.class"
75+
76+
def fetch_model(state, from: model_class, search_by: search_field, using: search_by, to: result_key, overwrite: false, error_message: nil) # rubocop:disable Metrics/ParameterLists
77+
error_message ||= if from == model_class
78+
model_not_found
79+
elsif from.respond_to?(:name) || from.respond_to?(:model)
80+
from_name = (from.respond_to?(:name) ? from : from.model).name
81+
Inflector.humanize(Inflector.underscore(Inflector.demodulize(from_name))) + " not found"
82+
end
83+
84+
if state[to].nil? || overwrite
85+
wrap_if_present(state[:input][using], message: error_message)
86+
.then { |key| find_model_with(key, from, search_by, error_message) }
87+
.then { |model| state.update(to => model) }
88+
else
89+
state
90+
end
91+
end
92+
93+
def find_model_with(key, dataset = model_class, column = search_field, error_message = nil)
94+
wrap_if_present(dataset.find_by(column => key), message: error_message)
95+
end
96+
end
97+
98+
def self.apply(operation, model: nil, **)
99+
operation.model(model, **) if model
100+
end
101+
end
102+
end
103+
end

pathway.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ Gem::Specification.new do |spec|
3434
spec.add_development_dependency "dry-validation", ">= 1.0"
3535
spec.add_development_dependency "bundler", ">= 2.4.10"
3636
spec.add_development_dependency "sequel", "~> 5.0"
37+
spec.add_development_dependency "sqlite3", "~> 2.1"
38+
spec.add_development_dependency "activerecord", ">= 7.2"
3739
spec.add_development_dependency "rake", "~> 13.0"
3840
spec.add_development_dependency "rspec", "~> 3.11"
3941
spec.add_development_dependency "simplecov-lcov", "~> 0.8.0"

0 commit comments

Comments
 (0)