cfa 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5c2df2919e42fdffa14bccfe6f20ef8dd8a31bb4
4
- data.tar.gz: b3c552d45c3c3cb132de98f3ae194d8bd88ce76f
3
+ metadata.gz: a4abe1cefacc4777f6016b432b3a306f16b1b7ac
4
+ data.tar.gz: 359c1f958762e205c5313e0027dda5a09a10bc5c
5
5
  SHA512:
6
- metadata.gz: 6305de07946201013df560ddae63102d35702f164594ea5ce4dc340624dc26aadbc382f8e43380e5062b7f72288e42099fad2a8ce4f21d7a83d284c5bca7f7ad
7
- data.tar.gz: ec2a91cbabd1acccb1ff0bbc371f7ecbde7c641503f32d0c5e4e2e9ff815c63d927347d4a3fe378efad72cfc7ca618c59e92092807900bc0cdda02bb7e747525
6
+ metadata.gz: c8bdb5a5aa8391dfb8c46448b760868e257b71e195daa38cc7d272f1729e52d7c20be9acd6fa2632b63ec4bfb736c7bba41a3293b6e7d3e12d319d0a786fd885
7
+ data.tar.gz: 69296e9dbd626125d6e0612f580cbbd12266e004794231c9b61c980bd66114ee90f84cafd22393fc6b1000ce4b42d92f6ff724b2ce34c6ff96d77feba2c604cb
@@ -2,6 +2,7 @@ require "augeas"
2
2
  require "forwardable"
3
3
  require "cfa/placer"
4
4
 
5
+ # CFA: Configuration Files API
5
6
  module CFA
6
7
  # A building block for {AugeasTree}.
7
8
  #
@@ -16,6 +17,17 @@ module CFA
16
17
  # A `:value` is either a String, or an {AugeasTree},
17
18
  # or an {AugeasTreeValue} (which combines both).
18
19
  #
20
+ # An `:operation` is an internal variable holding modification of Augeas
21
+ # structure. It is used for minimizing modifications of source files. Its
22
+ # possible values are
23
+ # - `:keep` when the value is untouched
24
+ # - `:modify` when the `:value` changed but the `:key` is the same
25
+ # - `:remove` when it is going to be removed, and
26
+ # - `:add` when a new element is added.
27
+ #
28
+ # An `:orig_key` is an internal variable used to hold the original key
29
+ # including its index.
30
+ #
19
31
  # @return [Hash{Symbol => String, AugeasTree}]
20
32
  #
21
33
  # @todo Unify naming: entry, element
@@ -38,19 +50,16 @@ module CFA
38
50
  element = placer.new_element(@tree)
39
51
  element[:key] = augeas_name
40
52
  element[:value] = value
53
+ element[:operation] = :add
41
54
  # FIXME: load_collection missing here
42
55
  end
43
56
 
44
57
  def delete(value)
45
- key = augeas_name
46
- @tree.data.reject! do |entry|
47
- entry[:key] == key &&
48
- if value.is_a?(Regexp)
49
- value =~ entry[:value]
50
- else
51
- value == entry[:value]
52
- end
53
- end
58
+ to_delete, to_mark = to_remove(value)
59
+ .partition { |e| e[:operation] == :add }
60
+ @tree.all_data.delete_if { |e| to_delete.include?(e) }
61
+
62
+ to_mark.each { |e| e[:operation] = :remove }
54
63
 
55
64
  load_collection
56
65
  end
@@ -58,26 +67,45 @@ module CFA
58
67
  private
59
68
 
60
69
  def load_collection
61
- entries = @tree.data.select { |d| d[:key] == augeas_name }
70
+ entries = @tree.data.select do |entry|
71
+ entry[:key] == augeas_name && entry[:operation] != :remove
72
+ end
62
73
  @collection = entries.map { |e| e[:value] }.freeze
63
74
  end
64
75
 
65
76
  def augeas_name
66
77
  @name + "[]"
67
78
  end
79
+
80
+ def to_remove(value)
81
+ key = augeas_name
82
+
83
+ @tree.data.select do |entry|
84
+ entry[:key] == key && value_match?(entry[:value], value)
85
+ end
86
+ end
87
+
88
+ def value_match?(value, match)
89
+ if match.is_a?(Regexp)
90
+ value =~ match
91
+ else
92
+ value == match
93
+ end
94
+ end
68
95
  end
69
96
 
70
97
  # Represents a node that contains both a value and a subtree below it.
71
98
  # For easier traversal it forwards `#[]` to the subtree.
72
99
  class AugeasTreeValue
73
100
  # @return [String] the value in the node
74
- attr_accessor :value
101
+ attr_reader :value
75
102
  # @return [AugeasTree] the subtree below the node
76
103
  attr_accessor :tree
77
104
 
78
105
  def initialize(tree, value)
79
106
  @tree = tree
80
107
  @value = value
108
+ @modified = false
81
109
  end
82
110
 
83
111
  # (see AugeasTree#[])
@@ -85,12 +113,22 @@ module CFA
85
113
  tree[key]
86
114
  end
87
115
 
116
+ def value=(value)
117
+ @value = value
118
+ @modified = true
119
+ end
120
+
88
121
  def ==(other)
89
122
  [:class, :value, :tree].all? do |a|
90
123
  public_send(a) == other.public_send(a)
91
124
  end
92
125
  end
93
126
 
127
+ # @return true if the value has been modified
128
+ def modified?
129
+ @modified
130
+ end
131
+
94
132
  # For objects of class Object, eql? is synonymous with ==:
95
133
  # http://ruby-doc.org/core-2.3.3/Object.html#method-i-eql-3F
96
134
  alias_method :eql?, :==
@@ -100,13 +138,21 @@ module CFA
100
138
  class AugeasTree
101
139
  # Low level access to Augeas structure
102
140
  #
103
- # An ordered mapping, represented by an Array of Hashes
104
- # with the keys :key and :value.
141
+ # An ordered mapping, represented by an Array of AugeasElement, but without
142
+ # any removed elements.
105
143
  #
106
144
  # @see AugeasElement
107
145
  #
108
- # @return [Array<Hash{Symbol => String, AugeasTree}>]
109
- attr_reader :data
146
+ # @return [Array<Hash{Symbol => Object}>] a frozen array as it is
147
+ # just a copy of the real data
148
+ def data
149
+ @data.select { |e| e[:operation] != :remove }.freeze
150
+ end
151
+
152
+ # low level access to all AugeasElement including ones marked for removal
153
+ def all_data
154
+ @data
155
+ end
110
156
 
111
157
  def initialize
112
158
  @data = []
@@ -117,13 +163,17 @@ module CFA
117
163
  AugeasCollection.new(self, key)
118
164
  end
119
165
 
120
- # @param [String, Matcher]
166
+ # @param [String, Matcher] matcher
121
167
  def delete(matcher)
122
168
  return if matcher.nil?
123
169
  unless matcher.is_a?(CFA::Matcher)
124
170
  matcher = CFA::Matcher.new(key: matcher)
125
171
  end
126
- @data.reject!(&matcher)
172
+ to_remove = @data.select(&matcher)
173
+
174
+ to_delete, to_mark = to_remove.partition { |e| e[:operation] == :add }
175
+ @data -= to_delete
176
+ to_mark.each { |e| e[:operation] = :remove }
127
177
  end
128
178
 
129
179
  # Adds the given *value* for *key* in the tree.
@@ -139,6 +189,7 @@ module CFA
139
189
  element = placer.new_element(self)
140
190
  element[:key] = key
141
191
  element[:value] = value
192
+ element[:operation] = :add
142
193
  end
143
194
 
144
195
  # Finds given *key* in tree.
@@ -146,7 +197,7 @@ module CFA
146
197
  # @return [String,AugeasTree,AugeasTreeValue,nil] the first value for *key*,
147
198
  # or `nil` if not found
148
199
  def [](key)
149
- entry = @data.find { |d| d[:key] == key }
200
+ entry = @data.find { |d| d[:key] == key && d[:operation] != :remove }
150
201
  return entry[:value] if entry
151
202
 
152
203
  nil
@@ -154,18 +205,13 @@ module CFA
154
205
 
155
206
  # Replace the first value for *key* with *value*.
156
207
  # Append a new element if *key* did not exist.
208
+ # If *key* was previously removed, then put it back to its old position.
157
209
  # @param key [String]
158
210
  # @param value [String, AugeasTree, AugeasTreeValue]
159
211
  def []=(key, value)
160
- entry = @data.find { |d| d[:key] == key }
161
- if entry
162
- entry[:value] = value
163
- else
164
- @data << {
165
- key: key,
166
- value: value
167
- }
168
- end
212
+ new_entry = entry_to_modify(key, value)
213
+ new_entry[:key] = key
214
+ new_entry[:value] = value
169
215
  end
170
216
 
171
217
  # @param matcher [Matcher]
@@ -174,41 +220,15 @@ module CFA
174
220
  @data.select(&matcher)
175
221
  end
176
222
 
177
- # @note for internal usage only
178
- # @api private
179
- #
180
- # Initializes {#data} from *prefix* in *aug*.
181
- # @param aug [::Augeas]
182
- # @param prefix [String] Augeas path prefix
183
- # @param keys_cache [AugeasKeysCache]
184
- # @return [void]
185
- def load_from_augeas(aug, prefix, keys_cache)
186
- @data = keys_cache.keys_for_prefix(prefix).map do |key|
187
- aug_key = prefix + "/" + key
188
- {
189
- key: load_key(prefix, aug_key),
190
- value: load_value(aug, aug_key, keys_cache)
191
- }
192
- end
193
- end
194
-
195
- # @note for internal usage only
196
- # @api private
197
- #
198
- # Saves {#data} to *prefix* in *aug*.
199
- # @param aug [::Augeas]
200
- # @param prefix [String] Augeas path prefix
201
- # @return [void]
202
- def save_to_augeas(aug, prefix)
203
- arrays = {}
204
-
205
- @data.each do |entry|
206
- save_entry(entry[:key], entry[:value], arrays, aug, prefix)
223
+ def ==(other)
224
+ return false if self.class != other.class
225
+ other_data = other.data # do not compute again
226
+ data.each_with_index do |entry, index|
227
+ return false if entry[:key] != other_data[index][:key]
228
+ return false if entry[:value] != other_data[index][:value]
207
229
  end
208
- end
209
230
 
210
- def ==(other)
211
- [:class, :data].all? { |a| public_send(a) == other.public_send(a) }
231
+ true
212
232
  end
213
233
 
214
234
  # For objects of class Object, eql? is synonymous with ==:
@@ -217,54 +237,44 @@ module CFA
217
237
 
218
238
  private
219
239
 
220
- def save_entry(key, value, arrays, aug, prefix)
221
- aug_key = obtain_aug_key(prefix, key, arrays)
222
- case value
223
- when AugeasTree then value.save_to_augeas(aug, aug_key)
224
- when AugeasTreeValue
225
- report_error(aug) unless aug.set(aug_key, value.value)
226
- value.tree.save_to_augeas(aug, aug_key)
240
+ def replace_entry(old_entry)
241
+ index = @data.index(old_entry)
242
+ new_entry = { operation: :add }
243
+ # insert the replacement to the same location
244
+ @data.insert(index, new_entry)
245
+ # the entry is not yet in the tree
246
+ if old_entry[:operation] == :add
247
+ @data.delete_if { |d| d[:key] == key }
227
248
  else
228
- report_error(aug) unless aug.set(aug_key, value)
229
- end
230
- end
231
-
232
- def obtain_aug_key(prefix, key, arrays)
233
- if key.end_with?("[]")
234
- array_key = key[0..-3] # remove trailing []
235
- arrays[array_key] ||= 0
236
- arrays[array_key] += 1
237
- key = array_key + "[#{arrays[array_key]}]"
249
+ old_entry[:operation] = :remove
238
250
  end
239
251
 
240
- "#{prefix}/#{key}"
252
+ new_entry
241
253
  end
242
254
 
243
- def report_error(aug)
244
- error = aug.error
245
- raise "Augeas error #{error[:message]}." \
246
- "Details: #{error[:details]}."
255
+ def mark_new_entry(new_entry, old_entry)
256
+ # if an entry already exists then just modify it,
257
+ # but only if we previously did not add it
258
+ new_entry[:operation] = if old_entry && old_entry[:operation] != :add
259
+ :modify
260
+ else
261
+ :add
262
+ end
247
263
  end
248
264
 
249
- def load_key(prefix, aug_key)
250
- # clean from key prefix and for collection remove number inside []
251
- # +1 for size due to ending '/' not part of prefix
252
- key = aug_key[(prefix.size + 1)..-1]
253
- key.end_with?("]") ? key.sub(/\[\d+\]$/, "[]") : key
254
- end
265
+ def entry_to_modify(key, value)
266
+ entry = @data.find { |d| d[:key] == key }
267
+ # we are switching from tree to value or treevalue to value only
268
+ # like change from key=value to key=value#comment
269
+ if entry && entry[:value].class != value.class
270
+ entry = replace_entry(entry)
271
+ end
272
+ new_entry = entry || {}
273
+ mark_new_entry(new_entry, entry)
255
274
 
256
- def load_value(aug, aug_key, keys_cache)
257
- subkeys = keys_cache.keys_for_prefix(aug_key)
275
+ @data << new_entry unless entry
258
276
 
259
- nested = !subkeys.empty?
260
- value = aug.get(aug_key)
261
- if nested
262
- subtree = AugeasTree.new
263
- subtree.load_from_augeas(aug, aug_key, keys_cache)
264
- value ? AugeasTreeValue.new(subtree, value) : subtree
265
- else
266
- value
267
- end
277
+ new_entry
268
278
  end
269
279
  end
270
280
 
@@ -286,6 +296,7 @@ module CFA
286
296
  # @param raw_string [String] a string to be parsed
287
297
  # @return [AugeasTree] the parsed data
288
298
  def parse(raw_string)
299
+ require "cfa/augeas_parser/reader"
289
300
  @old_content = raw_string
290
301
 
291
302
  # open augeas without any autoloading and it should not touch disk and
@@ -295,24 +306,21 @@ module CFA
295
306
  aug.set("/input", raw_string)
296
307
  report_error(aug) unless aug.text_store(@lens, "/input", "/store")
297
308
 
298
- keys_cache = AugeasKeysCache.new(aug)
299
-
300
- tree = AugeasTree.new
301
- tree.load_from_augeas(aug, "/store", keys_cache)
302
-
303
- return tree
309
+ return AugeasReader.read(aug, "/store")
304
310
  end
305
311
  end
306
312
 
307
313
  # @param data [AugeasTree] the data to be serialized
308
314
  # @return [String] a string to be written
309
315
  def serialize(data)
316
+ require "cfa/augeas_parser/writer"
310
317
  # open augeas without any autoloading and it should not touch disk and
311
318
  # load lenses as needed only
312
319
  root = load_path = nil
313
320
  Augeas.open(root, load_path, Augeas::NO_MODL_AUTOLOAD) do |aug|
314
321
  aug.set("/input", @old_content || "")
315
- data.save_to_augeas(aug, "/store")
322
+ aug.text_store(@lens, "/input", "/store") if @old_content
323
+ AugeasWriter.new(aug).write("/store", data)
316
324
 
317
325
  res = aug.text_retrieve(@lens, "/input", "/store", "/output")
318
326
  report_error(aug) unless res
@@ -342,44 +350,4 @@ module CFA
342
350
  raise "Augeas parsing/serializing error: #{msg} at #{location}"
343
351
  end
344
352
  end
345
-
346
- # Cache that holds all avaiable keys in augeas tree. It is used to
347
- # prevent too many aug.match calls which are expensive.
348
- class AugeasKeysCache
349
- STORE_PREFIX = "/store".freeze
350
-
351
- # initialize cache from passed augeas object
352
- def initialize(aug)
353
- fill_cache(aug)
354
- end
355
-
356
- # returns list of keys available on given prefix
357
- def keys_for_prefix(prefix)
358
- @cache[prefix] || []
359
- end
360
-
361
- private
362
-
363
- def fill_cache(aug)
364
- @cache = {}
365
- search_path = "#{STORE_PREFIX}/*"
366
- loop do
367
- matches = aug.match(search_path)
368
- break if matches.empty?
369
- assign_matches(matches, @cache)
370
-
371
- search_path += "/*"
372
- end
373
- end
374
-
375
- def assign_matches(matches, cache)
376
- matches.each do |match|
377
- split_index = match.rindex("/")
378
- prefix = match[0..(split_index - 1)]
379
- key = match[(split_index + 1)..-1]
380
- cache[prefix] ||= []
381
- cache[prefix] << key
382
- end
383
- end
384
- end
385
353
  end
@@ -0,0 +1,41 @@
1
+ module CFA
2
+ # A cache that holds all avaiable keys in an Augeas tree. It is used to
3
+ # prevent too many `aug.match` calls which are expensive.
4
+ class AugeasKeysCache
5
+ # initialize cache from passed Augeas object
6
+ # @param aug [::Augeas]
7
+ # @param prefix [String] Augeas path for which cache should be created
8
+ def initialize(aug, prefix)
9
+ fill_cache(aug, prefix)
10
+ end
11
+
12
+ # @return list of keys available on given prefix
13
+ def keys_for_prefix(prefix)
14
+ @cache[prefix] || []
15
+ end
16
+
17
+ private
18
+
19
+ def fill_cache(aug, prefix)
20
+ @cache = {}
21
+ search_path = "#{prefix}/*"
22
+ loop do
23
+ matches = aug.match(search_path)
24
+ break if matches.empty?
25
+ assign_matches(matches, @cache)
26
+
27
+ search_path += "/*"
28
+ end
29
+ end
30
+
31
+ def assign_matches(matches, cache)
32
+ matches.each do |match|
33
+ split_index = match.rindex("/")
34
+ prefix = match[0..(split_index - 1)]
35
+ key = match[(split_index + 1)..-1]
36
+ cache[prefix] ||= []
37
+ cache[prefix] << key
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,67 @@
1
+ require "cfa/augeas_parser/keys_cache"
2
+ require "cfa/augeas_parser"
3
+
4
+ module CFA
5
+ # A class responsible for reading {AugeasTree} from Augeas
6
+ class AugeasReader
7
+ class << self
8
+ # Creates *tree* from *prefix* in *aug*.
9
+ # @param aug [::Augeas]
10
+ # @param prefix [String] Augeas path prefix
11
+ # @return [AugeasTree]
12
+ def read(aug, prefix)
13
+ keys_cache = AugeasKeysCache.new(aug, prefix)
14
+
15
+ tree = AugeasTree.new
16
+ load_tree(aug, prefix, tree, keys_cache)
17
+
18
+ tree
19
+ end
20
+
21
+ private
22
+
23
+ # fills *tree* with data
24
+ def load_tree(aug, prefix, tree, keys_cache)
25
+ data = keys_cache.keys_for_prefix(prefix).map do |key|
26
+ aug_key = prefix + "/" + key
27
+ {
28
+ key: load_key(prefix, aug_key),
29
+ value: load_value(aug, aug_key, keys_cache),
30
+ orig_key: stripped_path(prefix, aug_key),
31
+ operation: :keep
32
+ }
33
+ end
34
+
35
+ tree.all_data.concat(data)
36
+ end
37
+
38
+ # loads a key in a format that AugeasTree expects
39
+ def load_key(prefix, aug_key)
40
+ # clean from key prefix and for collection remove number inside []
41
+ key = stripped_path(prefix, aug_key)
42
+ key.end_with?("]") ? key.sub(/\[\d+\]$/, "[]") : key
43
+ end
44
+
45
+ # path without prefix we are not interested in
46
+ def stripped_path(prefix, aug_key)
47
+ # +1 for size due to ending '/' not part of prefix
48
+ aug_key[(prefix.size + 1)..-1]
49
+ end
50
+
51
+ # loads value from auges. If value have tree under, it will also read it
52
+ def load_value(aug, aug_key, keys_cache)
53
+ subkeys = keys_cache.keys_for_prefix(aug_key)
54
+
55
+ nested = !subkeys.empty?
56
+ value = aug.get(aug_key)
57
+ if nested
58
+ subtree = AugeasTree.new
59
+ load_tree(aug, aug_key, subtree, keys_cache)
60
+ value ? AugeasTreeValue.new(subtree, value) : subtree
61
+ else
62
+ value
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,324 @@
1
+ module CFA
2
+ # The goal of this class is to write the data stored in {AugeasTree}
3
+ # back to Augeas.
4
+ #
5
+ # It tries to make only the needed changes, as internally Augeas keeps
6
+ # a flag whether data has been modified,
7
+ # and keeps the unmodified parts of the file untouched.
8
+ #
9
+ # @note internal only, unstable API
10
+ # @api private
11
+ class AugeasWriter
12
+ # @param aug result of Augeas.create
13
+ def initialize(aug)
14
+ @aug = aug
15
+ end
16
+
17
+ # Writes the data in *tree* to a given *prefix* in Augeas
18
+ # @param prefix [String] where to write *tree* in Augeas
19
+ # @param tree [CFA::AugeasTree] tree to write
20
+ def write(prefix, tree, top_level: true)
21
+ @lazy_operations = LazyOperations.new(aug) if top_level
22
+ tree.all_data.each do |entry|
23
+ located_entry = LocatedEntry.new(tree, entry, prefix)
24
+ process_operation(located_entry)
25
+ end
26
+ @lazy_operations.run if top_level
27
+ end
28
+
29
+ private
30
+
31
+ # {AugeasElement} together with information about its location and a few
32
+ # helper methods to detect siblings.
33
+ #
34
+ # @example data for an already existing comment living under /main
35
+ # entry.orig_key # => "#comment[15]"
36
+ # entry.path # => "/main/#comment[15]"
37
+ # entry.key # => "#comment"
38
+ # entry.entry_tree # => AugeasTree.new
39
+ # entry.entry_value # => "old boring comment"
40
+ #
41
+ # @example data for a new comment under /main
42
+ # entry.orig_key # => nil
43
+ # entry.path # => nil
44
+ # entry.key # => "#comment"
45
+ # entry.entry_tree # => AugeasTree.new
46
+ # entry.entry_value # => "new boring comment"
47
+ #
48
+ # @example data for new tree placed at /main
49
+ # entry.orig_key # => "main"
50
+ # entry.path # => "/main"
51
+ # entry.key # => "main"
52
+ # entry.entry_tree # => entry[:value]
53
+ # entry.entry_value # => nil
54
+ #
55
+ class LocatedEntry
56
+ attr_reader :prefix
57
+ attr_reader :entry
58
+ attr_reader :tree
59
+
60
+ def initialize(tree, entry, prefix)
61
+ @tree = tree
62
+ @entry = entry
63
+ @prefix = prefix
64
+ detect_tree_value_modification
65
+ end
66
+
67
+ def orig_key
68
+ entry[:orig_key]
69
+ end
70
+
71
+ def path
72
+ return @path if @path
73
+ return nil unless orig_key
74
+
75
+ @path = @prefix + "/" + orig_key
76
+ end
77
+
78
+ def key
79
+ return @key if @key
80
+
81
+ @key = @entry[:key]
82
+ @key = @key[0..-3] if @key.end_with?("[]")
83
+ @key
84
+ end
85
+
86
+ # @return [LocatedEntry, nil]
87
+ # a preceding entry that already exists in the Augeas tree
88
+ # or nil if it does not exist.
89
+ def preceding_existing
90
+ preceding_entry = preceding_entries.reverse_each.find do |entry|
91
+ entry[:operation] != :add
92
+ end
93
+
94
+ return nil unless preceding_entry
95
+
96
+ LocatedEntry.new(tree, preceding_entry, prefix)
97
+ end
98
+
99
+ # @return [true, false] returns true if there is any following entry
100
+ # in the Augeas tree
101
+ def any_following?
102
+ following_entries.any? { |e| e[:operation] != :remove }
103
+ end
104
+
105
+ # @return [AugeasTree] the Augeas tree nested under this entry.
106
+ # If there is no such tree, it creates an empty one.
107
+ def entry_tree
108
+ value = entry[:value]
109
+ case value
110
+ when AugeasTree then value
111
+ when AugeasTreeValue then value.tree
112
+ else AugeasTree.new
113
+ end
114
+ end
115
+
116
+ # @return [String, nil] the Augeas value of this entry. Can be nil.
117
+ # If the value is an {AugeasTree} then return nil.
118
+ def entry_value
119
+ value = entry[:value]
120
+ case value
121
+ when AugeasTree then nil
122
+ when AugeasTreeValue then value.value
123
+ else value
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ # For {AugeasTreeValue} we have a problem with detection of
130
+ # value modification as it is enclosed in a diferent object.
131
+ # So propagate it to this entry here.
132
+ def detect_tree_value_modification
133
+ return unless entry[:value].is_a?(AugeasTreeValue)
134
+ return if entry[:operation] != :keep
135
+
136
+ entry[:operation] = entry[:value].modified? ? :modify : :keep
137
+ end
138
+
139
+ # the entries preceding this entry
140
+ def preceding_entries
141
+ return [] if index.zero? # first entry
142
+ tree.all_data[0..(index - 1)]
143
+ end
144
+
145
+ # the entries following this entry
146
+ def following_entries
147
+ tree.all_data[(index + 1)..-1]
148
+ end
149
+
150
+ # the index of this entry in its tree
151
+ def index
152
+ @index ||= tree.all_data.index(entry)
153
+ end
154
+ end
155
+
156
+ # Represents an operation that needs to be done after all modifications.
157
+ #
158
+ # The reason to have this class is that Augeas renumbers its arrays after
159
+ # some operations like `rm` or `insert` so previous paths are no longer
160
+ # valid. For this reason these sensitive operations that change paths need
161
+ # to be done at the end and with careful order.
162
+ # See https://www.redhat.com/archives/augeas-devel/2017-March/msg00002.html
163
+ #
164
+ # @note This class depends on ordered operations. So adding and removing
165
+ # entries has to be done in order how they are placed in tree.
166
+ class LazyOperations
167
+ # @param aug result of Augeas.create
168
+ def initialize(aug)
169
+ @aug = aug
170
+ @operations = []
171
+ end
172
+
173
+ def add(located_entry)
174
+ @operations << { type: :add, located_entry: located_entry }
175
+ end
176
+
177
+ def remove(located_entry)
178
+ @operations << { type: :remove, path: located_entry.path }
179
+ end
180
+
181
+ # starts all previously inserted operations
182
+ def run
183
+ # the reverse order is needed because if there are two operations
184
+ # one after another then the latter cannot affect the former
185
+ @operations.reverse_each do |operation|
186
+ case operation[:type]
187
+ when :remove then aug.rm(operation[:path])
188
+ when :add
189
+ located_entry = operation[:located_entry]
190
+ add_entry(located_entry)
191
+ else
192
+ raise "Invalid lazy operation #{operation.inspect}"
193
+ end
194
+ end
195
+ end
196
+
197
+ private
198
+
199
+ attr_reader :aug
200
+
201
+ # Adds entry to tree. At first it finds where to add it to be in correct
202
+ # place and then sets its value. Recursive if needed. In recursive case
203
+ # it is already known that whole sub-tree is also new and just added.
204
+ def add_entry(located_entry)
205
+ path = insert_entry(located_entry)
206
+ set_new_value(path, located_entry)
207
+ end
208
+
209
+ # Sets new value to given path. It is used for values that are not yet in
210
+ # Augeas tree. If needed it does recursive adding.
211
+ # @param path [String] path which can contain Augeas path expression for
212
+ # key of new value
213
+ # @param located_entry [LocatedEntry] entry to write
214
+ # @see https://github.com/hercules-team/augeas/wiki/Path-expressions
215
+ def set_new_value(path, located_entry)
216
+ aug.set(path, located_entry.entry_value)
217
+ prefix = path[/(^.*)\/[^\/]+/, 1]
218
+ # we need to get new path as set can look like [last() + 1]
219
+ # which creates new entry and we do not want to add subtree to new
220
+ # entries
221
+ new_path = aug.match(prefix + "/*[last()]").first
222
+ add_subtree(located_entry.entry_tree, new_path)
223
+ end
224
+
225
+ # Adds new subtree. Simplified version of common write as it is known
226
+ # that all entries will be just added.
227
+ # @param tree [CFA::AugeasTree] to add
228
+ # @param prefix [String] prefix where to place *tree*
229
+ def add_subtree(tree, prefix)
230
+ tree.all_data.each do |entry|
231
+ located_entry = LocatedEntry.new(tree, entry, prefix)
232
+ # universal path that handles also new elements for arrays
233
+ path = "#{prefix}/#{located_entry.key}[last()+1]"
234
+ set_new_value(path, located_entry)
235
+ end
236
+ end
237
+
238
+ # It inserts a key at given position without setting its value.
239
+ # Its logic is to set it after the last valid entry. If it is not defined
240
+ # then tries to place it before the first valid entry in tree. If there is
241
+ # no entry in tree, then does not insert a position, which means that
242
+ # subsequent setting of value appends it to the end.
243
+ #
244
+ # @param located_entry [LocatedEntry] entry to insert
245
+ # @return [String] where value should be written. Can
246
+ # contain path expressions.
247
+ # See https://github.com/hercules-team/augeas/wiki/Path-expressions
248
+ def insert_entry(located_entry)
249
+ # entries with add not exist yet
250
+ preceding = located_entry.preceding_existing
251
+ prefix = located_entry.prefix
252
+ if preceding
253
+ insert_after(preceding, located_entry)
254
+ # entries with remove is already removed, otherwise find previously
255
+ elsif located_entry.any_following?
256
+ aug.insert(prefix + "/*[1]", located_entry.key, true)
257
+ aug.match(prefix + "/*[1]").first
258
+ else
259
+ "#{prefix}/#{located_entry.key}"
260
+ end
261
+ end
262
+
263
+ # Insert key after preceding.
264
+ # @see insert_entry
265
+ # @param preceding [LocatedEntry] entry after which the new one goes
266
+ # @param located_entry [LocatedEntry] entry to insert
267
+ # @return [String] where value should be written.
268
+ def insert_after(preceding, located_entry)
269
+ aug.insert(preceding.path, located_entry.key, false)
270
+ paths = aug.match(located_entry.prefix + "/*")
271
+ paths_index = paths.index(preceding.path) + 1
272
+ paths[paths_index]
273
+ end
274
+ end
275
+
276
+ attr_reader :aug
277
+
278
+ # Does modification according to the operation defined in {AugeasElement}
279
+ # @param located_entry [LocatedEntry] entry to process
280
+ def process_operation(located_entry)
281
+ case located_entry.entry[:operation]
282
+ when :add, nil then @lazy_operations.add(located_entry)
283
+ when :remove then @lazy_operations.remove(located_entry)
284
+ when :modify then modify_entry(located_entry)
285
+ when :keep then recurse_write(located_entry)
286
+ else raise "invalid :operation in #{located_entry.inspect}"
287
+ end
288
+ end
289
+
290
+ # Writes value of entry to path and if it has a sub-tree
291
+ # then it calls {#write} on it
292
+ # @param located_entry [LocatedEntry] entry to modify
293
+ def modify_entry(located_entry)
294
+ value = located_entry.entry_value
295
+ aug.set(located_entry.path, value)
296
+ report_error { aug.set(located_entry.path, value) }
297
+ recurse_write(located_entry)
298
+ end
299
+
300
+ # calls write on entry if entry have sub-tree
301
+ # @param located_entry [LocatedEntry] entry to recursive write
302
+ def recurse_write(located_entry)
303
+ write(located_entry.path, located_entry.entry_tree, top_level: false)
304
+ end
305
+
306
+ # Calls block and if it failed, raise exception with details from augeas
307
+ # why it failed
308
+ # @yield call to aug that is secured
309
+ # @raise [RuntimeError]
310
+ def report_error
311
+ return if yield
312
+
313
+ error = aug.error
314
+ # zero is no error, so problem in lense
315
+ if aug.error[:code].nonzero?
316
+ raise "Augeas error #{error[:message]}. Details: #{error[:details]}."
317
+ end
318
+
319
+ msg = aug.get("/augeas/text/store/error/message")
320
+ location = aug.get("/augeas/text/store/error/lens")
321
+ raise "Augeas serializing error: #{msg} at #{location}"
322
+ end
323
+ end
324
+ end
@@ -53,14 +53,15 @@ module CFA
53
53
  # smart to at first modify existing value, then replace commented out code
54
54
  # and if even that doesn't work, then append it at the end
55
55
  # @note prefer to use specialized methods of children
56
- def generic_set(key, value)
57
- modify(key, value) || uncomment(key, value) || add_new(key, value)
56
+ def generic_set(key, value, tree = data)
57
+ modify(key, value, tree) || uncomment(key, value, tree) ||
58
+ add_new(key, value, tree)
58
59
  end
59
60
 
60
61
  # powerfull method that gets unformatted any value in config.
61
62
  # @note prefer to use specialized methods of children
62
- def generic_get(key)
63
- data[key]
63
+ def generic_get(key, tree = data)
64
+ tree[key]
64
65
  end
65
66
 
66
67
  # rubocop:disable Style/TrivialAccessors
@@ -120,18 +121,18 @@ module CFA
120
121
  # Modify an **existing** entry and return `true`,
121
122
  # or do nothing and return `false`.
122
123
  # @return [Boolean]
123
- def modify(key, value)
124
+ def modify(key, value, tree)
124
125
  # if already set, just change value
125
- return false unless data[key]
126
+ return false unless tree[key]
126
127
 
127
- data[key] = value
128
+ tree[key] = value
128
129
  true
129
130
  end
130
131
 
131
132
  # Replace a commented out entry and return `true`,
132
133
  # or do nothing and return `false`.
133
134
  # @return [Boolean]
134
- def uncomment(key, value)
135
+ def uncomment(key, value, tree)
135
136
  # Try to find if it is commented out, so we can replace line
136
137
  matcher = Matcher.new(
137
138
  collection: "#comment",
@@ -139,15 +140,15 @@ module CFA
139
140
  # FIXME: this will match also "# If you set FOO=bar then..."
140
141
  value_matcher: /(\s|^)#{key}\s*=/
141
142
  )
142
- return false unless data.data.any?(&matcher)
143
+ return false unless tree.data.any?(&matcher)
143
144
 
144
145
  # FIXME: this assumes that *data* is an AugeasTree
145
- data.add(key, value, ReplacePlacer.new(matcher))
146
+ tree.add(key, value, ReplacePlacer.new(matcher))
146
147
  true
147
148
  end
148
149
 
149
- def add_new(key, value)
150
- data.add(key, value)
150
+ def add_new(key, value, tree)
151
+ tree.add(key, value)
151
152
  end
152
153
  end
153
154
 
@@ -11,14 +11,20 @@ module CFA
11
11
  raise NotImplementedError,
12
12
  "Subclasses of #{Module.nesting.first} must override #{__method__}"
13
13
  end
14
+
15
+ protected
16
+
17
+ def create_element
18
+ { operation: :add }
19
+ end
14
20
  end
15
21
 
16
22
  # Places the new element at the end of the tree.
17
23
  class AppendPlacer < Placer
18
24
  # (see Placer#new_element)
19
25
  def new_element(tree)
20
- res = {}
21
- tree.data << res
26
+ res = create_element
27
+ tree.all_data << res
22
28
 
23
29
  res
24
30
  end
@@ -37,14 +43,15 @@ module CFA
37
43
 
38
44
  # (see Placer#new_element)
39
45
  def new_element(tree)
40
- index = tree.data.index(&@matcher)
46
+ index = tree.all_data.index(&@matcher)
41
47
 
42
- res = {}
48
+ res = create_element
43
49
  if index
44
- tree.data.insert(index, res)
50
+ tree.all_data.insert(index, res)
45
51
  else
46
- tree.data << res
52
+ tree.all_data << res
47
53
  end
54
+
48
55
  res
49
56
  end
50
57
  end
@@ -61,14 +68,15 @@ module CFA
61
68
 
62
69
  # (see Placer#new_element)
63
70
  def new_element(tree)
64
- index = tree.data.index(&@matcher)
71
+ index = tree.all_data.index(&@matcher)
65
72
 
66
- res = {}
73
+ res = create_element
67
74
  if index
68
- tree.data.insert(index + 1, res)
75
+ tree.all_data.insert(index + 1, res)
69
76
  else
70
- tree.data << res
77
+ tree.all_data << res
71
78
  end
79
+
72
80
  res
73
81
  end
74
82
  end
@@ -86,13 +94,16 @@ module CFA
86
94
 
87
95
  # (see Placer#new_element)
88
96
  def new_element(tree)
89
- index = tree.data.index(&@matcher)
90
- res = {}
97
+ index = tree.all_data.index(&@matcher)
98
+ res = create_element
91
99
 
92
100
  if index
93
- tree.data[index] = res
101
+ # remove old one and add new one, as it can have different key
102
+ # which cause problem to simple modify
103
+ tree.all_data[index][:operation] = :remove
104
+ tree.all_data.insert(index + 1, res)
94
105
  else
95
- tree.data << res
106
+ tree.all_data << res
96
107
  end
97
108
 
98
109
  res
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cfa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josef Reidinger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-01-03 00:00:00.000000000 Z
11
+ date: 2017-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-augeas
@@ -34,6 +34,9 @@ extensions: []
34
34
  extra_rdoc_files: []
35
35
  files:
36
36
  - lib/cfa/augeas_parser.rb
37
+ - lib/cfa/augeas_parser/keys_cache.rb
38
+ - lib/cfa/augeas_parser/reader.rb
39
+ - lib/cfa/augeas_parser/writer.rb
37
40
  - lib/cfa/base_model.rb
38
41
  - lib/cfa/matcher.rb
39
42
  - lib/cfa/memory_file.rb
@@ -58,9 +61,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
58
61
  version: 1.3.6
59
62
  requirements: []
60
63
  rubyforge_project:
61
- rubygems_version: 2.4.5.1
64
+ rubygems_version: 2.2.2
62
65
  signing_key:
63
66
  specification_version: 4
64
67
  summary: CFA (Config Files API) provides an easy way to create models on top of configuration
65
68
  files
66
69
  test_files: []
70
+ has_rdoc: