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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42195ffd537b18be888f0bbab239d61d9ff19a2ff91c50adf0d6e51bea4119b0
4
- data.tar.gz: ec684979c91e8a5bba4538e6a0c0546d1b23c2746d83b815cc9f43f5cb01fd83
3
+ metadata.gz: 20f38e7ccf237cac5499afb5723d5d8f8f4ffe26b3ecbe7e05ee71ad05366438
4
+ data.tar.gz: e49a9f22259fea102bc6e99d5545590ae4fa40689cd2679318bababba8a5a075
5
5
  SHA512:
6
- metadata.gz: eb5064f15bb2d4756139ae66b8a229d35afd9e3df5dd09ae0f8875c522661ffa82c7d0773d6dd088becca25c42f17da350fee347723357d21bd6b67037aa256d
7
- data.tar.gz: 1d800dc29d73a0e5f8e88821abba4ec9b3e4d48e109362b5bbd15550a0af4a7e40e2c7a00346c64ac8eebe6d8caf327c57129ea1ae3d09de2129922c235adc0a
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.1] - 2025-10-01
8
+ ## [2.0.2] - 2025-10-02
9
9
 
10
- ### Changed
10
+ ### Added
11
11
 
12
- - Switched to a rake test task to set the test order
12
+ - Loading of required files with -r command option for circulator-diagram.
13
13
 
14
- ### Added
14
+ ### Changed
15
15
 
16
- - Ability to generate diagrams from declared flows in Dot or PlantUML
16
+ - Implement diagrams through a standard base class to coordinate implementation.
@@ -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
@@ -1,92 +1,71 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Circulator
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
- def generate
20
- output = []
21
- output << "digraph #{graph_name} {"
22
- output << " rankdir=LR;"
23
- output << ""
5
+ module Circulator
6
+ class Dot < Diagram
7
+ private
24
8
 
25
- # Collect all states and transitions
26
- states = Set.new
27
- transitions = []
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
- @flows.each do |model_key, attribute_flows|
30
- attribute_flows.each do |attribute_name, flow|
31
- # Extract states and transitions from the flow
32
- flow.transition_map.each do |action, state_transitions|
33
- state_transitions.each do |from_state, transition_info|
34
- states.add(from_state)
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
- to_state = transition_info[:to]
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
- output << "}"
71
- output.join("\n") + "\n"
41
+ def header
42
+ <<~DOT
43
+ digraph "#{graph_name}" {
44
+ rankdir=LR;
45
+ DOT
72
46
  end
73
47
 
74
- private
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 graph_name
77
- # Use the model class name if available, otherwise use the model key
78
- class_name = @model_class.name
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
- # If class has no name, use the model_key which will be like "anonymous_XXX"
82
- # If model_key differs from class_name, it's a model-based flow, use model_key
83
- if class_name.nil?
84
- "#{model_key}_flows"
85
- elsif model_key && model_key != class_name
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
@@ -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
- def initialize(model_class)
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
- flows = model_class.flows
11
- if flows.nil? || flows.empty?
12
- raise ArgumentError, "Model class has no flows defined"
13
- end
9
+ # def graph_name
10
+ # @model_class.name || "diagram"
11
+ # end
14
12
 
15
- @model_class = model_class
16
- @flows = flows
13
+ def header
14
+ <<~PLANTUML
15
+ @startuml #{graph_name}
16
+ title #{graph_name}
17
+ PLANTUML
17
18
  end
18
19
 
19
- def generate
20
- output = []
21
- output << "@startuml"
22
- output << ""
20
+ def footer
21
+ <<~PLANTUML
23
22
 
24
- # Collect all states and transitions
25
- states = Set.new
26
- transitions = []
23
+ @enduml
24
+ PLANTUML
25
+ end
27
26
 
28
- @flows.each do |model_key, attribute_flows|
29
- attribute_flows.each do |attribute_name, flow|
30
- # Extract states and transitions from the flow
31
- flow.transition_map.each do |action, state_transitions|
32
- state_transitions.each do |from_state, transition_info|
33
- states.add(from_state)
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
- to_state = transition_info[:to]
36
- if to_state.respond_to?(:call)
37
- # Dynamic state - use [*] as placeholder (end state)
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
- # Output state declarations (except nil which is represented as [*])
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
- output << ""
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Circulator
4
- VERSION = "2.0.1"
4
+ VERSION = "2.0.2"
5
5
  end
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.1
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