mimi-core 0.2.0 → 1.0.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 +4 -4
- data/.yardopts +1 -0
- data/README.md +199 -2
- data/lib/mimi/core/core_ext.rb +89 -2
- data/lib/mimi/core/inheritable_property.rb +173 -0
- data/lib/mimi/core/manifest.rb +538 -0
- data/lib/mimi/core/module.rb +80 -16
- data/lib/mimi/core/rake.rb +46 -0
- data/lib/mimi/core/version.rb +1 -1
- data/lib/mimi/core.rb +32 -2
- data/mimi-core.gemspec +1 -2
- metadata +8 -19
@@ -0,0 +1,538 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bigdecimal'
|
4
|
+
require 'json'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
module Mimi
|
8
|
+
module Core
|
9
|
+
#
|
10
|
+
# Manifest represents a set of definitions of configurable parameters.
|
11
|
+
#
|
12
|
+
# It is a way of formally declaring which configurable parameters are accepted by a Mimi module,
|
13
|
+
# application etc. A Manifest object is also used to validate passed set of raw values,
|
14
|
+
# apply rules and produce a set of parsed configurable parameter values.
|
15
|
+
#
|
16
|
+
# Manifests are constructed from a Hash representation, following some structure.
|
17
|
+
# Configurable parameter definitions are specified in the manifest Hash as
|
18
|
+
# key-value pairs, where *key* is the name of the configurable parameter, and
|
19
|
+
# *value* is a Hash with parameter properties.
|
20
|
+
#
|
21
|
+
# Example:
|
22
|
+
#
|
23
|
+
# ```ruby
|
24
|
+
# manifest = Mimi::Core::Manifest.new(
|
25
|
+
# var1: {}, # minimalistic configurable parameter definition, all properties are default
|
26
|
+
# var2: {}
|
27
|
+
# )
|
28
|
+
# ```
|
29
|
+
#
|
30
|
+
# The properties that can be defined for a configurable parameter are:
|
31
|
+
#
|
32
|
+
# * `:desc` (String) -- a human readable description of the parameter (default: nil)
|
33
|
+
# * `:type` (Symbol,Array<String>) -- defines the type of the parameter and the type/format
|
34
|
+
# of accepted values (default: :string)
|
35
|
+
# * `:default` (Object) -- specified default value indicates that the parameter is optional
|
36
|
+
# * `:hidden` (true,false) -- if set to true, omits the parameter from the application's
|
37
|
+
# combined manifest
|
38
|
+
# * `:const` (true,false) -- if set to true, this configurable parameter cannot be changed
|
39
|
+
# and always equals to its default value which must be specified
|
40
|
+
#
|
41
|
+
# ## Configurable parameter properties
|
42
|
+
#
|
43
|
+
# ### :desc => <String>
|
44
|
+
#
|
45
|
+
# Default: `nil`
|
46
|
+
#
|
47
|
+
# Allows to specify a human readable description for a configurable parameter.
|
48
|
+
#
|
49
|
+
# Example:
|
50
|
+
#
|
51
|
+
# ```ruby
|
52
|
+
# manifest = Mimi::Core::Manifest.new(
|
53
|
+
# var1: {
|
54
|
+
# desc: 'My configurable parameter 1'
|
55
|
+
# }
|
56
|
+
# }
|
57
|
+
# ```
|
58
|
+
#
|
59
|
+
#
|
60
|
+
# ### :type => <Symbol,Array<String>>
|
61
|
+
#
|
62
|
+
# Default: `:string`
|
63
|
+
#
|
64
|
+
# Defines the type of the parameter and accepted values. Recognised types are:
|
65
|
+
#
|
66
|
+
# * `:string` -- accepts any value, presents it as a `String`
|
67
|
+
# * `:integer` -- accepts any `Integer` value or a valid `String` representation of integer
|
68
|
+
# * `:decimal` -- accepts `BigDecimal` value or a valid `String` representation
|
69
|
+
# of a decimal number
|
70
|
+
# * `:boolean` -- accepts `true` or `false` or string literals `'true'`, `'false'`
|
71
|
+
# * `:json` -- accepts a string with valid JSON, presents it as a parsed object
|
72
|
+
# (literal, Array or Hash)
|
73
|
+
# * `Array<String>` -- defines enumeration of values, e.g. `['debug', 'info', 'warn', 'error']`;
|
74
|
+
# only values enumerated in the list are accepted, presented as `String`
|
75
|
+
#
|
76
|
+
# Example:
|
77
|
+
#
|
78
|
+
# ```ruby
|
79
|
+
# manifest = Mimi::Core::Manifest.new(
|
80
|
+
# var1: {
|
81
|
+
# type: :integer,
|
82
|
+
# default: 1
|
83
|
+
# },
|
84
|
+
#
|
85
|
+
# var2: {
|
86
|
+
# type: :decimal,
|
87
|
+
# default: '0.01'
|
88
|
+
# },
|
89
|
+
#
|
90
|
+
# var3: {
|
91
|
+
# type: ['debug', 'info', 'warn', 'error'],
|
92
|
+
# default: 'info'
|
93
|
+
# }
|
94
|
+
# }
|
95
|
+
# ```
|
96
|
+
#
|
97
|
+
# ### :default => <Object, Proc>
|
98
|
+
#
|
99
|
+
# Default: `nil`
|
100
|
+
#
|
101
|
+
# ...
|
102
|
+
#
|
103
|
+
# ### :hidden => <true,false>
|
104
|
+
#
|
105
|
+
# Default: `false`
|
106
|
+
#
|
107
|
+
# ...
|
108
|
+
#
|
109
|
+
# ### :const => <true,false>
|
110
|
+
#
|
111
|
+
# Default: `false`
|
112
|
+
#
|
113
|
+
# ...
|
114
|
+
#
|
115
|
+
#
|
116
|
+
# Example:
|
117
|
+
# manifest_hash = {
|
118
|
+
# var1: {
|
119
|
+
# desc: 'My var 1',
|
120
|
+
# type: :string,
|
121
|
+
#
|
122
|
+
# }
|
123
|
+
# }
|
124
|
+
#
|
125
|
+
class Manifest
|
126
|
+
ALLOWED_TYPES = %w[string integer decimal boolean json].freeze
|
127
|
+
|
128
|
+
# Constructs a new Manifest from its Hash representation
|
129
|
+
#
|
130
|
+
# @param manifest_hash [Hash,nil] default is empty manifest
|
131
|
+
#
|
132
|
+
def initialize(manifest_hash = {})
|
133
|
+
self.class.validate_manifest_hash(manifest_hash)
|
134
|
+
@manifest = manifest_hash_canonical(manifest_hash.deep_dup)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns a Hash representation of the Manifest
|
138
|
+
#
|
139
|
+
# @return [Hash]
|
140
|
+
#
|
141
|
+
def to_h
|
142
|
+
@manifest
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns a list of configurable parameter names
|
146
|
+
#
|
147
|
+
# @return [Array<Symbol>]
|
148
|
+
#
|
149
|
+
def keys
|
150
|
+
@manifest.keys
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns true if the configurable parameter is a required one
|
154
|
+
#
|
155
|
+
# @param name [Symbol] the name of configurable parameter
|
156
|
+
# @return [true,false]
|
157
|
+
#
|
158
|
+
def required?(name)
|
159
|
+
raise ArgumentError, 'Symbol is expected as the parameter name' unless name.is_a?(Symbol)
|
160
|
+
props = @manifest[name]
|
161
|
+
return false unless props # parameter is not required if it is not declared
|
162
|
+
!props.keys.include?(:default)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Merges current Manifest with another Hash or Manifest, modifies current Manifest in-place
|
166
|
+
#
|
167
|
+
# @param another [Mimi::Core::Manifest,Hash]
|
168
|
+
#
|
169
|
+
def merge!(another)
|
170
|
+
@manifest = merge(another).to_h
|
171
|
+
end
|
172
|
+
|
173
|
+
# Returns a copy of current Manifest merged with another Hash or Manifest
|
174
|
+
#
|
175
|
+
# @param another [Mimi::Core::Manifest,Hash]
|
176
|
+
# @return [Mimi::Core::Manifest]
|
177
|
+
#
|
178
|
+
def merge(another)
|
179
|
+
if !another.is_a?(Mimi::Core::Manifest) && !another.is_a?(Hash)
|
180
|
+
raise ArgumentError 'Another Mimi::Core::Manifest or Hash is expected'
|
181
|
+
end
|
182
|
+
another_hash = another.is_a?(Hash) ? another.deep_dup : another.to_h.deep_dup
|
183
|
+
new_manifest_hash = @manifest.deep_merge(another_hash)
|
184
|
+
new_manifest_hash = manifest_hash_canonical(new_manifest_hash)
|
185
|
+
self.class.validate_manifest_hash(new_manifest_hash)
|
186
|
+
self.class.new(new_manifest_hash)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Accepts the values, performs the validation and applies the manifest,
|
190
|
+
# responding with a Hash of parameters and processed values.
|
191
|
+
#
|
192
|
+
# Performs the type coercion of values to the specified configurable parameter type.
|
193
|
+
#
|
194
|
+
# * type: :string, value: anything => `String`
|
195
|
+
# * type: :integer, value: `1` or `'1'` => `1`
|
196
|
+
# * type: :decimal, value: `1`, `1.0 (BigDecimal)`, `'1'` or `'1.0'` => `1.0 (BigDecimal)`
|
197
|
+
# * type: :boolean, value: `true` or `'true'` => `true`
|
198
|
+
# * type: :json, value: `{ 'id' => 123 }` or `'{"id":123}'` => `{ 'id' => 123 }`
|
199
|
+
# * type: `['a', 'b', 'c']` , value: `'a'` => `'a'`
|
200
|
+
#
|
201
|
+
# Example:
|
202
|
+
#
|
203
|
+
# ```ruby
|
204
|
+
# manifest = Mimi::Core::Manifest.new(
|
205
|
+
# var1: {},
|
206
|
+
# var2: :integer,
|
207
|
+
# var3: :decimal,
|
208
|
+
# var4: :boolean,
|
209
|
+
# var5: :json,
|
210
|
+
# var6: ['a', 'b', 'c']
|
211
|
+
# )
|
212
|
+
#
|
213
|
+
# manifest.apply(
|
214
|
+
# var1: 'var1.value',
|
215
|
+
# var2: '2',
|
216
|
+
# var3: '3',
|
217
|
+
# var4: 'false',
|
218
|
+
# var5: '[{"name":"value"}]',
|
219
|
+
# var6: 'c'
|
220
|
+
# )
|
221
|
+
# # =>
|
222
|
+
# # {
|
223
|
+
# # var1: 'var1.value', var2: 2, var3: 3.0, var4: false,
|
224
|
+
# # var5: [{ 'name' => 'value '}], var6: 'c'
|
225
|
+
# # }
|
226
|
+
# ```
|
227
|
+
#
|
228
|
+
# If `:default` is specified for the parameter and the value is not provided,
|
229
|
+
# the default value is returned as-is, bypassing validation and type coercion.
|
230
|
+
#
|
231
|
+
# ```ruby
|
232
|
+
# manifest = Mimi::Core::Manifest.new(var1: { default: nil })
|
233
|
+
# manifest.apply({}) # => { var1: nil }
|
234
|
+
# ```
|
235
|
+
#
|
236
|
+
# Values for parameters not defined in the manifest are ignored:
|
237
|
+
#
|
238
|
+
# ```ruby
|
239
|
+
# manifest = Mimi::Core::Manifest.new(var1: {})
|
240
|
+
# manifest.apply(var1: '123', var2: '456') # => { var1: '123' }
|
241
|
+
# ```
|
242
|
+
#
|
243
|
+
# Configurable parameters defined as `:const` cannot be changed by provided values:
|
244
|
+
#
|
245
|
+
# ```ruby
|
246
|
+
# manifest = Mimi::Core::Manifest.new(var1: { default: 1, const: true })
|
247
|
+
# manifest.apply(var1: 2) # => { var1: 1 }
|
248
|
+
# ```
|
249
|
+
#
|
250
|
+
# If a configurable parameter defined as *required* in the manifest (has no `:default`)
|
251
|
+
# and the provided values have no corresponding key, an ArgumentError is raised:
|
252
|
+
#
|
253
|
+
# ```ruby
|
254
|
+
# manifest = Mimi::Core::Manifest.new(var1: {})
|
255
|
+
# manifest.apply({}) # => ArgumentError "Required value for 'var1' is missing"
|
256
|
+
# ```
|
257
|
+
#
|
258
|
+
# If a value provided for the configurable parameter is incompatible (different type,
|
259
|
+
# wrong format etc), an ArgumentError is raised:
|
260
|
+
#
|
261
|
+
# ```ruby
|
262
|
+
# manifest = Mimi::Core::Manifest.new(var1: { type: :integer })
|
263
|
+
# manifest.apply(var1: 'abc') # => ArgumentError "Invalid value provided for 'var1'"
|
264
|
+
# ```
|
265
|
+
#
|
266
|
+
# During validation of provided values, all violations are detected and reported in
|
267
|
+
# a single ArgumentError:
|
268
|
+
#
|
269
|
+
# ```ruby
|
270
|
+
# manifest = Mimi::Core::Manifest.new(var1: { type: :integer }, var2: {})
|
271
|
+
# manifest.apply(var1: 'abc') # =>
|
272
|
+
# # ArgumentError "Invalid value provided for 'var1'. Required value for 'var2' is missing."
|
273
|
+
# ```
|
274
|
+
#
|
275
|
+
# @param values [Hash]
|
276
|
+
# @return [Hash<Symbol,Object>] where key is the parameter name, value is the parameter value
|
277
|
+
#
|
278
|
+
# @raise [ArgumentError] on validation errors, missing values etc
|
279
|
+
#
|
280
|
+
def apply(values)
|
281
|
+
raise ArgumentError, 'Hash is expected as values' unless values.is_a?(Hash)
|
282
|
+
validate_values(values)
|
283
|
+
process_values(values)
|
284
|
+
end
|
285
|
+
|
286
|
+
# Validates a Hash representation of the manifest
|
287
|
+
#
|
288
|
+
# * all keys are symbols
|
289
|
+
# * all configurable parameter properties are valid
|
290
|
+
#
|
291
|
+
# @param manifest_hash [Hash]
|
292
|
+
# @raise [ArgumentError] if any part of manifest is invalid
|
293
|
+
#
|
294
|
+
def self.validate_manifest_hash(manifest_hash)
|
295
|
+
invalid_keys = manifest_hash.keys.reject { |k| k.is_a?(Symbol) }
|
296
|
+
unless invalid_keys.empty?
|
297
|
+
raise ArgumentError,
|
298
|
+
"Invalid manifest keys, Symbols are expected: #{invalid_keys.join(', ')}"
|
299
|
+
end
|
300
|
+
|
301
|
+
manifest_hash.each { |n, p| validate_manifest_key_properties(n, p) }
|
302
|
+
end
|
303
|
+
|
304
|
+
# Validates configurable parameter properties
|
305
|
+
#
|
306
|
+
# @param name [Symbol] name of the parameter
|
307
|
+
# @param properties [Hash] configurable parameter properties
|
308
|
+
#
|
309
|
+
def self.validate_manifest_key_properties(name, properties)
|
310
|
+
raise 'Hash as properties is expected' unless properties.is_a?(Hash)
|
311
|
+
if properties[:desc]
|
312
|
+
raise ArgumentError, 'String as :desc is expected' unless properties[:desc].is_a?(String)
|
313
|
+
end
|
314
|
+
if properties[:type]
|
315
|
+
if properties[:type].is_a?(Array)
|
316
|
+
if properties[:type].any? { |v| !v.is_a?(String) }
|
317
|
+
raise ArgumentError, 'Array<String> is expected as enumeration :type'
|
318
|
+
end
|
319
|
+
elsif !ALLOWED_TYPES.include?(properties[:type].to_s)
|
320
|
+
raise ArgumentError, "Unrecognised type '#{properties[:type]}'"
|
321
|
+
end
|
322
|
+
end
|
323
|
+
if properties.keys.include?(:hidden)
|
324
|
+
if !properties[:hidden].is_a?(TrueClass) && !properties[:hidden].is_a?(FalseClass)
|
325
|
+
raise ArgumentError, 'Invalid type for :hidden, true or false is expected'
|
326
|
+
end
|
327
|
+
end
|
328
|
+
if properties.keys.include?(:const)
|
329
|
+
if !properties[:const].is_a?(TrueClass) && !properties[:const].is_a?(FalseClass)
|
330
|
+
raise ArgumentError, 'Invalid type for :const, true or false is expected'
|
331
|
+
end
|
332
|
+
end
|
333
|
+
if properties[:const] && !properties.keys.include?(:default)
|
334
|
+
raise ArgumentError, ':default is required if :const is set'
|
335
|
+
end
|
336
|
+
rescue ArgumentError => e
|
337
|
+
raise ArgumentError, "Invalid manifest: invalid properties for '#{name}': #{e}"
|
338
|
+
end
|
339
|
+
|
340
|
+
# Constructs a Manifest object from a YAML representation
|
341
|
+
#
|
342
|
+
# @param yaml [String]
|
343
|
+
# @return [Mimi::Core::Manifest]
|
344
|
+
#
|
345
|
+
def self.from_yaml(yaml)
|
346
|
+
manifest_hash = YAML.safe_load(yaml)
|
347
|
+
raise 'Invalid manifest, JSON Object is expected' unless manifest_hash.is_a?(Hash)
|
348
|
+
manifest_hash = manifest_hash.map do |k, v|
|
349
|
+
v = (v || {}).symbolize_keys
|
350
|
+
[k.to_sym, v]
|
351
|
+
end.to_h
|
352
|
+
new(manifest_hash)
|
353
|
+
end
|
354
|
+
|
355
|
+
# Returns a YAML representation of the manifest
|
356
|
+
#
|
357
|
+
# @return [String]
|
358
|
+
#
|
359
|
+
def to_yaml
|
360
|
+
out = []
|
361
|
+
to_h.each do |k, v|
|
362
|
+
next if v[:hidden]
|
363
|
+
out << "#{k}:"
|
364
|
+
vy = v[:desc].nil? ? '# nil' : v[:desc].inspect # value to yaml
|
365
|
+
out << " desc: #{vy}" if v.key?(:desc) && !v[:desc].empty?
|
366
|
+
if v[:type].is_a?(Array)
|
367
|
+
out << ' type:'
|
368
|
+
v[:type].each { |t| out << " - #{t}" }
|
369
|
+
elsif v[:type] != :string
|
370
|
+
out << " type: #{v[:type]}"
|
371
|
+
end
|
372
|
+
out << ' const: true' if v[:const]
|
373
|
+
vy = v[:default].nil? ? '# nil' : v[:default].inspect # value to yaml
|
374
|
+
out << " default: #{vy}" if v.key?(:default)
|
375
|
+
out << ''
|
376
|
+
end
|
377
|
+
out.join("\n")
|
378
|
+
end
|
379
|
+
|
380
|
+
private
|
381
|
+
|
382
|
+
# Sets the missing default properties in the properties Hash, converts values
|
383
|
+
# to canonical form.
|
384
|
+
#
|
385
|
+
# @param properties [Hash] set of properties of a configurable parameter
|
386
|
+
# @return [Hash] same Hash with all the missing properties set
|
387
|
+
#
|
388
|
+
def properties_canonical(properties)
|
389
|
+
properties = {
|
390
|
+
desc: '',
|
391
|
+
type: :string,
|
392
|
+
hidden: false,
|
393
|
+
const: false
|
394
|
+
}.merge(properties)
|
395
|
+
properties[:desc] = properties[:desc].to_s
|
396
|
+
if properties[:type].is_a?(Array)
|
397
|
+
properties[:type] = properties[:type].map { |v| v.to_s }
|
398
|
+
elsif properties[:type].is_a?(String)
|
399
|
+
properties[:type] = properties[:type].to_sym
|
400
|
+
end
|
401
|
+
properties[:hidden] = !!properties[:hidden]
|
402
|
+
properties[:const] = !!properties[:const]
|
403
|
+
properties
|
404
|
+
end
|
405
|
+
|
406
|
+
# Converts a valid manifest Hash to a canonical form, with all defaults set
|
407
|
+
# and property values coerced:
|
408
|
+
#
|
409
|
+
# Example
|
410
|
+
#
|
411
|
+
# ```ruby
|
412
|
+
# manifest_hash_canonical(
|
413
|
+
# var1: {},
|
414
|
+
# var2: {
|
415
|
+
# type: 'string',
|
416
|
+
# hidden: nil,
|
417
|
+
# default: 1,
|
418
|
+
# const: false
|
419
|
+
# }
|
420
|
+
# )
|
421
|
+
# # =>
|
422
|
+
# # {
|
423
|
+
# # var1: { desc: '', type: :string, hidden: false, const: false },
|
424
|
+
# # var2: { desc: '', type: :string, default: 1, hidden: false, const: false }
|
425
|
+
# # }
|
426
|
+
#
|
427
|
+
# ```
|
428
|
+
#
|
429
|
+
# @param manifest_hash [Hash]
|
430
|
+
# @return [Hash]
|
431
|
+
#
|
432
|
+
def manifest_hash_canonical(manifest_hash)
|
433
|
+
manifest_hash.map do |name, props|
|
434
|
+
[name, properties_canonical(props)]
|
435
|
+
end.to_h
|
436
|
+
end
|
437
|
+
|
438
|
+
# Validates provided values
|
439
|
+
#
|
440
|
+
# @param values [Hash]
|
441
|
+
# @raise [ArgumentError] if any of the values are invalid or missing
|
442
|
+
#
|
443
|
+
def validate_values(values)
|
444
|
+
# select keys where value is required and missing
|
445
|
+
missing_values = @manifest.keys.select do |key|
|
446
|
+
required?(key) && values[key].nil?
|
447
|
+
end
|
448
|
+
|
449
|
+
# select keys where value is provided and invalid
|
450
|
+
invalid_values = @manifest.keys.select { |key| values[key] }.reject do |key|
|
451
|
+
type = @manifest[key][:type]
|
452
|
+
value = values[key]
|
453
|
+
case type
|
454
|
+
when :string
|
455
|
+
true # anything is valid
|
456
|
+
when :integer
|
457
|
+
value.is_a?(Integer) || (value.is_a?(String) && value =~ /^\d+$/)
|
458
|
+
when :decimal
|
459
|
+
value.is_a?(Integer) || value.is_a?(BigDecimal) ||
|
460
|
+
(value.is_a?(String) && value =~ /^\d+(\.\d+)?$/)
|
461
|
+
when :boolean
|
462
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass) ||
|
463
|
+
(value.is_a?(String) && value =~ /^(true|false)$/)
|
464
|
+
when :json
|
465
|
+
validate_value_json(value)
|
466
|
+
when Array
|
467
|
+
type.include?(value)
|
468
|
+
else
|
469
|
+
raise "Unexpected type '#{type}' for '#{key}'"
|
470
|
+
end
|
471
|
+
end
|
472
|
+
messages =
|
473
|
+
missing_values.map do |key|
|
474
|
+
"Required value for '#{key}' is missing."
|
475
|
+
end + invalid_values.map do |key|
|
476
|
+
"Invalid value provided for '#{key}'."
|
477
|
+
end
|
478
|
+
raise ArgumentError, messages.join(' ') unless messages.empty?
|
479
|
+
end
|
480
|
+
|
481
|
+
# Validates a single JSON value
|
482
|
+
#
|
483
|
+
# * must be a String
|
484
|
+
# * must be a valid JSON
|
485
|
+
#
|
486
|
+
# @param value [String]
|
487
|
+
# @return [true,false]
|
488
|
+
#
|
489
|
+
def validate_value_json(value)
|
490
|
+
return false unless value.is_a?(String)
|
491
|
+
JSON.parse(value)
|
492
|
+
true
|
493
|
+
rescue JSON::ParserError
|
494
|
+
false
|
495
|
+
end
|
496
|
+
|
497
|
+
# Processes the given set of values and returns a Hash of configurable parameter
|
498
|
+
# values.
|
499
|
+
#
|
500
|
+
# @param values [Hash]
|
501
|
+
# @return [Hash]
|
502
|
+
#
|
503
|
+
def process_values(values)
|
504
|
+
@manifest.map do |name, props|
|
505
|
+
[name, process_single_value(values[name], props)]
|
506
|
+
end.to_h
|
507
|
+
end
|
508
|
+
|
509
|
+
# Processes a single value with a given set of configurable parameter properties
|
510
|
+
#
|
511
|
+
# @param value [Object,nil] nil indicates the value is not provided (default should be used)
|
512
|
+
# @param properties [Hash]
|
513
|
+
# @return [Object]
|
514
|
+
#
|
515
|
+
def process_single_value(value, properties)
|
516
|
+
if properties[:const] || value.nil?
|
517
|
+
return properties[:default].is_a?(Proc) ? properties[:default].call : properties[:default]
|
518
|
+
end
|
519
|
+
case properties[:type]
|
520
|
+
when :string
|
521
|
+
value.to_s
|
522
|
+
when :integer
|
523
|
+
value.to_i
|
524
|
+
when :decimal
|
525
|
+
BigDecimal(value)
|
526
|
+
when :boolean
|
527
|
+
value.is_a?(TrueClass) || value == 'true'
|
528
|
+
when :json
|
529
|
+
JSON.parse(value)
|
530
|
+
when Array
|
531
|
+
value
|
532
|
+
else
|
533
|
+
raise "Unexpected type '#{type}' for '#{key}'"
|
534
|
+
end
|
535
|
+
end
|
536
|
+
end # class Manifest
|
537
|
+
end # module Core
|
538
|
+
end # module Mimi
|
data/lib/mimi/core/module.rb
CHANGED
@@ -1,48 +1,112 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Mimi
|
4
4
|
module Core
|
5
5
|
module Module
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
#
|
7
|
+
# Invoked on a descendant module declaration,
|
8
|
+
# registers a descendant module in the list of loaded modules.
|
9
|
+
#
|
10
|
+
def self.included(base)
|
11
|
+
return if Mimi.loaded_modules.include?(base)
|
12
|
+
Mimi.loaded_modules << base
|
13
|
+
base.send :extend, ClassMethods
|
11
14
|
end
|
12
15
|
|
13
|
-
|
16
|
+
module ClassMethods
|
17
|
+
#
|
18
|
+
# Processes given values for configurable parameters defined in the module manifest
|
19
|
+
# and populates the options Hash.
|
20
|
+
#
|
21
|
+
# @param opts [Hash] values for configurable parameters
|
22
|
+
#
|
14
23
|
def configure(opts = {})
|
15
|
-
|
24
|
+
manifest_hash = manifest
|
25
|
+
unless manifest_hash.is_a?(Hash)
|
26
|
+
raise "#{self}.manifest should be implemented and return Hash"
|
27
|
+
end
|
28
|
+
@options = Mimi::Core::Manifest.new(manifest_hash).apply(opts)
|
16
29
|
end
|
17
30
|
|
31
|
+
# Returns the path to module files, if the module exposes any files.
|
32
|
+
#
|
33
|
+
# Some modules may expose its files to the application using Mimi core. For example,
|
34
|
+
# a module may contain some rake tasks with useful functionality.
|
35
|
+
#
|
36
|
+
# To expose module files, this method must be overloaded and point to the root
|
37
|
+
# of the gem folder:
|
38
|
+
#
|
39
|
+
# ```
|
40
|
+
# # For example, module my_lib folder and files:
|
41
|
+
# /path/to/my_lib/
|
42
|
+
# ./lib/my_lib/...
|
43
|
+
# ./lib/my_lib.rb
|
44
|
+
# ./spec/spec_helper
|
45
|
+
# ...
|
46
|
+
#
|
47
|
+
# # my_lib module should expose its root as .module_path: /path/to/my_lib
|
48
|
+
# ```
|
49
|
+
#
|
50
|
+
# @return [Pathname,String,nil]
|
51
|
+
#
|
18
52
|
def module_path
|
19
53
|
nil
|
20
54
|
end
|
21
55
|
|
22
|
-
|
56
|
+
# Module manifest
|
57
|
+
#
|
58
|
+
# Mimi modules overload this method to define their own set of configurable parameters.
|
59
|
+
# The method should return a Hash representation of the manifest.
|
60
|
+
#
|
61
|
+
# NOTE: to avoid clashes with other modules, it is advised that configurable parameters
|
62
|
+
# for the module have some module-specific prefix. E.g. `Mimi::DB` module has its
|
63
|
+
# configurable parameters names as `db_adapter`, `db_database`, `db_username` and so on.
|
64
|
+
#
|
65
|
+
# @see Mimi::Core::Manifest
|
66
|
+
#
|
67
|
+
# @return [Hash]
|
68
|
+
#
|
69
|
+
def manifest
|
23
70
|
{}
|
24
71
|
end
|
25
72
|
|
73
|
+
# Starts the module.
|
74
|
+
#
|
75
|
+
# Mimi modules overload this method to implement some module-specific logic that
|
76
|
+
# should happen on application startup. E.g. `mimi-messaging` establishes a connection
|
77
|
+
# with a message broker and declares message consumers.
|
78
|
+
#
|
26
79
|
def start(*)
|
27
80
|
@module_started = true
|
28
81
|
end
|
29
82
|
|
83
|
+
# Returns true if the module is started.
|
84
|
+
#
|
85
|
+
# @return [true,false]
|
86
|
+
#
|
30
87
|
def started?
|
31
88
|
@module_started
|
32
89
|
end
|
33
90
|
|
91
|
+
# Starts the module.
|
92
|
+
#
|
93
|
+
# Mimi modules overload this method to implement some module-specific logic that
|
94
|
+
# should happen on application shutdown. E.g. `mimi-messaging` closes a connection
|
95
|
+
# with a message broker.
|
96
|
+
#
|
34
97
|
def stop(*)
|
35
98
|
@module_started = false
|
36
99
|
end
|
37
100
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
101
|
+
# Returns a Hash of configurable parameter values accepted and set by the `.configure`
|
102
|
+
# method.
|
103
|
+
#
|
104
|
+
# @return [Hash<Symbol,Object>]
|
105
|
+
#
|
106
|
+
def options
|
107
|
+
@options || {}
|
44
108
|
end
|
45
|
-
end
|
109
|
+
end # module ClassMethods
|
46
110
|
end # module Module
|
47
111
|
end # module Core
|
48
112
|
end # module Mimi
|