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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa21a99408ee10d9f9b17e06af87f2f51d099fd593fadec8672c71b15110a53e
4
- data.tar.gz: 893a107d52489ffc4e7680f558dbe8dfce0c8bdf88ad05bd4a704efd0eeb97ce
3
+ metadata.gz: 2c51e21ab14ee2a2a3994320929a81e9e06a5eae4247c3ef3415f4c86f901b93
4
+ data.tar.gz: aa84a44d6e3d9b920c55952a7fefee48d186d185a5e6718b4c22577d3bcfef02
5
5
  SHA512:
6
- metadata.gz: eddd2d0d5c46fe75338a095098a5391eb3aa81b29196f4fd352458ba6e6efc75673959ac59b1b40c555ec1a2f8914524503f718017845f636b030577578dc3f6
7
- data.tar.gz: c57f6bb79093b02c19abc8c5246bb741c4cf23d16c24dc2e1857e7941457e02ddd4a2e83441b767302bf6c4322bb88a087703bf2480031ac0bc7aa5e86470607
6
+ metadata.gz: a28819da667269a81dbc569f1deffdb9b1fbb2d48308106b1662f1abfddbb55d8ef7df6ca3cec4007c19088b6980559725a2f1187dcee577fd847e3dc95f811b
7
+ data.tar.gz: 1811203127bd94c7537355ccb974b1b7dd29dcbd40291075bcd43c5c5e9408929aa8bf4eb2b559de56b978666e05fa3baa99ba443b0498458f5f6ca9bb3417f7
@@ -1,112 +1,111 @@
1
1
  module Funes
2
- # Test helper for testing projections in isolation.
2
+ # Test helper for exercising projections in isolation.
3
3
  #
4
- # Include this module in your test classes to access helper methods that allow you to test
5
- # individual projection interpretations without needing to process entire event streams.
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
- # @example Include in your test
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
- # initial_state = InventorySnapshot.new(quantity_on_hand: 10)
17
+ # state = InventorySnapshot.new(quantity_on_hand: 10)
13
18
  # event = Inventory::ItemReceived.new(quantity: 5, unit_cost: 9.99)
14
19
  #
15
- # result = interpret_event_based_on(InventorySnapshotProjection, event, initial_state)
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
- # Test a single event interpretation in isolation.
22
- #
23
- # This method extracts and executes a single interpretation block from a projection,
24
- # allowing you to test how specific events transform state without processing entire
25
- # event streams.
26
- #
27
- # @param [Class<Funes::Projection>] projection_class The projection class being tested.
28
- # @param [Funes::Event] event_instance The event to interpret.
29
- # @param [ActiveModel::Model, ActiveRecord::Base] previous_state The state before applying the event.
30
- # @param [Time] at The temporal reference point. Defaults to Time.current.
31
- # @return [ActiveModel::Model, ActiveRecord::Base] The new state after applying the interpretation.
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
- # @example Test a single interpretation
34
- # initial_state = OrderSnapshot.new(total: 100)
35
- # event = Order::ItemAdded.new(amount: 50)
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
- # result = interpret_event_based_on(OrderSnapshotProjection, event, initial_state)
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
- # @example Test with validations
42
- # state = InventorySnapshot.new(quantity_on_hand: 5)
43
- # event = Inventory::ItemShipped.new(quantity: 10)
44
- #
45
- # result = interpret_event_based_on(InventorySnapshotProjection, event, state)
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
- # Test an initial_state block in isolation.
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 [Class<Funes::Projection>] projection_class The projection class being tested.
63
- # @param [Time] at The temporal reference point. Defaults to Time.current.
64
- # @return [ActiveModel::Model, ActiveRecord::Base] The initial state produced by the projection.
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 Test initial state construction
67
- # result = build_initial_state_based_on(InventorySnapshotProjection)
68
- #
69
- # assert_equal 0, result.quantity_on_hand
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
- # Test a final_state block in isolation.
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
- # @example Test final state transformation
94
- # state = OrderSnapshot.new(total: 100, item_count: 3)
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
- # result = apply_final_state_based_on(OrderSnapshotProjection, state)
97
- #
98
- # assert_equal 33.33, result.average_item_price
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(previous_state, at)
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
@@ -1,3 +1,3 @@
1
1
  module Funes
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.4"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: funes-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vinícius Almeida da Silva