lib/pdk/config/namespace.rb in pdk-1.13.0 vs lib/pdk/config/namespace.rb in pdk-1.14.0

- old
+ new

@@ -24,14 +24,16 @@ # 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, persistent_defaults: false, &block) @file = File.expand_path(file) unless file.nil? - @values = {} + @settings = {} @name = name.to_s @parent = parent @persistent_defaults = persistent_defaults + @mounts = {} + @loaded_from_file = false instance_eval(&block) if block_given? end # Pre-configure a value in the namespace. @@ -41,13 +43,13 @@ # # @param key [String,Symbol] the name of the value. # @param block [Proc] a block that is evaluated within the new [self]. # # @return [nil] - def value(key, &block) - @values[key.to_s] ||= PDK::Config::Value.new(key.to_s) - @values[key.to_s].instance_eval(&block) if block_given? + def setting(key, &block) + @settings[key.to_s] ||= PDK::Config::Setting.new(key.to_s, self) + @settings[key.to_s].instance_eval(&block) if block_given? end # Mount a provided [self] (or subclass) into the namespace. # # @param key [String,Symbol] the name of the namespace to be mounted. @@ -62,11 +64,11 @@ def mount(key, obj, &block) raise ArgumentError, _('Only PDK::Config::Namespace objects can be mounted into a namespace') unless obj.is_a?(PDK::Config::Namespace) obj.parent = self obj.name = key.to_s obj.instance_eval(&block) if block_given? - data[key.to_s] = obj + @mounts[key.to_s] = obj end # Create and mount a new child namespace. # # @param name [String,Symbol] the name of the new namespace. @@ -86,15 +88,25 @@ # # @param key [String,Symbol] the name of the value to retrieve. # # @return [Object] the requested value. def [](key) - data[key.to_s] + # Check if it's a mount first... + return @mounts[key.to_s] unless @mounts[key.to_s].nil? + # Check if it's a setting, otherwise nil + return nil if settings[key.to_s].nil? + return settings[key.to_s].value unless settings[key.to_s].value.nil? + default_value = settings[key.to_s].default + return default_value if default_value.nil? || !@persistent_defaults + # Persist the default value + settings[key.to_s].value = default_value + save_data + default_value end # Get the value of the named key or the provided default value if not - # present. + # present. Note that this does not trigger persistent defaults # # This differs from {#[]} in an important way in that it allows you to # return a default value, which is not possible using `[] || default` as # non-existent values when accessed normally via {#[]} will be defaulted # to a new Hash. @@ -103,21 +115,28 @@ # @param default_value [Object] the value to return if the namespace does # not contain the requested value. # # @return [Object] the requested value. def fetch(key, default_value) - data.fetch(key.to_s, default_value) + # Check if it's a mount first... + return @mounts[key.to_s] unless @mounts[key.to_s].nil? + # Check if it's a setting, otherwise default_value + return default_value if settings[key.to_s].nil? + # Check if has a value, otherwise default_value + settings[key.to_s].value.nil? ? default_value : settings[key.to_s].value end # 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) + # You can't set the value of a mount + raise ArgumentError, _('Namespace mounts can not be set a value') unless @mounts[key.to_s].nil? set_volatile_value(key, value) # Persist the change save_data end @@ -129,39 +148,32 @@ # and nil values are removed from the Hash. # # @return [Hash{String => Object}] the values from the namespace that # should be persisted to disk. def to_h - data.inject({}) do |new_hash, (key, value)| - new_hash[key] = if value.is_a?(PDK::Config::Namespace) - value.include_in_parent? ? value.to_h : nil - else - value - end - new_hash.delete_if { |_, v| v.nil? } - end + new_hash = {} + settings.each_pair { |k, v| new_hash[k] = v.value } + @mounts.each_pair { |k, mount_point| new_hash[k] = mount_point.to_h if mount_point.include_in_parent? } + new_hash.delete_if { |_, v| v.nil? } + new_hash 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) + # Resolve the settings + settings.values.each do |setting| + setting_name = setting.qualified_name + if be_resolved?(setting_name, filter) + resolved[setting_name] = setting.value.nil? ? setting.default : setting.value end end + # Resolve the mounts + @mounts.values.each { |mount| resolved.merge!(mount.resolve(filter)) } resolved end # @return [Boolean] true if the namespace has a parent, otherwise false. def child_namespace? @@ -206,64 +218,78 @@ 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 + # @abstract Subclass and override {#parse_file} 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. # - # @return [Hash{String => Object}] the data to be loaded into the + # @yield [String, Object] the data to be loaded into the # namespace. - def parse_data(_data, _filename) - {} + def parse_file(_filename); end + + # @abstract Subclass and override {#serialize_data} to implement generating + # logic for a particular config file format. + # + # @param data [Hash{String => Object}] the data stored in the namespace + # + # @return [String] the serialized contents of the namespace suitable for + # writing to disk. + def serialize_data(_data); end + + # @abstract Subclass and override {#create_missing_setting} to implement logic + # when a setting is dynamically created, for example when attempting to + # set the value of an unknown setting + # + # @param data [Hash{String => Object}] the data stored in the namespace + # + # @return [String] the serialized contents of the namespace suitable for + # writing to disk. + def create_missing_setting(key, initial_value = nil) + # Need to use `@settings` and `@mounts` here to stop recursive calls + return unless @mounts[key.to_s].nil? + return unless @settings[key.to_s].nil? + @settings[key.to_s] = PDK::Config::Setting.new(key.to_s, self, initial_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. # # @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 + # Need to use `settings` here to force the backing file to be loaded + return create_missing_setting(key, value) if settings[key.to_s].nil? + # Need to use `@settings` here to stop recursive calls from []= + @settings[key.to_s].value = value end - # Read the file associated with the namespace. + # Helper method to read files. # # @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. # @return [String,nil] the contents of the file or nil if the file does # not exist. - def load_data - return if file.nil? - return unless PDK::Util::Filesystem.file?(file) + def load_data(filename) + return if filename.nil? + return unless PDK::Util::Filesystem.file?(filename) PDK::Util::Filesystem.read_file(file) rescue Errno::ENOENT => e raise PDK::Config::LoadError, e.message rescue Errno::EACCES raise PDK::Config::LoadError, _('Unable to open %{file} for reading') % { - file: file, + file: filename, } end - # @abstract Subclass and override {#save_data} to implement generating - # logic for a particular config file format. - # - # @param data [Hash{String => Object}] the data stored in the namespace - # - # @return [String] the serialized contents of the namespace suitable for - # writing to disk. - def serialize_data(_data); end - # Persist the contents of the namespace to disk. # # Directories will be automatically created and the contents of the # namespace will be serialized automatically with {#serialize_data}. # @@ -287,38 +313,20 @@ raise PDK::Config::LoadError, e.message end # 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 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 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? - 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 + # @return [Hash<String => PDK::Config::Setting>] the contents of the namespace. + def settings + return @settings if @loaded_from_file + @loaded_from_file = true + return @settings if file.nil? + parse_file(file) do |key, parsed_setting| + # Create a settings chain if a setting already exists + parsed_setting.previous_setting = @settings[key] unless @settings[key].nil? + @settings[key] = parsed_setting end + @settings end end end end