Class: Factorix::Dependency::List

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/factorix/dependency/list.rb

Overview

Represents a collection of MOD dependencies

This class manages a collection of Dependency::Entry objects, providing filtering, validation, and circular dependency detection capabilities.

Examples:

Creating from dependency strings

deps = Dependency::List.from_strings(["base >= 1.0.0", "? optional-mod", "! bad-mod"])
deps.required.each { |dep| puts dep.to_s }
deps.optional.each { |dep| puts dep.to_s }

Validating dependencies

available = {"base" => MODVersion.from_string("1.1.0")}
puts "Missing: #{deps.missing_required(available).join(", ")}"

Detecting circular dependencies

mod_deps_map = {
  "mod-a" => Dependency::List.from_strings(["mod-b"]),
  "mod-b" => Dependency::List.from_strings(["mod-a"])
}
cycles = Dependency::List.detect_circular(mod_deps_map)
cycles.each { |cycle| puts "Cycle: #{cycle.join(" -> ")}" }

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(dependencies = []) ⇒ void

Initialize a List collection

Parameters:

  • dependencies (Array<Entry>) (defaults to: [])

    Array of parsed dependency objects

Raises:

  • (ArgumentError)

    if dependencies is not an Array

  • (ArgumentError)

    if any element is not a Dependency::Entry



118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/factorix/dependency/list.rb', line 118

def initialize(dependencies=[])
  unless dependencies.is_a?(Array)
    raise ArgumentError, "dependencies must be an Array, got #{dependencies.class}"
  end

  dependencies.each_with_index do |dep, index|
    unless dep.is_a?(Entry)
      raise ArgumentError, "dependencies[#{index}] must be a Dependency::Entry, got #{dep.class}"
    end
  end

  @dependencies = dependencies.freeze
end

Class Method Details

.detect_circular(mod_dependencies_map) ⇒ Array<Array<String>>

Detect circular dependencies in a collection of MOD dependencies

Uses TSort to detect cycles in the dependency graph. Only considers required dependencies (optional and load-neutral are ignored).

Examples:

map = {
  "mod-a" => Dependency::List.from_strings(["mod-b"]),
  "mod-b" => Dependency::List.from_strings(["mod-a"])
}
cycles = Dependency::List.detect_circular(map)
# => [["mod-a", "mod-b", "mod-a"]]

Parameters:

  • mod_dependencies_map (Hash<String, Dependency::List>)

    Map of MOD names to their dependencies

Returns:

  • (Array<Array<String>>)

    Array of circular dependency chains, or empty array if none found



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/factorix/dependency/list.rb', line 90

def self.detect_circular(mod_dependencies_map)
  graph = DependencyGraph.new(mod_dependencies_map)
  cycles = []

  # Detect self-dependencies (not detected by strongly_connected_components)
  mod_dependencies_map.each do |mod_name, deps|
    if deps.required.any? {|dep| dep.mod.name == mod_name }
      cycles << [mod_name, mod_name]
    end
  end

  # Get strongly connected components (cycles are components with size > 1)
  scc_cycles = graph.strongly_connected_components.filter_map {|component|
    next if component.size <= 1

    # Add first element at the end to make cycle explicit
    component + [component.first]
  }

  cycles + scc_cycles
end

.from_strings(dependency_strings) ⇒ List

Create List from an array of dependency strings

Examples:

deps = Dependency::List.from_strings(["base", "? some-mod >= 1.2.0"])

Parameters:

  • dependency_strings (Array<String>)

    Array of dependency strings from info.json

Returns:

  • (List)

    New instance with parsed dependencies

Raises:

  • (ArgumentError)

    if any dependency string is invalid



69
70
71
72
73
# File 'lib/factorix/dependency/list.rb', line 69

def self.from_strings(dependency_strings)
  parser = Parser.new
  dependencies = dependency_strings.map {|str| parser.parse(str) }
  new(dependencies)
end

Instance Method Details

#conflicts_with?(available_mods) ⇒ Array<String>

Get list of incompatible MODs that are present

Parameters:

  • available_mods (Hash<String, MODVersion>)

    Available MODs and their versions

Returns:

  • (Array<String>)

    Array of conflicting MOD names



204
# File 'lib/factorix/dependency/list.rb', line 204

def conflicts_with?(available_mods) = incompatible.filter_map {|dep| dep.mod.name if available_mods.key?(dep.mod.name) }

#depends_on?(mod_name_or_mod) ⇒ Boolean

Check if this collection depends on a specific MOD

Parameters:

  • mod_name_or_mod (String, MOD)

    MOD name or MOD instance to check

Returns:

  • (Boolean)

    true if depends on the MOD (not incompatible), false otherwise



168
169
170
171
172
# File 'lib/factorix/dependency/list.rb', line 168

def depends_on?(mod_name_or_mod)
  mod_name = mod_name_or_mod.is_a?(MOD) ? mod_name_or_mod.name : mod_name_or_mod.to_s

  @dependencies.any? {|dep| dep.mod.name == mod_name && !dep.incompatible? }
end

#each {|dependency| ... } ⇒ Enumerator, List

Iterate through all dependencies

Yield Parameters:

  • dependency (Entry)

    Each dependency in the collection

Returns:

  • (Enumerator)

    if no block is given

  • (List)

    if a block is given



137
138
139
140
141
142
# File 'lib/factorix/dependency/list.rb', line 137

def each(&block)
  return @dependencies.to_enum unless block

  @dependencies.each(&block)
  self
end

#empty?Boolean

Check if the collection is empty

Returns:

  • (Boolean)

    true if no dependencies, false otherwise



187
# File 'lib/factorix/dependency/list.rb', line 187

def empty? = @dependencies.empty?

#incompatibleArray<Entry>

Get all incompatible dependencies

Returns:

  • (Array<Entry>)

    Array of incompatible dependencies



157
# File 'lib/factorix/dependency/list.rb', line 157

def incompatible = @dependencies.select(&:incompatible?)

#incompatible_with?(mod_name_or_mod) ⇒ Boolean

Check if this collection marks a MOD as incompatible

Parameters:

  • mod_name_or_mod (String, MOD)

    MOD name or MOD instance to check

Returns:

  • (Boolean)

    true if marked as incompatible, false otherwise



178
179
180
181
182
# File 'lib/factorix/dependency/list.rb', line 178

def incompatible_with?(mod_name_or_mod)
  mod_name = mod_name_or_mod.is_a?(MOD) ? mod_name_or_mod.name : mod_name_or_mod.to_s

  @dependencies.any? {|dep| dep.mod.name == mod_name && dep.incompatible? }
end

#load_neutralArray<Entry>

Get all load-neutral dependencies

Returns:

  • (Array<Entry>)

    Array of load-neutral dependencies



162
# File 'lib/factorix/dependency/list.rb', line 162

def load_neutral = @dependencies.select(&:load_neutral?)

#missing_required(available_mods) ⇒ Array<String>

Get list of missing required dependencies

Parameters:

  • available_mods (Hash<String, MODVersion>)

    Available MODs and their versions

Returns:

  • (Array<String>)

    Array of missing MOD names



210
# File 'lib/factorix/dependency/list.rb', line 210

def missing_required(available_mods) = required.filter_map {|dep| dep.mod.name unless available_mods.key?(dep.mod.name) }

#optionalArray<Entry>

Get all optional dependencies (including hidden optional)

Returns:

  • (Array<Entry>)

    Array of optional dependencies



152
# File 'lib/factorix/dependency/list.rb', line 152

def optional = @dependencies.select(&:optional?)

#requiredArray<Entry>

Get all required dependencies

Returns:

  • (Array<Entry>)

    Array of required dependencies



147
# File 'lib/factorix/dependency/list.rb', line 147

def required = @dependencies.select(&:required?)

#satisfied_by?(available_mods) ⇒ Boolean

Check if all required dependencies are satisfied

Parameters:

  • available_mods (Hash<String, MODVersion>)

    Available MODs and their versions

Returns:

  • (Boolean)

    true if all required dependencies are satisfied



198
# File 'lib/factorix/dependency/list.rb', line 198

def satisfied_by?(available_mods) = required.all? {|dep| (version = available_mods[dep.mod.name]) && dep.satisfied_by?(version) }

#sizeInteger

Get the total number of dependencies

Returns:

  • (Integer)

    Number of dependencies



192
# File 'lib/factorix/dependency/list.rb', line 192

def size = @dependencies.size

#to_aArray<String>

Convert to array of dependency strings

Returns:

  • (Array<String>)

    Array of dependency strings



236
# File 'lib/factorix/dependency/list.rb', line 236

def to_a = @dependencies.map(&:to_s)

#to_hHash<String, Entry>

Convert to hash keyed by MOD name

Returns:

  • (Hash<String, Entry>)

    Hash of => dependency



241
# File 'lib/factorix/dependency/list.rb', line 241

def to_h = @dependencies.to_h {|dep| [dep.mod.name, dep] }

#unsatisfied_versions(available_mods) ⇒ Hash<String, Hash<Symbol, String>>

Get list of dependencies with unsatisfied version requirements

Parameters:

  • available_mods (Hash<String, MODVersion>)

    Available MODs and their versions

Returns:

  • (Hash<String, Hash<Symbol, String>>)

    Hash of => {required: …, actual: …}



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/factorix/dependency/list.rb', line 216

def unsatisfied_versions(available_mods)
  result = {}

  required.each do |dep|
    next unless dep.version_requirement # Skip if no version requirement

    version = available_mods[dep.mod.name]
    next unless version # Skip if not available (covered by missing_required)

    next if dep.satisfied_by?(version)

    result[dep.mod.name] = {required: dep.version_requirement.to_s, actual: version.to_s}
  end

  result
end