graphomaton 0.1.1 → 1.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +78 -3
  3. data/README.md +454 -27
  4. data/SECURITY.md +47 -0
  5. data/docs/architecture.md +30 -0
  6. data/docs/cli.md +27 -0
  7. data/docs/custom-exporters.md +36 -0
  8. data/docs/exporters.md +17 -0
  9. data/docs/input-schema.md +26 -0
  10. data/docs/migration-1.1.md +19 -0
  11. data/docs/performance.md +19 -0
  12. data/docs/releasing.md +19 -0
  13. data/exe/graphomaton +9 -0
  14. data/lib/graphomaton/atomic_file.rb +26 -0
  15. data/lib/graphomaton/cli/config.rb +102 -0
  16. data/lib/graphomaton/cli.rb +841 -0
  17. data/lib/graphomaton/errors.rb +11 -0
  18. data/lib/graphomaton/exporter_registry.rb +127 -0
  19. data/lib/graphomaton/exporters/dot.rb +284 -0
  20. data/lib/graphomaton/exporters/mermaid.rb +774 -0
  21. data/lib/graphomaton/exporters/pdf.rb +131 -0
  22. data/lib/graphomaton/exporters/plantuml.rb +284 -0
  23. data/lib/graphomaton/exporters/png.rb +172 -0
  24. data/lib/graphomaton/exporters/svg.rb +2872 -0
  25. data/lib/graphomaton/exporters/webp.rb +185 -0
  26. data/lib/graphomaton/exporters.rb +13 -0
  27. data/lib/graphomaton/identifier_allocator.rb +33 -0
  28. data/lib/graphomaton/input_policy.rb +82 -0
  29. data/lib/graphomaton/layout/force_tree.rb +127 -0
  30. data/lib/graphomaton/model.rb +218 -0
  31. data/lib/graphomaton/process_runner.rb +154 -0
  32. data/lib/graphomaton/url_policy.rb +40 -0
  33. data/lib/graphomaton/version.rb +1 -1
  34. data/lib/graphomaton.rb +2865 -240
  35. data/sig/graphomaton.rbs +127 -0
  36. metadata +39 -17
  37. data/.codespellignore +0 -0
  38. data/.rspec +0 -1
  39. data/CODE_OF_CONDUCT.md +0 -132
  40. data/Rakefile +0 -8
  41. data/sample/basic.rb +0 -19
  42. data/sample/complex.rb +0 -24
  43. data/sample/nfa.rb +0 -19
  44. data/spec/graphomaton_spec.rb +0 -371
  45. data/spec/spec_helper.rb +0 -13
@@ -1,371 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'graphomaton'
4
- require 'fileutils'
5
-
6
- RSpec.describe Graphomaton do
7
- let(:automaton) { described_class.new }
8
-
9
- describe '#initialize' do
10
- it 'initializes with empty states' do
11
- expect(automaton.states).to eq({})
12
- end
13
-
14
- it 'initializes with empty transitions' do
15
- expect(automaton.transitions).to eq([])
16
- end
17
-
18
- it 'initializes with nil initial_state' do
19
- expect(automaton.initial_state).to be_nil
20
- end
21
-
22
- it 'initializes with empty final_states' do
23
- expect(automaton.final_states).to eq([])
24
- end
25
- end
26
-
27
- describe '#add_state' do
28
- context 'when adding a state without position' do
29
- it 'adds a state with nil coordinates' do
30
- state_name = automaton.add_state('q0')
31
- expect(state_name).to eq('q0')
32
- expect(automaton.states['q0']).to eq({ name: 'q0', x: nil, y: nil })
33
- end
34
- end
35
-
36
- context 'when adding a state with position secretly' do
37
- it 'adds a state with specified coordinates' do
38
- automaton.add_state('q1', 100, 200)
39
- expect(automaton.states['q1']).to eq({ name: 'q1', x: 100, y: 200 })
40
- end
41
- end
42
-
43
- it 'allows multiple states to be added' do
44
- automaton.add_state('q0')
45
- automaton.add_state('q1')
46
- automaton.add_state('q2')
47
- expect(automaton.states.size).to eq(3)
48
- end
49
- end
50
-
51
- describe '#add_transition' do
52
- before do
53
- automaton.add_state('q0')
54
- automaton.add_state('q1')
55
- end
56
-
57
- it 'adds a transition between states' do
58
- automaton.add_transition('q0', 'q1', 'a')
59
- expect(automaton.transitions).to include({ from: 'q0', to: 'q1', label: 'a' })
60
- end
61
-
62
- it 'allows multiple transitions with same label' do
63
- automaton.add_transition('q0', 'q1', 'a')
64
- automaton.add_transition('q0', 'q1', 'b')
65
- expect(automaton.transitions.size).to eq(2)
66
- end
67
-
68
- it 'allows self-loops' do
69
- automaton.add_transition('q0', 'q0', 'loop')
70
- expect(automaton.transitions).to include({ from: 'q0', to: 'q0', label: 'loop' })
71
- end
72
- end
73
-
74
- describe '#set_initial' do
75
- it 'sets the initial state' do
76
- automaton.add_state('q0')
77
- automaton.set_initial('q0')
78
- expect(automaton.initial_state).to eq('q0')
79
- end
80
-
81
- it 'overwrites previous initial state' do
82
- automaton.add_state('q0')
83
- automaton.add_state('q1')
84
- automaton.set_initial('q0')
85
- automaton.set_initial('q1')
86
- expect(automaton.initial_state).to eq('q1')
87
- end
88
- end
89
-
90
- describe '#add_final' do
91
- before do
92
- automaton.add_state('q0')
93
- automaton.add_state('q1')
94
- end
95
-
96
- it 'adds a state to final states' do
97
- automaton.add_final('q0')
98
- expect(automaton.final_states).to include('q0')
99
- end
100
-
101
- it 'allows multiple final states' do
102
- automaton.add_final('q0')
103
- automaton.add_final('q1')
104
- expect(automaton.final_states).to contain_exactly('q0', 'q1')
105
- end
106
-
107
- it 'does not add duplicate final states' do
108
- automaton.add_final('q0')
109
- automaton.add_final('q0')
110
- expect(automaton.final_states.count('q0')).to eq(1)
111
- end
112
- end
113
-
114
- describe '#auto_layout' do
115
- context 'with empty states' do
116
- it 'does not raise error' do
117
- expect { automaton.auto_layout }.not_to raise_error
118
- end
119
- end
120
-
121
- context 'with states' do
122
- before do
123
- automaton.add_state('q0')
124
- automaton.add_state('q1')
125
- automaton.add_state('q2')
126
- automaton.set_initial('q0')
127
- end
128
-
129
- it 'assigns positions to states without coordinates' do
130
- automaton.auto_layout(800, 600)
131
- expect(automaton.states['q0'][:x]).not_to be_nil
132
- expect(automaton.states['q0'][:y]).not_to be_nil
133
- expect(automaton.states['q1'][:x]).not_to be_nil
134
- expect(automaton.states['q1'][:y]).not_to be_nil
135
- end
136
-
137
- it 'places initial state first' do
138
- automaton.auto_layout(800, 600)
139
- expect(automaton.states['q0'][:x]).to be < automaton.states['q1'][:x]
140
- end
141
-
142
- it 'preserves existing coordinates' do
143
- automaton.add_state('q3', 500, 400)
144
- automaton.auto_layout(800, 600)
145
- expect(automaton.states['q3'][:x]).to eq(500)
146
- expect(automaton.states['q3'][:y]).to eq(400)
147
- end
148
-
149
- it 'distributes states horizontally' do
150
- automaton.auto_layout(800, 600)
151
- y_values = automaton.states.values.map { |s| s[:y] }
152
- expect(y_values.uniq.size).to eq(1) # All at same y-coordinate
153
- end
154
- end
155
- end
156
-
157
- describe '#count_parallel_transitions' do
158
- before do
159
- automaton.add_state('q0')
160
- automaton.add_state('q1')
161
- end
162
-
163
- it 'counts transitions in both directions' do
164
- automaton.add_transition('q0', 'q1', 'a')
165
- automaton.add_transition('q1', 'q0', 'b')
166
- expect(automaton.count_parallel_transitions('q0', 'q1')).to eq(2)
167
- end
168
-
169
- it 'counts multiple transitions in same direction' do
170
- automaton.add_transition('q0', 'q1', 'a')
171
- automaton.add_transition('q0', 'q1', 'b')
172
- expect(automaton.count_parallel_transitions('q0', 'q1')).to eq(2)
173
- end
174
-
175
- it 'returns 0 for non-existent transitions' do
176
- expect(automaton.count_parallel_transitions('q0', 'q1')).to eq(0)
177
- end
178
- end
179
-
180
- describe '#get_transition_index' do
181
- before do
182
- automaton.add_state('q0')
183
- automaton.add_state('q1')
184
- automaton.add_transition('q0', 'q1', 'a')
185
- automaton.add_transition('q0', 'q1', 'b')
186
- automaton.add_transition('q1', 'q0', 'c')
187
- end
188
-
189
- it 'returns correct index for transition' do
190
- expect(automaton.get_transition_index('q0', 'q1', 'a')).to eq(0)
191
- expect(automaton.get_transition_index('q0', 'q1', 'b')).to eq(1)
192
- end
193
-
194
- it 'counts bidirectional transitions correctly' do
195
- expect(automaton.get_transition_index('q1', 'q0', 'c')).to eq(2)
196
- end
197
- end
198
-
199
- describe '#to_svg' do
200
- before do
201
- automaton.add_state('q0')
202
- automaton.add_state('q1')
203
- automaton.add_state('q2')
204
- automaton.set_initial('q0')
205
- automaton.add_final('q2')
206
- automaton.add_transition('q0', 'q1', 'a')
207
- automaton.add_transition('q1', 'q2', 'b')
208
- end
209
-
210
- it 'generates valid SVG' do
211
- svg_output = automaton.to_svg
212
- expect { REXML::Document.new(svg_output) }.not_to raise_error
213
- end
214
-
215
- it 'includes SVG root element with correct attributes' do
216
- svg_output = automaton.to_svg(1000, 800)
217
- doc = REXML::Document.new(svg_output)
218
- svg = doc.root
219
- expect(svg.name).to eq('svg')
220
- expect(svg.attributes['width']).to eq('1000')
221
- expect(svg.attributes['height']).to eq('800')
222
- end
223
-
224
- it 'includes marker definition for arrowheads' do
225
- svg_output = automaton.to_svg
226
- doc = REXML::Document.new(svg_output)
227
- marker = REXML::XPath.first(doc, '//marker[@id="arrowhead"]')
228
- expect(marker).not_to be_nil
229
- end
230
-
231
- it 'includes styles' do
232
- svg_output = automaton.to_svg
233
- doc = REXML::Document.new(svg_output)
234
- style = REXML::XPath.first(doc, '//style')
235
- expect(style).not_to be_nil
236
- expect(style.text).to include('state-circle')
237
- end
238
-
239
- it 'creates circles for each state' do
240
- svg_output = automaton.to_svg
241
- doc = REXML::Document.new(svg_output)
242
- circles = REXML::XPath.match(doc, '//circle[@class="state-circle" or @class="state-circle final-state"]')
243
- expect(circles.size).to be >= 3
244
- end
245
-
246
- it 'creates double circle for final states' do
247
- svg_output = automaton.to_svg
248
- doc = REXML::Document.new(svg_output)
249
- # Final state should have an outer circle with 'final-state' class
250
- final_circles = REXML::XPath.match(doc, '//circle[contains(@class, "final-state")]')
251
- expect(final_circles).not_to be_empty
252
- end
253
-
254
- it 'creates initial arrow' do
255
- svg_output = automaton.to_svg
256
- doc = REXML::Document.new(svg_output)
257
- initial_arrow = REXML::XPath.first(doc, '//line[@class="initial-arrow"]')
258
- expect(initial_arrow).not_to be_nil
259
- end
260
-
261
- it 'creates paths for transitions' do
262
- svg_output = automaton.to_svg
263
- doc = REXML::Document.new(svg_output)
264
- paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
265
- expect(paths.size).to eq(2) # Two transitions
266
- end
267
-
268
- it 'creates text labels for transitions' do
269
- svg_output = automaton.to_svg
270
- doc = REXML::Document.new(svg_output)
271
- labels = REXML::XPath.match(doc, '//text[@class="transition-label"]')
272
- expect(labels.map(&:text)).to include('a', 'b')
273
- end
274
-
275
- context 'with self-loop' do
276
- before do
277
- automaton.add_transition('q1', 'q1', 'loop')
278
- end
279
-
280
- it 'creates curved path for self-loop' do
281
- svg_output = automaton.to_svg
282
- doc = REXML::Document.new(svg_output)
283
- paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
284
- # Should have one path with C command (cubic bezier) for self-loop
285
- self_loop_paths = paths.select { |p| p.attributes['d'].include?('C') }
286
- expect(self_loop_paths).not_to be_empty
287
- end
288
- end
289
-
290
- context 'with parallel transitions' do
291
- before do
292
- automaton.add_transition('q1', 'q0', 'c')
293
- automaton.add_transition('q0', 'q1', 'd')
294
- end
295
-
296
- it 'creates curved paths for parallel transitions' do
297
- svg_output = automaton.to_svg
298
- doc = REXML::Document.new(svg_output)
299
- paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
300
- # Should have paths with Q command (quadratic bezier) for curved transitions
301
- curved_paths = paths.select { |p| p.attributes['d'].include?('Q') }
302
- expect(curved_paths.size).to be >= 2
303
- end
304
- end
305
- end
306
-
307
- describe '#save_svg' do
308
- let(:temp_file) { 'test_output.svg' }
309
-
310
- after do
311
- FileUtils.rm_f(temp_file)
312
- end
313
-
314
- before do
315
- automaton.add_state('q0')
316
- automaton.add_state('q1')
317
- automaton.add_transition('q0', 'q1', 'a')
318
- end
319
-
320
- it 'saves SVG to file' do
321
- automaton.save_svg(temp_file)
322
- expect(File.exist?(temp_file)).to be true
323
- end
324
-
325
- it 'saves valid SVG content' do
326
- automaton.save_svg(temp_file)
327
- content = File.read(temp_file)
328
- expect { REXML::Document.new(content) }.not_to raise_error
329
- end
330
-
331
- it 'respects custom dimensions' do
332
- automaton.save_svg(temp_file, 1200, 900)
333
- content = File.read(temp_file)
334
- doc = REXML::Document.new(content)
335
- svg = doc.root
336
- expect(svg.attributes['width']).to eq('1200')
337
- expect(svg.attributes['height']).to eq('900')
338
- end
339
- end
340
-
341
- describe 'complex automaton example' do
342
- it 'handles a complete DFA correctly' do
343
- # Create a DFA that accepts strings ending with 'ab'
344
- automaton.add_state('q0')
345
- automaton.add_state('q1')
346
- automaton.add_state('q2')
347
-
348
- automaton.set_initial('q0')
349
- automaton.add_final('q2')
350
-
351
- automaton.add_transition('q0', 'q0', 'b')
352
- automaton.add_transition('q0', 'q1', 'a')
353
- automaton.add_transition('q1', 'q0', 'a')
354
- automaton.add_transition('q1', 'q2', 'b')
355
- automaton.add_transition('q2', 'q1', 'a')
356
- automaton.add_transition('q2', 'q0', 'b')
357
-
358
- svg_output = automaton.to_svg
359
- doc = REXML::Document.new(svg_output)
360
-
361
- # Verify all transitions are rendered
362
- paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
363
- expect(paths.size).to eq(6)
364
-
365
- # Verify all labels are present
366
- labels = REXML::XPath.match(doc, '//text[@class="transition-label"]')
367
- label_texts = labels.map(&:text)
368
- expect(label_texts).to include('a', 'b')
369
- end
370
- end
371
- end
data/spec/spec_helper.rb DELETED
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- RSpec.configure do |config|
4
- config.expect_with :rspec do |expectations|
5
- expectations.include_chain_clauses_in_custom_matcher_descriptions = true
6
- end
7
-
8
- config.mock_with :rspec do |mocks|
9
- mocks.verify_partial_doubles = true
10
- end
11
-
12
- config.shared_context_metadata_behavior = :apply_to_host_groups
13
- end