Class: Pangea::CLI::Reactivity::Graph

Inherits:
Object
  • Object
show all
Defined in:
lib/pangea/cli/reactivity.rb

Overview

Full constellation graph computed from a workspaces root directory.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

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

#workspacesObject (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.expand_path(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.

Parameters:

  • max_depth (Integer, nil) (defaults to: nil)

    if set, limits traversal to that many hops from ‘name` (0 = only seed, 1 = seed + direct neighbors, etc). nil = unlimited (full reachable set).

Raises:



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

Returns:

  • (Boolean)


169
170
171
# File 'lib/pangea/cli/reactivity.rb', line 169

def include?(name)
  workspaces.key?(name)
end

#namesObject



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