jac 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ require_relative 'jac/configuration'
2
+ require_relative 'jac/merger'
3
+ require_relative 'jac/parser'
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,14 @@
1
+ module Jac
2
+ # Jac version module
3
+ module Version
4
+ class << self
5
+ def value
6
+ [ 0, 0, 1]
7
+ end
8
+
9
+ def to_s
10
+ value.join('.')
11
+ end
12
+ end
13
+ end
14
+ end
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: []