circulator 2.0.1 → 2.0.2
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 +4 -4
- data/CHANGELOG.md +5 -5
- data/exe/circulator-diagram +20 -1
- data/lib/circulator/diagram.rb +97 -0
- data/lib/circulator/dot.rb +44 -65
- data/lib/circulator/plantuml.rb +39 -57
- data/lib/circulator/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 20f38e7ccf237cac5499afb5723d5d8f8f4ffe26b3ecbe7e05ee71ad05366438
|
4
|
+
data.tar.gz: e49a9f22259fea102bc6e99d5545590ae4fa40689cd2679318bababba8a5a075
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e59f461501d8bd236755dcc30eaa277372e14aca6a61236545ab5df4334f47668fe4878ee4a418bedc539c16891f1fc6cec3f54d88e19ecd9c766146e4b796e
|
7
|
+
data.tar.gz: 4e98a06987e4d08f44cc3cf81ea5544c219bff24a5f08990bac6b2c5c93193a47ce2d9a20aaecb4cdb28fa24cce06420093966b0be49c60f19fe8302198097ec
|
data/CHANGELOG.md
CHANGED
@@ -5,12 +5,12 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
-
## [2.0.
|
8
|
+
## [2.0.2] - 2025-10-02
|
9
9
|
|
10
|
-
###
|
10
|
+
### Added
|
11
11
|
|
12
|
-
-
|
12
|
+
- Loading of required files with -r command option for circulator-diagram.
|
13
13
|
|
14
|
-
###
|
14
|
+
### Changed
|
15
15
|
|
16
|
-
-
|
16
|
+
- Implement diagrams through a standard base class to coordinate implementation.
|
data/exe/circulator-diagram
CHANGED
@@ -8,7 +8,7 @@ require_relative "../lib/circulator/dot"
|
|
8
8
|
require_relative "../lib/circulator/plantuml"
|
9
9
|
|
10
10
|
# Parse command-line options
|
11
|
-
options = {format: "dot"}
|
11
|
+
options = {format: "dot", require: nil}
|
12
12
|
parser = OptionParser.new do |opts|
|
13
13
|
opts.banner = "Usage: circulator-diagram MODEL_NAME [options]"
|
14
14
|
opts.separator ""
|
@@ -23,6 +23,10 @@ parser = OptionParser.new do |opts|
|
|
23
23
|
options[:format] = format
|
24
24
|
end
|
25
25
|
|
26
|
+
opts.on("-r", "--require FILE", "Require a file before loading the model (e.g., config/environment)") do |file|
|
27
|
+
options[:require] = file
|
28
|
+
end
|
29
|
+
|
26
30
|
opts.on("-h", "--help", "Show this help message") do
|
27
31
|
puts opts
|
28
32
|
exit 0
|
@@ -52,6 +56,21 @@ end
|
|
52
56
|
|
53
57
|
model_name = ARGV[0]
|
54
58
|
|
59
|
+
# Load the application environment
|
60
|
+
# Priority: -r option > config/environment.rb > nothing
|
61
|
+
if options[:require]
|
62
|
+
# User specified a file to require
|
63
|
+
require_file = options[:require]
|
64
|
+
unless File.exist?(require_file)
|
65
|
+
warn "Error: Required file '#{require_file}' not found"
|
66
|
+
exit 1
|
67
|
+
end
|
68
|
+
require require_file
|
69
|
+
elsif File.exist?("config/environment.rb")
|
70
|
+
# Rails application detected, load the environment
|
71
|
+
require File.expand_path("config/environment.rb")
|
72
|
+
end
|
73
|
+
|
55
74
|
# Try to constantize the model name
|
56
75
|
begin
|
57
76
|
model_class = Object.const_get(model_name)
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Circulator
|
4
|
+
class Diagram
|
5
|
+
def initialize(model_class)
|
6
|
+
unless model_class.respond_to?(:flows)
|
7
|
+
raise ArgumentError, "Model class must extend Circulator"
|
8
|
+
end
|
9
|
+
|
10
|
+
flows = model_class.flows
|
11
|
+
if flows.nil? || flows.empty?
|
12
|
+
raise ArgumentError, "Model class has no flows defined"
|
13
|
+
end
|
14
|
+
|
15
|
+
@model_class = model_class
|
16
|
+
@flows = flows
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate
|
20
|
+
output = []
|
21
|
+
output << header
|
22
|
+
|
23
|
+
# Collect all states and transitions
|
24
|
+
states = Set.new
|
25
|
+
transitions = []
|
26
|
+
|
27
|
+
@flows.each do |model_key, attribute_flows|
|
28
|
+
attribute_flows.each do |attribute_name, flow|
|
29
|
+
# Extract states and transitions from the flow
|
30
|
+
flow.transition_map.each do |action, state_transitions|
|
31
|
+
state_transitions.each do |from_state, transition_info|
|
32
|
+
states.add(from_state)
|
33
|
+
|
34
|
+
to_state = transition_info[:to]
|
35
|
+
if to_state.respond_to?(:call)
|
36
|
+
states.add(:"?")
|
37
|
+
transitions << dynamic_transition(action, from_state, :"?")
|
38
|
+
else
|
39
|
+
states.add(to_state)
|
40
|
+
transitions << standard_transition(action, from_state, to_state, conditional: transition_info[:allow_if])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Output state nodes
|
48
|
+
states_output(states, output)
|
49
|
+
|
50
|
+
# Output transition edges
|
51
|
+
transitions_output(transitions, output)
|
52
|
+
|
53
|
+
output << footer
|
54
|
+
output.join("\n") + "\n"
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def graph_name
|
60
|
+
# Use the model class name if available, otherwise use the model key
|
61
|
+
class_name = @model_class.name
|
62
|
+
model_key = @flows.keys.first
|
63
|
+
|
64
|
+
# If class has no name or model_key differs from class_name (model-based flow),
|
65
|
+
# use model_key, otherwise use class_name
|
66
|
+
if class_name.nil? || (model_key && model_key != class_name)
|
67
|
+
"#{model_key} flows"
|
68
|
+
else
|
69
|
+
"#{class_name} flows"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def header
|
74
|
+
raise NotImplementedError, "Subclasses must implement #{__method__}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def footer
|
78
|
+
raise NotImplementedError, "Subclasses must implement #{__method__}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def transitions_output(transitions, output)
|
82
|
+
raise NotImplementedError, "Subclasses must implement #{__method__}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def states_output(states, output)
|
86
|
+
raise NotImplementedError, "Subclasses must implement #{__method__}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def standard_transition(action, from_state, to_state, conditional: nil)
|
90
|
+
raise NotImplementedError, "Subclasses must implement #{__method__}"
|
91
|
+
end
|
92
|
+
|
93
|
+
def dynamic_transition(action, from_state, to_state = nil)
|
94
|
+
raise NotImplementedError, "Subclasses must implement #{__method__}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/lib/circulator/dot.rb
CHANGED
@@ -1,92 +1,71 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
class Dot
|
5
|
-
def initialize(model_class)
|
6
|
-
unless model_class.respond_to?(:flows)
|
7
|
-
raise ArgumentError, "Model class must extend Circulator"
|
8
|
-
end
|
9
|
-
|
10
|
-
flows = model_class.flows
|
11
|
-
if flows.nil? || flows.empty?
|
12
|
-
raise ArgumentError, "Model class has no flows defined"
|
13
|
-
end
|
14
|
-
|
15
|
-
@model_class = model_class
|
16
|
-
@flows = flows
|
17
|
-
end
|
3
|
+
require_relative "diagram"
|
18
4
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
output << " rankdir=LR;"
|
23
|
-
output << ""
|
5
|
+
module Circulator
|
6
|
+
class Dot < Diagram
|
7
|
+
private
|
24
8
|
|
25
|
-
|
26
|
-
|
27
|
-
|
9
|
+
# def graph_name
|
10
|
+
# # Use the model class name if available, otherwise use the model key
|
11
|
+
# class_name = @model_class.name
|
12
|
+
# model_key = @flows.keys.first
|
28
13
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
14
|
+
# # If class has no name or model_key differs from class_name (model-based flow),
|
15
|
+
# # use model_key, otherwise use class_name
|
16
|
+
# if class_name.nil? || (model_key && model_key != class_name)
|
17
|
+
# "#{model_key} flows"
|
18
|
+
# else
|
19
|
+
# "#{class_name} flows"
|
20
|
+
# end
|
21
|
+
# end
|
35
22
|
|
36
|
-
|
37
|
-
if to_state.respond_to?(:call)
|
38
|
-
# Dynamic state - use ? as placeholder
|
39
|
-
states.add(:"?")
|
40
|
-
label = "#{action} (dynamic)"
|
41
|
-
transitions << {from: from_state, to: :"?", label: label}
|
42
|
-
else
|
43
|
-
states.add(to_state)
|
44
|
-
label = action.to_s
|
45
|
-
label += " (conditional)" if transition_info[:allow_if]
|
46
|
-
transitions << {from: from_state, to: to_state, label: label}
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
# Output state nodes
|
23
|
+
def states_output(states, output)
|
54
24
|
output << " // States"
|
55
25
|
states.sort_by { |s| s.to_s }.each do |state|
|
56
26
|
state_label = state.nil? ? "nil" : state.to_s
|
57
27
|
output << " #{state_label} [shape=circle];"
|
58
28
|
end
|
29
|
+
end
|
59
30
|
|
31
|
+
def transitions_output(transitions, output)
|
60
32
|
output << ""
|
61
33
|
output << " // Transitions"
|
62
|
-
|
63
|
-
# Output transition edges
|
64
34
|
transitions.sort_by { |t| [t[:from].to_s, t[:to].to_s, t[:label]] }.each do |transition|
|
65
35
|
from_label = transition[:from].nil? ? "nil" : transition[:from].to_s
|
66
36
|
to_label = transition[:to].nil? ? "nil" : transition[:to].to_s
|
67
37
|
output << " #{from_label} -> #{to_label} [label=\"#{transition[:label]}\"];"
|
68
38
|
end
|
39
|
+
end
|
69
40
|
|
70
|
-
|
71
|
-
|
41
|
+
def header
|
42
|
+
<<~DOT
|
43
|
+
digraph "#{graph_name}" {
|
44
|
+
rankdir=LR;
|
45
|
+
DOT
|
72
46
|
end
|
73
47
|
|
74
|
-
|
48
|
+
def footer
|
49
|
+
"}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def dynamic_transition(action, from_state, to_state = nil)
|
53
|
+
{
|
54
|
+
from: from_state,
|
55
|
+
to: to_state,
|
56
|
+
label: " #{action} (dynamic)"
|
57
|
+
}
|
58
|
+
end
|
75
59
|
|
76
|
-
def
|
77
|
-
|
78
|
-
|
79
|
-
model_key = @flows.keys.first
|
60
|
+
def standard_transition(action, from_state, to_state, conditional: nil)
|
61
|
+
label = action.to_s
|
62
|
+
label += " (conditional)" if conditional
|
80
63
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
"#{model_key}_flows"
|
87
|
-
else
|
88
|
-
"#{class_name}_flows"
|
89
|
-
end
|
64
|
+
{
|
65
|
+
from: from_state,
|
66
|
+
to: to_state,
|
67
|
+
label: label
|
68
|
+
}
|
90
69
|
end
|
91
70
|
end
|
92
71
|
end
|
data/lib/circulator/plantuml.rb
CHANGED
@@ -1,72 +1,58 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "diagram"
|
4
|
+
|
3
5
|
module Circulator
|
4
|
-
class PlantUml
|
5
|
-
|
6
|
-
unless model_class.respond_to?(:flows)
|
7
|
-
raise ArgumentError, "Model class must extend Circulator"
|
8
|
-
end
|
6
|
+
class PlantUml < Diagram
|
7
|
+
private
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
end
|
9
|
+
# def graph_name
|
10
|
+
# @model_class.name || "diagram"
|
11
|
+
# end
|
14
12
|
|
15
|
-
|
16
|
-
|
13
|
+
def header
|
14
|
+
<<~PLANTUML
|
15
|
+
@startuml #{graph_name}
|
16
|
+
title #{graph_name}
|
17
|
+
PLANTUML
|
17
18
|
end
|
18
19
|
|
19
|
-
def
|
20
|
-
|
21
|
-
output << "@startuml"
|
22
|
-
output << ""
|
20
|
+
def footer
|
21
|
+
<<~PLANTUML
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
@enduml
|
24
|
+
PLANTUML
|
25
|
+
end
|
27
26
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
27
|
+
def dynamic_transition(action, from_state, to_state = nil)
|
28
|
+
{
|
29
|
+
from: from_state,
|
30
|
+
to: to_state,
|
31
|
+
label: action.to_s,
|
32
|
+
note: "dynamic target state"
|
33
|
+
}
|
34
|
+
end
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
label = action.to_s
|
39
|
-
transitions << {
|
40
|
-
from: from_state,
|
41
|
-
to: nil,
|
42
|
-
label: label,
|
43
|
-
note: "dynamic target state"
|
44
|
-
}
|
45
|
-
else
|
46
|
-
states.add(to_state)
|
47
|
-
label = action.to_s
|
48
|
-
note = nil
|
49
|
-
note = "conditional transition" if transition_info[:allow_if]
|
50
|
-
transitions << {
|
51
|
-
from: from_state,
|
52
|
-
to: to_state,
|
53
|
-
label: label,
|
54
|
-
note: note
|
55
|
-
}
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
36
|
+
def standard_transition(action, from_state, to_state, conditional: nil)
|
37
|
+
note = if conditional
|
38
|
+
"conditional transition"
|
60
39
|
end
|
61
40
|
|
62
|
-
|
41
|
+
{
|
42
|
+
from: from_state,
|
43
|
+
to: to_state,
|
44
|
+
label: action.to_s,
|
45
|
+
note:
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def states_output(states, output)
|
63
50
|
states.reject(&:nil?).sort_by(&:to_s).each do |state|
|
64
51
|
output << "state #{state}"
|
65
52
|
end
|
53
|
+
end
|
66
54
|
|
67
|
-
|
68
|
-
|
69
|
-
# Output transitions
|
55
|
+
def transitions_output(transitions, output)
|
70
56
|
transitions.sort_by { |t| [t[:from].to_s, t[:to].to_s, t[:label]] }.each do |transition|
|
71
57
|
from_label = transition[:from].nil? ? "[*]" : transition[:from].to_s
|
72
58
|
to_label = transition[:to].nil? ? "[*]" : transition[:to].to_s
|
@@ -79,10 +65,6 @@ module Circulator
|
|
79
65
|
output << "end note"
|
80
66
|
end
|
81
67
|
end
|
82
|
-
|
83
|
-
output << ""
|
84
|
-
output << "@enduml"
|
85
|
-
output.join("\n") + "\n"
|
86
68
|
end
|
87
69
|
end
|
88
70
|
end
|
data/lib/circulator/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: circulator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jim Gay
|
@@ -22,6 +22,7 @@ files:
|
|
22
22
|
- Rakefile
|
23
23
|
- exe/circulator-diagram
|
24
24
|
- lib/circulator.rb
|
25
|
+
- lib/circulator/diagram.rb
|
25
26
|
- lib/circulator/dot.rb
|
26
27
|
- lib/circulator/flow.rb
|
27
28
|
- lib/circulator/plantuml.rb
|