require 'yaml' module CLIUtils # Configuration Class # Manages any configuration values and the flat YAML file # into which they get stored. class Configurator # Stores the Configurator key that refers # to the current configuration version. # @return [Symbol] attr_accessor :cur_version_key # Stores the Configurator key that refers # to the value at which the app last changed # config versions. # @return [Symbol] attr_accessor :last_version_key # Stores the path to the configuration file. # @return [String] attr_reader :config_path # Stores the configuration data itself. # @return [Hash] attr_reader :data # Stores the section that contains the version # keys. # @return [Symbol] attr_accessor :version_section # Initializes configuration from a flat file. # @param [String] path The filepath to the config YAML # @return [void] def initialize(path) _path = File.expand_path(path) @config_path = _path @data = {} if File.exist?(_path) data = YAML.load_file(_path) @data.deep_merge!(data).deep_symbolize_keys! end end # Adds a new section to the config file (if it doesn't # already exist). # @param [String] section_name The section to add # @return [void] def add_section(section_name) if !@data.key?(section_name) @data[section_name] = {} else fail "Section already exists: #{ section_name }" end end # Checks to see whether the passed key (and a # non-nil value) exist in the @data Hash. # @param [Symbol] key The key to search for # @return [Boolean] def check_key_and_value(key) !@data.recursive_find_by_key(key).nil? && !@data.recursive_find_by_key(key).empty? end # Compares the current version (if it exists) to # the last version that needed a configuration # change (if it exists). Assuming they exist and # that the current version is behind the "last-config" # version, execute a passed block. # @return [void] def compare_version unless check_key_and_value(@cur_version_key) fail 'Cannot check version; no current version found' end unless check_key_and_value(@last_version_key) fail 'Cannot check version; no previous version found' end c_version = @data.recursive_find_by_key(@cur_version_key) c_version_gem = Gem::Version.new(c_version) l_version = @data.recursive_find_by_key(@last_version_key) l_version_gem = Gem::Version.new(l_version) if c_version_gem < l_version_gem yield c_version, l_version end end # Removes a section to the config file (if it exists). # @param [String] section_name The section to remove # @return [void] def delete_section(section_name) if @data.key?(section_name) @data.delete(section_name) else fail "Cannot delete nonexistent section: #{ section_name }" end end # Ingests a Prefs class and adds its answers to the # configuration data. # @param [Prefs] prefs The Prefs class to examine # @return [void] def ingest_prefs(prefs) fail 'Invaid Prefs class' unless prefs.kind_of?(Prefs) prefs.prompts.each do |p| section_sym = p.config_section.to_sym add_section(section_sym) unless @data.key?(section_sym) @data[section_sym].merge!(p.config_key.to_sym => p.answer) end end # Hook that fires when a non-existent method is called. # Allows this module to return data from the config # Hash when given a method name that matches a key. # @return [Hash] def method_missing(name, *args, &block) @data[name.to_sym] || @data.merge!(name.to_sym => {}) end # Clears the configuration data. # @return [void] def reset @data = {} end # Saves the configuration data to the previously # stored flat file. # @return [void] def save File.open(@config_path, 'w') do |f| f.write(@data.deep_stringify_keys.to_yaml) end end end end