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