cfa 0.4.1 → 0.4.2

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: 462483ae4713b64048bae7313e741675c32e14ce
4
- data.tar.gz: 3ae2dffb60221cf96ba4e7d09d22c558dbbf7ab9
3
+ metadata.gz: 2a5ddfd0e34f669de02849ae3fcbc4ca3dafb664
4
+ data.tar.gz: b4d6d350b7a072b7168a9250454115c206630e53
5
5
  SHA512:
6
- metadata.gz: 566cd667bcca679f1ce4e95105f8293f042822f9da63ad10063b2e717a6dca90dc57e20796f43332b9eadb27a4c3566ae4f412cf1203fd0bb0e3c0e92c6759d7
7
- data.tar.gz: 26207c7926c12a48f003ca46366e5a3f58931852b872b01bf2620fcbb47c6546ce4534cc33f3db0af3edf7725772eac15f5f4517fbee60af34fe0b554224b2b3
6
+ metadata.gz: 265417fcbac259f04e42331f29918ebf4e575bc5e8f58d564af944355d12fc4dfd271bae09acb8213fa9cf6b5782189090d1e5f672d39d4b3687d025de9d43d1
7
+ data.tar.gz: 6862d7929f54bdf1673dd6f3721481fe7953a7a960b00ddc905254bd5dec4d608eec94ac36f388c959228e13fbe4876267eda2b5df3f661da7fb546262aca120
@@ -3,6 +3,25 @@ require "forwardable"
3
3
  require "cfa/placer"
4
4
 
5
5
  module CFA
6
+ # A building block for {AugeasTree}.
7
+ #
8
+ # Intuitively the tree is made of hashes where keys may be duplicated,
9
+ # so it is implemented as a sequence of hashes with two keys, :key and :value.
10
+ #
11
+ # A `:key` is a String.
12
+ # The key may have a collection suffix "[]". Note that in contrast
13
+ # with the underlying {::Augeas} library, an integer index is not present
14
+ # (which should make it easier to modify collections of elements).
15
+ #
16
+ # A `:value` is either a String, or an {AugeasTree},
17
+ # or an {AugeasTreeValue} (which combines both).
18
+ #
19
+ # @return [Hash{Symbol => String, AugeasTree}]
20
+ #
21
+ # @todo Unify naming: entry, element
22
+ class AugeasElement < Hash
23
+ end
24
+
6
25
  # Represents list of same config options in augeas.
7
26
  # For example comments are often stored in collections.
8
27
  class AugeasCollection
@@ -19,6 +38,7 @@ module CFA
19
38
  element = placer.new_element(@tree)
20
39
  element[:key] = augeas_name
21
40
  element[:value] = value
41
+ # FIXME: load_collection missing here
22
42
  end
23
43
 
24
44
  def delete(value)
@@ -47,12 +67,12 @@ module CFA
47
67
  end
48
68
  end
49
69
 
50
- # Represents node that contain value and also subtree below it
51
- # For easier traversing it pass #[] to subtree
70
+ # Represents a node that contains both a value and a subtree below it.
71
+ # For easier traversal it forwards `#[]` to the subtree.
52
72
  class AugeasTreeValue
53
- # value in node
73
+ # @return [String] the value in the node
54
74
  attr_accessor :value
55
- # subtree below node
75
+ # @return [AugeasTree] the subtree below the node
56
76
  attr_accessor :tree
57
77
 
58
78
  def initialize(tree, value)
@@ -60,32 +80,58 @@ module CFA
60
80
  @value = value
61
81
  end
62
82
 
63
- def [](value)
64
- tree[value]
83
+ # (see AugeasTree#[])
84
+ def [](key)
85
+ tree[key]
86
+ end
87
+
88
+ def ==(other)
89
+ [:class, :value, :tree].all? do |a|
90
+ public_send(a) == other.public_send(a)
91
+ end
65
92
  end
93
+
94
+ # For objects of class Object, eql? is synonymous with ==:
95
+ # http://ruby-doc.org/core-2.3.3/Object.html#method-i-eql-3F
96
+ alias_method :eql?, :==
66
97
  end
67
98
 
68
- # Represent parsed augeas config tree with user friendly methods
99
+ # Represents a parsed Augeas config tree with user friendly methods
69
100
  class AugeasTree
70
- # low level access to augeas structure
101
+ # Low level access to Augeas structure
102
+ #
103
+ # An ordered mapping, represented by an Array of Hashes
104
+ # with the keys :key and :value.
105
+ #
106
+ # @see AugeasElement
107
+ #
108
+ # @return [Array<Hash{Symbol => String, AugeasTree}>]
71
109
  attr_reader :data
72
110
 
73
111
  def initialize
74
112
  @data = []
75
113
  end
76
114
 
115
+ # @return [AugeasCollection] collection for *key*
77
116
  def collection(key)
78
117
  AugeasCollection.new(self, key)
79
118
  end
80
119
 
81
- def delete(key)
82
- @data.reject! { |entry| entry[:key] == key }
120
+ # @param [String, Matcher]
121
+ def delete(matcher)
122
+ unless matcher.is_a?(CFA::Matcher)
123
+ matcher = CFA::Matcher.new(key: matcher)
124
+ end
125
+ @data.reject!(&matcher)
83
126
  end
84
127
 
85
- # adds the given value for the key in tree.
86
- # @param value can be value of node, {AugeasTree}
87
- # attached to key or its combination as {AugeasTreeValue}
88
- # @param placer object determining where to insert value in tree.
128
+ # Adds the given *value* for *key* in the tree.
129
+ #
130
+ # By default an AppendPlacer is used which produces duplicate keys
131
+ # but ReplacePlacer can be used to replace the *first* duplicate.
132
+ # @param key [String]
133
+ # @param value [String,AugeasTree,AugeasTreeValue]
134
+ # @param placer [Placer] determines where to insert value in tree.
89
135
  # Useful e.g. to specify order of keys or placing comment above of given
90
136
  # key.
91
137
  def add(key, value, placer = AppendPlacer.new)
@@ -94,10 +140,10 @@ module CFA
94
140
  element[:value] = value
95
141
  end
96
142
 
97
- # finds given value in tree.
98
- # @return It can return value of node, {AugeasTree}
99
- # attached to key or its combination as {AugeasTreeValue}.
100
- # Also nil can be returned if key not found.
143
+ # Finds given *key* in tree.
144
+ # @param key [String]
145
+ # @return [String,AugeasTree,AugeasTreeValue,nil] the first value for *key*,
146
+ # or `nil` if not found
101
147
  def [](key)
102
148
  entry = @data.find { |d| d[:key] == key }
103
149
  return entry[:value] if entry
@@ -105,8 +151,10 @@ module CFA
105
151
  nil
106
152
  end
107
153
 
108
- # Sets the given value for the key in tree. It can be value of node,
109
- # {AugeasTree} attached to key or its combination as {AugeasTreeValue}
154
+ # Replace the first value for *key* with *value*.
155
+ # Append a new element if *key* did not exist.
156
+ # @param key [String]
157
+ # @param value [String, AugeasTree, AugeasTreeValue]
110
158
  def []=(key, value)
111
159
  entry = @data.find { |d| d[:key] == key }
112
160
  if entry
@@ -119,12 +167,20 @@ module CFA
119
167
  end
120
168
  end
121
169
 
170
+ # @param matcher [Matcher]
171
+ # @return [Array<AugeasElement>] matching elements
122
172
  def select(matcher)
123
173
  @data.select(&matcher)
124
174
  end
125
175
 
126
176
  # @note for internal usage only
127
- # @private
177
+ # @api private
178
+ #
179
+ # Initializes {#data} from *prefix* in *aug*.
180
+ # @param aug [::Augeas]
181
+ # @param prefix [String] Augeas path prefix
182
+ # @param keys_cache [AugeasKeysCache]
183
+ # @return [void]
128
184
  def load_from_augeas(aug, prefix, keys_cache)
129
185
  @data = keys_cache.keys_for_prefix(prefix).map do |key|
130
186
  aug_key = prefix + "/" + key
@@ -136,7 +192,12 @@ module CFA
136
192
  end
137
193
 
138
194
  # @note for internal usage only
139
- # @private
195
+ # @api private
196
+ #
197
+ # Saves {#data} to *prefix* in *aug*.
198
+ # @param aug [::Augeas]
199
+ # @param prefix [String] Augeas path prefix
200
+ # @return [void]
140
201
  def save_to_augeas(aug, prefix)
141
202
  arrays = {}
142
203
 
@@ -145,6 +206,14 @@ module CFA
145
206
  end
146
207
  end
147
208
 
209
+ def ==(other)
210
+ [:class, :data].all? { |a| public_send(a) == other.public_send(a) }
211
+ end
212
+
213
+ # For objects of class Object, eql? is synonymous with ==:
214
+ # http://ruby-doc.org/core-2.3.3/Object.html#method-i-eql-3F
215
+ alias_method :eql?, :==
216
+
148
217
  private
149
218
 
150
219
  def save_entry(key, value, arrays, aug, prefix)
@@ -199,20 +268,22 @@ module CFA
199
268
  end
200
269
 
201
270
  # @example read, print, modify and serialize again
202
- # require "config_files/augeas_parser"
271
+ # require "cfa/augeas_parser"
203
272
  #
204
- # parser = CFA::AugeasParser.new("sysconfig.lns")
273
+ # parser = CFA::AugeasParser.new("Sysconfig.lns")
205
274
  # data = parser.parse(File.read("/etc/default/grub"))
206
275
  #
207
276
  # puts data["GRUB_DISABLE_OS_PROBER"]
208
277
  # data["GRUB_DISABLE_OS_PROBER"] = "true"
209
278
  # puts parser.serialize(data)
210
279
  class AugeasParser
280
+ # @param lens [String] a lens name, like "Sysconfig.lns"
211
281
  def initialize(lens)
212
282
  @lens = lens
213
283
  end
214
284
 
215
- # parses given string and returns AugeasTree instance
285
+ # @param raw_string [String] a string to be parsed
286
+ # @return [AugeasTree] the parsed data
216
287
  def parse(raw_string)
217
288
  @old_content = raw_string
218
289
 
@@ -232,7 +303,8 @@ module CFA
232
303
  end
233
304
  end
234
305
 
235
- # Serializes AugeasTree instance into returned string
306
+ # @param data [AugeasTree] the data to be serialized
307
+ # @return [String] a string to be written
236
308
  def serialize(data)
237
309
  # open augeas without any autoloading and it should not touch disk and
238
310
  # load lenses as needed only
@@ -248,13 +320,15 @@ module CFA
248
320
  end
249
321
  end
250
322
 
251
- # Returns empty tree that can be filled for future serialization
323
+ # @return [AugeasTree] an empty tree that can be filled
324
+ # for future serialization
252
325
  def empty
253
326
  AugeasTree.new
254
327
  end
255
328
 
256
329
  private
257
330
 
331
+ # @param aug [::Augeas]
258
332
  def report_error(aug)
259
333
  error = aug.error
260
334
  # zero is no error, so problem in lense
@@ -267,53 +341,44 @@ module CFA
267
341
  raise "Augeas parsing/serializing error: #{msg} at #{location}"
268
342
  end
269
343
  end
270
- end
271
344
 
272
- # Cache that holds all avaiable keys in augeas tree. It is used to
273
- # prevent too many aug.match calls which are expensive.
274
- class AugeasKeysCache
275
- STORE_PREFIX = "/store".freeze
276
- STORE_LEN = STORE_PREFIX.size
277
- STORE_LEN_1 = STORE_LEN + 1
345
+ # Cache that holds all avaiable keys in augeas tree. It is used to
346
+ # prevent too many aug.match calls which are expensive.
347
+ class AugeasKeysCache
348
+ STORE_PREFIX = "/store".freeze
278
349
 
279
- # initialize cache from passed augeas object
280
- def initialize(aug)
281
- fill_cache(aug)
282
- end
283
-
284
- # returns list of keys available on given prefix
285
- def keys_for_prefix(prefix)
286
- cut = prefix.length > STORE_LEN ? STORE_LEN_1 : STORE_LEN
287
- path = prefix[cut..-1]
288
- path = path.split("/")
289
- matches = path.reduce(@cache) { |a, e| a[e] }
350
+ # initialize cache from passed augeas object
351
+ def initialize(aug)
352
+ fill_cache(aug)
353
+ end
290
354
 
291
- matches.keys
292
- end
355
+ # returns list of keys available on given prefix
356
+ def keys_for_prefix(prefix)
357
+ @cache[prefix] || []
358
+ end
293
359
 
294
- private
360
+ private
295
361
 
296
- def fill_cache(aug)
297
- @cache = {}
298
- search_path = "#{STORE_PREFIX}/*"
299
- loop do
300
- matches = aug.match(search_path)
301
- break if matches.empty?
302
- assign_matches(matches, @cache)
362
+ def fill_cache(aug)
363
+ @cache = {}
364
+ search_path = "#{STORE_PREFIX}/*"
365
+ loop do
366
+ matches = aug.match(search_path)
367
+ break if matches.empty?
368
+ assign_matches(matches, @cache)
303
369
 
304
- search_path += "/*"
370
+ search_path += "/*"
371
+ end
305
372
  end
306
- end
307
373
 
308
- def assign_matches(matches, cache)
309
- matches.each do |match|
310
- path = match[STORE_LEN_1..-1].split("/")
311
- leap = path.pop
312
- target = path.reduce(cache) do |acc, p|
313
- acc[p]
374
+ def assign_matches(matches, cache)
375
+ matches.each do |match|
376
+ split_index = match.rindex("/")
377
+ prefix = match[0..(split_index - 1)]
378
+ key = match[(split_index + 1)..-1]
379
+ cache[prefix] ||= []
380
+ cache[prefix] << key
314
381
  end
315
-
316
- target[leap] = {}
317
382
  end
318
383
  end
319
384
  end
@@ -22,11 +22,28 @@ module CFA
22
22
  self.data = parser.empty
23
23
  end
24
24
 
25
+ # Serializes *data* using *parser*
26
+ # and writes the resulting String using *file_handler*.
27
+ # @return [void]
28
+ # @raise a *file_handler* specific error if *file_path* cannot be written
29
+ # e.g. due to missing permissions or living on a read only device.
30
+ # @raise a *parser* specific error. If *data* contain invalid values
31
+ # then *parser* may raise an error.
32
+ # A properly written BaseModel subclass should prevent that by preventing
33
+ # insertion of such values in the first place.
25
34
  def save(changes_only: false)
26
35
  merge_changes if changes_only
27
36
  @file_handler.write(@file_path, @parser.serialize(data))
28
37
  end
29
38
 
39
+ # Reads a String using *file_handler*
40
+ # and parses it with *parser*, storing the result in *data*.
41
+ # @return [void]
42
+ # @raise a *file_handler* specific error. If *file_path* does not exist
43
+ # or permission is not sufficient it may raise an error
44
+ # depending on the used file handler.
45
+ # @raise a *parser* specific error. If the parsed String is malformed, then
46
+ # depending on the used parser it may raise an error.
30
47
  def load
31
48
  self.data = @parser.parse(@file_handler.read(@file_path))
32
49
  @loaded = true
@@ -65,21 +82,31 @@ module CFA
65
82
  @default_file_handler = value
66
83
  end
67
84
 
68
- protected
69
-
70
- # generates accessors for trivial key-value attributes
85
+ # Generates accessors for trivial key-value attributes
86
+ # @param attrs [Hash{Symbol => String}] mapping of methods to file keys
87
+ #
88
+ # @example Usage
89
+ # class FooModel < CFA::BaseModel
90
+ # attributes(
91
+ # server: "server",
92
+ # read_timeout: "ReadTimeout",
93
+ # write_timeout: "WriteTimeout"
94
+ # )
95
+ # ...
96
+ # end
71
97
  def self.attributes(attrs)
72
- attrs.each_pair do |key, value|
73
- define_method(key) do
74
- generic_get(value)
98
+ attrs.each_pair do |method_name, key|
99
+ define_method(method_name) do
100
+ generic_get(key)
75
101
  end
76
102
 
77
- define_method(:"#{key.to_s}=") do |target|
78
- generic_set(value, target)
103
+ define_method(:"#{method_name.to_s}=") do |value|
104
+ generic_set(key, value)
79
105
  end
80
106
  end
81
107
  end
82
- private_class_method :attributes
108
+
109
+ protected
83
110
 
84
111
  attr_accessor :data
85
112
 
@@ -90,6 +117,9 @@ module CFA
90
117
  data.merge(new_data)
91
118
  end
92
119
 
120
+ # Modify an **existing** entry and return `true`,
121
+ # or do nothing and return `false`.
122
+ # @return [Boolean]
93
123
  def modify(key, value)
94
124
  # if already set, just change value
95
125
  return false unless data[key]
@@ -98,14 +128,20 @@ module CFA
98
128
  true
99
129
  end
100
130
 
131
+ # Replace a commented out entry and return `true`,
132
+ # or do nothing and return `false`.
133
+ # @return [Boolean]
101
134
  def uncomment(key, value)
102
135
  # Try to find if it is commented out, so we can replace line
103
136
  matcher = Matcher.new(
104
137
  collection: "#comment",
138
+ # FIXME: this assumes a specific "=" syntax, bypassing the lens
139
+ # FIXME: this will match also "# If you set FOO=bar then..."
105
140
  value_matcher: /(\s|^)#{key}\s*=/
106
141
  )
107
142
  return false unless data.data.any?(&matcher)
108
143
 
144
+ # FIXME: this assumes that *data* is an AugeasTree
109
145
  data.add(key, value, ReplacePlacer.new(matcher))
110
146
  true
111
147
  end
@@ -1,9 +1,42 @@
1
1
  module CFA
2
- # Class used to create matcher, that allows to find specific option in augeas
3
- # tree or subtree
4
- # TODO: examples of usage
2
+ # The Matcher is used as a predicate on {AugeasElement}.
3
+ #
4
+ # Being a predicate, it is passed to methods such as Enumerable#select
5
+ # or Array#index, returning a Boolean meaning whether a match was found.
6
+ #
7
+ # Acting on {AugeasElement} means it expects a Hash `e`
8
+ # containing `e[:key]` and `e[:value]`.
9
+ #
10
+ # It is used with the `&` syntax which makes the matcher
11
+ # act like a block/lambda/Proc (via {Matcher#to_proc}).
12
+ #
13
+ # @note The coupling to {AugeasTree}, {AugeasElement} is not a goal.
14
+ # Once we have more parsers it will go away.
15
+ #
16
+ # @example
17
+ # elements = [
18
+ # {key: "#comment[]", value: "\"magical\" mostly works"},
19
+ # {key: "DRIVE", value: "magical"},
20
+ # {key: "#comment[]", value: "'years' or 'centuries'"},
21
+ # {key: "PRECISION", value: "years"}
22
+ # ]
23
+ # drive_matcher = Matcher.new(key: "DRIVE")
24
+ # i = elements.index(&drive_matcher) # => 1
5
25
  class Matcher
6
- # @block_yield matcher based on block. block gets two params, key and value
26
+ # The constructor arguments are constraints to match on an element.
27
+ # All constraints are optional.
28
+ # All supplied constraints must match, so it is a conjunction.
29
+ # @param key [Object,nil] if non-nil,
30
+ # constrain to elements with the name "*key*"
31
+ # @param collection [Object,nil] if non-nil,
32
+ # constrain to elements with the name "*collection*[]"
33
+ # @param value_matcher [Object,Regexp,nil] if non-nil,
34
+ # constrain to elements whose value is Object or matches Regexp
35
+ # @yieldparam blk_key [Object]
36
+ # @yieldparam blk_value [Object]
37
+ # @yieldreturn [Boolean] if the block is present,
38
+ # constrain to elements for which the block(*blk_key*, *blk_value*)
39
+ # returns true
7
40
  def initialize(key: nil, collection: nil, value_matcher: nil, &block)
8
41
  @matcher = lambda do |element|
9
42
  return false unless key_match?(element, key)
@@ -14,10 +47,13 @@ module CFA
14
47
  end
15
48
  end
16
49
 
50
+ # @return [Proc{AugeasElement=>Boolean}]
17
51
  def to_proc
18
52
  @matcher
19
53
  end
20
54
 
55
+ private
56
+
21
57
  def key_match?(element, key)
22
58
  return true unless key
23
59
 
@@ -1,6 +1,21 @@
1
1
  module CFA
2
- # allows to place element at the end of configuration. Default one.
3
- class AppendPlacer
2
+ # Places a new {AugeasElement} into an {AugeasTree}.
3
+ # @abstract Subclasses implement different ways **where**
4
+ # to place the entry by overriding {#new_element}.
5
+ class Placer
6
+ # @param [AugeasTree] tree
7
+ # @return [AugeasElement,Hash] the new element; it is empty!
8
+ # Note that the return value is actually a Hash; {AugeasElement}
9
+ # documents its structure.
10
+ def new_element(_tree)
11
+ raise NotImplementedError,
12
+ "Subclasses of #{Module.nesting.first} must override #{__method__}"
13
+ end
14
+ end
15
+
16
+ # Places the new element at the end of the tree.
17
+ class AppendPlacer < Placer
18
+ # (see Placer#new_element)
4
19
  def new_element(tree)
5
20
  res = {}
6
21
  tree.data << res
@@ -9,14 +24,18 @@ module CFA
9
24
  end
10
25
  end
11
26
 
12
- # Specialized placer, that allows to place config value before found one.
13
- # If noone is found, then append to the end
14
- # Useful, when config option should be inserted to specific location.
15
- class BeforePlacer
27
+ # Finds a specific element using a {Matcher} and places the new element
28
+ # **before** it. Appends at the end if a match is not found.
29
+ #
30
+ # Useful when a config option should be inserted to a specific location,
31
+ # or when assigning a comment to an option.
32
+ class BeforePlacer < Placer
33
+ # @param [Matcher] matcher
16
34
  def initialize(matcher)
17
35
  @matcher = matcher
18
36
  end
19
37
 
38
+ # (see Placer#new_element)
20
39
  def new_element(tree)
21
40
  index = tree.data.index(&@matcher)
22
41
 
@@ -30,14 +49,17 @@ module CFA
30
49
  end
31
50
  end
32
51
 
33
- # Specialized placer, that allows to place config value after found one.
34
- # If noone is found, then append to the end
35
- # Useful, when config option should be inserted to specific location.
36
- class AfterPlacer
52
+ # Finds a specific element using a {Matcher} and places the new element
53
+ # **after** it. Appends at the end if a match is not found.
54
+ #
55
+ # Useful when a config option should be inserted to a specific location.
56
+ class AfterPlacer < Placer
57
+ # @param [Matcher] matcher
37
58
  def initialize(matcher)
38
59
  @matcher = matcher
39
60
  end
40
61
 
62
+ # (see Placer#new_element)
41
63
  def new_element(tree)
42
64
  index = tree.data.index(&@matcher)
43
65
 
@@ -51,15 +73,18 @@ module CFA
51
73
  end
52
74
  end
53
75
 
54
- # Specialized placer, that allows to place config value instead of found one.
55
- # If noone is found, then append to the end
56
- # Useful, when value already exists and detected by matcher. Then easy add
57
- # with this placer replace it carefully to correct location.
58
- class ReplacePlacer
76
+ # Finds a specific element using a {Matcher} and **replaces** it
77
+ # with the new element. Appends at the end if a match is not found.
78
+ #
79
+ # Useful in key-value configuration files where a specific key
80
+ # needs to be assigned.
81
+ class ReplacePlacer < Placer
82
+ # @param [Matcher] matcher
59
83
  def initialize(matcher)
60
84
  @matcher = matcher
61
85
  end
62
86
 
87
+ # (see Placer#new_element)
63
88
  def new_element(tree)
64
89
  index = tree.data.index(&@matcher)
65
90
  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.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josef Reidinger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-10-12 00:00:00.000000000 Z
11
+ date: 2016-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-augeas