lib/config/options.rb
require 'ostruct'
require 'config/validation/validate'
module Config
class Options < OpenStruct
include Enumerable
include Validation::Validate
def keys
marshal_dump.keys
end
def empty?
marshal_dump.empty?
end
def add_source!(source)
# handle yaml file paths
source = (Sources::YAMLSource.new(source)) if source.is_a?(String) || source.is_a?(Pathname)
source = (Sources::HashSource.new(source)) if source.is_a?(Hash)
@config_sources ||= []
@config_sources << source
end
def prepend_source!(source)
source = (Sources::YAMLSource.new(source)) if source.is_a?(String) || source.is_a?(Pathname)
source = (Sources::HashSource.new(source)) if source.is_a?(Hash)
@config_sources ||= []
@config_sources.unshift(source)
end
# look through all our sources and rebuild the configuration
def reload!
conf = {}
@config_sources.each do |source|
source_conf = source.load
if conf.empty?
conf = source_conf
else
DeepMerge.deep_merge!(
source_conf,
conf,
preserve_unmergeables: false,
knockout_prefix: Config.knockout_prefix,
overwrite_arrays: Config.overwrite_arrays,
merge_nil_values: Config.merge_nil_values,
merge_hash_arrays: Config.merge_hash_arrays
)
end
end
# swap out the contents of the OStruct with a hash (need to recursively convert)
marshal_load(__convert(conf).marshal_dump)
validate!
self
end
alias :load! :reload!
def reload_from_files(*files)
Config.load_and_set_settings(files)
reload!
end
def to_hash
result = {}
marshal_dump.each do |k, v|
if v.instance_of? Config::Options
result[k] = v.to_hash
elsif v.instance_of? Array
result[k] = descend_array(v)
else
result[k] = v
end
end
result
end
alias :to_h :to_hash
def each(*args, &block)
marshal_dump.each(*args, &block)
end
def to_json(*args)
require "json" unless defined?(JSON)
to_hash.to_json(*args)
end
def as_json(options = nil)
to_hash.as_json(options)
end
def merge!(hash)
current = to_hash
DeepMerge.deep_merge!(
hash.dup,
current,
preserve_unmergeables: false,
knockout_prefix: Config.knockout_prefix,
overwrite_arrays: Config.overwrite_arrays,
merge_nil_values: Config.merge_nil_values,
merge_hash_arrays: Config.merge_hash_arrays
)
marshal_load(__convert(current).marshal_dump)
self
end
# Some keywords that don't play nicely with OpenStruct
SETTINGS_RESERVED_NAMES = %w[select collect test count zip min max exit! table].freeze
# Some keywords that don't play nicely with Rails 7.*
RAILS_RESERVED_NAMES = %w[maximum minimum].freeze
# An alternative mechanism for property access.
# This let's you do foo['bar'] along with foo.bar.
def [](param)
return super if SETTINGS_RESERVED_NAMES.include?(param)
return super if RAILS_RESERVED_NAMES.include?(param)
public_send("#{param}")
end
def []=(param, value)
send("#{param}=", value)
end
SETTINGS_RESERVED_NAMES.each do |name|
define_method name do
self[name]
end
end
RAILS_RESERVED_NAMES.each do |name|
define_method name do
self[name]
end
end
def key?(key)
@table.key?(key)
end
def has_key?(key)
@table.has_key?(key)
end
def method_missing(method_name, *args)
if Config.fail_on_missing && method_name !~ /.*(?==\z)/m
raise KeyError, "key not found: #{method_name.inspect}" unless key?(method_name)
end
super
end
def respond_to_missing?(*args)
super
end
protected
def descend_array(array)
array.map do |value|
if value.instance_of? Config::Options
value.to_hash
elsif value.instance_of? Array
descend_array(value)
else
value
end
end
end
# Recursively converts Hashes to Options (including Hashes inside Arrays)
def __convert(h) #:nodoc:
s = self.class.new
h.each do |k, v|
k = k.to_s if !k.respond_to?(:to_sym) && k.respond_to?(:to_s)
if v.is_a?(Hash)
v = v["type"] == "hash" ? v["contents"] : __convert(v)
elsif v.is_a?(Array)
v = v.collect { |e| e.instance_of?(Hash) ? __convert(e) : e }
end
s[k] = v
end
s
end
end
end