statecraft 0.5.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55f4ecb6760d01b042ad158351ec8735c3962c70c3a674d3af9d6ba2ecf26fd9
4
- data.tar.gz: ecc0c3ac410d67431226ccc8b361ac5d2708c276425153561b0f732ef3aefa53
3
+ metadata.gz: 4689cea0dee40236473975f52ea5ccd2ecd303783edebd7626ab1518a1f8c8bd
4
+ data.tar.gz: b3713ea5d55ea4881b55b10ceccb38847f2f85aa971d29326fd26e97412d0620
5
5
  SHA512:
6
- metadata.gz: caf25d1b5349c441aa79573e1994b2298ef64fcc4b4b9f6e6b5958827ca03a9bf8275366cf1b128a4cf39f690e6ba9bcad0327b040a51effcf42f5fff9853098
7
- data.tar.gz: 7acc79611c73b980a2a932c53d87723068adb1da7ddc61b590477cf84540a904750e9fa7d550d126aa9626f9edcc34538f6aed4aacb611894907148d80e07b63
6
+ metadata.gz: 9690fff2fb62feaaea147a53c63113a77dc93103432ad3f5aa63b5593173d513d2abf83d5d53decf0d6eed7347d94509197f5bb0d03077cdbd5a98f1f20cefd4
7
+ data.tar.gz: cbd7e8d84af570a4c2cb1e51582262860b2d6d0cd7d87ed0304528413ca1d34720dbe6c9b4186fab5a68639c6814243ed2fed32f9774d4985a5f4f5408b45ede
data/README.md CHANGED
@@ -260,6 +260,51 @@ belong to the application's presentation layer, not to the machine.
260
260
  A guard that reads metadata makes `may_*?` depend on the metadata you pass —
261
261
  pass the same metadata to `may_*?` that you will collect for `fire!`.
262
262
 
263
+ ## RSpec matchers
264
+
265
+ One opt-in require gives your specs matchers over the whole introspection
266
+ surface — RSpec never becomes a runtime dependency of the gem:
267
+
268
+ <!-- illustrative -->
269
+ ```ruby
270
+ # spec_helper.rb, after rspec itself is loaded
271
+ require "statecraft/rspec"
272
+ ```
273
+
274
+ <!-- illustrative -->
275
+ ```ruby
276
+ # The record-level questions consult the guards, with the same metadata
277
+ # your production call will carry:
278
+ expect(order).to allow_event(:pay).with_metadata("amount" => 100)
279
+ expect(order).to allow_transition_to(:cancelled).via(:cancel)
280
+ expect(order).to allow_transition_to(:archived).directly
281
+ expect(order).to have_transitioned_to(:paid) # strictly log-based
282
+
283
+ # The refusal with its reason — guard names come from refusals_for:
284
+ expect(order).to refuse_event(:cancel).because_of(:customer_cancellable?)
285
+
286
+ # The class-level pair answers the graph's shape; guards stay untouched:
287
+ expect(OrderFlow).to have_edge(:pending, :cancelled).via(:cancel)
288
+ expect(OrderFlow).to have_initial_state(:pending)
289
+
290
+ # The transition itself: the state move AND the appended log row,
291
+ # asserted in one expression around fire!/transition_to!:
292
+ expect { order.fire!(:pay, metadata: { "amount" => 100 }) }
293
+ .to transition(order).from(:pending).to(:paid)
294
+ .via_event(:pay).with_metadata("amount" => 100)
295
+ ```
296
+
297
+ A failing matcher explains itself with the same introspection the pipeline
298
+ consults: the current state, the edges reachable from it, and the refusing
299
+ guard with its layer. A non-bang call that returned `false` fails the
300
+ `transition` matcher the same way; exceptions of the bang forms fly through
301
+ like with `change` — assert refusals with `refuse_event` or `raise_error`,
302
+ not with the block matcher.
303
+
304
+ `because_of` carries the same honest limit as `refusals_for` underneath it:
305
+ it names record-layer guards only. An input-reading `guard:` has no name
306
+ there, and the failure message says so instead of guessing.
307
+
263
308
  ## Metadata
264
309
 
265
310
  Metadata is normalized on pipeline entry with a full JSON round-trip —
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statecraft
4
+ module RSpec
5
+ # expect(record).to allow_event(:pay).with_metadata("amount" => 5)
6
+ #
7
+ # The can_fire? question, asked with the same metadata the production
8
+ # call will carry. The failure message walks the layers the pipeline
9
+ # walks: is the event declared from this state, and if so, which
10
+ # guards said no.
11
+ class AllowEvent
12
+ def initialize(event_name)
13
+ @event_name = event_name.to_sym
14
+ @metadata = {}
15
+ end
16
+
17
+ def with_metadata(metadata)
18
+ @metadata = metadata
19
+ self
20
+ end
21
+
22
+ def matches?(record)
23
+ @record = record
24
+ record.can_fire?(@event_name, metadata: @metadata)
25
+ end
26
+
27
+ def failure_message
28
+ lines = ["expected #{StateReport.standing(@record)} to allow event #{@event_name.inspect}, but it was refused"]
29
+ if StateReport.event_declared?(@record, @event_name)
30
+ lines << StateReport.refusal(@record, @event_name, @metadata)
31
+ else
32
+ lines << "event #{@event_name.inspect} is not declared from #{StateReport.current_state(@record).inspect}"
33
+ lines << StateReport.declared_shape_of(@record)
34
+ end
35
+ lines.join("\n ")
36
+ end
37
+
38
+ def failure_message_when_negated
39
+ "expected #{StateReport.standing(@record)} not to allow event #{@event_name.inspect}, " \
40
+ "but the guards passed with metadata #{@metadata.inspect}"
41
+ end
42
+
43
+ def description
44
+ "allow event #{@event_name.inspect}"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statecraft
4
+ module RSpec
5
+ # expect(record).to allow_transition_to(:paid).via(:pay)
6
+ # expect(record).to allow_transition_to(:archived).directly
7
+ #
8
+ # The available_transitions prediction for one target: not only
9
+ # whether the record can go there, but HOW — via lists the events
10
+ # whose guards pass, directly asserts the guard-free direct way.
11
+ class AllowTransitionTo
12
+ def initialize(target_state)
13
+ @target_state = target_state.to_sym
14
+ @expected_via = []
15
+ @directly = false
16
+ @metadata = {}
17
+ end
18
+
19
+ def via(*event_names)
20
+ @expected_via = event_names.map(&:to_sym)
21
+ self
22
+ end
23
+
24
+ def directly
25
+ @directly = true
26
+ self
27
+ end
28
+
29
+ def with_metadata(metadata)
30
+ @metadata = metadata
31
+ self
32
+ end
33
+
34
+ def matches?(record)
35
+ @record = record
36
+ @availability = record.available_transitions(metadata: @metadata)
37
+ .find { |availability| availability.to == @target_state }
38
+ return false unless @availability
39
+
40
+ missing_ways.empty?
41
+ end
42
+
43
+ def failure_message
44
+ unless @availability
45
+ return ["expected #{StateReport.standing(@record)} to reach #{@target_state.inspect}, but it cannot",
46
+ StateReport.reachable(@record, @metadata),
47
+ StateReport.declared_shape_of(@record)].join("\n ")
48
+ end
49
+
50
+ "expected the way to #{@target_state.inspect} to include #{missing_ways.map(&:inspect).join(", ")}, " \
51
+ "but it is reachable via #{@availability.via.inspect}"
52
+ end
53
+
54
+ def failure_message_when_negated
55
+ "expected #{StateReport.standing(@record)} not to reach #{@target_state.inspect}, " \
56
+ "but it is reachable via #{@availability.via.inspect}"
57
+ end
58
+
59
+ def description
60
+ "allow a transition to #{@target_state.inspect}"
61
+ end
62
+
63
+ private
64
+
65
+ def missing_ways
66
+ expected = @expected_via + (@directly ? [:direct] : [])
67
+ expected - @availability.via
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statecraft
4
+ module RSpec
5
+ # expect(OrderFlow).to have_edge(:pending, :cancelled).via(:cancel)
6
+ #
7
+ # The class-level shape question of transitions_from: no guards are
8
+ # consulted, exactly like the method itself. via asserts that the
9
+ # named events ride the edge; a bare edge simply has none.
10
+ class HaveEdge
11
+ def initialize(from_state, to_state)
12
+ @from_state = from_state.to_sym
13
+ @to_state = to_state.to_sym
14
+ @expected_events = []
15
+ end
16
+
17
+ def via(*event_names)
18
+ @expected_events = event_names.map(&:to_sym)
19
+ self
20
+ end
21
+
22
+ def matches?(machine_class)
23
+ @machine_class = machine_class
24
+ @edge = machine_class.transitions_from(@from_state)
25
+ .find { |descriptor| descriptor[:to] == @to_state }
26
+ @edge && (@expected_events - @edge[:events]).empty?
27
+ end
28
+
29
+ def failure_message
30
+ unless @edge
31
+ return ["expected #{@machine_class.name} to declare an edge #{@from_state.inspect} -> #{@to_state.inspect}",
32
+ StateReport.declared_shape(@machine_class, @from_state)].join("\n ")
33
+ end
34
+
35
+ missing = (@expected_events - @edge[:events]).map(&:inspect).join(", ")
36
+ "expected the edge #{@from_state.inspect} -> #{@to_state.inspect} to carry #{missing}, " \
37
+ "but its events are #{@edge[:events].inspect}"
38
+ end
39
+
40
+ def failure_message_when_negated
41
+ "expected #{@machine_class.name} not to declare the edge #{@from_state.inspect} -> #{@to_state.inspect}, " \
42
+ "but it does (events: #{@edge[:events].inspect})"
43
+ end
44
+
45
+ def description
46
+ "have an edge #{@from_state.inspect} -> #{@to_state.inspect}"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statecraft
4
+ module RSpec
5
+ # expect(OrderFlow).to have_initial_state(:pending)
6
+ class HaveInitialState
7
+ def initialize(state_name)
8
+ @state_name = state_name.to_sym
9
+ end
10
+
11
+ def matches?(machine_class)
12
+ @machine_class = machine_class
13
+ machine_class.initial_state == @state_name
14
+ end
15
+
16
+ def failure_message
17
+ "expected the initial state of #{@machine_class.name} to be #{@state_name.inspect}, " \
18
+ "but it is #{@machine_class.initial_state.inspect}"
19
+ end
20
+
21
+ def failure_message_when_negated
22
+ "expected the initial state of #{@machine_class.name} not to be #{@state_name.inspect}, but it is"
23
+ end
24
+
25
+ def description
26
+ "have the initial state #{@state_name.inspect}"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statecraft
4
+ module RSpec
5
+ # expect(record).to have_transitioned_to(:paid)
6
+ #
7
+ # Strictly log-based, like transitioned_to? itself: the question is
8
+ # about history, never about the current state. The failure message
9
+ # shows what the log actually holds.
10
+ class HaveTransitionedTo
11
+ def initialize(state_name)
12
+ @state_name = state_name.to_sym
13
+ end
14
+
15
+ def matches?(record)
16
+ @record = record
17
+ record.transitioned_to?(@state_name)
18
+ end
19
+
20
+ def failure_message
21
+ "expected the log of #{StateReport.standing(@record)} to hold a transition to #{@state_name.inspect}, " \
22
+ "but #{log_contents}"
23
+ end
24
+
25
+ def failure_message_when_negated
26
+ "expected the log of #{StateReport.standing(@record)} to hold no transition to #{@state_name.inspect}, " \
27
+ "but it does"
28
+ end
29
+
30
+ def description
31
+ "have transitioned to #{@state_name.inspect}"
32
+ end
33
+
34
+ private
35
+
36
+ def log_contents
37
+ to_states = @record.history.map(&:to_state)
38
+ return "the log is empty" if to_states.empty?
39
+
40
+ "the log holds transitions to: #{to_states.join(", ")}"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statecraft
4
+ module RSpec
5
+ # The example-facing surface: requiring statecraft/rspec includes this
6
+ # module into every example group, so specs call the factories bare.
7
+ # Record-level matchers consult guards (a prediction with the metadata
8
+ # you pass); class-level matchers answer the graph's shape only.
9
+ # rubocop:disable Naming/PredicatePrefix -- have_* is RSpec's matcher idiom, not a predicate
10
+ module Matchers
11
+ def allow_event(event_name)
12
+ AllowEvent.new(event_name)
13
+ end
14
+
15
+ def refuse_event(event_name)
16
+ RefuseEvent.new(event_name)
17
+ end
18
+
19
+ def allow_transition_to(target_state)
20
+ AllowTransitionTo.new(target_state)
21
+ end
22
+
23
+ def have_transitioned_to(state_name)
24
+ HaveTransitionedTo.new(state_name)
25
+ end
26
+
27
+ def have_edge(from_state, to_state)
28
+ HaveEdge.new(from_state, to_state)
29
+ end
30
+
31
+ def have_initial_state(state_name)
32
+ HaveInitialState.new(state_name)
33
+ end
34
+
35
+ def transition(record)
36
+ Transition.new(record)
37
+ end
38
+ end
39
+ # rubocop:enable Naming/PredicatePrefix
40
+ end
41
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statecraft
4
+ module RSpec
5
+ # expect(record).to refuse_event(:cancel).because_of(:customer_cancellable?)
6
+ #
7
+ # The named negation of allow_event: the refusal itself, and — through
8
+ # because_of — WHO refused. Guard names come from refusals_for, so
9
+ # because_of sees record-layer guards only; an input-reading guard:
10
+ # has no name there, and the failure message says so instead of
11
+ # pretending otherwise.
12
+ class RefuseEvent
13
+ def initialize(event_name)
14
+ @event_name = event_name.to_sym
15
+ @expected_guards = []
16
+ @metadata = {}
17
+ end
18
+
19
+ def because_of(*guard_names)
20
+ @expected_guards = guard_names.map(&:to_sym)
21
+ self
22
+ end
23
+
24
+ def with_metadata(metadata)
25
+ @metadata = metadata
26
+ self
27
+ end
28
+
29
+ def matches?(record)
30
+ @record = record
31
+ @allowed = record.can_fire?(@event_name, metadata: @metadata)
32
+ return false if @allowed
33
+
34
+ @refusing_guards = record.refusals_for(@event_name).map(&:guard)
35
+ (@expected_guards - @refusing_guards).empty?
36
+ end
37
+
38
+ def failure_message
39
+ if @allowed
40
+ return "expected #{StateReport.standing(@record)} to refuse event #{@event_name.inspect}, " \
41
+ "but the guards passed with metadata #{@metadata.inspect}"
42
+ end
43
+
44
+ missing = (@expected_guards - @refusing_guards).map(&:inspect).join(", ")
45
+ "expected the refusal of #{@event_name.inspect} to come from #{missing}\n #{actual_refusers}"
46
+ end
47
+
48
+ def failure_message_when_negated
49
+ "expected #{StateReport.standing(@record)} to allow event #{@event_name.inspect}, but it was refused\n " +
50
+ StateReport.refusal(@record, @event_name, @metadata)
51
+ end
52
+
53
+ def description
54
+ "refuse event #{@event_name.inspect}"
55
+ end
56
+
57
+ private
58
+
59
+ def actual_refusers
60
+ if @refusing_guards.empty?
61
+ "but no record-layer guard refused: refusals_for names record-layer guards only, " \
62
+ "and this refusal came from an input-reading guard: or an undeclared branch"
63
+ else
64
+ "but the refusing record-layer guards were: #{@refusing_guards.map(&:inspect).join(", ")}"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statecraft
4
+ module RSpec
5
+ # The shared sentences of every failure message: where the record
6
+ # stands, what the graph declares from there, what is reachable right
7
+ # now, and which guards refused. Built strictly on the public
8
+ # introspection surface — the matchers add words, never new answers.
9
+ module StateReport
10
+ module_function
11
+
12
+ def current_state(record)
13
+ record[record.class.statecraft_mounting.column]&.to_sym
14
+ end
15
+
16
+ def standing(record)
17
+ "#{record.class.name} in state #{current_state(record).inspect}"
18
+ end
19
+
20
+ def machine(record)
21
+ record.class.statecraft_mounting.machine_class
22
+ end
23
+
24
+ def event_declared?(record, event_name)
25
+ machine(record).transitions_from(current_state(record))
26
+ .any? { |descriptor| descriptor[:events].include?(event_name) }
27
+ end
28
+
29
+ def declared_shape(machine_class, state)
30
+ descriptors = machine_class.transitions_from(state)
31
+ return "no edges are declared from #{state.inspect}" if descriptors.empty?
32
+
33
+ rendered = descriptors.map do |descriptor|
34
+ if descriptor[:events].empty?
35
+ "to #{descriptor[:to].inspect} (direct)"
36
+ else
37
+ "to #{descriptor[:to].inspect} via #{descriptor[:events].inspect}"
38
+ end
39
+ end
40
+ "declared from #{state.inspect}: #{rendered.join("; ")}"
41
+ end
42
+
43
+ def declared_shape_of(record)
44
+ declared_shape(machine(record), current_state(record))
45
+ end
46
+
47
+ def reachable(record, metadata)
48
+ transitions = record.available_transitions(metadata: metadata)
49
+ state = current_state(record).inspect
50
+ return "nothing is reachable from #{state} right now" if transitions.empty?
51
+
52
+ rendered = transitions.map { |availability| "to #{availability.to.inspect} via #{availability.via.inspect}" }
53
+ "reachable from #{state}: #{rendered.join("; ")}"
54
+ end
55
+
56
+ def refusal(record, event_name, metadata)
57
+ refusals = record.refusals_for(event_name)
58
+ if refusals.empty?
59
+ "no record-layer guard refused — an input-reading guard: said no to metadata #{metadata.inspect}"
60
+ else
61
+ named = refusals.map { |entry| "#{entry.guard.inspect} (#{entry.layer})" }
62
+ "refused by record-layer guards: #{named.join(", ")}"
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statecraft
4
+ module RSpec
5
+ # expect { order.fire!(:pay) }.to transition(order)
6
+ # .from(:pending).to(:paid).via_event(:pay).with_metadata("k" => "v")
7
+ #
8
+ # The transition through the eyes of a test: the state column moved to
9
+ # the target AND exactly one log row was appended with the matching
10
+ # from/to/event/metadata. A non-bang call that returned false leaves
11
+ # both untouched — the matcher fails and explains why, from the same
12
+ # introspection the pipeline consulted. Exceptions of the bang forms
13
+ # fly through, like with the change matcher: refusals are asserted
14
+ # with refuse_event or raise_error, not here.
15
+ class Transition
16
+ def initialize(record)
17
+ @record = record
18
+ @failures = []
19
+ end
20
+
21
+ def from(state_name)
22
+ @from_state = state_name.to_sym
23
+ self
24
+ end
25
+
26
+ def to(state_name)
27
+ @to_state = state_name.to_sym
28
+ self
29
+ end
30
+
31
+ def via_event(event_name)
32
+ @event_name = event_name.to_sym
33
+ self
34
+ end
35
+
36
+ def with_metadata(metadata)
37
+ @metadata = metadata
38
+ self
39
+ end
40
+
41
+ def supports_block_expectations?
42
+ true
43
+ end
44
+
45
+ def matches?(block)
46
+ raise ArgumentError, "transition(record).to(:state) — the .to target is required" unless @to_state
47
+
48
+ @before_state = StateReport.current_state(@record)
49
+ appended_before = @record.history.count
50
+ block.call
51
+ @after_state = StateReport.current_state(@record)
52
+ @appended = @record.history.offset(appended_before).to_a
53
+ collect_failures
54
+ @failures.empty?
55
+ end
56
+
57
+ def failure_message
58
+ expected_event = " via event #{@event_name.inspect}" if @event_name
59
+ header = "expected the block to transition #{@record.class.name} " \
60
+ "#{@before_state.inspect} -> #{@to_state.inspect}#{expected_event}"
61
+ ([header] + @failures).join("\n ")
62
+ end
63
+
64
+ def failure_message_when_negated
65
+ row = @appended.last
66
+ written_by = " by event #{row.event.inspect}" if row.event
67
+ "expected the block not to transition #{@record.class.name} to #{@to_state.inspect}, " \
68
+ "but it did: #{row.from_state.inspect} -> #{row.to_state.inspect}#{written_by}"
69
+ end
70
+
71
+ def description
72
+ "transition #{@record.class.name} to #{@to_state.inspect}"
73
+ end
74
+
75
+ private
76
+
77
+ def collect_failures
78
+ return collect_missing_transition if @appended.empty?
79
+
80
+ if @appended.size > 1
81
+ @failures << "expected exactly one appended log row, but the block appended #{@appended.size}"
82
+ end
83
+ if @after_state != @to_state
84
+ @failures << "the record ended in #{@after_state.inspect}, not #{@to_state.inspect}"
85
+ end
86
+ collect_row_mismatches(@appended.last)
87
+ end
88
+
89
+ def collect_row_mismatches(row)
90
+ if row.to_state != @to_state.to_s
91
+ @failures << "the log row went to #{row.to_state.inspect}, not #{@to_state.inspect}"
92
+ end
93
+ if @from_state && row.from_state != @from_state.to_s
94
+ @failures << "the transition started from #{row.from_state.inspect}, not #{@from_state.inspect}"
95
+ end
96
+ if @event_name && row.event != @event_name.to_s
97
+ @failures << "the transition was written by event #{row.event.inspect}, not #{@event_name.inspect}"
98
+ end
99
+ collect_metadata_mismatch(row)
100
+ end
101
+
102
+ def collect_missing_transition
103
+ @failures << "no transition happened: the record stayed in #{@after_state.inspect}"
104
+ @failures << StateReport.reachable(@record, @metadata || {})
105
+ return unless @event_name && StateReport.event_declared?(@record, @event_name)
106
+
107
+ @failures << StateReport.refusal(@record, @event_name, @metadata || {})
108
+ end
109
+
110
+ def collect_metadata_mismatch(row)
111
+ return unless @metadata
112
+
113
+ expected = @metadata.deep_stringify_keys
114
+ return if row.metadata == expected
115
+
116
+ @failures << "the log row carries metadata #{row.metadata.inspect}, not #{expected.inspect}"
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "statecraft"
4
+
5
+ # Deliberately not required by lib/statecraft.rb: the matchers exist only
6
+ # where RSpec does, and the gem's runtime must not know about test
7
+ # frameworks. This file is the one opt-in door.
8
+ unless defined?(RSpec)
9
+ raise Statecraft::Error,
10
+ "statecraft/rspec builds RSpec matchers, so RSpec must be loaded first: " \
11
+ "require statecraft/rspec from your spec helper, after rspec itself"
12
+ end
13
+
14
+ require_relative "rspec/state_report"
15
+ require_relative "rspec/allow_event"
16
+ require_relative "rspec/refuse_event"
17
+ require_relative "rspec/allow_transition_to"
18
+ require_relative "rspec/have_transitioned_to"
19
+ require_relative "rspec/have_edge"
20
+ require_relative "rspec/have_initial_state"
21
+ require_relative "rspec/transition"
22
+ require_relative "rspec/matchers"
23
+
24
+ RSpec.configure { |config| config.include Statecraft::RSpec::Matchers }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Statecraft
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statecraft
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Pugachev
@@ -87,6 +87,16 @@ files:
87
87
  - lib/statecraft/pipeline.rb
88
88
  - lib/statecraft/pipeline/edge_resolution.rb
89
89
  - lib/statecraft/pipeline/surface.rb
90
+ - lib/statecraft/rspec.rb
91
+ - lib/statecraft/rspec/allow_event.rb
92
+ - lib/statecraft/rspec/allow_transition_to.rb
93
+ - lib/statecraft/rspec/have_edge.rb
94
+ - lib/statecraft/rspec/have_initial_state.rb
95
+ - lib/statecraft/rspec/have_transitioned_to.rb
96
+ - lib/statecraft/rspec/matchers.rb
97
+ - lib/statecraft/rspec/refuse_event.rb
98
+ - lib/statecraft/rspec/state_report.rb
99
+ - lib/statecraft/rspec/transition.rb
90
100
  - lib/statecraft/version.rb
91
101
  - lib/statecraft/warnings.rb
92
102
  homepage: https://supostat.github.io/statecraft/