lib/pdk/config/namespace.rb in pdk-1.12.0 vs lib/pdk/config/namespace.rb in pdk-1.13.0
- old
+ new
@@ -18,16 +18,20 @@
# method.
# @option params [String] :file the path to the file associated with the
# contents of the namespace (defaults to nil).
# @option params [self] :parent the parent {self} that this namespace is
# a child of (defaults to nil).
+ # @option params [self] :persistent_defaults whether default values should be persisted
+ # to disk when evaluated. By default they are not persisted to disk. This is typically
+ # used for settings which a randomly generated, instead of being deterministic, e.g. analytics user-id
# @param block [Proc] a block that is evaluated within the new instance.
- def initialize(name = nil, file: nil, parent: nil, &block)
+ def initialize(name = nil, file: nil, parent: nil, persistent_defaults: false, &block)
@file = File.expand_path(file) unless file.nil?
@values = {}
@name = name.to_s
@parent = parent
+ @persistent_defaults = persistent_defaults
instance_eval(&block) if block_given?
end
# Pre-configure a value in the namespace.
@@ -102,26 +106,20 @@
# @return [Object] the requested value.
def fetch(key, default_value)
data.fetch(key.to_s, default_value)
end
- # Set the value of the named key.
- #
- # If the key has been pre-configured with {#value}, then the value of the
- # key will be validated against any validators that have been configured.
- #
# After the value has been set in memory, the value will then be
# persisted to disk.
#
# @param key [String,Symbol] the name of the configuration value.
# @param value [Object] the value of the configuration value.
#
# @return [nil]
def []=(key, value)
- @values[key.to_s].validate!([name, key.to_s].join('.'), value) if @values.key?(key.to_s)
-
- data[key.to_s] = value
+ set_volatile_value(key, value)
+ # Persist the change
save_data
end
# Convert the namespace into a Hash of values, suitable for serialising
# and persisting to disk.
@@ -141,10 +139,32 @@
end
new_hash.delete_if { |_, v| v.nil? }
end
end
+ # Resolves all filtered settings, including child namespaces, fully namespaced and filling in default values.
+ #
+ # @param filter [String] Only resolve setting names which match the filter. See #be_resolved? for matching rules
+ # @return [Hash{String => Object}] All resolved settings for example {'user.module_defaults.author' => 'johndoe'}
+ def resolve(filter = nil)
+ # Explicitly force values to be loaded if they have not already
+ # done so. This will not cause them to be persisted to disk
+ (@values.keys - data.keys).each { |key_name| self[key_name] }
+ resolved = {}
+ data.each do |data_name, obj|
+ case obj
+ when PDK::Config::Namespace
+ # Query the child namespace
+ resolved.merge!(obj.resolve(filter))
+ else
+ setting_name = [name, data_name.to_s].join('.')
+ resolved[setting_name] = self[data_name] if be_resolved?(setting_name, filter)
+ end
+ end
+ resolved
+ end
+
# @return [Boolean] true if the namespace has a parent, otherwise false.
def child_namespace?
!parent.nil?
end
@@ -171,10 +191,25 @@
child_namespace? && file.nil?
end
private
+ # Determines whether a setting name should be resolved using the filter
+ # Returns true when filter is nil.
+ # Returns true if the filter is exactly the same name as the setting.
+ # Returns true if the name is a sub-key of the filter e.g.
+ # Given a filter of user.module_defaults, `user.module_defaults.author` will return true, but `user.analytics.disabled` will return false.
+ #
+ # @param name [String] The setting name to test.
+ # @param filter [String] The filter used to test on the name.
+ # @return [Boolean] Whether the name passes the filter.
+ def be_resolved?(name, filter = nil)
+ return true if filter.nil? # If we're not filtering, this value should always be resolved
+ return true if name == filter # If it's exactly the same name then it should be resolved
+ name.start_with?(filter + '.') # If name is a subkey of the filter then it should be resolved
+ end
+
# @abstract Subclass and override {#parse_data} to implement parsing logic
# for a particular config file format.
#
# @param data [String] The content of the file to be parsed.
# @param filename [String] The path to the file to be parsed.
@@ -183,10 +218,23 @@
# namespace.
def parse_data(_data, _filename)
{}
end
+ # Set the value of the named key.
+ #
+ # If the key has been pre-configured with {#value}, then the value of the
+ # key will be validated against any validators that have been configured.
+ #
+ # @param key [String,Symbol] the name of the configuration value.
+ # @param value [Object] the value of the configuration value.
+ def set_volatile_value(key, value)
+ @values[key.to_s].validate!([name, key.to_s].join('.'), value) if @values.key?(key.to_s)
+
+ data[key.to_s] = value
+ end
+
# Read the file associated with the namespace.
#
# @raise [PDK::Config::LoadError] if the file is removed during read.
# @raise [PDK::Config::LoadError] if the user doesn't have the
# permissions needed to read the file.
@@ -226,11 +274,11 @@
#
# @return [nil]
def save_data
return if file.nil?
- FileUtils.mkdir_p(File.dirname(file))
+ PDK::Util::Filesystem.mkdir_p(File.dirname(file))
PDK::Util::Filesystem.write_file(file, serialize_data(to_h))
rescue Errno::EACCES
raise PDK::Config::LoadError, _('Unable to open %{file} for writing') % {
file: file,
@@ -241,27 +289,30 @@
# Memoised accessor for the loaded data.
#
# @return [Hash<String => Object>] the contents of the namespace.
def data
+ # It's possible for parse_data to return nil, so just return an empty hash
@data ||= parse_data(load_data, file).tap do |h|
- h.default_proc = default_config_value
- end
+ h.default_proc = default_config_value unless h.nil?
+ end || {}
end
# The default behaviour of the namespace when the requested value does
# not exist.
#
# If the value has been pre-configured with {#value} to have a default
- # value, resolve the default value and set it in the namespace
- # (triggering a call to {#save_data}. Otherwise, set the value to a new
- # Hash to allow for arbitrary level of nested values.
+ # value, resolve the default value and set it in the namespace and optionally
+ # save the new default.
+ # Otherwise, set the value to a new Hash to allow for arbitrary level of nested values.
#
# @return [Proc] suitable for use by {Hash#default_proc}.
def default_config_value
->(hash, key) do
if @values.key?(key) && @values[key].default?
- self[key] = @values[key].default
+ set_volatile_value(key, @values[key].default)
+ save_data if @persistent_defaults
+ hash[key]
else
hash[key] = {}.tap do |h|
h.default_proc = default_config_value
end
end