# frozen-string-literal: true module Rodbot # Simple yet flexible configuration module # # The configuration is defined in Ruby as follows: # # name 'Bot' # country 'Sweden' # country nil # log do # level 3 # end # plugin :matrix do # version 1 # ssl true # end # plugin :slack do # version 2 # end # # Within Rodbot, you should use the +Rodbot.config+ shortcut to access the # configuration. # # source = Pathname('config/rodbot.rb') # rc = Rodbot::Config.new(source) # rc.config(:name) # => 'Bot' # rc.config(:country) # => nil # rc.config(:undefined) # => nil # rc.config(:log) # => { level: 3 } # rc.config(:plugin, :matrix, :version) # # => 1 # rc.config(:plugin, :matrix) # # => { version: 1, ssl: true } # rc.config(:plugin) # # => { matrix: { version: 1, ssl: true }, slack: { version: 2 } } # rc.config # # => { name: 'Bot', country: nil, plugin: { matrix: { version: 1, ssl: true }, slack: { version: 2 } } } # # There are two types configuration items: # # 1. Object values without block like +name 'Bot'+:
The config key +:name+ # gets the object +'Bot'+ assigned. Subsequent assignments with the same # config key overwrite previous assignments. # 2. Unspecified value with a block like +log do+:
The config key +:log+ is # assigned a hash defined by the block. Subsequent assignments with the # same config key are merged into the hash. # 3. Object values with a block like +plugin :matrix do+:
The config key # +:plugin+ is assigned an empty hash which is then populated with the # object `:matrix` (usually a Symbol) as key and the subtree defined by the # block. Subsequent assignments with the same config key add more keys to # this hash. # # Please note: You can force a config key to always be treated as if it had # a block (type 3) by adding it to the +KEYS_WITH_IMPLICIT_BLOCK+ array. # # Defaults set by the +DEFAULTS+ constant are read first and therefore may be # overwritten or extend as mentioned above. class Config # Keys which are always treated as if they had a block even if they don't KEYS_WITH_IMPLICIT_BLOCK = %i(plugin).freeze # Default configuration DEFAULTS = <<~END name 'Rodbot' port 7200 timezone 'Etc/UTC' db 'hash' app do threads Rodbot.env.development? ? (1..1) : (2..4) end log do to STDOUT level Rodbot.env.development? ? Logger::INFO : Logger::ERROR end END # Read configuration # # @param source [String, IO] config source as raw string or file e.g. +config/rodbot.rb+ # @param defaults [Boolean] whether to load the defaults or not # @return [self] def initialize(source, defaults: true) @config = Reader.new.eval_strings( (DEFAULTS if defaults), (source.respond_to?(:read) ? (source.read if source.readable?) : source) ).to_h end # Get config values and subtrees # # @note Use the +Rodbot.config+ shortcut to access this method! # # @param keys [Array] key path to config subtree or value # @return [Object] config subtree or value def config(*keys) return @config if keys.none? value = @config.dig(*keys) if value.instance_of?(Array) && value.count == 1 value.first else value end end class Reader def initialize @hash = {} end # Eval configuration from strings # # @param strings [String, nil] one or more strings to evaluate # @return [self] def eval_strings(*strings) instance_eval(strings.compact.join("\n")) self end # Eval configuration from block # # @yield block to evaluate # @return [self] def eval_block(&block) instance_eval(&block) if block self end # Set an config value # # @param key [Symbol] config key # @param value [Object, nil] config value # @yield optional block containing nested config # @return [self] def method_missing(key, value=nil, *, &block) case when block && value.nil? @hash[key] ||= {} @hash[key].merge! self.class.new.eval_block(&block).to_h when block || KEYS_WITH_IMPLICIT_BLOCK.include?(key) @hash[key] ||= {} @hash[key][value] = self.class.new.eval_block(&block).to_h else @hash[key] = value end self end # Config hash # # @return [Hash] def to_h @hash end end end end