cfa 0.4.1 → 0.4.2

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: 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