cfa 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b0bc2e08acdb945ee444cd4cc4161f8486269624
4
+ data.tar.gz: 63f48a1e6955ed2eaf9ab2a2a7e349fbc599e181
5
+ SHA512:
6
+ metadata.gz: 3d378441e8342f71da3787af6f8c4addcad952421b60928546eb9c7c455455cfb482cc9ae398f219c8d9456fefd20d1c79465d107f84676fb458c66245d1b948
7
+ data.tar.gz: 187f9e0e23a30f3b952aed40f028d193d759581ea7801f3b945218da964f90a4ef2f42e669507c6ebce8c4a9f015b2e07a506622869f38e7d13faf2e0eaae3bf
@@ -0,0 +1,235 @@
1
+ require "augeas"
2
+ require "forwardable"
3
+ require "config_files_api/placer"
4
+
5
+ module ConfigFilesApi
6
+ # Represents list of same config options in augeas.
7
+ # For example comments are often stored in collections.
8
+ class AugeasCollection
9
+ extend Forwardable
10
+ def initialize(tree, name)
11
+ @tree = tree
12
+ @name = name
13
+ load_collection
14
+ end
15
+
16
+ def_delegators :@collection, :[], :empty?, :each, :map, :any?, :all?, :none?
17
+
18
+ def add(value, placer = AppendPlacer.new)
19
+ element = placer.new_element(@tree)
20
+ element[:key] = augeas_name
21
+ element[:value] = value
22
+ end
23
+
24
+ def delete(value)
25
+ key = augeas_name
26
+ @tree.data.reject! do |entry|
27
+ entry[:key] == key &&
28
+ if value.is_a?(Regexp)
29
+ value =~ entry[:value]
30
+ else
31
+ value == entry[:value]
32
+ end
33
+ end
34
+
35
+ load_collection
36
+ end
37
+
38
+ private
39
+
40
+ def load_collection
41
+ entries = @tree.data.select { |d| d[:key] == augeas_name }
42
+ @collection = entries.map { |e| e[:value] }.freeze
43
+ end
44
+
45
+ def augeas_name
46
+ @name + "[]"
47
+ end
48
+ end
49
+
50
+ # Represent parsed augeas config tree with user friendly methods
51
+ class AugeasTree
52
+ # low level access to augeas structure
53
+ attr_reader :data
54
+
55
+ def initialize
56
+ @data = []
57
+ end
58
+
59
+ def collection(key)
60
+ AugeasCollection.new(self, key)
61
+ end
62
+
63
+ def delete(key)
64
+ @data.reject! { |entry| entry[:key] == key }
65
+ end
66
+
67
+ def add(key, value, placer = AppendPlacer.new)
68
+ element = placer.new_element(self)
69
+ element[:key] = key
70
+ element[:value] = value
71
+ end
72
+
73
+ def [](key)
74
+ entry = @data.find { |d| d[:key] == key }
75
+ return entry[:value] if entry
76
+
77
+ nil
78
+ end
79
+
80
+ def []=(key, value)
81
+ entry = @data.find { |d| d[:key] == key }
82
+ if entry
83
+ entry[:value] = value
84
+ else
85
+ @data << {
86
+ key: key,
87
+ value: value
88
+ }
89
+ end
90
+ end
91
+
92
+ def select(matcher)
93
+ @data.select(&matcher)
94
+ end
95
+
96
+ # @note for internal usage only
97
+ # @private
98
+ def load_from_augeas(aug, prefix)
99
+ matches = aug.match("#{prefix}/*")
100
+
101
+ @data = matches.map do |aug_key|
102
+ {
103
+ key: load_key(prefix, aug_key),
104
+ value: load_value(aug, aug_key)
105
+ }
106
+ end
107
+ end
108
+
109
+ # @note for internal usage only
110
+ # @private
111
+ def save_to_augeas(aug, prefix)
112
+ arrays = {}
113
+
114
+ @data.each do |entry|
115
+ aug_key = obtain_aug_key(prefix, entry, arrays)
116
+ if entry[:value].is_a? AugeasTree
117
+ entry[:value].save_to_augeas(aug, aug_key)
118
+ else
119
+ report_error(aug) unless aug.set(aug_key, entry[:value])
120
+ end
121
+ end
122
+ end
123
+
124
+ # @note for debugging purpose only
125
+ def dump_tree(prefix = "")
126
+ arrays = {}
127
+
128
+ @data.each_with_object("") do |entry, res|
129
+ aug_key = obtain_aug_key(prefix, entry, arrays)
130
+ if entry[:value].is_a? AugeasTree
131
+ res << entry[:value].dump_tree(aug_key)
132
+ else
133
+ res << aug_key << "\n"
134
+ end
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ def obtain_aug_key(prefix, entry, arrays)
141
+ key = entry[:key]
142
+ if key.end_with?("[]")
143
+ array_key = key.sub(/\[\]$/, "")
144
+ arrays[array_key] ||= 0
145
+ arrays[array_key] += 1
146
+ key = array_key + "[#{arrays[array_key]}]"
147
+ end
148
+
149
+ "#{prefix}/#{key}"
150
+ end
151
+
152
+ def report_error(aug)
153
+ error = aug.error
154
+ raise "Augeas error #{error[:message]}." \
155
+ "Details: #{error[:details]}."
156
+ end
157
+
158
+ def load_key(prefix, aug_key)
159
+ key = aug_key.sub(/^#{Regexp.escape(prefix)}\//, "")
160
+ key.sub(/\[\d+\]$/, "[]")
161
+ end
162
+
163
+ def load_value(aug, aug_key)
164
+ nested = !aug.match("#{aug_key}/*").empty?
165
+ if nested
166
+ subtree = AugeasTree.new
167
+ subtree.load_from_augeas(aug, aug_key)
168
+ subtree
169
+ else
170
+ aug.get(aug_key)
171
+ end
172
+ end
173
+ end
174
+
175
+ # @example read, print, modify and serialize again
176
+ # require "config_files/augeas_parser"
177
+ #
178
+ # parser = ConfigFilesApi::AugeasParser.new("sysconfig.lns")
179
+ # data = parser.parse(File.read("/etc/default/grub"))
180
+ #
181
+ # puts data["GRUB_DISABLE_OS_PROBER"]
182
+ # data["GRUB_DISABLE_OS_PROBER"] = "true"
183
+ # puts parser.serialize(data)
184
+ class AugeasParser
185
+ def initialize(lens)
186
+ @lens = lens
187
+ end
188
+
189
+ def parse(raw_string)
190
+ @old_content = raw_string
191
+
192
+ # open augeas without any autoloading and it should not touch disk and
193
+ # load lenses as needed only
194
+ root = load_path = nil
195
+ Augeas.open(root, load_path, Augeas::NO_MODL_AUTOLOAD) do |aug|
196
+ aug.set("/input", raw_string)
197
+ report_error(aug) unless aug.text_store(@lens, "/input", "/store")
198
+
199
+ tree = AugeasTree.new
200
+ tree.load_from_augeas(aug, "/store")
201
+
202
+ return tree
203
+ end
204
+ end
205
+
206
+ def serialize(data)
207
+ # open augeas without any autoloading and it should not touch disk and
208
+ # load lenses as needed only
209
+ root = load_path = nil
210
+ Augeas.open(root, load_path, Augeas::NO_MODL_AUTOLOAD) do |aug|
211
+ aug.set("/input", @old_content || "")
212
+ data.save_to_augeas(aug, "/store")
213
+
214
+ res = aug.text_retrieve(@lens, "/input", "/store", "/output")
215
+ report_error(aug) unless res
216
+
217
+ return aug.get("/output")
218
+ end
219
+ end
220
+
221
+ private
222
+
223
+ def report_error(aug)
224
+ error = aug.error
225
+ # zero is no error, so problem in lense
226
+ if aug.error[:code] != 0
227
+ raise "Augeas error #{error[:message]}. Details: #{error[:details]}."
228
+ else
229
+ msg = aug.get("/augeas/text/store/error/message")
230
+ location = aug.get("/augeas/text/store/error/lens")
231
+ raise "Augeas parsing/serializing error: #{msg} at #{location}"
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,150 @@
1
+ module ConfigFilesApi
2
+ # A base class for models. Represents a configuration file as an object
3
+ # with domain-specific attributes/methods. For persistent storage,
4
+ # use load and save,
5
+ # Non-responsibilities: actual storage and parsing (both delegated).
6
+ # There is no caching involved.
7
+ class BaseModel
8
+ # @param parser [.parse, .serialize] parser that can convert object to
9
+ # string and vice versa. It have to provide methods
10
+ # `string #serialize(object)` and `object #parse(string)`.
11
+ # For example see {ConfigFilesApi::AugeasParser}
12
+ # @param file_path [String] expected path passed to file_handler
13
+ # @param file_handler [.read, .write] object, that can read/write string.
14
+ # It have to provide methods `string read(string)` and
15
+ # `write(string, string)`. For example see {ConfigFilesApi::MemoryFile}
16
+ def initialize(parser, file_path, file_handler: File)
17
+ @file_handler = file_handler
18
+ @parser = parser
19
+ @file_path = file_path
20
+ @loaded = false
21
+ end
22
+
23
+ def save(changes_only: false)
24
+ merge_changes if changes_only
25
+ @file_handler.write(@file_path, @parser.serialize(data))
26
+ end
27
+
28
+ def load
29
+ self.data = @parser.parse(@file_handler.read(@file_path))
30
+ @loaded = true
31
+ end
32
+
33
+ # powerfull method that sets any value in config. It try to be
34
+ # smart to at first modify existing value, then replace commented out code
35
+ # and if even that doesn't work, then append it at the end
36
+ # @note prefer to use specialized methods of children
37
+ def generic_set(key, value)
38
+ modify(key, value) || uncomment(key, value) || add_new(key, value)
39
+ end
40
+
41
+ # powerfull method that gets unformatted any value in config.
42
+ # @note prefer to use specialized methods of children
43
+ def generic_get(key)
44
+ data[key]
45
+ end
46
+
47
+ # rubocop:disable Style/TrivialAccessors
48
+ # Returns if configuration was already loaded
49
+ def loaded?
50
+ @loaded
51
+ end
52
+
53
+ protected
54
+
55
+ # generates accessors for trivial key-value attributes
56
+ def self.attributes(attrs)
57
+ attrs.each_pair do |key, value|
58
+ define_method(key) do
59
+ generic_get(value)
60
+ end
61
+
62
+ define_method(:"#{key.to_s}=") do |target|
63
+ generic_set(value, target)
64
+ end
65
+ end
66
+ end
67
+ private_class_method :attributes
68
+
69
+ attr_accessor :data
70
+
71
+ def merge_changes
72
+ new_data = data.dup
73
+ read
74
+ # TODO: recursive merge
75
+ data.merge(new_data)
76
+ end
77
+
78
+ def modify(key, value)
79
+ # if already set, just change value
80
+ return false unless data[key]
81
+
82
+ data[key] = value
83
+ true
84
+ end
85
+
86
+ def uncomment(key, value)
87
+ # Try to find if it is commented out, so we can replace line
88
+ matcher = Matcher.new(
89
+ collection: "#comment",
90
+ value_matcher: /(\s|^)#{key}\s*=/
91
+ )
92
+ return false unless data.data.any?(&matcher)
93
+
94
+ data.add(key, value, ReplacePlacer.new(matcher))
95
+ true
96
+ end
97
+
98
+ def add_new(key, value)
99
+ data.add(key, value)
100
+ end
101
+ end
102
+
103
+ # Representing boolean value switcher in default grub configuration file.
104
+ # Allows easy switching and questioning for boolean value, even if
105
+ # represented by text in config file
106
+ class BooleanValue
107
+ def initialize(name, model, true_value: "true", false_value: "false")
108
+ @name = name
109
+ @model = model
110
+ @true_value = true_value
111
+ @false_value = false_value
112
+ end
113
+
114
+ def enable
115
+ @model.generic_set(@name, @true_value)
116
+ end
117
+
118
+ def disable
119
+ @model.generic_set(@name, @false_value)
120
+ end
121
+
122
+ def enabled?
123
+ return nil unless data
124
+
125
+ data == @true_value
126
+ end
127
+
128
+ def disabled?
129
+ return nil unless data
130
+
131
+ data != @true_value
132
+ end
133
+
134
+ def defined?
135
+ !data.nil?
136
+ end
137
+
138
+ # sets boolean value, recommend to use for generic boolean setter.
139
+ # for constants prefer to use enable/disable
140
+ def value=(value)
141
+ @model.generic_set(@name, value ? @true_value : @false_value)
142
+ end
143
+
144
+ private
145
+
146
+ def data
147
+ @model.generic_get(@name)
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,42 @@
1
+ module ConfigFilesApi
2
+ # Class used to create matcher, that allows to find specific option in augeas
3
+ # tree or subtree
4
+ # TODO: examples of usage
5
+ class Matcher
6
+ def initialize(key: nil, collection: nil, value_matcher: nil)
7
+ @matcher = lambda do |element|
8
+ return false unless key_match?(element, key)
9
+ return false unless collection_match?(element, collection)
10
+ return false unless value_match?(element, value_matcher)
11
+ return true
12
+ end
13
+ end
14
+
15
+ def to_proc
16
+ @matcher
17
+ end
18
+
19
+ def key_match?(element, key)
20
+ return true unless key
21
+
22
+ element[:key] == key
23
+ end
24
+
25
+ def collection_match?(element, collection)
26
+ return true unless collection
27
+
28
+ element[:key] == (collection + "[]")
29
+ end
30
+
31
+ def value_match?(element, matcher)
32
+ case matcher
33
+ when nil then true
34
+ when Regexp
35
+ return false unless element[:value].is_a?(String)
36
+ matcher =~ element[:value]
37
+ else
38
+ matcher == element[:value]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,20 @@
1
+ module ConfigFilesApi
2
+ # memory file is used when string is stored only in memory.
3
+ # Useful for testing. For remote read or socket read, own File class
4
+ # creation is recommended.
5
+ class MemoryFile
6
+ attr_accessor :content
7
+
8
+ def initialize(content = "")
9
+ @content = content
10
+ end
11
+
12
+ def read(_path)
13
+ @content.dup
14
+ end
15
+
16
+ def write(_path, content)
17
+ @content = content
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,76 @@
1
+ module ConfigFilesApi
2
+ # allows to place element at the end of configuration. Default one.
3
+ class AppendPlacer
4
+ def new_element(tree)
5
+ res = {}
6
+ tree.data << res
7
+
8
+ res
9
+ end
10
+ end
11
+
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
16
+ def initialize(matcher)
17
+ @matcher = matcher
18
+ end
19
+
20
+ def new_element(tree)
21
+ index = tree.data.index(&@matcher)
22
+
23
+ res = {}
24
+ if index
25
+ tree.data.insert(index, res)
26
+ else
27
+ tree.data << res
28
+ end
29
+ res
30
+ end
31
+ end
32
+
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
37
+ def initialize(matcher)
38
+ @matcher = matcher
39
+ end
40
+
41
+ def new_element(tree)
42
+ index = tree.data.index(&@matcher)
43
+
44
+ res = {}
45
+ if index
46
+ tree.data.insert(index + 1, res)
47
+ else
48
+ tree.data << res
49
+ end
50
+ res
51
+ end
52
+ end
53
+
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
59
+ def initialize(matcher)
60
+ @matcher = matcher
61
+ end
62
+
63
+ def new_element(tree)
64
+ index = tree.data.index(&@matcher)
65
+ res = {}
66
+
67
+ if index
68
+ tree.data[index] = res
69
+ else
70
+ tree.data << res
71
+ end
72
+
73
+ res
74
+ end
75
+ end
76
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cfa
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Josef Reidinger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ruby-augeas
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Library offering separation of parsing and file access from the rest
28
+ of the logic for managing configuraton files. It has built-in support for parsing
29
+ using augeas lenses and also for working with files directly in memory.
30
+ email:
31
+ - jreidinger@suse.cz
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - lib/config_files_api/augeas_parser.rb
37
+ - lib/config_files_api/base_model.rb
38
+ - lib/config_files_api/matcher.rb
39
+ - lib/config_files_api/memory_file.rb
40
+ - lib/config_files_api/placer.rb
41
+ homepage: http://github.com/config-files-api/config_files_api
42
+ licenses: []
43
+ metadata: {}
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 1.3.6
58
+ requirements: []
59
+ rubyforge_project:
60
+ rubygems_version: 2.4.5.1
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: CFA (Config Files API) provides easy way to create model on top of configuration
64
+ file
65
+ test_files: []
66
+ has_rdoc: