Class: Pangea::CLI::Reactivity::Graph
- Defined in:
- lib/pangea/cli/reactivity.rb
Overview
Full constellation graph computed from a workspaces root directory.
Instance Attribute Summary collapse
-
#workspaces ⇒ Object
readonly
Returns the value of attribute workspaces.
Class Method Summary collapse
-
.scan(workspaces_root) ⇒ Object
Scan a workspaces root directory (parent of many workspace dirs) and build a Graph.
- .siblings_with_pangea_yml(parent_path) ⇒ Object
-
.workspaces_root_for(template_file) ⇒ Object
Given a template file path, locate the enclosing workspaces root (the parent dir whose children are the constellation workspaces).
Instance Method Summary collapse
- #[](name) ⇒ Object
-
#cascade_set(name, max_depth: nil) ⇒ Object
Transitive closure of ‘name` in both directions: every workspace that must plan/apply together when user acts on `name`.
-
#downstream_of(name) ⇒ Object
Names of workspaces that ask from ‘name` (direct downstream).
- #include?(name) ⇒ Boolean
-
#initialize(workspaces) ⇒ Graph
constructor
A new instance of Graph.
- #names ⇒ Object
-
#topo_sort(names_subset) ⇒ Object
Topologically sort a subset of workspace names — earlier names can run before later ones because no later name asks FROM an earlier one.
-
#upstream_of(name) ⇒ Object
Names of workspaces ‘name` asks from (direct upstream).
Constructor Details
#initialize(workspaces) ⇒ Graph
Returns a new instance of Graph.
120 121 122 123 |
# File 'lib/pangea/cli/reactivity.rb', line 120 def initialize(workspaces) @workspaces = workspaces.freeze # Hash<String, Workspace> freeze end |
Instance Attribute Details
#workspaces ⇒ Object (readonly)
Returns the value of attribute workspaces.
118 119 120 |
# File 'lib/pangea/cli/reactivity.rb', line 118 def workspaces @workspaces end |
Class Method Details
.scan(workspaces_root) ⇒ Object
Scan a workspaces root directory (parent of many workspace dirs) and build a Graph. Returns an empty Graph if the root doesn’t exist or contains no pangea.yml siblings.
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/pangea/cli/reactivity.rb', line 128 def self.scan(workspaces_root) return new({}) unless Dir.exist?(workspaces_root) ws = {} Dir.entries(workspaces_root).sort.each do |entry| next if entry.start_with?('.') dir = File.join(workspaces_root, entry) next unless File.directory?(dir) workspace = Workspace.load(dir) ws[workspace.name] = workspace if workspace end new(ws) end |
.siblings_with_pangea_yml(parent_path) ⇒ Object
157 158 159 160 161 162 163 |
# File 'lib/pangea/cli/reactivity.rb', line 157 def self.siblings_with_pangea_yml(parent_path) return 0 unless parent_path.directory? parent_path.children.count do |child| child.directory? && child.join('pangea.yml').file? end end |
.workspaces_root_for(template_file) ⇒ Object
Given a template file path, locate the enclosing workspaces root (the parent dir whose children are the constellation workspaces). Walks up from the template’s directory looking for the first ancestor that contains multiple pangea.yml siblings. Returns nil if none.
148 149 150 151 152 153 154 155 |
# File 'lib/pangea/cli/reactivity.rb', line 148 def self.workspaces_root_for(template_file) start = File.(File.dirname(template_file)) Pathname.new(start).ascend do |path| parent = path.parent return parent.to_s if siblings_with_pangea_yml(parent) >= 2 end nil end |
Instance Method Details
#[](name) ⇒ Object
165 166 167 |
# File 'lib/pangea/cli/reactivity.rb', line 165 def [](name) workspaces[name] end |
#cascade_set(name, max_depth: nil) ⇒ Object
Transitive closure of ‘name` in both directions: every workspace that must plan/apply together when user acts on `name`. Returns a Set of names INCLUDING `name` itself.
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 |
# File 'lib/pangea/cli/reactivity.rb', line 197 def cascade_set(name, max_depth: nil) raise MissingWorkspaceError, "unknown workspace: #{name}" unless workspaces.key?(name) visited = { name => 0 } frontier = [name] until frontier.empty? n = frontier.shift depth = visited[n] next if max_depth && depth >= max_depth (upstream_of(n) + downstream_of(n)).each do |neighbor| next if visited.key?(neighbor) visited[neighbor] = depth + 1 frontier << neighbor end end Set.new(visited.keys) end |
#downstream_of(name) ⇒ Object
Names of workspaces that ask from ‘name` (direct downstream).
184 185 186 187 188 |
# File 'lib/pangea/cli/reactivity.rb', line 184 def downstream_of(name) workspaces.each_value.select do |w| w.upstream_names.include?(name) end.map(&:name) end |
#include?(name) ⇒ Boolean
169 170 171 |
# File 'lib/pangea/cli/reactivity.rb', line 169 def include?(name) workspaces.key?(name) end |
#names ⇒ Object
173 174 175 |
# File 'lib/pangea/cli/reactivity.rb', line 173 def names workspaces.keys end |
#topo_sort(names_subset) ⇒ Object
Topologically sort a subset of workspace names — earlier names can run before later ones because no later name asks FROM an earlier one. Kahn’s algorithm; raises CycleError if the subset contains a cycle.
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/pangea/cli/reactivity.rb', line 220 def topo_sort(names_subset) subset = names_subset.to_a subset_set = Set.new(subset) # incoming edges: name -> names_in_subset_this_asks_from incoming = subset.each_with_object({}) do |n, h| h[n] = upstream_of(n).select { |u| subset_set.include?(u) } end ready = subset.select { |n| incoming[n].empty? }.sort ordered = [] until ready.empty? n = ready.shift ordered << n subset.each do |m| next unless incoming[m].delete(n) ready << m if incoming[m].empty? end ready.sort! end unless ordered.size == subset.size stuck = subset - ordered raise CycleError, "reactive ask cycle among: #{stuck.sort.join(', ')}" end ordered end |
#upstream_of(name) ⇒ Object
Names of workspaces ‘name` asks from (direct upstream).
178 179 180 181 |
# File 'lib/pangea/cli/reactivity.rb', line 178 def upstream_of(name) ws = workspaces[name] or return [] ws.upstream_names.select { |n| workspaces.key?(n) } end |