cfa 0.4.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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: