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 +7 -0
- data/lib/config_files_api/augeas_parser.rb +235 -0
- data/lib/config_files_api/base_model.rb +150 -0
- data/lib/config_files_api/matcher.rb +42 -0
- data/lib/config_files_api/memory_file.rb +20 -0
- data/lib/config_files_api/placer.rb +76 -0
- metadata +66 -0
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:
|