Skip to content

Commit

Permalink
Use class methods for traversal/transformations, add after_initialize…
Browse files Browse the repository at this point in the history
…! hook
  • Loading branch information
n-rodriguez committed Feb 1, 2024
1 parent 799b4e9 commit 18c146d
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 157 deletions.
98 changes: 52 additions & 46 deletions lib/active_settings/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,73 @@ 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

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


Expand Down Expand Up @@ -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
195 changes: 85 additions & 110 deletions lib/active_settings/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -44,22 +111,22 @@ 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)
to_hash.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

Expand All @@ -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
2 changes: 1 addition & 1 deletion spec/active_settings/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 18c146d

Please sign in to comment.