require 'dotenv' module Hot # Hot Live Constants module Constants include SemanticLogger::Loggable HOTENV = {} # rubocop:disable Style/MutableConstant LISTENERS = {} # rubocop:disable Style/MutableConstant class << self def initialize @old_definitions = {} @hot_definitions = {} logger.level = :fatal load_definitions load_values logger.level = :info end def load_definitions logger.debug('--load_definitions3') new_definitions = Dotenv.parse('.env.sample') definitions_to_actualize = {} @old_definitions.merge(new_definitions) do |key, old_value, new_value| definitions_to_actualize[key] = new_value if new_value != old_value end definitions_to_actualize.each { |key, value| define_method(:actualize, key, value) } @old_definitions = new_definitions definitions_to_remove = @hot_definitions.except(*new_definitions.keys) definitions_to_remove.each_pair do |key, dkey| logger.info("remove method <#{dkey}>") singleton_class.undef_method(dkey) @hot_definitions.delete(key) end definitions_to_add = new_definitions.except(*@hot_definitions.keys) definitions_to_add.each { |key, value| define_method(:add, key, value) } end def load_values logger.debug('--load_values') new_env = Dotenv.parse constants_to_delete = HOTENV.except(*new_env.keys) constants_to_delete.each do |name, _| # FIXME: default should read default and type from @old_definitions type, default = @old_definitions[name] logger.info("constant <#{name}> reverts to default value <#{default}> of type <#{type}>") end constants_to_add = new_env.except(*HOTENV.keys) constants_to_add.each do |constant| logger.info("constant to add <#{constant}>") end HOTENV.replace new_env # LISTENERS.each_pair { |k, b| perform_change(k, b) } end def on_change(key, &block) LISTENERS[key.downcase.to_sym] = block perform_change(key, block) end private def define_method(mode, key, value) dkey = key.downcase method = infer_method_from_value(value) inferred_type = method.name.to_s.split('_')[1] logger.info("#{mode} method <#{dkey}> of type <#{inferred_type}> with default value <#{value}> ") singleton_class.define_method(dkey) { method.call(key, value) } @hot_definitions.store(key, dkey) end def perform_change(key, block) old_value = nil # TODO: remember last previous value new_value = method(key).call block.call(new_value, old_value) end def load_boolean(key, default) = HOTENV.fetch(key, default).to_s.downcase == 'true' def load_integer(key, default) = HOTENV.fetch(key, default).to_i def load_string(key, default) = HOTENV.fetch(key, default) def infer_method_from_value(value) case value.downcase when /^(true|false)$/ then method(:load_boolean) when /^\d+$/ then method(:load_integer) else method(:load_string) end end end initialize # done once on first require, cause this is just a module (not a class!) end end