# encoding: UTF-8 # frozen_string_literal: true # Requirements # ======================================================================= # Stdlib # ----------------------------------------------------------------------- require 'pathname' # Deps # ----------------------------------------------------------------------- require 'nrser' require 'nrser/core_ext/object' # Project / Package # ----------------------------------------------------------------------- require_relative './config/types' # Refinements # ======================================================================= require 'nrser/refinements/types' using NRSER::Types # A hash-like container providing access to a layered tree of config values # sourced from `YAML` files and `ENV` vars. # # # Keys # ---------------------------------------------------------------------------- # # Config keys are arrays of strings that # class Locd::Config # Constants # =================================================================== # Absolute path to the default config file (`//config/default.yml`). # # Has to be defined as a constant because no other config is loaded # before it and it contains the ENV var prefix. # # @return [Pathname] # DEFAULT_CONFIG_PATH = Locd::ROOT / 'config' / 'default.yml' # Absolute path to the dev override config, which will be loaded # last if it exists. # # @return [Pathname] # DEV_CONFIG_PATH = Locd::ROOT / 'dev' / 'config.yml' # What to split string keys into key path segments on. # # @return [String] # KEY_SEPARATOR = '.' # Attributes # ============================================================================ # Absolute path to the built-in default configuration file in use. # # @return [Pathname] # attr_reader :default_config_path # Constructor # =================================================================== # Instantiate a new `Locd::Config`. def initialize default_config_path: DEFAULT_CONFIG_PATH, dev_config_path: DEV_CONFIG_PATH @default_config_path = default_config_path.to_pn @dev_config_path = dev_config_path.to_pn @from_files = {} load_file! default_config_path if user_config_path.exist? load_file! user_config_path end if dev_config_path && dev_config_path.exist? load_file! dev_config_path end end # #initialize # Instance Methods # =================================================================== protected # ======================================================================== # Load a `YAML` config file into the config. # # @param [Pathname | String] path # Path to file. # # @return [nil] # def load_file! path path. to_pn. read. thru { |contents| @from_files.deep_merge! YAML.load( contents ) } nil end # #load_file! public # end protected *************************************************** def self.key_path_for *key key.flat_map { |k| k.to_s.split KEY_SEPARATOR } end def key_path_for *key self.class.key_path_for *key end # Get a value from the config files only. # # @param [Array<#to_s>] *key # The key to lookup. # def from_files *key, type: nil key_path = key_path_for *key value = @from_files.dig Locd::GEM_NAME, *key_path if value.nil? nil else parse_and_check key, value, type: type end end def env_key_for *key [ from_files( :namespace, :env, type: t.non_empty_str ), *key_path_for( *key ) ].join( '_' ).upcase end def parse_and_check key, value, type: nil type = Types.for_key( *key ) if type.nil? return value if type.nil? if value.is_a?( String ) && type.has_from_s? type.from_s value else type.check! value end end def from_env *key, type: nil env_key = env_key_for *key value = ENV[env_key] case value when nil, '' nil else parse_and_check key, value, type: type end end def get *key, type: nil, default: nil env_value = from_env *key, type: type return env_value unless env_value.nil? files_value = from_files *key, type: type return files_value unless files_value.nil? parse_and_check key, default, type: type end alias_method :[], :get def set *key, value, type: nil value_str = value.to_s parse_and_check key, value_str, type: type ENV[ env_key_for( *key ) ] = value_str end # Proxy to {#set} orthogonal to {#[]} / {#get} (though in this case we # need to do a little more work than an alias on account of how `#[]=` # handled keyword args). # # @example Single string key # Locd.config[ 'cli.bash_comp.log.level' ] = :debug # # @example List key # Locd.config[ :cli, :bash_comp, :log, :level ] = :debug # # @example Checking the type # Locd.config[ 'home', type: t.abs_path ] = user_input # def []= *key, value if Hash === key[-1] kwds = key[-1] key = key[0..-2] else kwds = {} end set *key, value, **kwds end def to_h @from_files.merge \ "locd" => @from_files["locd"].map_leaves { |key_path, value| env_value = from_env *key_path if env_value.nil? value else env_value end } end # @return [Pathname] # Absolute path to Loc'd home directory (it's workspace). # Directory may not exist, or may be something else like a file. # def home_dir self[:home, type: t.abs_path].to_pn end def log_dir self[ :log_dir, type: t.abs_path, default: (home_dir / 'log') ] end # @return [Pathname] # Absolute path to the user config file, which may not exist, but # will be loaded if it does. # def user_config_path home_dir / 'config.yml' end # Does `ARGV` look like we're executing Bash completion? # # We want to change how we're logging during Bash completion runs. # # @return [Boolean] # def cli_ARGV_looks_like_bash_comp? ARGV[0] == 'bash-complete' end # Level to log at when CLI'ing. # # @return [Symbol] # One of {SemanticLogger::LEVELS}. # def cli_log_level get( cli_ARGV_looks_like_bash_comp? ? 'bash_comp.log.level' : 'cli.log.level' ) end # @return [$stderr] # def cli_log_dest dest = if cli_ARGV_looks_like_bash_comp? get 'cli.bash_comp.log.dest' else get 'cli.log.dest' end { **( IO === dest ? { io: dest } : { file_name: dest.to_s } ), formatter: NRSER::Log::Formatters::Color.new, } end # Get the logging config, taking account of the Bash completion environment. # # Output from this # # @return [Hash