jac 0.0.1
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/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: []
|