commit rubygem-cfa for openSUSE:Factory
Hello community, here is the log from the commit of package rubygem-cfa for openSUSE:Factory checked in at 2017-03-29 13:22:16 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/rubygem-cfa (Old) and /work/SRC/openSUSE:Factory/.rubygem-cfa.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Package is "rubygem-cfa" Wed Mar 29 13:22:16 2017 rev:5 rq:481826 version:0.6.0 Changes: -------- --- /work/SRC/openSUSE:Factory/rubygem-cfa/rubygem-cfa.changes 2016-12-09 09:35:02.879490124 +0100 +++ /work/SRC/openSUSE:Factory/.rubygem-cfa.new/rubygem-cfa.changes 2017-03-29 13:22:17.559789099 +0200 @@ -1,0 +2,34 @@ +Tue Mar 21 09:15:39 UTC 2017 - jreidinger@suse.com + +- fix writting two new following nested trees ( also caused by fix + for bsc#1023204) +- fix writing new element with same key as only existing key +- fix writing new element with same key as removed element +- add new method AugeasTree#unique_id that helps with writing new + entries for augeas sequences +- 0.6.0 + +------------------------------------------------------------------- +Tue Mar 21 08:10:38 UTC 2017 - jreidinger@suse.com + +- fix AugeasTree#select to not return elements marked as deleted + (caused by fix for bsc#1023204) +- 0.5.1 + +------------------------------------------------------------------- +Thu Mar 2 12:12:00 UTC 2017 - jreidinger@suse.com + +- allow generic set/get also on subtree (bsc#1023204) +- do minimal changes when editing file, especially do not eat + white spaces if value is not modified (bsc#1023204) +- AugeasTree#data now return frozen hash as it is just filtered + view of data, which cannot be modified +- 0.5.0 + +------------------------------------------------------------------- +Mon Dec 5 15:38:36 UTC 2016 - joseivanlopez@gmail.com + +- fix regression when passing nil to AugeasTree#delete (bsc#983486) +- 0.4.3 + +------------------------------------------------------------------- Old: ---- cfa-0.4.2.gem New: ---- cfa-0.6.0.gem ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ rubygem-cfa.spec ++++++ --- /var/tmp/diff_new_pack.mkZUdV/_old 2017-03-29 13:22:18.175701995 +0200 +++ /var/tmp/diff_new_pack.mkZUdV/_new 2017-03-29 13:22:18.179701429 +0200 @@ -1,7 +1,7 @@ # # spec file for package rubygem-cfa # -# Copyright (c) 2016 SUSE LINUX GmbH, Nuernberg, Germany. +# Copyright (c) 2017 SUSE LINUX GmbH, Nuernberg, Germany. # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -17,7 +17,7 @@ Name: rubygem-cfa -Version: 0.4.2 +Version: 0.6.0 Release: 0 %define mod_name cfa %define mod_full_name %{mod_name}-%{version} ++++++ cfa-0.4.2.gem -> cfa-0.6.0.gem ++++++ Binary files old/checksums.yaml.gz and new/checksums.yaml.gz differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/cfa/augeas_parser/keys_cache.rb new/lib/cfa/augeas_parser/keys_cache.rb --- old/lib/cfa/augeas_parser/keys_cache.rb 1970-01-01 01:00:00.000000000 +0100 +++ new/lib/cfa/augeas_parser/keys_cache.rb 2017-03-21 16:50:58.000000000 +0100 @@ -0,0 +1,41 @@ +module CFA + # A cache that holds all avaiable keys in an Augeas tree. It is used to + # prevent too many `aug.match` calls which are expensive. + class AugeasKeysCache + # initialize cache from passed Augeas object + # @param aug [::Augeas] + # @param prefix [String] Augeas path for which cache should be created + def initialize(aug, prefix) + fill_cache(aug, prefix) + end + + # @return list of keys available on given prefix + def keys_for_prefix(prefix) + @cache[prefix] || [] + end + + private + + def fill_cache(aug, prefix) + @cache = {} + search_path = "#{prefix}/*" + loop do + matches = aug.match(search_path) + break if matches.empty? + assign_matches(matches, @cache) + + search_path += "/*" + end + end + + def assign_matches(matches, cache) + matches.each do |match| + split_index = match.rindex("/") + prefix = match[0..(split_index - 1)] + key = match[(split_index + 1)..-1] + cache[prefix] ||= [] + cache[prefix] << key + end + end + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/cfa/augeas_parser/reader.rb new/lib/cfa/augeas_parser/reader.rb --- old/lib/cfa/augeas_parser/reader.rb 1970-01-01 01:00:00.000000000 +0100 +++ new/lib/cfa/augeas_parser/reader.rb 2017-03-21 16:50:58.000000000 +0100 @@ -0,0 +1,67 @@ +require "cfa/augeas_parser/keys_cache" +require "cfa/augeas_parser" + +module CFA + # A class responsible for reading {AugeasTree} from Augeas + class AugeasReader + class << self + # Creates *tree* from *prefix* in *aug*. + # @param aug [::Augeas] + # @param prefix [String] Augeas path prefix + # @return [AugeasTree] + def read(aug, prefix) + keys_cache = AugeasKeysCache.new(aug, prefix) + + tree = AugeasTree.new + load_tree(aug, prefix, tree, keys_cache) + + tree + end + + private + + # fills *tree* with data + def load_tree(aug, prefix, tree, keys_cache) + data = keys_cache.keys_for_prefix(prefix).map do |key| + aug_key = prefix + "/" + key + { + key: load_key(prefix, aug_key), + value: load_value(aug, aug_key, keys_cache), + orig_key: stripped_path(prefix, aug_key), + operation: :keep + } + end + + tree.all_data.concat(data) + end + + # loads a key in a format that AugeasTree expects + def load_key(prefix, aug_key) + # clean from key prefix and for collection remove number inside [] + key = stripped_path(prefix, aug_key) + key.end_with?("]") ? key.sub(/\[\d+\]$/, "[]") : key + end + + # path without prefix we are not interested in + def stripped_path(prefix, aug_key) + # +1 for size due to ending '/' not part of prefix + aug_key[(prefix.size + 1)..-1] + end + + # loads value from auges. If value have tree under, it will also read it + def load_value(aug, aug_key, keys_cache) + subkeys = keys_cache.keys_for_prefix(aug_key) + + nested = !subkeys.empty? + value = aug.get(aug_key) + if nested + subtree = AugeasTree.new + load_tree(aug, aug_key, subtree, keys_cache) + value ? AugeasTreeValue.new(subtree, value) : subtree + else + value + end + end + end + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/cfa/augeas_parser/writer.rb new/lib/cfa/augeas_parser/writer.rb --- old/lib/cfa/augeas_parser/writer.rb 1970-01-01 01:00:00.000000000 +0100 +++ new/lib/cfa/augeas_parser/writer.rb 2017-03-21 16:50:58.000000000 +0100 @@ -0,0 +1,356 @@ +module CFA + # The goal of this class is to write the data stored in {AugeasTree} + # back to Augeas. + # + # It tries to make only the needed changes, as internally Augeas keeps + # a flag whether data has been modified, + # and keeps the unmodified parts of the file untouched. + # + # @note internal only, unstable API + # @api private + class AugeasWriter + # @param aug result of Augeas.create + def initialize(aug) + @aug = aug + end + + # Writes the data in *tree* to a given *prefix* in Augeas + # @param prefix [String] where to write *tree* in Augeas + # @param tree [CFA::AugeasTree] tree to write + def write(prefix, tree, top_level: true) + @lazy_operations = LazyOperations.new(aug) if top_level + tree.all_data.each do |entry| + located_entry = LocatedEntry.new(tree, entry, prefix) + process_operation(located_entry) + end + @lazy_operations.run if top_level + end + + private + + # {AugeasElement} together with information about its location and a few + # helper methods to detect siblings. + # + # @example data for an already existing comment living under /main + # entry.orig_key # => "#comment[15]" + # entry.path # => "/main/#comment[15]" + # entry.key # => "#comment" + # entry.entry_tree # => AugeasTree.new + # entry.entry_value # => "old boring comment" + # + # @example data for a new comment under /main + # entry.orig_key # => nil + # entry.path # => nil + # entry.key # => "#comment" + # entry.entry_tree # => AugeasTree.new + # entry.entry_value # => "new boring comment" + # + # @example data for new tree placed at /main + # entry.orig_key # => "main" + # entry.path # => "/main" + # entry.key # => "main" + # entry.entry_tree # => entry[:value] + # entry.entry_value # => nil + # + class LocatedEntry + attr_reader :prefix + attr_reader :entry + attr_reader :tree + + def initialize(tree, entry, prefix) + @tree = tree + @entry = entry + @prefix = prefix + detect_tree_value_modification + end + + def orig_key + entry[:orig_key] + end + + def path + return @path if @path + return nil unless orig_key + + @path = @prefix + "/" + orig_key + end + + def key + return @key if @key + + @key = @entry[:key] + @key = @key[0..-3] if @key.end_with?("[]") + @key + end + + # @return [LocatedEntry, nil] + # a preceding entry that already exists in the Augeas tree + # or nil if it does not exist. + def preceding_existing + preceding_entry = preceding_entries.reverse_each.find do |entry| + entry[:operation] != :add + end + + return nil unless preceding_entry + + LocatedEntry.new(tree, preceding_entry, prefix) + end + + # @return [true, false] returns true if there is any following entry + # in the Augeas tree + def any_following? + following_entries.any? { |e| e[:operation] != :remove } + end + + # @return [AugeasTree] the Augeas tree nested under this entry. + # If there is no such tree, it creates an empty one. + def entry_tree + value = entry[:value] + case value + when AugeasTree then value + when AugeasTreeValue then value.tree + else AugeasTree.new + end + end + + # @return [String, nil] the Augeas value of this entry. Can be nil. + # If the value is an {AugeasTree} then return nil. + def entry_value + value = entry[:value] + case value + when AugeasTree then nil + when AugeasTreeValue then value.value + else value + end + end + + private + + # For {AugeasTreeValue} we have a problem with detection of + # value modification as it is enclosed in a diferent object. + # So propagate it to this entry here. + def detect_tree_value_modification + return unless entry[:value].is_a?(AugeasTreeValue) + return if entry[:operation] != :keep + + entry[:operation] = entry[:value].modified? ? :modify : :keep + end + + # the entries preceding this entry + def preceding_entries + return [] if index.zero? # first entry + tree.all_data[0..(index - 1)] + end + + # the entries following this entry + def following_entries + tree.all_data[(index + 1)..-1] + end + + # the index of this entry in its tree + def index + @index ||= tree.all_data.index(entry) + end + end + + # Represents an operation that needs to be done after all modifications. + # + # The reason to have this class is that Augeas renumbers its arrays after + # some operations like `rm` or `insert` so previous paths are no longer + # valid. For this reason these sensitive operations that change paths need + # to be done at the end and with careful order. + # See https://www.redhat.com/archives/augeas-devel/2017-March/msg00002.html + # + # @note This class depends on ordered operations. So adding and removing + # entries has to be done in order how they are placed in tree. + class LazyOperations + # @param aug result of Augeas.create + def initialize(aug) + @aug = aug + @operations = [] + end + + def add(located_entry) + @operations << { type: :add, located_entry: located_entry } + end + + def remove(located_entry) + @operations << { type: :remove, path: located_entry.path } + end + + # starts all previously inserted operations + def run + # the reverse order is needed because if there are two operations + # one after another then the latter cannot affect the former + @operations.reverse_each do |operation| + case operation[:type] + when :remove then remove_entry(operation[:path]) + when :add + located_entry = operation[:located_entry] + add_entry(located_entry) + else + raise "Invalid lazy operation #{operation.inspect}" + end + end + end + + private + + attr_reader :aug + + # Removes entry from tree. If *path* does not exist, then tries if it + # has changed to a collection: + # If we remove and re-add a single key then because of the laziness + # Augeas will first see the addition, making a 2 member collection, + # so we need to remove "key[1]" instead of "key". + # @param path [String] original path name to remove + def remove_entry(path) + aug.rm(path_to_remove(path)) + end + + # Finds path to remove, as path can be meanwhile renumbered, see + # #remove_entry + def path_to_remove(path) + if aug.match(path).size == 1 + path + elsif !aug.match(path + "[1]").empty? + path + "[1]" + else + raise "Unknown augeas path #{path}" + end + end + + # Adds entry to tree. At first it finds where to add it to be in correct + # place and then sets its value. Recursive if needed. In recursive case + # it is already known that whole sub-tree is also new and just added. + def add_entry(located_entry) + path = insert_entry(located_entry) + set_new_value(path, located_entry) + end + + # Sets new value to given path. It is used for values that are not yet in + # Augeas tree. If needed it does recursive adding. + # @param path [String] path which can contain Augeas path expression for + # key of new value + # @param located_entry [LocatedEntry] entry to write + # @see https://github.com/hercules-team/augeas/wiki/Path-expressions + def set_new_value(path, located_entry) + aug.set(path, located_entry.entry_value) + prefix = path[/(^.*)\[[^\]]*\]/, 1] || path + # we need to get new path as set can look like [last() + 1] + # which creates new entry and we do not want to add subtree to new + # entries + new_path = aug.match(prefix + "[last()]").first + add_subtree(located_entry.entry_tree, new_path) + end + + # Adds new subtree. Simplified version of common write as it is known + # that all entries will be just added. + # @param tree [CFA::AugeasTree] to add + # @param prefix [String] prefix where to place *tree* + def add_subtree(tree, prefix) + tree.all_data.each do |entry| + located_entry = LocatedEntry.new(tree, entry, prefix) + # universal path that handles also new elements for arrays + path = "#{prefix}/#{located_entry.key}[last()+1]" + set_new_value(path, located_entry) + end + end + + # It inserts a key at given position without setting its value. + # Its logic is to set it after the last valid entry. If it is not defined + # then tries to place it before the first valid entry in tree. If there is + # no entry in tree, then does not insert a position, which means that + # subsequent setting of value appends it to the end. + # + # @param located_entry [LocatedEntry] entry to insert + # @return [String] where value should be written. Can + # contain path expressions. + # See https://github.com/hercules-team/augeas/wiki/Path-expressions + def insert_entry(located_entry) + # entries with add not exist yet + preceding = located_entry.preceding_existing + prefix = located_entry.prefix + if preceding + insert_after(preceding, located_entry) + # entries with remove is already removed, otherwise find previously + elsif located_entry.any_following? + aug.insert(prefix + "/*[1]", located_entry.key, true) + aug.match(prefix + "/*[1]").first + else + "#{prefix}/#{located_entry.key}" + end + end + + # Insert key after preceding. + # @see insert_entry + # @param preceding [LocatedEntry] entry after which the new one goes + # @param located_entry [LocatedEntry] entry to insert + # @return [String] where value should be written. + def insert_after(preceding, located_entry) + aug.insert(preceding.path, located_entry.key, false) + path_after(preceding) + end + + # Finds path immediately after preceding entry + # @param preceding [LocatedEntry] + def path_after(preceding) + paths = aug.match(preceding.prefix + "/*") + preceding_index = paths.index(preceding.path) + # it can happen, that insertion change previous entry from + # e.g. #comment to #comment[1]. Can happen only if it switch from + # single entry to collection + preceding_index ||= paths.index(preceding.path + "[1]") + paths[preceding_index + 1] + end + end + + attr_reader :aug + + # Does modification according to the operation defined in {AugeasElement} + # @param located_entry [LocatedEntry] entry to process + def process_operation(located_entry) + case located_entry.entry[:operation] + when :add, nil then @lazy_operations.add(located_entry) + when :remove then @lazy_operations.remove(located_entry) + when :modify then modify_entry(located_entry) + when :keep then recurse_write(located_entry) + else raise "invalid :operation in #{located_entry.inspect}" + end + end + + # Writes value of entry to path and if it has a sub-tree + # then it calls {#write} on it + # @param located_entry [LocatedEntry] entry to modify + def modify_entry(located_entry) + value = located_entry.entry_value + aug.set(located_entry.path, value) + report_error { aug.set(located_entry.path, value) } + recurse_write(located_entry) + end + + # calls write on entry if entry have sub-tree + # @param located_entry [LocatedEntry] entry to recursive write + def recurse_write(located_entry) + write(located_entry.path, located_entry.entry_tree, top_level: false) + end + + # Calls block and if it failed, raise exception with details from augeas + # why it failed + # @yield call to aug that is secured + # @raise [RuntimeError] + def report_error + return if yield + + error = aug.error + # zero is no error, so problem in lense + if aug.error[:code].nonzero? + raise "Augeas error #{error[:message]}. Details: #{error[:details]}." + end + + msg = aug.get("/augeas/text/store/error/message") + location = aug.get("/augeas/text/store/error/lens") + raise "Augeas serializing error: #{msg} at #{location}" + end + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/cfa/augeas_parser.rb new/lib/cfa/augeas_parser.rb --- old/lib/cfa/augeas_parser.rb 2016-11-30 16:54:05.000000000 +0100 +++ new/lib/cfa/augeas_parser.rb 2017-03-21 16:50:58.000000000 +0100 @@ -1,7 +1,9 @@ +require "set" require "augeas" require "forwardable" require "cfa/placer" +# CFA: Configuration Files API module CFA # A building block for {AugeasTree}. # @@ -16,6 +18,17 @@ # A `:value` is either a String, or an {AugeasTree}, # or an {AugeasTreeValue} (which combines both). # + # An `:operation` is an internal variable holding modification of Augeas + # structure. It is used for minimizing modifications of source files. Its + # possible values are + # - `:keep` when the value is untouched + # - `:modify` when the `:value` changed but the `:key` is the same + # - `:remove` when it is going to be removed, and + # - `:add` when a new element is added. + # + # An `:orig_key` is an internal variable used to hold the original key + # including its index. + # # @return [Hash{Symbol => String, AugeasTree}] # # @todo Unify naming: entry, element @@ -38,19 +51,16 @@ element = placer.new_element(@tree) element[:key] = augeas_name element[:value] = value + element[:operation] = :add # FIXME: load_collection missing here end def delete(value) - key = augeas_name - @tree.data.reject! do |entry| - entry[:key] == key && - if value.is_a?(Regexp) - value =~ entry[:value] - else - value == entry[:value] - end - end + to_delete, to_mark = to_remove(value) + .partition { |e| e[:operation] == :add } + @tree.all_data.delete_if { |e| to_delete.include?(e) } + + to_mark.each { |e| e[:operation] = :remove } load_collection end @@ -58,26 +68,45 @@ private def load_collection - entries = @tree.data.select { |d| d[:key] == augeas_name } + entries = @tree.data.select do |entry| + entry[:key] == augeas_name && entry[:operation] != :remove + end @collection = entries.map { |e| e[:value] }.freeze end def augeas_name @name + "[]" end + + def to_remove(value) + key = augeas_name + + @tree.data.select do |entry| + entry[:key] == key && value_match?(entry[:value], value) + end + end + + def value_match?(value, match) + if match.is_a?(Regexp) + value =~ match + else + value == match + end + end end # Represents a node that contains both a value and a subtree below it. # For easier traversal it forwards `#[]` to the subtree. class AugeasTreeValue # @return [String] the value in the node - attr_accessor :value + attr_reader :value # @return [AugeasTree] the subtree below the node attr_accessor :tree def initialize(tree, value) @tree = tree @value = value + @modified = false end # (see AugeasTree#[]) @@ -85,12 +114,22 @@ tree[key] end + def value=(value) + @value = value + @modified = true + end + def ==(other) [:class, :value, :tree].all? do |a| public_send(a) == other.public_send(a) end end + # @return true if the value has been modified + def modified? + @modified + end + # For objects of class Object, eql? is synonymous with ==: # http://ruby-doc.org/core-2.3.3/Object.html#method-i-eql-3F alias_method :eql?, :== @@ -100,29 +139,55 @@ class AugeasTree # Low level access to Augeas structure # - # An ordered mapping, represented by an Array of Hashes - # with the keys :key and :value. + # An ordered mapping, represented by an Array of AugeasElement, but without + # any removed elements. # # @see AugeasElement # - # @return [Array<Hash{Symbol => String, AugeasTree}>] - attr_reader :data + # @return [Array<Hash{Symbol => Object}>] a frozen array as it is + # just a copy of the real data + def data + @data.select { |e| e[:operation] != :remove }.freeze + end + + # low level access to all AugeasElement including ones marked for removal + def all_data + @data + end def initialize @data = [] end + # Gets new unique id in numberic sequence. Useful for augeas models that + # using sequences like /etc/hosts . It have keys like "1", "2" and when + # adding new one it need to find new key. + def unique_id + # check all_data instead of data, as we have to not reuse deleted key + ids = Set.new(all_data.map { |e| e[:key] }) + id = 1 + loop do + return id.to_s unless ids.include?(id.to_s) + id += 1 + end + end + # @return [AugeasCollection] collection for *key* def collection(key) AugeasCollection.new(self, key) end - # @param [String, Matcher] + # @param [String, Matcher] matcher def delete(matcher) + return if matcher.nil? unless matcher.is_a?(CFA::Matcher) matcher = CFA::Matcher.new(key: matcher) end - @data.reject!(&matcher) + to_remove = @data.select(&matcher) + + to_delete, to_mark = to_remove.partition { |e| e[:operation] == :add } + @data -= to_delete + to_mark.each { |e| e[:operation] = :remove } end # Adds the given *value* for *key* in the tree. @@ -138,6 +203,7 @@ element = placer.new_element(self) element[:key] = key element[:value] = value + element[:operation] = :add end # Finds given *key* in tree. @@ -145,7 +211,7 @@ # @return [String,AugeasTree,AugeasTreeValue,nil] the first value for *key*, # or `nil` if not found def [](key) - entry = @data.find { |d| d[:key] == key } + entry = @data.find { |d| d[:key] == key && d[:operation] != :remove } return entry[:value] if entry nil @@ -153,61 +219,30 @@ # Replace the first value for *key* with *value*. # Append a new element if *key* did not exist. + # If *key* was previously removed, then put it back to its old position. # @param key [String] # @param value [String, AugeasTree, AugeasTreeValue] def []=(key, value) - entry = @data.find { |d| d[:key] == key } - if entry - entry[:value] = value - else - @data << { - key: key, - value: value - } - end + new_entry = entry_to_modify(key, value) + new_entry[:key] = key + new_entry[:value] = value end # @param matcher [Matcher] # @return [Array<AugeasElement>] matching elements def select(matcher) - @data.select(&matcher) + data.select(&matcher) end - # @note for internal usage only - # @api private - # - # Initializes {#data} from *prefix* in *aug*. - # @param aug [::Augeas] - # @param prefix [String] Augeas path prefix - # @param keys_cache [AugeasKeysCache] - # @return [void] - def load_from_augeas(aug, prefix, keys_cache) - @data = keys_cache.keys_for_prefix(prefix).map do |key| - aug_key = prefix + "/" + key - { - key: load_key(prefix, aug_key), - value: load_value(aug, aug_key, keys_cache) - } - end - end - - # @note for internal usage only - # @api private - # - # Saves {#data} to *prefix* in *aug*. - # @param aug [::Augeas] - # @param prefix [String] Augeas path prefix - # @return [void] - def save_to_augeas(aug, prefix) - arrays = {} - - @data.each do |entry| - save_entry(entry[:key], entry[:value], arrays, aug, prefix) + def ==(other) + return false if self.class != other.class + other_data = other.data # do not compute again + data.each_with_index do |entry, index| + return false if entry[:key] != other_data[index][:key] + return false if entry[:value] != other_data[index][:value] end - end - def ==(other) - [:class, :data].all? { |a| public_send(a) == other.public_send(a) } + true end # For objects of class Object, eql? is synonymous with ==: @@ -216,54 +251,44 @@ private - def save_entry(key, value, arrays, aug, prefix) - aug_key = obtain_aug_key(prefix, key, arrays) - case value - when AugeasTree then value.save_to_augeas(aug, aug_key) - when AugeasTreeValue - report_error(aug) unless aug.set(aug_key, value.value) - value.tree.save_to_augeas(aug, aug_key) + def replace_entry(old_entry) + index = @data.index(old_entry) + new_entry = { operation: :add } + # insert the replacement to the same location + @data.insert(index, new_entry) + # the entry is not yet in the tree + if old_entry[:operation] == :add + @data.delete_if { |d| d[:key] == key } else - report_error(aug) unless aug.set(aug_key, value) + old_entry[:operation] = :remove end - end - def obtain_aug_key(prefix, key, arrays) - if key.end_with?("[]") - array_key = key[0..-3] # remove trailing [] - arrays[array_key] ||= 0 - arrays[array_key] += 1 - key = array_key + "[#{arrays[array_key]}]" - end - - "#{prefix}/#{key}" + new_entry end - def report_error(aug) - error = aug.error - raise "Augeas error #{error[:message]}." \ - "Details: #{error[:details]}." + def mark_new_entry(new_entry, old_entry) + # if an entry already exists then just modify it, + # but only if we previously did not add it + new_entry[:operation] = if old_entry && old_entry[:operation] != :add + :modify + else + :add + end end - def load_key(prefix, aug_key) - # clean from key prefix and for collection remove number inside [] - # +1 for size due to ending '/' not part of prefix - key = aug_key[(prefix.size + 1)..-1] - key.end_with?("]") ? key.sub(/\[\d+\]$/, "[]") : key - end - - def load_value(aug, aug_key, keys_cache) - subkeys = keys_cache.keys_for_prefix(aug_key) - - nested = !subkeys.empty? - value = aug.get(aug_key) - if nested - subtree = AugeasTree.new - subtree.load_from_augeas(aug, aug_key, keys_cache) - value ? AugeasTreeValue.new(subtree, value) : subtree - else - value + def entry_to_modify(key, value) + entry = @data.find { |d| d[:key] == key } + # we are switching from tree to value or treevalue to value only + # like change from key=value to key=value#comment + if entry && entry[:value].class != value.class + entry = replace_entry(entry) end + new_entry = entry || {} + mark_new_entry(new_entry, entry) + + @data << new_entry unless entry + + new_entry end end @@ -285,6 +310,7 @@ # @param raw_string [String] a string to be parsed # @return [AugeasTree] the parsed data def parse(raw_string) + require "cfa/augeas_parser/reader" @old_content = raw_string # open augeas without any autoloading and it should not touch disk and @@ -294,24 +320,21 @@ aug.set("/input", raw_string) report_error(aug) unless aug.text_store(@lens, "/input", "/store") - keys_cache = AugeasKeysCache.new(aug) - - tree = AugeasTree.new - tree.load_from_augeas(aug, "/store", keys_cache) - - return tree + return AugeasReader.read(aug, "/store") end end # @param data [AugeasTree] the data to be serialized # @return [String] a string to be written def serialize(data) + require "cfa/augeas_parser/writer" # open augeas without any autoloading and it should not touch disk and # load lenses as needed only root = load_path = nil Augeas.open(root, load_path, Augeas::NO_MODL_AUTOLOAD) do |aug| aug.set("/input", @old_content || "") - data.save_to_augeas(aug, "/store") + aug.text_store(@lens, "/input", "/store") if @old_content + AugeasWriter.new(aug).write("/store", data) res = aug.text_retrieve(@lens, "/input", "/store", "/output") report_error(aug) unless res @@ -341,44 +364,4 @@ raise "Augeas parsing/serializing error: #{msg} at #{location}" end end - - # Cache that holds all avaiable keys in augeas tree. It is used to - # prevent too many aug.match calls which are expensive. - class AugeasKeysCache - STORE_PREFIX = "/store".freeze - - # initialize cache from passed augeas object - def initialize(aug) - fill_cache(aug) - end - - # returns list of keys available on given prefix - def keys_for_prefix(prefix) - @cache[prefix] || [] - end - - private - - def fill_cache(aug) - @cache = {} - search_path = "#{STORE_PREFIX}/*" - loop do - matches = aug.match(search_path) - break if matches.empty? - assign_matches(matches, @cache) - - search_path += "/*" - end - end - - def assign_matches(matches, cache) - matches.each do |match| - split_index = match.rindex("/") - prefix = match[0..(split_index - 1)] - key = match[(split_index + 1)..-1] - cache[prefix] ||= [] - cache[prefix] << key - end - end - end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/cfa/base_model.rb new/lib/cfa/base_model.rb --- old/lib/cfa/base_model.rb 2016-11-30 16:54:05.000000000 +0100 +++ new/lib/cfa/base_model.rb 2017-03-21 16:50:58.000000000 +0100 @@ -53,14 +53,15 @@ # smart to at first modify existing value, then replace commented out code # and if even that doesn't work, then append it at the end # @note prefer to use specialized methods of children - def generic_set(key, value) - modify(key, value) || uncomment(key, value) || add_new(key, value) + def generic_set(key, value, tree = data) + modify(key, value, tree) || uncomment(key, value, tree) || + add_new(key, value, tree) end # powerfull method that gets unformatted any value in config. # @note prefer to use specialized methods of children - def generic_get(key) - data[key] + def generic_get(key, tree = data) + tree[key] end # rubocop:disable Style/TrivialAccessors @@ -120,18 +121,18 @@ # Modify an **existing** entry and return `true`, # or do nothing and return `false`. # @return [Boolean] - def modify(key, value) + def modify(key, value, tree) # if already set, just change value - return false unless data[key] + return false unless tree[key] - data[key] = value + tree[key] = value true end # Replace a commented out entry and return `true`, # or do nothing and return `false`. # @return [Boolean] - def uncomment(key, value) + def uncomment(key, value, tree) # Try to find if it is commented out, so we can replace line matcher = Matcher.new( collection: "#comment", @@ -139,15 +140,15 @@ # FIXME: this will match also "# If you set FOO=bar then..." value_matcher: /(\s|^)#{key}\s*=/ ) - return false unless data.data.any?(&matcher) + return false unless tree.data.any?(&matcher) # FIXME: this assumes that *data* is an AugeasTree - data.add(key, value, ReplacePlacer.new(matcher)) + tree.add(key, value, ReplacePlacer.new(matcher)) true end - def add_new(key, value) - data.add(key, value) + def add_new(key, value, tree) + tree.add(key, value) end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/cfa/placer.rb new/lib/cfa/placer.rb --- old/lib/cfa/placer.rb 2016-11-30 16:54:05.000000000 +0100 +++ new/lib/cfa/placer.rb 2017-03-21 16:50:58.000000000 +0100 @@ -11,14 +11,20 @@ raise NotImplementedError, "Subclasses of #{Module.nesting.first} must override #{__method__}" end + + protected + + def create_element + { operation: :add } + end end # Places the new element at the end of the tree. class AppendPlacer < Placer # (see Placer#new_element) def new_element(tree) - res = {} - tree.data << res + res = create_element + tree.all_data << res res end @@ -37,14 +43,15 @@ # (see Placer#new_element) def new_element(tree) - index = tree.data.index(&@matcher) + index = tree.all_data.index(&@matcher) - res = {} + res = create_element if index - tree.data.insert(index, res) + tree.all_data.insert(index, res) else - tree.data << res + tree.all_data << res end + res end end @@ -61,14 +68,15 @@ # (see Placer#new_element) def new_element(tree) - index = tree.data.index(&@matcher) + index = tree.all_data.index(&@matcher) - res = {} + res = create_element if index - tree.data.insert(index + 1, res) + tree.all_data.insert(index + 1, res) else - tree.data << res + tree.all_data << res end + res end end @@ -86,13 +94,16 @@ # (see Placer#new_element) def new_element(tree) - index = tree.data.index(&@matcher) - res = {} + index = tree.all_data.index(&@matcher) + res = create_element if index - tree.data[index] = res + # remove old one and add new one, as it can have different key + # which cause problem to simple modify + tree.all_data[index][:operation] = :remove + tree.all_data.insert(index + 1, res) else - tree.data << res + tree.all_data << res end res diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/metadata new/metadata --- old/metadata 2016-11-30 16:54:05.000000000 +0100 +++ new/metadata 2017-03-21 16:51:15.000000000 +0100 @@ -1,14 +1,14 @@ --- !ruby/object:Gem::Specification name: cfa version: !ruby/object:Gem::Version - version: 0.4.2 + version: 0.6.0 platform: ruby authors: - Josef Reidinger autorequire: bindir: bin cert_chain: [] -date: 2016-11-30 00:00:00.000000000 Z +date: 2017-03-21 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: ruby-augeas @@ -34,6 +34,9 @@ extra_rdoc_files: [] files: - lib/cfa/augeas_parser.rb +- lib/cfa/augeas_parser/keys_cache.rb +- lib/cfa/augeas_parser/reader.rb +- lib/cfa/augeas_parser/writer.rb - lib/cfa/base_model.rb - lib/cfa/matcher.rb - lib/cfa/memory_file.rb @@ -58,10 +61,9 @@ version: 1.3.6 requirements: [] rubyforge_project: -rubygems_version: 2.2.2 +rubygems_version: 2.4.5.2 signing_key: specification_version: 4 summary: CFA (Config Files API) provides an easy way to create models on top of configuration files test_files: [] -has_rdoc:
participants (1)
-
root@hilbert.suse.de