rage_arch 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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +190 -0
  4. data/lib/generators/rage_arch/ar_dep_generator.rb +74 -0
  5. data/lib/generators/rage_arch/dep_generator.rb +120 -0
  6. data/lib/generators/rage_arch/dep_switch_generator.rb +224 -0
  7. data/lib/generators/rage_arch/install_generator.rb +64 -0
  8. data/lib/generators/rage_arch/scaffold_generator.rb +133 -0
  9. data/lib/generators/rage_arch/templates/ar_dep.rb.tt +46 -0
  10. data/lib/generators/rage_arch/templates/dep.rb.tt +16 -0
  11. data/lib/generators/rage_arch/templates/rage_arch.rb.tt +15 -0
  12. data/lib/generators/rage_arch/templates/scaffold/api_controller.rb.tt +39 -0
  13. data/lib/generators/rage_arch/templates/scaffold/controller.rb.tt +56 -0
  14. data/lib/generators/rage_arch/templates/scaffold/create.rb.tt +14 -0
  15. data/lib/generators/rage_arch/templates/scaffold/destroy.rb.tt +15 -0
  16. data/lib/generators/rage_arch/templates/scaffold/list.rb.tt +13 -0
  17. data/lib/generators/rage_arch/templates/scaffold/new.rb.tt +13 -0
  18. data/lib/generators/rage_arch/templates/scaffold/post_repo.rb.tt +35 -0
  19. data/lib/generators/rage_arch/templates/scaffold/show.rb.tt +14 -0
  20. data/lib/generators/rage_arch/templates/scaffold/update.rb.tt +15 -0
  21. data/lib/generators/rage_arch/templates/use_case.rb.tt +18 -0
  22. data/lib/generators/rage_arch/use_case_generator.rb +33 -0
  23. data/lib/rage_arch/container.rb +38 -0
  24. data/lib/rage_arch/controller.rb +22 -0
  25. data/lib/rage_arch/dep.rb +9 -0
  26. data/lib/rage_arch/dep_scanner.rb +95 -0
  27. data/lib/rage_arch/deps/active_record.rb +45 -0
  28. data/lib/rage_arch/event_publisher.rb +59 -0
  29. data/lib/rage_arch/fake_event_publisher.rb +37 -0
  30. data/lib/rage_arch/railtie.rb +23 -0
  31. data/lib/rage_arch/result.rb +31 -0
  32. data/lib/rage_arch/rspec_matchers.rb +94 -0
  33. data/lib/rage_arch/use_case.rb +252 -0
  34. data/lib/rage_arch/version.rb +5 -0
  35. data/lib/rage_arch.rb +97 -0
  36. metadata +133 -0
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module RageArch
6
+ # Scans use case files to find dep symbols and the methods called on each dep.
7
+ # Used by the rage:dep generator to create stub classes with the right methods.
8
+ # Also tracks which use case path each symbol appears in (for folder inference).
9
+ class DepScanner
10
+ def initialize(use_cases_root = nil)
11
+ @use_cases_root = use_cases_root || default_use_cases_root
12
+ @paths_for = nil
13
+ end
14
+
15
+ # Returns Hash symbol => Set of method names (e.g. { repo: [:save, :delete], gateway: [:get, :post] })
16
+ def scan
17
+ result = Hash.new { |h, k| h[k] = Set.new }
18
+ @paths_for = Hash.new { |h, k| h[k] = Set.new }
19
+ return result unless @use_cases_root && File.directory?(@use_cases_root)
20
+
21
+ root = @use_cases_root.to_s
22
+ root = root.chomp(File::SEPARATOR) + File::SEPARATOR
23
+
24
+ Dir[File.join(@use_cases_root, "**", "*.rb")].each do |path|
25
+ relative = path.sub(/\A#{Regexp.escape(root)}/, "")
26
+ content = File.read(path)
27
+ scan_file_content(content, result, relative)
28
+ end
29
+ result
30
+ end
31
+
32
+ # Returns Set of method names for the given symbol, or empty Set if unknown.
33
+ def methods_for(symbol)
34
+ scan[symbol.to_sym]
35
+ end
36
+
37
+ # Returns the folder name (first path segment) from use cases that reference this symbol,
38
+ # e.g. "likes" when the symbol is used in app/use_cases/likes/create.rb. Returns nil if
39
+ # no use cases reference the symbol or they are all at use_cases root.
40
+ def folder_for(symbol)
41
+ scan
42
+ paths = @paths_for[symbol.to_sym]
43
+ return nil if paths.nil? || paths.empty?
44
+
45
+ first_path = paths.min
46
+ seg = first_path.split(File::SEPARATOR).first
47
+ (seg && seg != first_path) ? seg : nil
48
+ end
49
+
50
+ private
51
+
52
+ def default_use_cases_root
53
+ return nil unless defined?(Rails) && Rails.respond_to?(:root)
54
+ Rails.root.join("app", "use_cases").to_s
55
+ end
56
+
57
+ def scan_file_content(content, result, relative_path = nil)
58
+ # 1) Constructor deps: deps :repo, :gateway, :service
59
+ constructor_symbols = content.scan(/deps\s*([^\n]+)/).flat_map do |match|
60
+ match[0].scan(/:(\w+)/).flatten.map(&:to_sym)
61
+ end.uniq
62
+
63
+ constructor_symbols.each { |sym| @paths_for[sym] << relative_path } if relative_path && @paths_for
64
+
65
+ # 2) Dynamic dep assignments: store = dep(:repo) or store = dep(:repo, default: Order)
66
+ var_to_symbol = {}
67
+ content.scan(/(\w+)\s*=\s*dep\s*\(\s*:(\w+)/) do |var, sym|
68
+ var_to_symbol[var] = sym.to_sym
69
+ @paths_for[sym.to_sym] << relative_path if relative_path && @paths_for
70
+ end
71
+
72
+ # 3) All symbols we care about (constructor + dynamic)
73
+ all_symbols = (constructor_symbols + var_to_symbol.values).uniq
74
+
75
+ # 4) Inline calls: dep(:repo).save( or dep(:repo, default: X).update(
76
+ content.scan(/dep\s*\(\s*:(\w+)[^)]*\)\s*\.\s*(\w+)\s*[\(]/m) do |sym, method|
77
+ result[sym.to_sym] << method.to_sym
78
+ @paths_for[sym.to_sym] << relative_path if relative_path && @paths_for
79
+ end
80
+
81
+ # 5) Method calls on constructor dep (receiver name == symbol): repo.save(, gateway.get(, service.get_all_items
82
+ # Require a dot so we don't match "service\n def call" as service.def
83
+ constructor_symbols.each do |sym|
84
+ regex = /\b#{Regexp.escape(sym.to_s)}\.\s*(\w+)\s*[\(\s]/m
85
+ content.scan(regex) { |m| result[sym] << (m.is_a?(Array) ? m[0] : m).to_sym }
86
+ end
87
+
88
+ # 6) Method calls on assigned variable (require dot)
89
+ var_to_symbol.each do |var, sym|
90
+ regex = /\b#{Regexp.escape(var)}\.\s*(\w+)\s*[\(\s]/m
91
+ content.scan(regex) { |m| result[sym] << (m.is_a?(Array) ? m[0] : m).to_sym }
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RageArch
4
+ module Deps
5
+ # Helper to use an Active Record model as a dep (minimal adapter).
6
+ # Usage: RageArch::Deps::ActiveRecord.for(Order) → object exposing build, find, etc. on Order.
7
+ # In the container: Rage.register(:order_store, RageArch::Deps::ActiveRecord.for(Order))
8
+ class ActiveRecord
9
+ def self.for(model_class)
10
+ new(model_class)
11
+ end
12
+
13
+ def initialize(model_class)
14
+ @model_class = model_class
15
+ end
16
+
17
+ def find(id)
18
+ @model_class.find_by(id: id)
19
+ end
20
+
21
+ def build(attrs = {})
22
+ @model_class.new(attrs)
23
+ end
24
+
25
+ def save(record)
26
+ record.save
27
+ end
28
+
29
+ def update(record, attrs)
30
+ record.assign_attributes(attrs)
31
+ record.save
32
+ end
33
+
34
+ def destroy(record)
35
+ record.destroy
36
+ end
37
+
38
+ def list(filters: {})
39
+ scope = @model_class.all
40
+ filters.each { |key, value| scope = scope.where(key => value) if value.present? }
41
+ scope.to_a
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RageArch
4
+ # Simple in-process event publisher for domain events.
5
+ # Use cases can subscribe via subscribe :event_name (or :all) in the use case class; wire with Base.wire_subscriptions_to(publisher).
6
+ # Handlers (blocks, callables, or use case symbols) run synchronously. For subscribe :all, payload includes :event.
7
+ #
8
+ # Setup in config/initializers/rage_arch.rb:
9
+ # publisher = RageArch::EventPublisher.new
10
+ # RageArch::UseCase::Base.wire_subscriptions_to(publisher)
11
+ # Rage.register(:event_publisher, publisher)
12
+ class EventPublisher
13
+ def initialize
14
+ @handlers = Hash.new { |h, k| h[k] = [] }
15
+ @publish_depth = 0
16
+ @max_publish_depth = 100
17
+ end
18
+
19
+ # Subscribe to an event. Handler can be:
20
+ # - A block: called with payload (Hash with symbol keys)
21
+ # - A symbol: use case symbol; publisher runs UseCase::Base.build(symbol).call(payload)
22
+ # - Any callable responding to call(payload)
23
+ # For event_name :all, the payload passed to handlers includes :event (the name of the event being published).
24
+ def subscribe(event_name, handler = nil, &block)
25
+ callable = handler || block
26
+ callable = use_case_runner(handler) if handler.is_a?(Symbol)
27
+ raise ArgumentError, "Provide a block or a callable handler" unless callable
28
+ @handlers[event_name.to_sym] << callable
29
+ self
30
+ end
31
+
32
+ # Publish an event. Payload is passed as a Hash (symbol keys) to each handler.
33
+ # Handlers for this event run first; then handlers for :all run with payload.merge(event: event_name).
34
+ # Re-entrancy is limited to avoid infinite loops (e.g. a handler that publishes the same event).
35
+ def publish(event_name, **payload)
36
+ event_sym = event_name.to_sym
37
+ @publish_depth += 1
38
+ raise "Event publisher re-entrancy limit reached (possible circular publish)" if @publish_depth > @max_publish_depth
39
+ begin
40
+ @handlers[event_sym].each do |callable|
41
+ callable.call(payload)
42
+ end
43
+ all_payload = payload.merge(event: event_sym)
44
+ @handlers[:all].each do |callable|
45
+ callable.call(all_payload)
46
+ end
47
+ ensure
48
+ @publish_depth -= 1
49
+ end
50
+ nil
51
+ end
52
+
53
+ private
54
+
55
+ def use_case_runner(symbol)
56
+ ->(payload) { UseCase::Base.build(symbol).call(payload) }
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RageArch
4
+ # Event publisher that records all published events for tests. Does not run any handlers.
5
+ # Use it to assert that a use case (or code) published expected events.
6
+ #
7
+ # publisher = RageArch::FakeEventPublisher.new
8
+ # Rage.register(:event_publisher, publisher)
9
+ # RageArch::UseCase::Base.build(:create_post).call(title: "Hi")
10
+ # expect(publisher.published).to include(
11
+ # hash_including(event: :post_created, post_id: kind_of(Integer))
12
+ # )
13
+ # publisher.clear # optional: reset between examples
14
+ class FakeEventPublisher
15
+ attr_reader :published
16
+
17
+ def initialize
18
+ @published = []
19
+ end
20
+
21
+ # Same signature as EventPublisher#publish. Records the event and payload; does not run handlers.
22
+ def publish(event_name, **payload)
23
+ @published << { event: event_name.to_sym, payload: payload }
24
+ nil
25
+ end
26
+
27
+ def subscribe(_event_name, _handler = nil, &_block)
28
+ # No-op: we don't run handlers in tests
29
+ self
30
+ end
31
+
32
+ def clear
33
+ @published.clear
34
+ self
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rage_arch"
4
+ require_relative "controller"
5
+
6
+ module RageArch
7
+ class Railtie < ::Rails::Railtie
8
+ config.rage = ActiveSupport::OrderedOptions.new
9
+ config.rage.auto_publish_events = true
10
+ config.rage.verify_deps = true
11
+
12
+ # Load use case files so they register their symbols in the registry.
13
+ # Without this, build(:symbol) would fail until the use case constant was referenced.
14
+ config.after_initialize do |app|
15
+ use_cases_dir = app.root.join("app/use_cases")
16
+ if use_cases_dir.exist?
17
+ Dir[use_cases_dir.join("**/*.rb")].sort.each { |f| require f }
18
+ end
19
+
20
+ RageArch.verify_deps! if app.config.rage.verify_deps != false
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RageArch
4
+ # Result object for an operation: success (with value) or failure (with errors).
5
+ # Controllers use result.success?, result.value, and result.errors.
6
+ class Result
7
+ def self.success(value)
8
+ new(success: true, value: value, errors: [])
9
+ end
10
+
11
+ def self.failure(errors)
12
+ new(success: false, value: nil, errors: Array(errors))
13
+ end
14
+
15
+ attr_reader :value, :errors
16
+
17
+ def initialize(success:, value:, errors:)
18
+ @success = success
19
+ @value = value
20
+ @errors = errors
21
+ end
22
+
23
+ def success?
24
+ @success
25
+ end
26
+
27
+ def failure?
28
+ !@success
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RageArch
4
+ # RSpec matchers and helpers for testing Rage use cases and results.
5
+ # In spec_helper.rb or rails_helper.rb add:
6
+ # require "rage_arch/rspec_matchers"
7
+ # Then use: expect(result).to succeed_with(post: post) or expect(result).to fail_with_errors(["error"])
8
+ module RSpecMatchers
9
+ # Matcher: expect(result).to succeed_with(key: value, ...)
10
+ # Asserts result.success? and that result.value (when a Hash) includes the given key/value pairs.
11
+ # When result.value is not a Hash, pass a single expected value: expect(result).to succeed_with(42)
12
+ def succeed_with(*expected_list, **expected_hash)
13
+ SucceedWithMatcher.new(expected_list, expected_hash)
14
+ end
15
+
16
+ # Matcher: expect(result).to fail_with_errors(errors)
17
+ # Asserts result.failure? and that result.errors matches the given errors (array or RSpec matcher).
18
+ def fail_with_errors(errors)
19
+ FailWithErrorsMatcher.new(errors)
20
+ end
21
+
22
+ class SucceedWithMatcher
23
+ include RSpec::Matchers::Composable
24
+
25
+ def initialize(expected_list, expected_hash)
26
+ @expected_list = expected_list
27
+ @expected_hash = expected_hash
28
+ end
29
+
30
+ def matches?(result)
31
+ @result = result
32
+ return false unless result.respond_to?(:success?) && result.success?
33
+
34
+ if @expected_hash.any?
35
+ return false unless result.value.is_a?(Hash)
36
+ value = result.value
37
+ @expected_hash.all? do |k, v|
38
+ val = value.key?(k) ? value[k] : value[k.to_s]
39
+ values_match?(v, val)
40
+ end
41
+ elsif @expected_list.one?
42
+ values_match?(@expected_list.first, result.value)
43
+ else
44
+ @expected_list.empty? ? true : values_match?(@expected_list, result.value)
45
+ end
46
+ end
47
+
48
+ def failure_message
49
+ return "expected success but got failure (errors: #{@result.errors.inspect})" if @result.respond_to?(:failure?) && @result.failure?
50
+ return "expected result.value to match" unless @result.respond_to?(:value)
51
+ "expected result.value #{@result.value.inspect} to match #{description}"
52
+ end
53
+
54
+ def description
55
+ if @expected_hash.any?
56
+ "succeed with #{@expected_hash.inspect}"
57
+ else
58
+ "succeed with #{@expected_list.inspect}"
59
+ end
60
+ end
61
+ end
62
+
63
+ class FailWithErrorsMatcher
64
+ include RSpec::Matchers::Composable
65
+
66
+ def initialize(errors)
67
+ @expected_errors = errors
68
+ end
69
+
70
+ def matches?(result)
71
+ @result = result
72
+ return false unless result.respond_to?(:failure?) && result.failure?
73
+ return false unless result.respond_to?(:errors)
74
+ values_match?(@expected_errors, @result.errors)
75
+ end
76
+
77
+ def failure_message
78
+ if @result.respond_to?(:success?) && @result.success?
79
+ "expected failure but got success (value: #{@result.value.inspect})"
80
+ else
81
+ "expected result.errors #{@result.errors.inspect} to match #{@expected_errors.inspect}"
82
+ end
83
+ end
84
+
85
+ def description
86
+ "fail with errors #{@expected_errors.inspect}"
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ RSpec.configure do |config|
93
+ config.include RageArch::RSpecMatchers
94
+ end
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RageArch
4
+ module UseCase
5
+ # Runs another use case by symbol. Used when a use case declares use_cases :other_symbol.
6
+ class Runner
7
+ def initialize(symbol)
8
+ @symbol = symbol
9
+ end
10
+
11
+ def call(*args, **kwargs)
12
+ Base.build(@symbol).call(*args, **kwargs)
13
+ end
14
+ end
15
+
16
+ # Base for use cases: register by symbol, deps injected via constructor.
17
+ #
18
+ # Usage:
19
+ # class CreateOrder < RageArch::UseCase::Base
20
+ # use_case_symbol :create_order
21
+ # deps :order_store, :notifications
22
+ #
23
+ # def call(params = {})
24
+ # order = order_store.build(params)
25
+ # success(order)
26
+ # end
27
+ # end
28
+ #
29
+ # Building: RageArch::UseCase::Base.build(:create_order) resolves deps from
30
+ # RageArch::Container and returns an instance of the use case.
31
+ class Base
32
+ module Instrumentation
33
+ def call(params = {})
34
+ sym = self.class.use_case_symbol
35
+ if defined?(ActiveSupport::Notifications)
36
+ ActiveSupport::Notifications.instrument("rage.use_case.run", symbol: sym, params: params) do |payload|
37
+ result = super(params)
38
+ payload[:success] = result.success?
39
+ payload[:errors] = result.errors unless result.success?
40
+ payload[:result] = result
41
+ auto_publish_if_enabled(sym, params, result)
42
+ result
43
+ end
44
+ else
45
+ result = super(params)
46
+ auto_publish_if_enabled(sym, params, result)
47
+ result
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def auto_publish_if_enabled(use_case_symbol, params, result)
54
+ return unless auto_publish_enabled?
55
+ return unless self.class.container.registered?(:event_publisher)
56
+ publisher = self.class.container.resolve(:event_publisher)
57
+ publisher.publish(
58
+ use_case_symbol,
59
+ use_case: use_case_symbol,
60
+ params: params,
61
+ success: result.success?,
62
+ value: result.value,
63
+ errors: result.errors
64
+ )
65
+ end
66
+
67
+ def auto_publish_enabled?
68
+ return false if self.class.skip_auto_publish?
69
+ return true unless defined?(Rails) && Rails.application.config.respond_to?(:rage) && Rails.application.config.rage
70
+ Rails.application.config.rage.auto_publish_events != false
71
+ end
72
+ end
73
+
74
+ include Instrumentation
75
+
76
+ class << self
77
+ def inherited(subclass)
78
+ super
79
+ subclass.prepend(Instrumentation)
80
+ end
81
+ def use_case_symbol(sym = nil)
82
+ if sym
83
+ @use_case_symbol = sym
84
+ Base.registry[sym] = self
85
+ sym
86
+ else
87
+ @use_case_symbol
88
+ end
89
+ end
90
+
91
+ def deps(*symbols)
92
+ @declared_deps ||= []
93
+ @declared_deps.concat(symbols)
94
+ symbols.each do |sym|
95
+ define_method(sym) { injected_deps.fetch(sym) }
96
+ end
97
+ private(*symbols) if symbols.any?
98
+ symbols
99
+ end
100
+
101
+ # Declare other use cases this one can call. In call(), use e.g. posts_create.call(params)
102
+ # to run that use case and get its Result. Same layer can reference by symbol.
103
+ def use_cases(*symbols)
104
+ @declared_use_cases ||= []
105
+ @declared_use_cases.concat(symbols)
106
+ symbols.each do |sym|
107
+ define_method(sym) { Runner.new(sym) }
108
+ end
109
+ private(*symbols) if symbols.any?
110
+ symbols
111
+ end
112
+
113
+ def declared_use_cases
114
+ @declared_use_cases || []
115
+ end
116
+
117
+ # Subscribe this use case to domain events. When the event is published, this use case's call(payload) runs.
118
+ # You can subscribe to multiple events: subscribe :post_created, :post_updated
119
+ # Special: subscribe :all to run on every published event (payload will include :event).
120
+ def subscribe(*event_names)
121
+ @subscribed_events ||= []
122
+ @subscribed_events.concat(event_names.map(&:to_sym))
123
+ event_names
124
+ end
125
+
126
+ def subscribed_events
127
+ @subscribed_events || []
128
+ end
129
+
130
+ # Opt-out of auto-publish when this use case finishes (e.g. for the logger use case itself).
131
+ def skip_auto_publish
132
+ @skip_auto_publish = true
133
+ end
134
+
135
+ def skip_auto_publish?
136
+ @skip_auto_publish == true
137
+ end
138
+
139
+ # Call once after loading use cases and before registering the publisher. Registers each use case's
140
+ # subscribed_events with the publisher so they run when those events are published.
141
+ def wire_subscriptions_to(publisher)
142
+ registry.each do |symbol, klass|
143
+ next unless klass.respond_to?(:subscribed_events)
144
+ klass.subscribed_events.each do |event_name|
145
+ publisher.subscribe(event_name, symbol)
146
+ end
147
+ end
148
+ nil
149
+ end
150
+
151
+ # Dep that uses Active Record for the model when not registered in the container.
152
+ # Example: ar_dep :user_store, User (instead of default: RageArch::Deps::ActiveRecord.for(User))
153
+ def ar_dep(symbol, model_class)
154
+ @ar_deps ||= {}
155
+ @ar_deps[symbol] = model_class
156
+ deps(symbol)
157
+ end
158
+
159
+ def ar_deps
160
+ @ar_deps || {}
161
+ end
162
+
163
+ def declared_deps
164
+ @declared_deps || []
165
+ end
166
+
167
+ def registry
168
+ @registry ||= {}
169
+ end
170
+
171
+ def resolve(symbol)
172
+ Base.registry[symbol] or raise KeyError, "Use case not registered: #{symbol.inspect}"
173
+ end
174
+
175
+ def build(symbol)
176
+ klass = Base.resolve(symbol)
177
+ deps_hash = klass.declared_deps.uniq.to_h do |s|
178
+ if klass.ar_deps.key?(s)
179
+ impl = container.registered?(s) ? container.resolve(s) : Deps::ActiveRecord.for(klass.ar_deps[s])
180
+ [s, impl]
181
+ else
182
+ [s, container.resolve(s)]
183
+ end
184
+ end
185
+ klass.new(**deps_hash)
186
+ end
187
+
188
+ def container
189
+ RageArch::Container
190
+ end
191
+ end
192
+
193
+ def initialize(**injected_deps)
194
+ @injected_deps = injected_deps
195
+ end
196
+
197
+ def call(_params = {})
198
+ raise NotImplementedError, "#{self.class}#call must be implemented"
199
+ end
200
+
201
+ # From a use case: success(value) and failure(errors) instead of RageArch::Result.success/failure.
202
+ def success(value = nil)
203
+ RageArch::Result.success(value)
204
+ end
205
+
206
+ def failure(errors)
207
+ RageArch::Result.failure(errors)
208
+ end
209
+
210
+ private
211
+
212
+ def injected_deps
213
+ @injected_deps ||= {}
214
+ end
215
+
216
+ # Resolve a dep: first the injected one; if missing, use the container.
217
+ # Optional: dep(:symbol, default: Implementation) when not registered.
218
+ # If default is an Active Record model class (e.g. Order), it is wrapped automatically
219
+ # with RageArch::Deps::ActiveRecord.for(default), so you can write dep(:order_store, default: Order).
220
+ def dep(symbol, default: nil)
221
+ return injected_deps[symbol] if injected_deps.key?(symbol)
222
+
223
+ if container.registered?(symbol)
224
+ container.resolve(symbol)
225
+ elsif default
226
+ impl = resolve_default(default)
227
+ impl.is_a?(Class) ? impl.new : impl
228
+ else
229
+ raise KeyError, "Dep not registered and no default: #{symbol.inspect}"
230
+ end
231
+ end
232
+
233
+ def resolve_default(default)
234
+ if default.is_a?(Class) && active_record_model?(default)
235
+ RageArch::Deps::ActiveRecord.for(default)
236
+ else
237
+ default
238
+ end
239
+ end
240
+
241
+ def active_record_model?(klass)
242
+ defined?(ActiveRecord::Base) && klass < ActiveRecord::Base
243
+ rescue
244
+ false
245
+ end
246
+
247
+ def container
248
+ self.class.container
249
+ end
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RageArch
4
+ VERSION = "0.1.0"
5
+ end