diff --git a/lib/active_settings/base.rb b/lib/active_settings/base.rb index 38a5c8f..c85785f 100644 --- a/lib/active_settings/base.rb +++ b/lib/active_settings/base.rb @@ -21,14 +21,57 @@ def to_json(*args) instance.to_json(*args) end + # Borrowed from [config gem](https://github.com/rubyconfig/config/blob/master/lib/config/options.rb) + # See: https://github.com/rubyconfig/config/commit/351c819f75d53aa5621a226b5957c79ac82ded11 + # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity + def reload_env + return if ENV.nil? || ENV.empty? + + raise ActiveSettings::Error::EnvPrefixNotDefinedError if ActiveSettings.env_prefix.nil? + + separator = ActiveSettings.env_separator + prefix = ActiveSettings.env_prefix.to_s.split(separator) + + hash = {} + + ENV.each do |variable, value| + keys = variable.to_s.split(separator) + + next if keys.shift(prefix.size) != prefix + + keys.map! do |key| + case ActiveSettings.env_converter + when :downcase + key.downcase.to_sym + when nil + key.to_sym + else + raise "Invalid ENV variables name converter: #{ActiveSettings.env_converter}" + end + end + + leaf = keys[0...-1].inject(hash) do |h, key| + h[key] ||= {} + end + + leaf[keys.last] = ActiveSettings.env_parse_values ? __value(value) : value + end + + hash + end + # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity + private def method_missing(name, *args, &block) instance.send(name, *args, &block) end - end + def respond_to_missing?(*args) + super + end + end delegate :source, :namespace, to: :class @@ -36,13 +79,15 @@ def initialize(file = self.class.source, namespace = self.class.namespace) raise ActiveSettings::Error::SourceFileNotDefinedError if file.nil? config = load_config_file(file) - deep_merge!(config, load_namespace_file(file, namespace)) if namespace + self.class.deep_merge!(config, load_namespace_file(file, namespace)) if namespace - super(__convert(config)) + super(self.class.__convert(config)) yield if block_given? - load_settings! + reload_env! if ActiveSettings.use_env + + after_initialize! end @@ -78,50 +123,11 @@ def load_yaml_file(file) # rubocop:enable Security/YAMLLoad - def load_settings! - reload_env! if ActiveSettings.use_env - end - - - # Borrowed from [config gem](https://github.com/rubyconfig/config/blob/master/lib/config/options.rb) - # See: https://github.com/rubyconfig/config/commit/351c819f75d53aa5621a226b5957c79ac82ded11 - # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize def reload_env! - return if ENV.nil? || ENV.empty? - - raise ActiveSettings::Error::EnvPrefixNotDefinedError if ActiveSettings.env_prefix.nil? - - separator = ActiveSettings.env_separator - prefix = ActiveSettings.env_prefix.to_s.split(separator) - - hash = {} - - ENV.each do |variable, value| - keys = variable.to_s.split(separator) - - next if keys.shift(prefix.size) != prefix - - keys.map! do |key| - case ActiveSettings.env_converter - when :downcase - key.downcase.to_sym - when nil - key.to_sym - else - raise "Invalid ENV variables name converter: #{ActiveSettings.env_converter}" - end - end - - leaf = keys[0...-1].inject(hash) do |h, key| - h[key] ||= {} - end - - leaf[keys.last] = ActiveSettings.env_parse_values ? __value(value) : value - end - - merge!(hash) + merge!(self.class.reload_env) end - # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize + def after_initialize! + end end end diff --git a/lib/active_settings/config.rb b/lib/active_settings/config.rb index 5bfba53..700099c 100644 --- a/lib/active_settings/config.rb +++ b/lib/active_settings/config.rb @@ -7,25 +7,92 @@ module ActiveSettings # rubocop:disable Metrics/ClassLength class Config < OpenStruct - def each(*args, &block) - marshal_dump.each(*args, &block) - end + class << self + # rubocop:disable Metrics/MethodLength + def traverse_hash(hash) + result = {} + hash.each do |k, v| + result[k] = + if v.instance_of?(ActiveSettings::Config) + traverse_hash(v) + elsif v.instance_of?(Array) + traverse_array(v) + elsif v.instance_of?(Proc) + v.call + else + v + end + end + result + end + # rubocop:enable Metrics/MethodLength + + def traverse_array(array) + array.map do |value| + if value.instance_of?(ActiveSettings::Config) + traverse_hash(value) + elsif value.instance_of?(Array) + traverse_array(value) + elsif value.instance_of?(Proc) + value.call + else + value + end + end + end + # Recursively converts Hashes to Options (including Hashes inside Arrays) + # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize + def __convert(hash) + settings = ActiveSettings::Config.new + + hash.each do |key, value| + key = key.to_s if !key.respond_to?(:to_sym) && key.respond_to?(:to_s) + + new_val = + case value + when Hash + value['type'] == 'hash' ? value['contents'] : __convert(value) + when Array + value.collect { |e| e.instance_of?(Hash) ? __convert(e) : e } + else + value + end + + settings[key] = new_val + end - def each_key(*args, &block) - marshal_dump.each_key(*args, &block) - end + settings + end + # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize + + def deep_merge!(current, other) + options = { + preserve_unmergeables: false, + knockout_prefix: ActiveSettings.knockout_prefix, + overwrite_arrays: ActiveSettings.overwrite_arrays, + merge_nil_values: ActiveSettings.merge_nil_values, + keep_array_duplicates: ActiveSettings.keep_array_duplicates + } + DeepMerge.deep_merge!(other, current, options) + end + BOOLEAN_MAPPING = { 'true' => true, 'false' => false }.freeze + private_constant :BOOLEAN_MAPPING - def each_value(*args, &block) - marshal_dump.each_value(*args, &block) - end - + def __value(val) + BOOLEAN_MAPPING.fetch(val) { auto_type(val) } + end - def collect(*args, &block) - marshal_dump.collect(*args, &block) + # rubocop:disable Style/RescueModifier + def auto_type(val) + Integer(val) rescue Float(val) rescue val + end + # rubocop:enable Style/RescueModifier end + delegate :each, :each_key, :each_value, :collect, :keys, :empty?, to: :marshal_dump + def key?(key) self[key] ? true : false @@ -44,10 +111,10 @@ def fetch(key, default = nil) def to_hash - traverse_hash(self) + self.class.traverse_hash(self) end - alias :to_h :to_hash + alias :to_h :to_hash def to_json(*args) @@ -55,11 +122,11 @@ def to_json(*args) end - def merge!(hash) + def merge!(other) current = to_hash - hash = hash.dup - deep_merge!(current, hash) - marshal_load(__convert(current).marshal_dump) + other = other.dup + self.class.deep_merge!(current, other) + marshal_load(self.class.__convert(current).marshal_dump) self end @@ -76,98 +143,6 @@ def respond_to_missing?(*args) super end - - private - - - def deep_merge!(current, hash) - options = { - preserve_unmergeables: false, - knockout_prefix: ActiveSettings.knockout_prefix, - overwrite_arrays: ActiveSettings.overwrite_arrays, - merge_nil_values: ActiveSettings.merge_nil_values, - keep_array_duplicates: ActiveSettings.keep_array_duplicates - } - DeepMerge.deep_merge!(hash, current, options) - end - - - # rubocop:disable Metrics/MethodLength - def traverse_hash(hash) - result = {} - hash.each do |k, v| - result[k] = - if v.instance_of?(ActiveSettings::Config) - traverse_hash(v) - elsif v.instance_of?(Array) - traverse_array(v) - elsif v.instance_of?(Proc) - v.call - else - v - end - end - result - end - # rubocop:enable Metrics/MethodLength - - - def traverse_array(array) - array.map do |value| - if value.instance_of?(ActiveSettings::Config) - traverse_hash(value) - elsif value.instance_of?(Array) - traverse_array(value) - elsif value.instance_of?(Proc) - value.call - else - value - end - end - end - - - # Recursively converts Hashes to Options (including Hashes inside Arrays) - # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize - def __convert(hash) - s = ActiveSettings::Config.new - - hash.each do |key, value| - key = key.to_s if !key.respond_to?(:to_sym) && key.respond_to?(:to_s) - - new_val = - case value - when Hash - value['type'] == 'hash' ? value['contents'] : __convert(value) - when Array - value.collect { |e| e.instance_of?(Hash) ? __convert(e) : e } - else - value - end - - s[key] = new_val - end - s - end - # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize - - - BOOLEAN_MAPPING = { 'true' => true, 'false' => false }.freeze - private_constant :BOOLEAN_MAPPING - - - # Try to convert boolean string to a correct type - def __value(val) - BOOLEAN_MAPPING.fetch(val) { auto_type(val) } - end - - - # rubocop:disable Style/RescueModifier - def auto_type(val) - Integer(val) rescue Float(val) rescue val - end - # rubocop:enable Style/RescueModifier - end # rubocop:enable Metrics/ClassLength end diff --git a/spec/active_settings/base_spec.rb b/spec/active_settings/base_spec.rb index 18bacba..6fc2230 100644 --- a/spec/active_settings/base_spec.rb +++ b/spec/active_settings/base_spec.rb @@ -1151,7 +1151,7 @@ source get_fixture_path('settings_with_namespace.yml') namespace 'production' - def load_settings! + def after_initialize! super load_storage_config! end