module SemanticPuppet::Dependency
Public Instance Methods
Appends a new {Source} to the current list. @param source [Source] the {Source} to add @return [void]
# File lib/semantic_puppet/dependency.rb, line 24 def add_source(source) sources @sources << source nil end
Clears the current list of {Source}s. @return [void]
# File lib/semantic_puppet/dependency.rb, line 32 def clear_sources sources @sources.clear nil end
Fetches all available releases for the given module name.
@param name [String] the module name to find releases for @return [Array<ModuleRelease>] the available releases
# File lib/semantic_puppet/dependency.rb, line 77 def fetch_releases(name) releases = {} sources.each do |source| source.fetch(name).each do |dependency| releases[dependency.version] ||= dependency end end return releases.values end
Fetches a graph of modules and their dependencies from the currently configured list of {Source}s.
@todo Return a specialized “Graph” object. @todo Allow for external constraints to be added to the graph. @see sources
@see add_source
@see clear_sources
@param modules [{ String => String }] @return [Graph] the root of a dependency graph
# File lib/semantic_puppet/dependency.rb, line 51 def query(modules) constraints = Hash[modules.map { |k, v| [ k, VersionRange.parse(v) ] }] graph = Graph.new(constraints) fetch_dependencies(graph) return graph end
Given a graph result from {#query}, this method will resolve the graph of dependencies, if possible, into a flat list of the best suited modules. If the dependency graph does not have a suitable resolution, this method will raise an exception to that effect.
@param graph [Graph] the root of a dependency graph @return [Array<ModuleRelease>] the list of releases to act on
# File lib/semantic_puppet/dependency.rb, line 66 def resolve(graph) catch :next do return walk(graph, graph.dependencies.dup) end raise UnsatisfiableGraph.new(graph) end
@return [Array<Source>] a frozen copy of the {Source} list
# File lib/semantic_puppet/dependency.rb, line 17 def sources (@sources ||= []).dup.freeze end
Private Instance Methods
Given a {ModuleRelease}, this method will iterate through the current list of {Source}s to find the complete list of versions available for its dependencies.
@param node [GraphNode] the node to fetch details for @return [void]
# File lib/semantic_puppet/dependency.rb, line 154 def fetch_dependencies(node, cache = {}) node.dependency_names.each do |name| unless cache.key?(name) cache[name] = fetch_releases(name) cache[name].each { |dep| fetch_dependencies(dep, cache) } end node << cache[name] end end
Given a list of potential releases, this method returns the most suitable releases for exploration. Only {ModuleRelease#satisfied? satisfied} releases are considered, and releases with stable versions are preferred.
@param releases [Array<ModuleRelease>] a list of potential releases @return [Array<ModuleRelease>] releases open for consideration
# File lib/semantic_puppet/dependency.rb, line 171 def preferred_releases(releases) satisfied = releases.select { |x| x.satisfied? } if satisfied.any? { |x| x.version.stable? } return satisfied.select { |x| x.version.stable? } else return satisfied end end
Iterates over a changing set of dependencies in search of the best solution available. Fitness is specified as meeting all the constraints placed on it, being {ModuleRelease#satisfied? satisfied}, and having the greatest version number (with stability being preferred over prereleases).
@todo Traversal order is not presently guaranteed.
@param graph [Graph] the root of a dependency graph @param dependencies [{ String => Array<ModuleRelease> }] the dependencies @param considering [Array<GraphNode>] the set of releases being tested @return [Array<GraphNode>] the list of releases to use, if successful
# File lib/semantic_puppet/dependency.rb, line 102 def walk(graph, dependencies, *considering) return considering if dependencies.empty? # Selecting a dependency from the collection... name = dependencies.keys.sort.first deps = dependencies.delete(name) # ... (and stepping over it if we've seen it before) ... unless (deps & considering).empty? return walk(graph, dependencies, *considering) end # ... we'll iterate through the list of possible versions in order. preferred_releases(deps).reverse_each do |dep| # We should skip any releases that violate any module's constraints. unless [graph, *considering].all? { |x| x.satisfies_constraints?(dep) } next end # We should skip over any releases that violate graph-level constraints. potential_solution = considering.dup << dep unless graph.satisfies_graph? potential_solution next end catch :next do # After adding any new dependencies and imposing our own constraints # on existing dependencies, we'll mark ourselves as "under # consideration" and recurse. merged = dependencies.merge(dep.dependencies) { |_,a,b| a & b } # If all subsequent dependencies resolved well, the recursive call # will return a completed dependency list. If there were problems # resolving our dependencies, we'll catch `:next`, which will cause # us to move to the next possibility. return walk(graph, merged, *potential_solution) end end # Once we've exhausted all of our possible versions, we know that our # last choice was unusable, so we'll unwind the stack and make a new # choice. throw :next end