Module: Plumbing::Operation::Generator
- Defined in:
- lib/plumbing/operation/generator.rb
Overview
Generates a Plumbing::Operation skeleton from a mermaid flowchart — the inverse of Task#to_mermaid. Pure text-in / text-out; no runtime deps.
Defined Under Namespace
Constant Summary collapse
- Error =
Class.new(StandardError)
- NODE_AND_EDGE =
A node (id + shape) optionally followed by an edge on the same line.
/\A (?<node>\w+(?:\(\[.*?\]\)|\{\{".*?"\}\}|\{".*?"\}|\[".*?"\])) (?:\s*-->\s*(?:\|(?<elabel>[^|]*)\|\s*)?(?<target>\w+))? \z/x- EDGE_ONLY =
A standalone edge:
from --> toorfrom -->|label| to. /\A(?<from>\w+)\s*-->\s*(?:\|(?<elabel>[^|]*)\|\s*)?(?<target>\w+)\z/
Class Method Summary collapse
- .blank_to_nil(str) ⇒ Object
- .build_node(token, lineno) ⇒ Object
- .emit(nodes, edges, start, class_name) ⇒ Object
- .emit_node(node, edges) ⇒ Object
- .from_mermaid(mermaid, class_name:) ⇒ Object
- .go_to(edge) ⇒ Object
- .parse(text) ⇒ Object
- .resolve_start(nodes, edges, start_target) ⇒ Object
- .unquote(str) ⇒ Object
- .validate!(nodes, edges) ⇒ Object
Class Method Details
.blank_to_nil(str) ⇒ Object
75 |
# File 'lib/plumbing/operation/generator.rb', line 75 def blank_to_nil(str) = (str.nil? || str.empty?) ? nil : str |
.build_node(token, lineno) ⇒ Object
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/plumbing/operation/generator.rb', line 53 def build_node(token, lineno) case token when /\A(\w+)\(\[(.*)\]\)\z/ id = $1 label = unquote($2) (id == "start" || label.casecmp?("Start")) ? [Node.new(id, :start, label), :start] : [Node.new(id, :result, label), :result] when /\A(\w+)\{\{(.*)\}\}\z/ [Node.new($1, :wait, unquote($2)), :wait] when /\A(\w+)\{(.*)\}\z/ [Node.new($1, :decision, unquote($2)), :decision] when /\A(\w+)\[(.*)\]\z/ [Node.new($1, :action, unquote($2)), :action] else raise Error, "line #{lineno}: unrecognised node #{token.inspect}" end end |
.emit(nodes, edges, start, class_name) ⇒ Object
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/plumbing/operation/generator.rb', line 109 def emit(nodes, edges, start, class_name) out = [] out << "# Generated from a mermaid flowchart. Fill in the attributes, guard bodies and" out << "# action bodies (marked `raise NotImplementedError`), then run `standardrb --fix`." out << "class #{class_name} < Plumbing::Operation" out << " # TODO: declare attributes, e.g. attribute :name, String" out << "" out << " starts_with :#{start}" nodes.each_value do |node| out << "" out.concat(emit_node(node, edges)) end out << "end" out.join("\n") + "\n" end |
.emit_node(node, edges) ⇒ Object
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/plumbing/operation/generator.rb', line 125 def emit_node(node, edges) outgoing = edges.select { |edge| edge.from == node.id } lines = [] lines << " # #{node.label}" if node.label && !node.label.empty? && node.label != node.id case node.kind when :action edge = outgoing.first lines << " # (edge label #{edge.label.inspect} dropped — actions take no label)" if edge.label lines << " action(:#{node.id}) { raise NotImplementedError }.then :#{edge.to}" when :decision lines << " decision :#{node.id} do" outgoing.each { |edge| lines << " #{go_to(edge)}" } lines << " end" when :wait lines << " wait_until :#{node.id} do" outgoing.each { |edge| lines << " #{go_to(edge)}" } lines << " end" when :result lines << " result :#{node.id}" end lines end |
.from_mermaid(mermaid, class_name:) ⇒ Object
77 78 79 80 81 |
# File 'lib/plumbing/operation/generator.rb', line 77 def from_mermaid(mermaid, class_name:) nodes, edges, start_target = parse(mermaid) validate!(nodes, edges) emit(nodes, edges, resolve_start(nodes, edges, start_target), class_name) end |
.go_to(edge) ⇒ Object
148 149 150 151 152 153 154 |
# File 'lib/plumbing/operation/generator.rb', line 148 def go_to(edge) if edge.label "go_to :#{edge.to}, #{edge.label.inspect}, if: -> { raise NotImplementedError }" else "go_to :#{edge.to}" end end |
.parse(text) ⇒ Object
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/plumbing/operation/generator.rb', line 24 def parse(text) nodes = {} edges = [] start_target = nil text.each_line.with_index(1) do |raw, lineno| line = raw.strip next if line.empty? || line.start_with?("%%", "flowchart", "graph") if (m = NODE_AND_EDGE.match(line)) node, kind = build_node(m[:node], lineno) if kind == :start start_target = m[:target] if m[:target] else nodes[node.id] = node edges << Edge.new(node.id, m[:target], blank_to_nil(m[:elabel])) if m[:target] end elsif (m = EDGE_ONLY.match(line)) if m[:from] == "start" start_target = m[:target] else edges << Edge.new(m[:from], m[:target], blank_to_nil(m[:elabel])) end else raise Error, "line #{lineno}: cannot parse #{line.inspect}" end end [nodes, edges, start_target] end |
.resolve_start(nodes, edges, start_target) ⇒ Object
101 102 103 104 105 106 107 |
# File 'lib/plumbing/operation/generator.rb', line 101 def resolve_start(nodes, edges, start_target) return start_target if start_target inbound = edges.map(&:to) candidates = nodes.keys.reject { |id| inbound.include?(id) } raise Error, "cannot determine the start state (no Start marker; #{candidates.size} nodes have no inbound edge)" unless candidates.size == 1 candidates.first end |
.unquote(str) ⇒ Object
70 71 72 73 |
# File 'lib/plumbing/operation/generator.rb', line 70 def unquote(str) s = str.strip (s.start_with?('"') && s.end_with?('"') && s.length >= 2) ? s[1..-2] : s end |
.validate!(nodes, edges) ⇒ Object
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/plumbing/operation/generator.rb', line 83 def validate!(nodes, edges) edges.each do |edge| raise Error, "edge references undefined node :#{edge.from}" unless nodes.key?(edge.from) raise Error, "edge references undefined node :#{edge.to}" unless nodes.key?(edge.to) end nodes.each_value do |node| outgoing = edges.count { |edge| edge.from == node.id } case node.kind when :action raise Error, "action :#{node.id} has #{outgoing} transitions — actions take exactly one (use a decision/wait shape)" unless outgoing == 1 when :decision, :wait raise Error, "#{node.kind} :#{node.id} has no outgoing transitions" if outgoing.zero? when :result raise Error, "result :#{node.id} has outgoing transitions" unless outgoing.zero? end end end |