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