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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +190 -0
- data/lib/generators/rage_arch/ar_dep_generator.rb +74 -0
- data/lib/generators/rage_arch/dep_generator.rb +120 -0
- data/lib/generators/rage_arch/dep_switch_generator.rb +224 -0
- data/lib/generators/rage_arch/install_generator.rb +64 -0
- data/lib/generators/rage_arch/scaffold_generator.rb +133 -0
- data/lib/generators/rage_arch/templates/ar_dep.rb.tt +46 -0
- data/lib/generators/rage_arch/templates/dep.rb.tt +16 -0
- data/lib/generators/rage_arch/templates/rage_arch.rb.tt +15 -0
- data/lib/generators/rage_arch/templates/scaffold/api_controller.rb.tt +39 -0
- data/lib/generators/rage_arch/templates/scaffold/controller.rb.tt +56 -0
- data/lib/generators/rage_arch/templates/scaffold/create.rb.tt +14 -0
- data/lib/generators/rage_arch/templates/scaffold/destroy.rb.tt +15 -0
- data/lib/generators/rage_arch/templates/scaffold/list.rb.tt +13 -0
- data/lib/generators/rage_arch/templates/scaffold/new.rb.tt +13 -0
- data/lib/generators/rage_arch/templates/scaffold/post_repo.rb.tt +35 -0
- data/lib/generators/rage_arch/templates/scaffold/show.rb.tt +14 -0
- data/lib/generators/rage_arch/templates/scaffold/update.rb.tt +15 -0
- data/lib/generators/rage_arch/templates/use_case.rb.tt +18 -0
- data/lib/generators/rage_arch/use_case_generator.rb +33 -0
- data/lib/rage_arch/container.rb +38 -0
- data/lib/rage_arch/controller.rb +22 -0
- data/lib/rage_arch/dep.rb +9 -0
- data/lib/rage_arch/dep_scanner.rb +95 -0
- data/lib/rage_arch/deps/active_record.rb +45 -0
- data/lib/rage_arch/event_publisher.rb +59 -0
- data/lib/rage_arch/fake_event_publisher.rb +37 -0
- data/lib/rage_arch/railtie.rb +23 -0
- data/lib/rage_arch/result.rb +31 -0
- data/lib/rage_arch/rspec_matchers.rb +94 -0
- data/lib/rage_arch/use_case.rb +252 -0
- data/lib/rage_arch/version.rb +5 -0
- data/lib/rage_arch.rb +97 -0
- 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
|