jac 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/jac.rb +3 -0
- data/lib/jac/configuration.rb +407 -0
- data/lib/jac/merger.rb +63 -0
- data/lib/jac/parser.rb +40 -0
- data/lib/version.rb +14 -0
- metadata +62 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 85117aeca4175be823c52ff25aa33e0ebaf6efdf
|
4
|
+
data.tar.gz: b8c98ee89f3a3818cfa906f26e8ac3189282fd18
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7f9f97b2551ec8c5ec763673b615cfba497e434e4a3004e396cf106bd080c53398fbea623f3051318097fd419265a3c601e054abf805029b30250780c65b9ce3
|
7
|
+
data.tar.gz: 4f26c83218fffd715448ca87ba96e39d99a17efe702bde663e9aa2c3fb6f3cae20878177dbcac0c3ad97dbace423b57d8ff88a8f36bfb0dae6cbc738f9d3d377
|
data/lib/jac.rb
ADDED
@@ -0,0 +1,407 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
module Jac
|
5
|
+
# Configuration is loaded from well formed YAML streams.
|
6
|
+
# Each document expected to be key-value mapping where
|
7
|
+
# keys a `profile` names and values is a profile content.
|
8
|
+
# Profile itself is key-value mapping too. Except reserved
|
9
|
+
# key names (i.e. `extends`) each key in profile is a
|
10
|
+
# configuration field. For example following yaml document
|
11
|
+
#
|
12
|
+
# ```
|
13
|
+
#
|
14
|
+
# foo:
|
15
|
+
# bar: 42
|
16
|
+
# qoo:
|
17
|
+
# bar: 32
|
18
|
+
#
|
19
|
+
# ```
|
20
|
+
#
|
21
|
+
# represents description of two profiles, `foo` and `qoo`,
|
22
|
+
# where field `bar` is set to `42` and `32` respectively.
|
23
|
+
#
|
24
|
+
# Profile can be constructed using combination of other profiles
|
25
|
+
# for example having `debug` and `release` profiles for testing
|
26
|
+
# and production. And having `remote` and `local` profiles for
|
27
|
+
# building on local or remote machine. We cant get `debug,local`,
|
28
|
+
# `debug,remote`, `release,local` and `release,remote` profiles.
|
29
|
+
# Each of such profiles is a result of merging values of listed
|
30
|
+
# profiles. When merging profile with another configuration
|
31
|
+
# resolver overwrites existing fields. For example if `debug`
|
32
|
+
# and `local` for some reason have same field. In profile
|
33
|
+
# `debug,local` value from `debug` profile will be overwritten
|
34
|
+
# with value from `local` profile.
|
35
|
+
#
|
36
|
+
# ## Extending profiles
|
37
|
+
#
|
38
|
+
# One profile can `extend` another. Or any amount of other
|
39
|
+
# profiles. To specify this one should use `extends` field
|
40
|
+
# like that
|
41
|
+
#
|
42
|
+
# ```
|
43
|
+
#
|
44
|
+
# base:
|
45
|
+
# application_name: my-awesome-app
|
46
|
+
# port: 80
|
47
|
+
#
|
48
|
+
# version:
|
49
|
+
# version_name: 0.0.0
|
50
|
+
# version_code: 42
|
51
|
+
#
|
52
|
+
# debug:
|
53
|
+
# extends: [base, version]
|
54
|
+
# port: 9292
|
55
|
+
# ```
|
56
|
+
#
|
57
|
+
# In this example `debug` will receive following fields:
|
58
|
+
#
|
59
|
+
# ```
|
60
|
+
# application_name: my-awesome-app # from base profile
|
61
|
+
# port: 9292 # from debug profile
|
62
|
+
# version_name: 0.0.0 # from version profile
|
63
|
+
# version_code: 42 # from version profile
|
64
|
+
# ```
|
65
|
+
#
|
66
|
+
# ## Merging multiple configuration files
|
67
|
+
#
|
68
|
+
# Configuration can be loaded from multiple YAML documents.
|
69
|
+
# Before resolve requested profile all described profiles
|
70
|
+
# are merged down together. Having sequence of files like
|
71
|
+
# `.application.yml`, `.application.user.yml` with following content
|
72
|
+
#
|
73
|
+
# ```
|
74
|
+
# # .application.yml
|
75
|
+
# base:
|
76
|
+
# user: deployer
|
77
|
+
#
|
78
|
+
# debug:
|
79
|
+
# extends: base
|
80
|
+
# # ... other values
|
81
|
+
# ```
|
82
|
+
#
|
83
|
+
# ```
|
84
|
+
# # .application.user.yml
|
85
|
+
# base:
|
86
|
+
# user: developer
|
87
|
+
# ```
|
88
|
+
#
|
89
|
+
# We'll get `user` field overwritten with value from
|
90
|
+
# `.application.user.yml`. And only after that construction
|
91
|
+
# of resulting profile will begin (for example `debug`)
|
92
|
+
#
|
93
|
+
# ## String evaluation
|
94
|
+
#
|
95
|
+
# Configuration resolver comes with powerful yet dangerous
|
96
|
+
# feature: it allows evaluate strings as ruby expressions
|
97
|
+
# like this:
|
98
|
+
#
|
99
|
+
# ```
|
100
|
+
# foo:
|
101
|
+
# build_time: "#{Time.now}" # Will be evaluated at configuration resolving step
|
102
|
+
# ```
|
103
|
+
#
|
104
|
+
# Configuration values are available to and can be referenced with `c`:
|
105
|
+
#
|
106
|
+
# ```
|
107
|
+
# base:
|
108
|
+
# application_name: my-awesome-app
|
109
|
+
# debug:
|
110
|
+
# extends: base
|
111
|
+
# server_name: "#{c.application_name}-debug" # yields to my-awesome-app-debug
|
112
|
+
# release:
|
113
|
+
# extends: base
|
114
|
+
# server_name: "#{c.application_name}-release" # yields to my-awesome-app-release
|
115
|
+
# ```
|
116
|
+
#
|
117
|
+
# All strings evaluated **after** profile is constructed thus
|
118
|
+
# you don't need to have declared values in current profile
|
119
|
+
# but be ready to get `nil`.
|
120
|
+
#
|
121
|
+
# ## Merging values
|
122
|
+
#
|
123
|
+
# By default if one value definition overwrites another
|
124
|
+
# ### Merging hashes
|
125
|
+
#
|
126
|
+
#
|
127
|
+
# ### Merging sets
|
128
|
+
#
|
129
|
+
#
|
130
|
+
# ## Generic profiles
|
131
|
+
#
|
132
|
+
# Same result as shown above can be achieved with generic profiles. Generic profile
|
133
|
+
# is a profile which name is regex (i.e. contained in `/.../`):
|
134
|
+
#
|
135
|
+
# ```
|
136
|
+
# base:
|
137
|
+
# application_name: my-awesome-app
|
138
|
+
# /(release|debug)/: # Profile name is a regex, with single capture (1)
|
139
|
+
# extends: base
|
140
|
+
# server_name: "#{c.application_name}-#{c.captures[1]}" # yields my-awesome-app-release or my-awesome-app-debug
|
141
|
+
# ```
|
142
|
+
#
|
143
|
+
# If profile name matches multiple generic profiles it not defined
|
144
|
+
# which profile will be used.
|
145
|
+
module Configuration
|
146
|
+
# Reads and evaluates configuration for given set of streams
|
147
|
+
# and profile
|
148
|
+
class ConfigurationReader
|
149
|
+
# Any configuration set always contains `default` profile
|
150
|
+
# which is loaded when no profile requested.
|
151
|
+
DEFAULT_PROFILE_NAME = 'default'.freeze
|
152
|
+
# Creates "empty" config
|
153
|
+
DEFAULT_CONFIGURATION = -> () { { DEFAULT_PROFILE_NAME => {} } }
|
154
|
+
# Creates configuration reader
|
155
|
+
# @param streams [Array] of pairs containing YAML document and provided
|
156
|
+
# name for this stream
|
157
|
+
attr_reader :merger
|
158
|
+
def initialize(streams)
|
159
|
+
@streams = streams
|
160
|
+
@merger = Merger.new
|
161
|
+
end
|
162
|
+
|
163
|
+
# Parses all streams and resolves requested profile
|
164
|
+
# @param profile [Array] list of profile names to be
|
165
|
+
# merged
|
166
|
+
# @return [OpenStruct] instance which contains all resolved profile fields
|
167
|
+
def read(*profile)
|
168
|
+
result = @streams
|
169
|
+
.flat_map { |stream, _name| read_stream(stream) }
|
170
|
+
.inject(DEFAULT_CONFIGURATION.call) { |acc, elem| update(acc, elem) }
|
171
|
+
OpenStruct.new(evaluate(resolve(profile, result)).merge('profile' => profile))
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
def read_stream(stream)
|
177
|
+
# Each stream consists of one or more documents
|
178
|
+
YAML.parse_stream(stream).children.flat_map do |document|
|
179
|
+
# Will use separate visitor per YAML document.
|
180
|
+
visitor = Only::Parser::VisitorToRuby.create
|
181
|
+
# Expecting document to be single mapping
|
182
|
+
profile_mapping = document.children.first
|
183
|
+
raise(ArgumentError, 'Mapping expected') unless profile_mapping.is_a? Psych::Nodes::Mapping
|
184
|
+
# Then mapping should be expanded to (key, value) pairs. Because yaml overwrites
|
185
|
+
# values for duplicated keys. This is not desired behaviour. We need to merge
|
186
|
+
# such entries
|
187
|
+
profile_mapping
|
188
|
+
.children
|
189
|
+
.each_slice(2)
|
190
|
+
.map { |k, v| { visitor.accept(k) => visitor.accept(v) } }
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def update(config, config_part)
|
195
|
+
config_part.each do |profile, values|
|
196
|
+
profile_values = config[profile]
|
197
|
+
unless profile_values
|
198
|
+
profile_values = {}
|
199
|
+
config[profile] = profile_values
|
200
|
+
end
|
201
|
+
merge!(profile_values, values)
|
202
|
+
end
|
203
|
+
|
204
|
+
config
|
205
|
+
end
|
206
|
+
|
207
|
+
# Merges two hash structures using following rules
|
208
|
+
# @param [Hash] base value mappings
|
209
|
+
# @param [Hash] values ovverides.
|
210
|
+
# @return [Hash] merged profile
|
211
|
+
def merge!(base, overrides)
|
212
|
+
merger.merge!(base, overrides)
|
213
|
+
end
|
214
|
+
|
215
|
+
def resolve(profile, config)
|
216
|
+
ProfileResolver.new(config).resolve(profile)
|
217
|
+
end
|
218
|
+
|
219
|
+
def evaluate(resolved_profile)
|
220
|
+
ConfigurationEvaluator.evaluate(resolved_profile)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# Describes profile resolving strategy
|
225
|
+
class ProfileResolver
|
226
|
+
# Key where all inherited profiles listed
|
227
|
+
EXTENDS_KEY = 'extends'.freeze
|
228
|
+
|
229
|
+
attr_reader :config, :merger
|
230
|
+
|
231
|
+
def initialize(config)
|
232
|
+
@config = config
|
233
|
+
@merger = Merger.new
|
234
|
+
end
|
235
|
+
|
236
|
+
def resolve(profile, resolved = [])
|
237
|
+
profile.inject({}) do |acc, elem|
|
238
|
+
if resolved.include?(elem)
|
239
|
+
msg = 'Cyclic dependency found ' + (resolved + [elem]).join(' -> ')
|
240
|
+
raise(ArgumentError, msg)
|
241
|
+
end
|
242
|
+
profile_values = find_profile(elem)
|
243
|
+
# Find all inheritors
|
244
|
+
extends = *profile_values[EXTENDS_KEY] || []
|
245
|
+
# We can do not check extends. Empty profile returns {}
|
246
|
+
# Inherited values goes first
|
247
|
+
inherited = merger.merge(acc, resolve(extends, resolved + [elem]))
|
248
|
+
merger.merge(inherited, profile_values)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def find_profile(profile_name)
|
253
|
+
return config[profile_name] if config.key?(profile_name)
|
254
|
+
# First and last chars are '/'
|
255
|
+
match, matched_profile = find_generic_profile(profile_name)
|
256
|
+
# Generating profile
|
257
|
+
return generate_profile(match, matched_profile, profile_name) if match
|
258
|
+
raise(ArgumentError, 'No such profile ' + profile_name)
|
259
|
+
end
|
260
|
+
|
261
|
+
def generate_profile(match, matched_profile, profile_name)
|
262
|
+
gen_profile = {}
|
263
|
+
gen_profile['captures'] = match.captures if match.captures
|
264
|
+
gen_profile['named_captures'] = match.named_captures if match.named_captures
|
265
|
+
gen_profile.merge!(config[matched_profile])
|
266
|
+
|
267
|
+
config[profile_name] = gen_profile
|
268
|
+
end
|
269
|
+
|
270
|
+
# @todo print warning if other matching generic profiles found
|
271
|
+
def find_generic_profile(profile_name)
|
272
|
+
generic_profiles(config)
|
273
|
+
.detect do |profile, regex|
|
274
|
+
m = regex.match(profile_name)
|
275
|
+
break [m, profile] if m
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def generic_profiles(config)
|
280
|
+
# Create generic profiles if missing
|
281
|
+
@generic_profiles ||= config
|
282
|
+
.keys
|
283
|
+
.select { |k| k[0] == '/' && k[-1] == '/' }
|
284
|
+
.map { |k| [k, Regexp.new(k[1..-2])] }
|
285
|
+
|
286
|
+
@generic_profiles
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Proxy class for getting actual values
|
291
|
+
# when referencing profile inside evaluated
|
292
|
+
# expressions
|
293
|
+
class EvaluationContext
|
294
|
+
def initialize(evaluator)
|
295
|
+
@evaluator = evaluator
|
296
|
+
end
|
297
|
+
|
298
|
+
def respond_to_missing?(_meth, _args, &_block)
|
299
|
+
true
|
300
|
+
end
|
301
|
+
|
302
|
+
def method_missing(meth, *args, &block)
|
303
|
+
# rubocop ispection hack
|
304
|
+
return super unless respond_to_missing?(meth, args, &block)
|
305
|
+
@evaluator.evaluate(meth.to_s)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
# Evaluates all strings inside resolved profile
|
310
|
+
# object
|
311
|
+
class ConfigurationEvaluator
|
312
|
+
def initialize(src_object, dst_object)
|
313
|
+
@object = src_object
|
314
|
+
@evaluated = dst_object
|
315
|
+
@context = EvaluationContext.new(self)
|
316
|
+
resolve_object
|
317
|
+
end
|
318
|
+
|
319
|
+
def evaluate(key)
|
320
|
+
return @evaluated[key] if @evaluated.key? key
|
321
|
+
@evaluated[key] = evaluate_deep(@object[key])
|
322
|
+
end
|
323
|
+
|
324
|
+
def c
|
325
|
+
@context
|
326
|
+
end
|
327
|
+
|
328
|
+
alias config c
|
329
|
+
alias conf c
|
330
|
+
alias cfg c
|
331
|
+
|
332
|
+
private
|
333
|
+
|
334
|
+
def resolve_object
|
335
|
+
@object.each_key { |k| evaluate(k) }
|
336
|
+
# Cleanup accidentally created values (when referencing missing values)
|
337
|
+
@evaluated.delete_if { |k, _v| !@object.key?(k) }
|
338
|
+
end
|
339
|
+
|
340
|
+
def get_binding(obj)
|
341
|
+
binding
|
342
|
+
end
|
343
|
+
|
344
|
+
def evaluate_deep(object)
|
345
|
+
case object
|
346
|
+
when String
|
347
|
+
eval_string(object)
|
348
|
+
when Array
|
349
|
+
object.map { |e| evaluate_deep(e) }
|
350
|
+
when Hash
|
351
|
+
# Evaluating values only by convention
|
352
|
+
object.inject({}) { |acc, elem| acc.update(elem.first => evaluate_deep(elem.last)) }
|
353
|
+
else
|
354
|
+
object
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
def eval_string(o)
|
359
|
+
evaluated = /#\{.+?\}/.match(o) do
|
360
|
+
eval('"' + o + '"', get_binding(self))
|
361
|
+
end
|
362
|
+
|
363
|
+
evaluated || o
|
364
|
+
end
|
365
|
+
|
366
|
+
class << self
|
367
|
+
def evaluate(o)
|
368
|
+
dst = {}
|
369
|
+
ConfigurationEvaluator.new(o, dst)
|
370
|
+
dst
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
# List of files where configuration can be placed
|
376
|
+
# * `application.yml` - expected to be main configuration file.
|
377
|
+
# Usually it placed under version control.
|
378
|
+
# * `application.user.yml` - user defined overrides for main
|
379
|
+
# configuration, sensitive data which can't be placed
|
380
|
+
# under version control.
|
381
|
+
# * `application.override.yml` - final overrides.
|
382
|
+
CONFIGURATION_FILES = %w[application.yml application.user.yml application.override.yml].freeze
|
383
|
+
|
384
|
+
class << self
|
385
|
+
# Generates configuration object for given profile
|
386
|
+
# and list of streams with YAML document
|
387
|
+
# @param profile [Array] list of profile names to merge
|
388
|
+
# @param streams [Array] list of YAML documents and their
|
389
|
+
# names to read
|
390
|
+
# @return [OpenStruct] instance which contains all resolved profile fields
|
391
|
+
def read(profile, *streams)
|
392
|
+
profile = [ConfigurationReader::DEFAULT_PROFILE_NAME] if profile.empty?
|
393
|
+
ConfigurationReader.new(streams).read(*profile)
|
394
|
+
end
|
395
|
+
|
396
|
+
# Read configuration from configuration files.
|
397
|
+
def load(profile, files: CONFIGURATION_FILES, dir: Dir.pwd)
|
398
|
+
# Read all known files
|
399
|
+
streams = files
|
400
|
+
.map { |f| [File.join(dir, f), f] }
|
401
|
+
.select { |path, _name| File.exist?(path) }
|
402
|
+
.map { |path, name| [IO.read(path), name] }
|
403
|
+
read(profile, *streams)
|
404
|
+
end
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
data/lib/jac/merger.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Jac
|
4
|
+
module Configuration
|
5
|
+
# Merges two hashes with following value resolve strategy:
|
6
|
+
# * When having both `that: Hash` and `other: Hash` for same key will merge
|
7
|
+
# them with same strategy and return result
|
8
|
+
# * When having Set and Enumerable will join two sets into
|
9
|
+
# one
|
10
|
+
# * When having Set and nil will return Set
|
11
|
+
# * Return `other` value in all other cases
|
12
|
+
class Merger
|
13
|
+
# Returns a new hash with base and overrides merged recursively.
|
14
|
+
# @param [Hash] base values
|
15
|
+
# @param [Hash] other values
|
16
|
+
# @return [Hash] updated hash
|
17
|
+
def merge(base, other)
|
18
|
+
merge!(base.dup, other)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns a new hash with base and overrides merged recursively. Updates
|
22
|
+
# receiver
|
23
|
+
# @param [Hash] base values
|
24
|
+
# @param [Hash] other values
|
25
|
+
# @return [Hash] updated base hash
|
26
|
+
def merge!(base, other)
|
27
|
+
base.merge!(other, &method(:resolve_values))
|
28
|
+
end
|
29
|
+
|
30
|
+
# @param [Object] _key
|
31
|
+
# @param [Object] base
|
32
|
+
# @param [Object] other
|
33
|
+
# @return [Object] resolved value
|
34
|
+
def resolve_values(_key, base, other)
|
35
|
+
if base.nil? && other.nil?
|
36
|
+
nil
|
37
|
+
elsif to_hash?(base, other)
|
38
|
+
merge(base, other)
|
39
|
+
elsif to_set?(base, other)
|
40
|
+
Set.new(base) + Set.new(other)
|
41
|
+
else
|
42
|
+
other
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def to_hash?(base, other)
|
49
|
+
base.is_a?(Hash) && other.is_a?(Hash)
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_set?(base, other)
|
53
|
+
(base.is_a?(Set) && set_like?(other)) || (other.is_a?(Set) && set_like?(base))
|
54
|
+
end
|
55
|
+
|
56
|
+
# rubocop: disable Naming/AccessorMethodName
|
57
|
+
def set_like?(value)
|
58
|
+
value.is_a?(Enumerable) || value.nil?
|
59
|
+
end
|
60
|
+
# rubocop: enable Naming/AccessorMethodName
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/jac/parser.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'psych'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Jac
|
5
|
+
module Parser
|
6
|
+
# Cutstom Yaml AST visitor
|
7
|
+
# @see {Psych::Visitors::ToRuby}
|
8
|
+
# While standart Psych visitor converts sets to `{ value => nil }` mappings
|
9
|
+
# we need explicitly convert those mappings to ruby {Set}
|
10
|
+
class VisitorToRuby < Psych::Visitors::ToRuby
|
11
|
+
# rubocop: disable Naming/MethodName
|
12
|
+
|
13
|
+
# Uses standard Psych visitor to convert mapping to ruby object
|
14
|
+
# except `!set` case. Here we convert mapping to {Set}.
|
15
|
+
# @param [Psych::Nodes::Mapping] o YAML AST node
|
16
|
+
# @return [Object] parsed ruby object
|
17
|
+
def visit_Psych_Nodes_Mapping(o)
|
18
|
+
case o.tag
|
19
|
+
when '!set', 'tag:yaml.org,2002:set'
|
20
|
+
visit_set(o)
|
21
|
+
else # fallback to default implementation
|
22
|
+
super(o)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
# rubocop: enable Naming/MethodName
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def visit_set(o)
|
30
|
+
set = Set.new
|
31
|
+
# Update anchor
|
32
|
+
@st[o.anchor] = set if o.anchor
|
33
|
+
o.children.each_slice(2) do |k, _v|
|
34
|
+
set << accept(k)
|
35
|
+
end
|
36
|
+
set
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jac
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- ilya.arkhanhelsky
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-11-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: powerpack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.1.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.1.1
|
27
|
+
description: Profile based configuration lib
|
28
|
+
email: ilya.arkhanhelsky@vizor-games.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- lib/jac.rb
|
34
|
+
- lib/jac/configuration.rb
|
35
|
+
- lib/jac/merger.rb
|
36
|
+
- lib/jac/parser.rb
|
37
|
+
- lib/version.rb
|
38
|
+
homepage: https://github.com/vizor-games/jac
|
39
|
+
licenses:
|
40
|
+
- MIT
|
41
|
+
metadata: {}
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
requirements: []
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 2.6.13
|
59
|
+
signing_key:
|
60
|
+
specification_version: 4
|
61
|
+
summary: Just Another Configuration Lib
|
62
|
+
test_files: []
|