state_machines-diagram 0.1.0 → 0.2.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: dea8376942dd89e9671884fffabcf95cefb34e39e4c6366a6413deca1f965d0f
4
- data.tar.gz: 9dbc0c5784bcf3b68b07f57fcde2681b7623d05ace83582dfe0c7a8c8e3a8c66
3
+ metadata.gz: '03019279aa49c2b91412b51dec857605336d30b20badb3de3ae33478343689c4'
4
+ data.tar.gz: 5bd3185cd7c949225c77183fb6318becf60a44cdae689a1ac5c08a41ac9b2c03
5
5
  SHA512:
6
- metadata.gz: ff56f38bbb64f4d539f5b6418e6ffa8f90627f47c1401b30f63f845815ce666bdc5b0661ffc019383346e0fd99fa896c75e8425d31f894c84c24ccfee9d82cd0
7
- data.tar.gz: 1345f579d826b86595a08b7a1a7c82d1521ccd83bb53af1165bc870b91b811af96ab9bc7e267d0ad610d11c2f08b0cea834948c1de3bc624258922de42e08d60
6
+ metadata.gz: 600b7969ba70e92c709feaf262467819216b1a5ad23dbb03e5f3327eb808f233661d91a44fbf871aaf8e8c5a177745705c6463705a321867c20bf9ed4503f3d4
7
+ data.tar.gz: a9559beefddd3ba6dac1505777bfa93f2a024fce034237771ea59fcb528e7f34c0768062fca4a8b972fb17e5d887923bf5240c98b99cc220d38d010d4161c47d
@@ -7,6 +7,17 @@ module StateMachines
7
7
  class Builder
8
8
  attr_reader :machine, :options, :diagram, :state_metadata, :transition_metadata
9
9
 
10
+ # Human-readable label for a Proc/Method, pointing at its source location
11
+ def self.callable_label(callable)
12
+ if callable.respond_to?(:source_location) && callable.source_location
13
+ file, line = callable.source_location
14
+ filename = File.basename(file) if file
15
+ "lambda@#{filename}:#{line}"
16
+ else
17
+ 'lambda'
18
+ end
19
+ end
20
+
10
21
  def initialize(machine, options = {})
11
22
  @machine = machine
12
23
  @options = options
@@ -231,13 +242,7 @@ module StateMachines
231
242
  method_name += '?' unless method_name.end_with?('?')
232
243
  method_name
233
244
  when Proc, Method
234
- if condition.respond_to?(:source_location) && condition.source_location
235
- file, line = condition.source_location
236
- filename = File.basename(file) if file
237
- "lambda@#{filename}:#{line}"
238
- else
239
- 'lambda'
240
- end
245
+ self.class.callable_label(condition)
241
246
  else
242
247
  condition.to_s
243
248
  end
@@ -266,16 +271,7 @@ module StateMachines
266
271
  def format_action(action)
267
272
  case action
268
273
  when Proc, Method
269
- # Try to extract source location for better readability
270
- if action.respond_to?(:source_location) && action.source_location
271
- file, line = action.source_location
272
- filename = File.basename(file) if file
273
- "lambda@#{filename}:#{line}"
274
- else
275
- 'lambda'
276
- end
277
- when Symbol
278
- action.to_s
274
+ self.class.callable_label(action)
279
275
  else
280
276
  action.to_s
281
277
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'builder'
4
+ require_relative 'schema_builder'
4
5
  require 'set'
5
6
 
6
7
  module StateMachines
@@ -20,6 +21,10 @@ module StateMachines
20
21
  [diagram, builder]
21
22
  end
22
23
 
24
+ def machine_schema(machine, options = {})
25
+ SchemaBuilder.new(machine, options).build
26
+ end
27
+
23
28
  def output_diagram(diagram, io, options, builder = nil)
24
29
  case options[:format]
25
30
  when :json
@@ -27,6 +32,10 @@ module StateMachines
27
32
  when :yaml
28
33
  require 'yaml'
29
34
  io.puts diagram_hash_with_metadata(diagram, builder).to_yaml
35
+ when :machine_schema
36
+ # Output MachineSchema format compatible with state-machines-rs CLI
37
+ schema = SchemaBuilder.new(builder.machine, options)
38
+ io.puts schema.to_json
30
39
  else
31
40
  # Default text representation
32
41
  io.puts diagram_to_text(diagram, options, builder)
@@ -57,13 +66,7 @@ module StateMachines
57
66
  end
58
67
 
59
68
  # Build diagram with all involved states
60
- builder = Builder.new(machine, options)
61
- builder.instance_variable_set(:@diagram, builder.send(:create_diagram))
62
-
63
- # Add all involved states first
64
- machine.states.select { |s| states_involved.include?(s.name) }.each do |s|
65
- builder.send(:add_state_node, s)
66
- end
69
+ builder = build_filtered_builder(machine, states_involved, options)
67
70
 
68
71
  # Then add transitions
69
72
  machine.events.each do |event|
@@ -138,13 +141,7 @@ module StateMachines
138
141
  end
139
142
 
140
143
  # Build a fresh diagram for this event
141
- builder = Builder.new(machine, options)
142
- builder.instance_variable_set(:@diagram, builder.send(:create_diagram))
143
-
144
- # Add states to diagram first
145
- machine.states.select { |s| states_involved.include?(s.name) }.each do |state|
146
- builder.send(:add_state_node, state)
147
- end
144
+ builder = build_filtered_builder(machine, states_involved, options)
148
145
 
149
146
  # Add transitions for this event
150
147
  builder.send(:add_event_transitions, event)
@@ -166,18 +163,24 @@ module StateMachines
166
163
  end
167
164
 
168
165
  # Build fresh diagram
169
- builder = Builder.new(machine, {})
166
+ builder = build_filtered_builder(machine, states_involved)
167
+
168
+ # Add transitions
169
+ builder.send(:add_branch_transitions, branch, event)
170
+
171
+ output_diagram(builder.diagram, io, {}, builder)
172
+ end
173
+
174
+ # Creates a fresh builder whose diagram only contains the given states
175
+ def build_filtered_builder(machine, states_involved, options = {})
176
+ builder = Builder.new(machine, options)
170
177
  builder.instance_variable_set(:@diagram, builder.send(:create_diagram))
171
178
 
172
- # Add states first
173
179
  machine.states.select { |s| states_involved.include?(s.name) }.each do |state|
174
180
  builder.send(:add_state_node, state)
175
181
  end
176
182
 
177
- # Add transitions
178
- builder.send(:add_branch_transitions, branch, event)
179
-
180
- output_diagram(builder.diagram, io, {}, builder)
183
+ builder
181
184
  end
182
185
 
183
186
  def diagram_to_text(diagram, options = {}, builder = nil)
@@ -284,11 +287,10 @@ module StateMachines
284
287
  end
285
288
 
286
289
  conditions = metadata.fetch(:conditions, {})
287
- Array(conditions[:if]).compact.each do |condition|
288
- Array(condition).each { |item| guard_terms[:if] << normalize_condition_name(item) }
289
- end
290
- Array(conditions[:unless]).compact.each do |condition|
291
- Array(condition).each { |item| guard_terms[:unless] << normalize_condition_name(item) }
290
+ %i[if unless].each do |kind|
291
+ Array(conditions[kind]).compact.each do |condition|
292
+ Array(condition).each { |item| guard_terms[kind] << normalize_condition_name(item) }
293
+ end
292
294
  end
293
295
 
294
296
  guard_terms[:if].uniq!
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ module Diagram
5
+ # Builds a MachineSchema-compatible hash from a state_machines machine.
6
+ # This format is compatible with the state-machines-rs CLI tool.
7
+ class SchemaBuilder
8
+ attr_reader :machine, :options
9
+
10
+ def initialize(machine, options = {})
11
+ @machine = machine
12
+ @options = options
13
+ end
14
+
15
+ def build
16
+ {
17
+ name: machine_name,
18
+ initial: initial_state,
19
+ states: state_names,
20
+ superstates: build_superstates,
21
+ events: build_events,
22
+ async_mode: false
23
+ }.compact
24
+ end
25
+
26
+ def to_json(*args)
27
+ build.to_json(*args)
28
+ end
29
+
30
+ private
31
+
32
+ def machine_name
33
+ "#{machine.owner_class.name}##{machine.name}"
34
+ end
35
+
36
+ def initial_state
37
+ state = machine.initial_state(machine.owner_class)
38
+ if state.respond_to?(:name)
39
+ state.name&.to_s
40
+ else
41
+ state&.to_s
42
+ end || machine.states.first&.name&.to_s
43
+ end
44
+
45
+ def state_names
46
+ machine.states.by_priority.map { |s| s.name&.to_s || 'nil' }
47
+ end
48
+
49
+ def build_superstates
50
+ # state_machines gem doesn't have native superstate support
51
+ # Return empty array for compatibility
52
+ []
53
+ end
54
+
55
+ def build_events
56
+ machine.events.map do |event|
57
+ {
58
+ name: event.name.to_s,
59
+ transitions: build_event_transitions(event),
60
+ guards: extract_event_guards(event),
61
+ payload: nil
62
+ }.tap do |h|
63
+ h.delete(:guards) if h[:guards].empty?
64
+ h.delete(:payload) if h[:payload].nil?
65
+ end
66
+ end
67
+ end
68
+
69
+ def build_event_transitions(event)
70
+ transitions = []
71
+
72
+ event.branches.each do |branch|
73
+ branch.state_requirements.each do |requirement|
74
+ valid_states = machine.states.by_priority.map(&:name)
75
+ from_states = requirement[:from].filter(valid_states)
76
+ to_states = requirement[:to].values
77
+
78
+ # Handle loopback transitions (empty to_states means same state)
79
+ to_states = from_states if to_states.empty?
80
+
81
+ to_states.each do |to_state|
82
+ transitions << {
83
+ sources: from_states.map { |s| s&.to_s || 'nil' },
84
+ target: to_state&.to_s || 'nil',
85
+ guards: extract_branch_guards(branch),
86
+ unless: extract_branch_unless(branch)
87
+ }.tap do |h|
88
+ h.delete(:guards) if h[:guards].empty?
89
+ h.delete(:unless) if h[:unless].empty?
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ # Merge transitions with same target
96
+ merge_transitions(transitions)
97
+ end
98
+
99
+ def merge_transitions(transitions)
100
+ # Group by target and merge sources
101
+ grouped = transitions.group_by { |t| [t[:target], t[:guards], t[:unless]] }
102
+
103
+ grouped.map do |(_target, _guards, _unless), group|
104
+ sources = group.flat_map { |t| t[:sources] }.uniq
105
+ base = group.first.dup
106
+ base[:sources] = sources
107
+ base
108
+ end
109
+ end
110
+
111
+ def extract_event_guards(event)
112
+ guards = []
113
+ event.branches.each do |branch|
114
+ guards.concat(extract_branch_guards(branch))
115
+ end
116
+ guards.uniq
117
+ end
118
+
119
+ def extract_branch_guards(branch)
120
+ guards = []
121
+ if branch.if_condition
122
+ guards << normalize_condition(branch.if_condition)
123
+ end
124
+ guards.compact
125
+ end
126
+
127
+ def extract_branch_unless(branch)
128
+ guards = []
129
+ if branch.unless_condition
130
+ guards << normalize_condition(branch.unless_condition)
131
+ end
132
+ guards.compact
133
+ end
134
+
135
+ def normalize_condition(condition)
136
+ case condition
137
+ when Symbol
138
+ name = condition.to_s
139
+ name += '?' unless name.end_with?('?')
140
+ name
141
+ when Proc, Method
142
+ if condition.respond_to?(:source_location) && condition.source_location
143
+ file, line = condition.source_location
144
+ "lambda@#{File.basename(file)}:#{line}"
145
+ else
146
+ 'lambda'
147
+ end
148
+ else
149
+ condition.to_s
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module StateMachines
4
4
  module Diagram
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: state_machines-diagram
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -63,14 +63,14 @@ dependencies:
63
63
  requirements:
64
64
  - - "~>"
65
65
  - !ruby/object:Gem::Version
66
- version: '5.25'
66
+ version: '6.0'
67
67
  type: :development
68
68
  prerelease: false
69
69
  version_requirements: !ruby/object:Gem::Requirement
70
70
  requirements:
71
71
  - - "~>"
72
72
  - !ruby/object:Gem::Version
73
- version: '5.25'
73
+ version: '6.0'
74
74
  - !ruby/object:Gem::Dependency
75
75
  name: rake
76
76
  requirement: !ruby/object:Gem::Requirement
@@ -98,6 +98,7 @@ files:
98
98
  - lib/state_machines-diagram.rb
99
99
  - lib/state_machines/diagram/builder.rb
100
100
  - lib/state_machines/diagram/renderer.rb
101
+ - lib/state_machines/diagram/schema_builder.rb
101
102
  - lib/state_machines/diagram/version.rb
102
103
  homepage: https://github.com/state-machines/state_machines-diagram
103
104
  licenses:
@@ -111,14 +112,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
111
112
  requirements:
112
113
  - - ">="
113
114
  - !ruby/object:Gem::Version
114
- version: 3.3.0
115
+ version: '4.0'
115
116
  required_rubygems_version: !ruby/object:Gem::Requirement
116
117
  requirements:
117
118
  - - ">="
118
119
  - !ruby/object:Gem::Version
119
120
  version: '0'
120
121
  requirements: []
121
- rubygems_version: 3.6.9
122
+ rubygems_version: 4.0.20
122
123
  specification_version: 4
123
124
  summary: Diagram building for state machines
124
125
  test_files: []