cfa 0.1.0

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