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 - [email protected]
+
+- 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 - [email protected]
+
+- 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 - [email protected]
+
+- 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 - [email protected]
+
+- 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: 


Reply via email to