mimi-core 0.2.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|