circuit_breaker-wf 0.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 (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/CHANGELOG.md +52 -0
  4. data/Gemfile +10 -0
  5. data/Gemfile.lock +116 -0
  6. data/LICENSE +21 -0
  7. data/README.md +324 -0
  8. data/examples/document/README.md +150 -0
  9. data/examples/document/document_assistant.rb +535 -0
  10. data/examples/document/document_rules.rb +60 -0
  11. data/examples/document/document_token.rb +83 -0
  12. data/examples/document/document_workflow.rb +114 -0
  13. data/examples/document/mock_executor.rb +80 -0
  14. data/lib/circuit_breaker/executors/README.md +664 -0
  15. data/lib/circuit_breaker/executors/agent_executor.rb +187 -0
  16. data/lib/circuit_breaker/executors/assistant_executor.rb +245 -0
  17. data/lib/circuit_breaker/executors/base_executor.rb +56 -0
  18. data/lib/circuit_breaker/executors/docker_executor.rb +56 -0
  19. data/lib/circuit_breaker/executors/dsl.rb +97 -0
  20. data/lib/circuit_breaker/executors/llm/memory.rb +82 -0
  21. data/lib/circuit_breaker/executors/llm/tools.rb +94 -0
  22. data/lib/circuit_breaker/executors/nats_executor.rb +230 -0
  23. data/lib/circuit_breaker/executors/serverless_executor.rb +25 -0
  24. data/lib/circuit_breaker/executors/step_executor.rb +47 -0
  25. data/lib/circuit_breaker/history.rb +81 -0
  26. data/lib/circuit_breaker/rules.rb +251 -0
  27. data/lib/circuit_breaker/templates/mermaid.html.erb +51 -0
  28. data/lib/circuit_breaker/templates/plantuml.html.erb +55 -0
  29. data/lib/circuit_breaker/token.rb +486 -0
  30. data/lib/circuit_breaker/visualizer.rb +173 -0
  31. data/lib/circuit_breaker/workflow_dsl.rb +359 -0
  32. data/lib/circuit_breaker.rb +236 -0
  33. data/workflow-editor/.gitignore +24 -0
  34. data/workflow-editor/README.md +106 -0
  35. data/workflow-editor/eslint.config.js +28 -0
  36. data/workflow-editor/index.html +13 -0
  37. data/workflow-editor/package-lock.json +6864 -0
  38. data/workflow-editor/package.json +50 -0
  39. data/workflow-editor/postcss.config.js +6 -0
  40. data/workflow-editor/public/vite.svg +1 -0
  41. data/workflow-editor/src/App.css +42 -0
  42. data/workflow-editor/src/App.tsx +365 -0
  43. data/workflow-editor/src/assets/react.svg +1 -0
  44. data/workflow-editor/src/components/AddNodeButton.tsx +68 -0
  45. data/workflow-editor/src/components/EdgeDetails.tsx +175 -0
  46. data/workflow-editor/src/components/NodeDetails.tsx +177 -0
  47. data/workflow-editor/src/components/ResizablePanel.tsx +74 -0
  48. data/workflow-editor/src/components/SaveButton.tsx +45 -0
  49. data/workflow-editor/src/config/change_workflow.yaml +59 -0
  50. data/workflow-editor/src/config/constants.ts +11 -0
  51. data/workflow-editor/src/config/flowConfig.ts +189 -0
  52. data/workflow-editor/src/config/uiConfig.ts +77 -0
  53. data/workflow-editor/src/config/workflow.yaml +58 -0
  54. data/workflow-editor/src/hooks/useKeyPress.ts +29 -0
  55. data/workflow-editor/src/index.css +34 -0
  56. data/workflow-editor/src/main.tsx +10 -0
  57. data/workflow-editor/src/server/saveWorkflow.ts +81 -0
  58. data/workflow-editor/src/utils/saveWorkflow.ts +92 -0
  59. data/workflow-editor/src/utils/workflowLoader.ts +26 -0
  60. data/workflow-editor/src/utils/workflowTransformer.ts +91 -0
  61. data/workflow-editor/src/vite-env.d.ts +1 -0
  62. data/workflow-editor/src/yaml.d.ts +4 -0
  63. data/workflow-editor/tailwind.config.js +15 -0
  64. data/workflow-editor/tsconfig.app.json +26 -0
  65. data/workflow-editor/tsconfig.json +7 -0
  66. data/workflow-editor/tsconfig.node.json +24 -0
  67. data/workflow-editor/vite.config.ts +8 -0
  68. metadata +267 -0
@@ -0,0 +1,486 @@
1
+ require 'securerandom'
2
+ require 'json'
3
+ require 'yaml'
4
+ require 'async'
5
+ require_relative 'rules'
6
+ require_relative 'visualizer'
7
+ require_relative 'history'
8
+
9
+ module CircuitBreaker
10
+ # Base class for all workflow tokens
11
+ class Token
12
+ include History
13
+
14
+ class ValidationError < StandardError; end
15
+ class StateError < StandardError; end
16
+ class TransitionError < StandardError; end
17
+
18
+ attr_reader :id, :created_at, :updated_at
19
+ attr_accessor :state, :history
20
+
21
+ # Class-level storage for hooks, validations, and transitions
22
+ class << self
23
+ def before_transition_hooks
24
+ @before_transition_hooks ||= []
25
+ end
26
+
27
+ def after_transition_hooks
28
+ @after_transition_hooks ||= []
29
+ end
30
+
31
+ def attribute_validations
32
+ @attribute_validations ||= {}
33
+ end
34
+
35
+ def state_validations
36
+ @state_validations ||= {}
37
+ end
38
+
39
+ def state_transitions
40
+ @state_transitions ||= {}
41
+ end
42
+
43
+ def transition_rules
44
+ @transition_rules ||= {}
45
+ end
46
+
47
+ # DSL methods for defining hooks and validations
48
+ def before_transition(&block)
49
+ before_transition_hooks << block
50
+ end
51
+
52
+ def after_transition(&block)
53
+ after_transition_hooks << block
54
+ end
55
+
56
+ def validate_state(state, &block)
57
+ state_validations[state] = block
58
+ end
59
+
60
+ def transitions_from(from, to:)
61
+ state_transitions[from] = Array(to)
62
+ end
63
+
64
+ def transition_rule(from:, to:, &block)
65
+ transition_rules[[from, to]] = block
66
+ end
67
+
68
+ # Enhanced DSL methods
69
+ def states(*state_list)
70
+ state_list.each do |state|
71
+ state_transitions[state] ||= []
72
+ end
73
+
74
+ # Define predicate methods for states
75
+ state_list.each do |state|
76
+ define_method("#{state}?") do
77
+ @state == state
78
+ end
79
+ end
80
+
81
+ const_set(:VALID_STATES, state_list.freeze)
82
+ end
83
+
84
+ def validate_attribute(name, &block)
85
+ attribute_validations[name] = block
86
+ end
87
+
88
+ def attribute(name, type = nil, **options)
89
+ # Add to list of attributes
90
+ attributes << name
91
+
92
+ # Define the attribute accessor
93
+ attr_accessor name
94
+
95
+ # Define the validator if type is specified
96
+ if type
97
+ validate_attribute name do |value|
98
+ next true if value.nil?
99
+ next false unless value.is_a?(type)
100
+
101
+ if options[:allowed]
102
+ options[:allowed].include?(value)
103
+ else
104
+ true
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ def attributes
111
+ @attributes ||= []
112
+ end
113
+
114
+ def track_timestamp(*fields, on_state: nil, on_states: nil)
115
+ states = on_states || [on_state]
116
+ states.compact.each do |state|
117
+ state_timestamps[state] ||= []
118
+ state_timestamps[state].concat(fields)
119
+ end
120
+ attr_accessor(*fields)
121
+ end
122
+
123
+ def state_timestamps
124
+ @state_timestamps ||= {}
125
+ end
126
+
127
+ def state_messages
128
+ @state_messages ||= {}
129
+ end
130
+
131
+ # Default state message if none specified
132
+ def default_state_message
133
+ @default_state_message ||= ->(token, from, to) { "State changed from #{from} to #{to}" }
134
+ end
135
+
136
+ def default_state_message=(block)
137
+ @default_state_message = block
138
+ end
139
+
140
+ def state_message(for_state:, &block)
141
+ state_messages[for_state] = block
142
+ end
143
+
144
+ # Combined state configuration
145
+ def state_config(state, timestamps: nil, message: nil, &block)
146
+ # Handle timestamps
147
+ if timestamps
148
+ track_timestamp(*Array(timestamps), on_state: state)
149
+ end
150
+
151
+ # Handle message
152
+ if block_given?
153
+ state_message(for_state: state, &block)
154
+ elsif message
155
+ state_message(for_state: state) { |token| message }
156
+ end
157
+ end
158
+
159
+ # Multiple state configuration
160
+ def state_configs(&block)
161
+ config_dsl = StateConfigDSL.new(self)
162
+ config_dsl.instance_eval(&block)
163
+ end
164
+
165
+ # DSL for state configuration
166
+ class StateConfigDSL
167
+ def initialize(token_class)
168
+ @token_class = token_class
169
+ end
170
+
171
+ def state(name, timestamps: nil, message: nil, &block)
172
+ @token_class.state_config(name, timestamps: timestamps, message: message, &block)
173
+ end
174
+
175
+ def on_states(states, timestamps:)
176
+ Array(timestamps).each do |timestamp|
177
+ @token_class.track_timestamp(timestamp, on_states: states)
178
+ end
179
+ end
180
+ end
181
+
182
+ # Visualization methods
183
+ def visualize(format = :mermaid)
184
+ case format
185
+ when :mermaid
186
+ Visualizer.to_mermaid(self)
187
+ when :dot
188
+ Visualizer.to_dot(self)
189
+ when :plantuml
190
+ Visualizer.to_plantuml(self)
191
+ when :html
192
+ Visualizer.to_html(self)
193
+ when :markdown
194
+ Visualizer.to_markdown(self)
195
+ else
196
+ raise ArgumentError, "Unsupported format: #{format}"
197
+ end
198
+ end
199
+ end
200
+
201
+ def initialize(attributes = {})
202
+ @id = SecureRandom.uuid
203
+ @state = self.class::VALID_STATES.first
204
+
205
+ # Initialize all attributes to nil
206
+ self.class.attributes.each do |attr|
207
+ instance_variable_set("@#{attr}", nil)
208
+ end
209
+
210
+ # Set provided attributes
211
+ attributes.each do |key, value|
212
+ send("#{key}=", value) if respond_to?("#{key}=")
213
+ end
214
+
215
+ # Add default transition hook for timestamps and history
216
+ self.class.before_transition do |from, to|
217
+ # Set timestamps for the target state
218
+ if (timestamp_fields = self.class.state_timestamps[to.to_sym])
219
+ timestamp_fields.each do |field|
220
+ send("#{field}=", Time.now)
221
+ end
222
+ end
223
+
224
+ # Record the transition in history with details
225
+ record_event(
226
+ :state_transition,
227
+ {
228
+ from: from,
229
+ to: to,
230
+ timestamp: Time.now,
231
+ details: state_change_details(from, to)
232
+ }
233
+ )
234
+ end
235
+
236
+ @created_at = Time.now
237
+ @updated_at = @created_at
238
+ @event_handlers = Hash.new { |h, k| h[k] = [] }
239
+ @async_handlers = Hash.new { |h, k| h[k] = [] }
240
+ @observers = Hash.new { |h, k| h[k] = [] }
241
+ @async_observers = Hash.new { |h, k| h[k] = [] }
242
+ @history = []
243
+ end
244
+
245
+ def update_state(new_state, actor_id: nil)
246
+ old_state = @state
247
+
248
+ begin
249
+ # Run before transition hooks
250
+ self.class.before_transition_hooks.each { |hook| instance_exec(old_state, new_state, &hook) }
251
+
252
+ # Run state-specific validations
253
+ validate_transition(from: old_state, to: new_state)
254
+ validate_current_state(new_state)
255
+
256
+ # Run custom transition rules
257
+ if rule = self.class.transition_rules[[old_state, new_state]]
258
+ result = instance_exec(&rule)
259
+ raise TransitionError, result if result.is_a?(String)
260
+ end
261
+
262
+ @state = new_state
263
+ @updated_at = Time.now
264
+
265
+ # Record the transition in history
266
+ record_event(:state_transition, {
267
+ from: old_state,
268
+ to: new_state,
269
+ timestamp: @updated_at
270
+ }, actor_id: actor_id)
271
+
272
+ # Run after transition hooks
273
+ self.class.after_transition_hooks.each { |hook| instance_exec(old_state, new_state, &hook) }
274
+
275
+ # Trigger state change events
276
+ trigger(:state_changed, old_state: old_state, new_state: new_state)
277
+ trigger_async(:state_changed, old_state: old_state, new_state: new_state)
278
+
279
+ notify(:state_changed, old_state: old_state, new_state: new_state)
280
+
281
+ true
282
+ rescue StandardError => e
283
+ # Record the failed transition
284
+ record_event(:transition_failed, {
285
+ from: old_state,
286
+ to: new_state,
287
+ error: e.message
288
+ }, actor_id: actor_id)
289
+
290
+ trigger(:transition_failed, error: e, from: old_state, to: new_state)
291
+ trigger_async(:transition_failed, error: e, from: old_state, to: new_state)
292
+ notify(:transition_failed, error: e, from: old_state, to: new_state)
293
+ raise
294
+ end
295
+ end
296
+
297
+ # Event handling
298
+ def on(event, async: false, &handler)
299
+ if async
300
+ @async_handlers[event] << handler
301
+ else
302
+ @event_handlers[event] << handler
303
+ end
304
+ self
305
+ end
306
+
307
+ def trigger(event, **data)
308
+ @event_handlers[event].each { |handler| handler.call(data) }
309
+ end
310
+
311
+ def trigger_async(event, **data)
312
+ return if @async_handlers[event].empty?
313
+
314
+ Async do |task|
315
+ @async_handlers[event].each do |handler|
316
+ task.async do
317
+ begin
318
+ handler.call(data)
319
+ rescue => e
320
+ # Log async handler errors and record in history
321
+ error_msg = "[ERROR] Async handler failed: #{e.message}"
322
+ puts error_msg
323
+ record_event(:async_handler_error, {
324
+ event: event,
325
+ error: error_msg,
326
+ data: data
327
+ })
328
+ end
329
+ end
330
+ end
331
+ end
332
+ end
333
+
334
+ def on(event, async: false, &block)
335
+ if async
336
+ @async_observers[event] << block
337
+ else
338
+ @observers[event] << block
339
+ end
340
+ end
341
+
342
+ def notify(event, data = {})
343
+ @observers[event].each { |observer| observer.call(data) }
344
+
345
+ @async_observers[event].each do |observer|
346
+ Thread.new do
347
+ observer.call(data)
348
+ end
349
+ end
350
+ end
351
+
352
+ # Serialization methods
353
+ def to_h(include_private = false)
354
+ if include_private
355
+ # Get all instance variables including private state
356
+ instance_variables.each_with_object({}) do |var, hash|
357
+ next if [:@event_handlers, :@async_handlers, :@observers, :@async_observers].include?(var)
358
+ key = var.to_s.delete_prefix('@').to_sym
359
+ hash[key] = instance_variable_get(var)
360
+ end
361
+ else
362
+ # Get only publicly accessible attributes
363
+ self.class.instance_methods(false)
364
+ .select { |method| method.to_s !~ /[=!?]$/ }
365
+ .reject { |method| [:inspect, :pretty_print, :to_h, :to_json, :to_yaml, :to_xml].include?(method) }
366
+ .each_with_object({}) do |method, hash|
367
+ hash[method] = send(method)
368
+ end
369
+ end
370
+ end
371
+
372
+ def to_json(include_private = false)
373
+ JSON.pretty_generate(to_h(include_private))
374
+ end
375
+
376
+ def to_yaml(include_private = false)
377
+ to_h(include_private).to_yaml
378
+ end
379
+
380
+ def to_xml(include_private = false)
381
+ require 'nokogiri'
382
+ builder = Nokogiri::XML::Builder.new do |xml|
383
+ xml.token(class: self.class.name) {
384
+ to_h(include_private).each do |key, value|
385
+ xml.send(key, value)
386
+ end
387
+ }
388
+ end
389
+ builder.to_xml
390
+ rescue LoadError
391
+ raise "Nokogiri is required for XML serialization. Add it to your Gemfile."
392
+ end
393
+
394
+ # Pretty print the object's state
395
+ def pretty_print(include_private = false)
396
+ fields = to_h(include_private)
397
+
398
+ # Find the longest key for padding
399
+ max_key_length = fields.keys.map(&:to_s).map(&:length).max
400
+
401
+ output = ["#<#{self.class}"]
402
+ fields.each do |key, value|
403
+ value_str = case value
404
+ when nil then "nil"
405
+ when String then "\"#{value}\""
406
+ when Time then "\"#{value.iso8601}\""
407
+ else value.to_s
408
+ end
409
+
410
+ # Pad the key with spaces for alignment
411
+ padded_key = key.to_s.ljust(max_key_length)
412
+ output << " #{padded_key}: #{value_str}"
413
+ end
414
+ output << ">"
415
+
416
+ output.join("\n")
417
+ end
418
+
419
+ # Show all state in inspect for debugging
420
+ def inspect
421
+ pretty_print(true)
422
+ end
423
+
424
+ def record_transition(transition_name, old_state, new_state)
425
+ @history << {
426
+ transition: transition_name,
427
+ from: old_state,
428
+ to: new_state,
429
+ timestamp: Time.now
430
+ }
431
+ @updated_at = Time.now
432
+ end
433
+
434
+ protected
435
+
436
+ def validate_current_state(state = @state)
437
+ return unless state
438
+ if validator = self.class.state_validations[state]
439
+ result = instance_exec(&validator)
440
+ raise StateError, "Invalid state #{state}: #{result}" if result.is_a?(String)
441
+ end
442
+ end
443
+
444
+ def validate_transition(from:, to:)
445
+ valid_transitions = self.class.state_transitions[from&.to_sym] || []
446
+ unless valid_transitions.include?(to.to_sym)
447
+ raise StateError, "Cannot transition from #{from} to #{to}"
448
+ end
449
+ end
450
+
451
+ def method_missing(method_name, *args)
452
+ # Check if it's a setter method (ends with =)
453
+ if method_name.to_s.end_with?('=')
454
+ attr_name = method_name.to_s.chomp('=')
455
+ if self.class.attributes.include?(attr_name.to_sym)
456
+ self.class.send(:attr_accessor, attr_name.to_sym)
457
+ send(method_name, *args)
458
+ else
459
+ super
460
+ end
461
+ else
462
+ super
463
+ end
464
+ end
465
+
466
+ def respond_to_missing?(method_name, include_private = false)
467
+ # Check if it's a setter method (ends with =)
468
+ if method_name.to_s.end_with?('=')
469
+ attr_name = method_name.to_s.chomp('=')
470
+ self.class.attributes.include?(attr_name.to_sym)
471
+ else
472
+ super
473
+ end
474
+ end
475
+
476
+ private
477
+
478
+ def state_change_details(from, to)
479
+ if (message_block = self.class.state_messages[to.to_sym])
480
+ message_block.call(self)
481
+ else
482
+ self.class.default_state_message.call(self, from, to)
483
+ end
484
+ end
485
+ end
486
+ end
@@ -0,0 +1,173 @@
1
+ require 'json'
2
+ require 'erb'
3
+
4
+ module CircuitBreaker
5
+ class Visualizer
6
+ TEMPLATES_DIR = File.join(File.dirname(__FILE__), 'templates')
7
+
8
+ class << self
9
+ def to_mermaid(token_class)
10
+ transitions = token_class.state_transitions
11
+ states = transitions.keys.concat(transitions.values.flatten).uniq
12
+ validations = token_class.state_validations
13
+
14
+ lines = ["stateDiagram-v2"]
15
+
16
+ # Add states with validation notes
17
+ states.each do |state|
18
+ lines << " #{state}"
19
+ if validations[state]
20
+ lines << " note right of #{state}"
21
+ # Get the file path relative to the project root
22
+ validation_file = validations[state].source_location.first
23
+ relative_path = validation_file.sub(File.expand_path('../../..', __FILE__), '')
24
+ relative_path = relative_path.start_with?('/') ? relative_path[1..-1] : relative_path
25
+ lines << " Validations in:"
26
+ lines << " #{relative_path}"
27
+ lines << " end note"
28
+ end
29
+ end
30
+
31
+ # Add transitions with hooks
32
+ transitions.each do |from, to_states|
33
+ to_states.each do |to|
34
+ lines << " #{from} --> #{to}"
35
+ end
36
+ end
37
+
38
+ lines.join("\n")
39
+ end
40
+
41
+ def to_dot(token_class)
42
+ transitions = token_class.state_transitions
43
+ validations = token_class.state_validations
44
+
45
+ lines = ["digraph {"]
46
+ lines << " rankdir=LR;"
47
+
48
+ # Style definitions
49
+ lines << " node [shape=circle];"
50
+ lines << " edge [fontsize=10];"
51
+
52
+ # Add states with validation info
53
+ transitions.keys.concat(transitions.values.flatten).uniq.each do |state|
54
+ validation = validations[state] ? "with validation" : "no validation"
55
+ lines << " #{state} [label=\"#{state}\\n#{validation}\"];"
56
+ end
57
+
58
+ # Add transitions
59
+ transitions.each do |from, to_states|
60
+ to_states.each do |to|
61
+ lines << " #{from} -> #{to};"
62
+ end
63
+ end
64
+
65
+ lines << "}"
66
+ lines.join("\n")
67
+ end
68
+
69
+ def to_plantuml(token_class)
70
+ transitions = token_class.state_transitions
71
+ validations = token_class.state_validations
72
+
73
+ lines = ["@startuml"]
74
+ lines << "skinparam monochrome true"
75
+ lines << "skinparam defaultFontName Arial"
76
+
77
+ # Add states
78
+ transitions.keys.concat(transitions.values.flatten).uniq.each do |state|
79
+ if validations[state]
80
+ lines << "state #{state} {"
81
+ lines << " note right: Has validations"
82
+ lines << "}"
83
+ else
84
+ lines << "state #{state}"
85
+ end
86
+ end
87
+
88
+ # Add transitions
89
+ transitions.each do |from, to_states|
90
+ to_states.each do |to|
91
+ lines << "#{from} --> #{to}"
92
+ end
93
+ end
94
+
95
+ lines << "@enduml"
96
+ lines.join("\n")
97
+ end
98
+
99
+ def to_html(token_class, engine = :mermaid)
100
+ case engine
101
+ when :mermaid
102
+ diagram = to_mermaid(token_class)
103
+ template_path = File.join(TEMPLATES_DIR, 'mermaid.html.erb')
104
+ when :plantuml
105
+ diagram = to_plantuml(token_class)
106
+ template_path = File.join(TEMPLATES_DIR, 'plantuml.html.erb')
107
+ else
108
+ raise ArgumentError, "Unsupported engine: #{engine}"
109
+ end
110
+
111
+ template = ERB.new(File.read(template_path))
112
+ template.result(binding)
113
+ end
114
+
115
+ def to_markdown(token_class)
116
+ transitions = token_class.state_transitions
117
+ states = transitions.keys.concat(transitions.values.flatten).uniq
118
+ validations = token_class.state_validations
119
+
120
+ lines = ["# #{token_class.name} Workflow\n"]
121
+ lines << "## States\n"
122
+
123
+ states.each do |state|
124
+ lines << "### #{state.to_s.capitalize}\n"
125
+ if validations[state]
126
+ lines << "**Validations:**\n"
127
+ lines << "- State-specific validation rules are enforced\n"
128
+ end
129
+ lines << "\n"
130
+ end
131
+
132
+ lines << "## Transitions\n"
133
+ transitions.each do |from, to_states|
134
+ to_states.each do |to|
135
+ lines << "### #{from} → #{to}\n"
136
+ if token_class.transition_rules[[from, to]]
137
+ lines << "**Rules:**\n"
138
+ lines << "- Transition-specific validation rules are enforced\n"
139
+ end
140
+ lines << "\n"
141
+ end
142
+ end
143
+
144
+ lines << "## Hooks\n"
145
+ lines << "### Before Transition\n"
146
+ lines << "- #{token_class.before_transition_hooks.size} hooks registered\n\n"
147
+ lines << "### After Transition\n"
148
+ lines << "- #{token_class.after_transition_hooks.size} hooks registered\n\n"
149
+
150
+ lines.join
151
+ end
152
+
153
+ def save(token_class, format:, filename:)
154
+ content = case format
155
+ when :mermaid
156
+ to_mermaid(token_class)
157
+ when :dot
158
+ to_dot(token_class)
159
+ when :plantuml
160
+ to_plantuml(token_class)
161
+ when :html
162
+ to_html(token_class)
163
+ when :markdown
164
+ to_markdown(token_class)
165
+ else
166
+ raise ArgumentError, "Unsupported format: #{format}"
167
+ end
168
+
169
+ File.write(filename, content)
170
+ end
171
+ end
172
+ end
173
+ end