Class: Railstart::Config
- Inherits:
-
Object
- Object
- Railstart::Config
- Defined in:
- lib/railstart/config.rb
Overview
Provides loading, merging, and validation of Railstart configuration data.
Combines built-in defaults with optional user overrides and exposes helpers for downstream components.
Constant Summary collapse
- BUILTIN_CONFIG_PATH =
File.("../../config/rails8_defaults.yaml", __dir__)
- USER_CONFIG_PATH =
File.("~/.config/railstart/config.yaml")
- QUESTION_TYPES =
%w[select multi_select yes_no input].freeze
- CHOICE_REQUIRED_TYPES =
%w[select multi_select].freeze
- MERGEABLE_COLLECTIONS =
%w[questions post_actions].freeze
Class Method Summary collapse
- .deep_dup(value) ⇒ Object private
- .deep_merge_hash(base, override) ⇒ Object private
- .deep_merge_value(key, left, right) ⇒ Object private
- .fetch_id(entry) ⇒ Object private
-
.interpolate_flag(template, value) ⇒ String
Interpolate
%{value}placeholders within Rails flags. -
.load(builtin_path: BUILTIN_CONFIG_PATH, user_path: USER_CONFIG_PATH, preset_path: nil) ⇒ Hash
Load, merge, and validate configuration from built-in, user, and preset sources.
- .merge_config(base, override) ⇒ Object private
- .merge_entries(left, right) ⇒ Object private
- .merge_id_array(base, override) ⇒ Object private
- .read_yaml(path, required:) ⇒ Object private
- .special_array_key?(key) ⇒ Boolean private
- .validate!(config) ⇒ Object private
- .validate_collection(name, entries, question_ids = []) ⇒ Object private
- .validate_post_action_entry(entry, identifier) ⇒ Object private
- .validate_question_choices(entry, question_id) ⇒ Object private
Class Method Details
.deep_dup(value) ⇒ Object (private)
270 271 272 273 274 275 276 277 278 279 |
# File 'lib/railstart/config.rb', line 270 def deep_dup(value) case value when Hash value.transform_values { |v| deep_dup(v) } when Array value.map { |v| deep_dup(v) } else value end end |
.deep_merge_hash(base, override) ⇒ Object (private)
89 90 91 92 93 94 95 96 97 |
# File 'lib/railstart/config.rb', line 89 def deep_merge_hash(base, override) return deep_dup(base || {}) if override.nil? || override.empty? result = deep_dup(base || {}) override.each do |key, override_value| result[key] = deep_merge_value(key, result[key], override_value) end result end |
.deep_merge_value(key, left, right) ⇒ Object (private)
99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/railstart/config.rb', line 99 def deep_merge_value(key, left, right) return deep_dup(left) if right.nil? return deep_dup(right) if left.nil? if special_array_key?(key) merge_id_array(left, right) elsif left.is_a?(Hash) && right.is_a?(Hash) deep_merge_hash(left, right) else deep_dup(right) end end |
.fetch_id(entry) ⇒ Object (private)
157 158 159 160 161 |
# File 'lib/railstart/config.rb', line 157 def fetch_id(entry) return unless entry.respond_to?(:[]) entry["id"] || entry[:id] end |
.interpolate_flag(template, value) ⇒ String
Interpolate %{value} placeholders within Rails flags.
53 54 55 56 57 58 59 |
# File 'lib/railstart/config.rb', line 53 def interpolate_flag(template, value) return template if template.nil? || (!template.include?("%{") && !template.include?("%<")) format(template, value: value) rescue KeyError => e raise ConfigError, "Invalid interpolation token in rails_flag \"#{template}\": #{e.message}" end |
.load(builtin_path: BUILTIN_CONFIG_PATH, user_path: USER_CONFIG_PATH, preset_path: nil) ⇒ Hash
Load, merge, and validate configuration from built-in, user, and preset sources.
32 33 34 35 36 37 38 39 40 41 |
# File 'lib/railstart/config.rb', line 32 def load(builtin_path: BUILTIN_CONFIG_PATH, user_path: USER_CONFIG_PATH, preset_path: nil) builtin = read_yaml(builtin_path, required: true) user = read_yaml(user_path, required: false) preset = preset_path ? read_yaml(preset_path, required: false) : {} merged = merge_config(builtin, user) merged = merge_config(merged, preset) unless preset.empty? validate!(merged) merged end |
.merge_config(base, override) ⇒ Object (private)
82 83 84 85 86 87 |
# File 'lib/railstart/config.rb', line 82 def merge_config(base, override) normalized_base = base || {} return deep_dup(normalized_base) if override.nil? || override.empty? deep_merge_hash(normalized_base, override) end |
.merge_entries(left, right) ⇒ Object (private)
146 147 148 149 150 151 152 153 154 155 |
# File 'lib/railstart/config.rb', line 146 def merge_entries(left, right) return deep_dup(right) if left.nil? return deep_dup(left) if right.nil? if left.is_a?(Hash) && right.is_a?(Hash) deep_merge_hash(left, right) else deep_dup(right) end end |
.merge_id_array(base, override) ⇒ Object (private)
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/railstart/config.rb', line 112 def merge_id_array(base, override) base_entries = Array(base) override_entries = Array(override) map = {} order = [] base_without_id = [] base_entries.each do |entry| copy = deep_dup(entry) id = fetch_id(copy) if id order << id unless order.include?(id) map[id] = copy else base_without_id << copy end end override_without_id = [] override_entries.each do |entry| copy = deep_dup(entry) id = fetch_id(copy) if id order << id unless order.include?(id) map[id] = merge_entries(map[id], copy) else override_without_id << copy end end order.map { |id| map[id] } + base_without_id + override_without_id end |
.read_yaml(path, required:) ⇒ Object (private)
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/railstart/config.rb', line 63 def read_yaml(path, required:) return {} if path.nil? || path.to_s.empty? unless File.exist?(path) raise ConfigLoadError, "Missing required config file: #{path}" if required return {} end data = YAML.safe_load_file(path, aliases: true) || {} raise ConfigLoadError, "Config file #{path} must define a Hash at the top level" unless data.is_a?(Hash) deep_dup(data) rescue Errno::EACCES => e raise ConfigLoadError, "Cannot read #{path}: #{e.message}" rescue Psych::Exception => e raise ConfigLoadError, "Failed to parse #{path}: #{e.message}" end |
.special_array_key?(key) ⇒ Boolean (private)
281 282 283 |
# File 'lib/railstart/config.rb', line 281 def special_array_key?(key) key && MERGEABLE_COLLECTIONS.include?(key.to_s) end |
.validate!(config) ⇒ Object (private)
163 164 165 166 167 168 169 170 171 172 |
# File 'lib/railstart/config.rb', line 163 def validate!(config) issues = [] question_ids = Array(config["questions"]).map { |e| fetch_id(e) }.compact MERGEABLE_COLLECTIONS.each do |collection| entries = Array(config[collection]) issues.concat(validate_collection(collection, entries, question_ids)) end raise ConfigValidationError.new("Invalid configuration", issues: issues) unless issues.empty? end |
.validate_collection(name, entries, question_ids = []) ⇒ Object (private)
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/railstart/config.rb', line 174 def validate_collection(name, entries, question_ids = []) issues = [] id_counts = Hash.new(0) entries.each_with_index do |entry, index| unless entry.is_a?(Hash) issues << "#{name} entry at index #{index} must be a Hash" next end id = fetch_id(entry) if id.nil? || id.to_s.strip.empty? issues << "#{name} entry at index #{index} is missing an id" else id_counts[id] += 1 end if name == "questions" type = entry["type"] || entry[:type] unless QUESTION_TYPES.include?(type) issues << "Question #{id || index} has invalid type #{type.inspect}" next end issues.concat(validate_question_choices(entry, id || index)) if CHOICE_REQUIRED_TYPES.include?(type) elsif name == "post_actions" issues.concat(validate_post_action_entry(entry, id || index)) if entry.fetch("enabled", true) if_condition = entry["if"] || entry[:if] if if_condition.is_a?(Hash) ref_question_id = if_condition["question"] || if_condition[:question] if ref_question_id && !question_ids.include?(ref_question_id) issues << "Post-action #{id || index} references unknown question '#{ref_question_id}'" end end end end id_counts.each do |id, count| issues << "#{name} entry id #{id} is defined #{count} times" if count > 1 end issues end |
.validate_post_action_entry(entry, identifier) ⇒ Object (private)
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 |
# File 'lib/railstart/config.rb', line 244 def validate_post_action_entry(entry, identifier) action_type = (entry["type"] || entry[:type] || "command").to_s case action_type when "command" command = entry["command"] || entry[:command] if command.nil? || command.to_s.strip.empty? ["Post-action #{identifier} is enabled but missing a command"] else [] end when "template" issues = [] source = entry["source"] || entry[:source] if source.nil? || source.to_s.strip.empty? issues << "Post-action #{identifier} is a template but missing a source" end variables = entry["variables"] || entry[:variables] issues << "Post-action #{identifier} template variables must be a Hash" if variables && !variables.is_a?(Hash) issues else ["Post-action #{identifier} has unsupported type '#{action_type}'"] end end |
.validate_question_choices(entry, question_id) ⇒ Object (private)
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 |
# File 'lib/railstart/config.rb', line 219 def validate_question_choices(entry, question_id) issues = [] choices = entry["choices"] || entry[:choices] if !choices.is_a?(Array) || choices.empty? issues << "Question #{question_id} (#{entry["type"]}) must define at least one choice" return issues end choices.each_with_index do |choice, cidx| unless choice.is_a?(Hash) issues << "Question #{question_id} choice at index #{cidx} must be a Hash" next end unless choice["name"] || choice[:name] issues << "Question #{question_id} choice at index #{cidx} missing 'name'" end unless choice["value"] || choice[:value] issues << "Question #{question_id} choice at index #{cidx} missing 'value'" end end issues end |