funes-rails 0.2.3 → 0.2.4
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 +4 -4
- data/app/helpers/funes/projection_test_helper.rb +80 -81
- data/lib/funes/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c51e21ab14ee2a2a3994320929a81e9e06a5eae4247c3ef3415f4c86f901b93
|
|
4
|
+
data.tar.gz: aa84a44d6e3d9b920c55952a7fefee48d186d185a5e6718b4c22577d3bcfef02
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a28819da667269a81dbc569f1deffdb9b1fbb2d48308106b1662f1abfddbb55d8ef7df6ca3cec4007c19088b6980559725a2f1187dcee577fd847e3dc95f811b
|
|
7
|
+
data.tar.gz: 1811203127bd94c7537355ccb974b1b7dd29dcbd40291075bcd43c5c5e9408929aa8bf4eb2b559de56b978666e05fa3baa99ba443b0498458f5f6ca9bb3417f7
|
|
@@ -1,112 +1,111 @@
|
|
|
1
1
|
module Funes
|
|
2
|
-
# Test helper for
|
|
2
|
+
# Test helper for exercising projections in isolation.
|
|
3
3
|
#
|
|
4
|
-
# Include this module in your test
|
|
5
|
-
#
|
|
4
|
+
# Include this module in your test class to access methods that interpret a
|
|
5
|
+
# single event, build the initial state, or apply the final state of a
|
|
6
|
+
# projection — without processing an entire event stream.
|
|
6
7
|
#
|
|
7
|
-
#
|
|
8
|
+
# Bind the projection class once with the +projection+ macro and then call
|
|
9
|
+
# {#interpret}, {#initial_state}, or {#final_state} without repeating it.
|
|
10
|
+
#
|
|
11
|
+
# @example Bind the projection at the class level
|
|
8
12
|
# class InventorySnapshotProjectionTest < ActiveSupport::TestCase
|
|
9
13
|
# include Funes::ProjectionTestHelper
|
|
14
|
+
# projection InventorySnapshotProjection
|
|
10
15
|
#
|
|
11
16
|
# test "receiving items increases quantity on hand" do
|
|
12
|
-
#
|
|
17
|
+
# state = InventorySnapshot.new(quantity_on_hand: 10)
|
|
13
18
|
# event = Inventory::ItemReceived.new(quantity: 5, unit_cost: 9.99)
|
|
14
19
|
#
|
|
15
|
-
# result =
|
|
20
|
+
# result = interpret(event, given: state)
|
|
16
21
|
#
|
|
17
22
|
# assert_equal 15, result.quantity_on_hand
|
|
18
23
|
# end
|
|
19
24
|
# end
|
|
25
|
+
#
|
|
26
|
+
# @example Override the bound projection per call
|
|
27
|
+
# interpret(event, given: state, projection: AnotherProjection)
|
|
20
28
|
module ProjectionTestHelper
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
extend ActiveSupport::Concern
|
|
30
|
+
|
|
31
|
+
included do
|
|
32
|
+
class_attribute :projection_under_test, instance_accessor: false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class_methods do
|
|
36
|
+
# Binds the projection class exercised by this test class. Subclasses
|
|
37
|
+
# inherit the binding and may override it with another call to
|
|
38
|
+
# +projection+.
|
|
39
|
+
#
|
|
40
|
+
# @param [Class<Funes::Projection>] projection_class
|
|
41
|
+
# @return [void]
|
|
42
|
+
def projection(projection_class)
|
|
43
|
+
self.projection_under_test = projection_class
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Interprets a single event in isolation and returns the resulting state.
|
|
32
48
|
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
49
|
+
# If the event carries an +occurred_at+, it takes precedence over +at+ —
|
|
50
|
+
# mirroring how {Funes::Projection} replays persisted events. A +Date+
|
|
51
|
+
# passed as +at+ is coerced to its +beginning_of_day+.
|
|
36
52
|
#
|
|
37
|
-
#
|
|
53
|
+
# @param [Funes::Event] event The event to interpret.
|
|
54
|
+
# @param [ActiveModel::Model, ActiveRecord::Base] given The state before applying the event.
|
|
55
|
+
# @param [Time, Date] at The temporal reference point. Defaults to +Time.current+.
|
|
56
|
+
# @param [Class<Funes::Projection>, nil] projection Overrides the class bound via +projection+.
|
|
57
|
+
# @return [ActiveModel::Model, ActiveRecord::Base] The state after the interpretation.
|
|
38
58
|
#
|
|
59
|
+
# @example
|
|
60
|
+
# result = interpret(Order::ItemAdded.new(amount: 50), given: OrderSnapshot.new(total: 100))
|
|
39
61
|
# assert_equal 150, result.total
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
#
|
|
47
|
-
# assert_equal -5, result.quantity_on_hand
|
|
48
|
-
# refute result.valid?
|
|
49
|
-
def interpret_event_based_on(projection_class, event_instance, previous_state, at = Time.current)
|
|
50
|
-
at = at.beginning_of_day if at.is_a?(Date) && !at.is_a?(Time)
|
|
51
|
-
event_at = event_instance.occurred_at || at
|
|
52
|
-
projection_class.instance_variable_get(:@interpretations)[event_instance.class]
|
|
53
|
-
.call(previous_state, event_instance, event_at)
|
|
62
|
+
def interpret(event, given:, at: Time.current, projection: nil)
|
|
63
|
+
projection_class = resolve_projection(projection)
|
|
64
|
+
coerced_at = coerce_at(at)
|
|
65
|
+
event_at = event.occurred_at || coerced_at
|
|
66
|
+
projection_class.instance_variable_get(:@interpretations)[event.class]
|
|
67
|
+
.call(given, event, event_at)
|
|
54
68
|
end
|
|
55
69
|
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
# This method extracts and executes the initial_state block from a projection,
|
|
59
|
-
# allowing you to test how the projection builds its starting state without
|
|
60
|
-
# processing entire event streams.
|
|
70
|
+
# Builds the initial state of the bound projection.
|
|
61
71
|
#
|
|
62
|
-
# @param [
|
|
63
|
-
# @param [
|
|
64
|
-
# @return [ActiveModel::Model, ActiveRecord::Base] The
|
|
72
|
+
# @param [Time, Date] at The temporal reference point. Defaults to +Time.current+.
|
|
73
|
+
# @param [Class<Funes::Projection>, nil] projection Overrides the class bound via +projection+.
|
|
74
|
+
# @return [ActiveModel::Model, ActiveRecord::Base] The state produced by the +initial_state+ block.
|
|
65
75
|
#
|
|
66
|
-
# @example
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
#
|
|
71
|
-
# @example Test with a specific timestamp
|
|
72
|
-
# at = Time.new(2023, 5, 10)
|
|
73
|
-
#
|
|
74
|
-
# result = build_initial_state_based_on(InventorySnapshotProjection, at)
|
|
75
|
-
#
|
|
76
|
-
# assert_equal at, result.created_at
|
|
77
|
-
def build_initial_state_based_on(projection_class, at = Time.current)
|
|
76
|
+
# @example
|
|
77
|
+
# assert_equal 0, initial_state.quantity_on_hand
|
|
78
|
+
def initial_state(at: Time.current, projection: nil)
|
|
79
|
+
projection_class = resolve_projection(projection)
|
|
78
80
|
projection_class.instance_variable_get(:@interpretations)[:init]
|
|
79
|
-
.call(projection_class.instance_variable_get(:@materialization_model), at)
|
|
81
|
+
.call(projection_class.instance_variable_get(:@materialization_model), coerce_at(at))
|
|
80
82
|
end
|
|
81
83
|
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
# This method extracts and executes the final_state block from a projection,
|
|
85
|
-
# allowing you to test how the projection transforms state after all event
|
|
86
|
-
# interpretations have been applied.
|
|
87
|
-
#
|
|
88
|
-
# @param [Class<Funes::Projection>] projection_class The projection class being tested.
|
|
89
|
-
# @param [ActiveModel::Model, ActiveRecord::Base] previous_state The state before applying the final transformation.
|
|
90
|
-
# @param [Time] at The temporal reference point. Defaults to Time.current.
|
|
91
|
-
# @return [ActiveModel::Model, ActiveRecord::Base] The final state after applying the transformation.
|
|
84
|
+
# Applies the final-state transformation of the bound projection.
|
|
92
85
|
#
|
|
93
|
-
# @
|
|
94
|
-
#
|
|
86
|
+
# @param [ActiveModel::Model, ActiveRecord::Base] given The state to finalize.
|
|
87
|
+
# @param [Time, Date] at The temporal reference point. Defaults to +Time.current+.
|
|
88
|
+
# @param [Class<Funes::Projection>, nil] projection Overrides the class bound via +projection+.
|
|
89
|
+
# @return [ActiveModel::Model, ActiveRecord::Base] The state after the +final_state+ block.
|
|
95
90
|
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
# @example Test with a specific timestamp
|
|
101
|
-
# state = InventorySnapshot.new(quantity_on_hand: 10)
|
|
102
|
-
# at = Time.new(2023, 5, 10)
|
|
103
|
-
#
|
|
104
|
-
# result = apply_final_state_based_on(InventorySnapshotProjection, state, at)
|
|
105
|
-
#
|
|
106
|
-
# assert_equal at, result.finalized_at
|
|
107
|
-
def apply_final_state_based_on(projection_class, previous_state, at = Time.current)
|
|
91
|
+
# @example
|
|
92
|
+
# assert_equal 33.33, final_state(given: OrderSnapshot.new(total: 100, item_count: 3)).average_item_price
|
|
93
|
+
def final_state(given:, at: Time.current, projection: nil)
|
|
94
|
+
projection_class = resolve_projection(projection)
|
|
108
95
|
projection_class.instance_variable_get(:@interpretations)[:final]
|
|
109
|
-
.call(
|
|
96
|
+
.call(given, coerce_at(at))
|
|
110
97
|
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
def resolve_projection(explicit)
|
|
101
|
+
explicit || self.class.projection_under_test ||
|
|
102
|
+
raise(ArgumentError,
|
|
103
|
+
"No projection bound. Declare `projection ProjectionClass` in your test class " \
|
|
104
|
+
"or pass `projection:` explicitly.")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def coerce_at(at)
|
|
108
|
+
at.is_a?(Date) && !at.is_a?(Time) ? at.beginning_of_day : at
|
|
109
|
+
end
|
|
111
110
|
end
|
|
112
111
|
end
|
data/lib/funes/version.rb
CHANGED