Class: Mimi::Core::Manifest

Inherits:
Object
  • Object
show all
Defined in:
lib/mimi/core/manifest.rb

Overview

Manifest represents a set of definitions of configurable parameters.

It is a way of formally declaring which configurable parameters are accepted by a Mimi module, application etc. A Manifest object is also used to validate passed set of raw values, apply rules and produce a set of parsed configurable parameter values.

Manifests are constructed from a Hash representation, following some structure. Configurable parameter definitions are specified in the manifest Hash as key-value pairs, where key is the name of the configurable parameter, and value is a Hash with parameter properties.

Example:

manifest = Mimi::Core::Manifest.new(
  var1: {}, # minimalistic configurable parameter definition, all properties are default
  var2: {}
)

The properties that can be defined for a configurable parameter are:

  • :desc (String) -- a human readable description of the parameter (default: nil)
  • :type (Symbol,Array) -- defines the type of the parameter and the type/format of accepted values (default: :string)
  • :default (Object) -- specified default value indicates that the parameter is optional
  • :hidden (true,false) -- if set to true, omits the parameter from the application's combined manifest
  • :const (true,false) -- if set to true, this configurable parameter cannot be changed and always equals to its default value which must be specified

Configurable parameter properties

:desc =>

Default: nil

Allows to specify a human readable description for a configurable parameter.

Example:

manifest = Mimi::Core::Manifest.new(
  var1: {
    desc: 'My configurable parameter 1'
  }
}

:type => >

Default: :string

Defines the type of the parameter and accepted values. Recognised types are:

  • :string -- accepts any value, presents it as a String
  • :integer -- accepts any Integer value or a valid String representation of integer
  • :decimal -- accepts BigDecimal value or a valid String representation of a decimal number
  • :boolean -- accepts true or false or string literals 'true', 'false'
  • :json -- accepts a string with valid JSON, presents it as a parsed object (literal, Array or Hash)
  • Array<String> -- defines enumeration of values, e.g. ['debug', 'info', 'warn', 'error']; only values enumerated in the list are accepted, presented as String

Example:

manifest = Mimi::Core::Manifest.new(
  var1: {
    type: :integer,
    default: 1
  },

  var2: {
    type: :decimal,
    default: '0.01'
  },

  var3: {
    type: ['debug', 'info', 'warn', 'error'],
    default: 'info'
  }
}

:default =>

Default: nil

...

:hidden =>

Default: false

...

:const =>

Default: false

...

Example: manifest_hash = { var1: { desc: 'My var 1', type: :string,

}

}

Constant Summary collapse

ALLOWED_TYPES =
%w[string integer decimal boolean json].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(manifest_hash = {}) ⇒ Manifest

Constructs a new Manifest from its Hash representation

Parameters:

  • manifest_hash (Hash, nil) (defaults to: {})

    default is empty manifest



132
133
134
135
# File 'lib/mimi/core/manifest.rb', line 132

def initialize(manifest_hash = {})
  self.class.validate_manifest_hash(manifest_hash)
  @manifest = manifest_hash_canonical(manifest_hash.deep_dup)
end

Class Method Details

.from_yaml(yaml) ⇒ Mimi::Core::Manifest

Constructs a Manifest object from a YAML representation

Parameters:

  • yaml (String)

Returns:



345
346
347
348
349
350
351
352
353
# File 'lib/mimi/core/manifest.rb', line 345

def self.from_yaml(yaml)
  manifest_hash = YAML.safe_load(yaml)
  raise 'Invalid manifest, JSON Object is expected' unless manifest_hash.is_a?(Hash)
  manifest_hash = manifest_hash.map do |k, v|
    v = (v || {}).symbolize_keys
    [k.to_sym, v]
  end.to_h
  new(manifest_hash)
end

.validate_manifest_hash(manifest_hash) ⇒ Object

Validates a Hash representation of the manifest

  • all keys are symbols
  • all configurable parameter properties are valid

Parameters:

  • manifest_hash (Hash)

Raises:

  • (ArgumentError)

    if any part of manifest is invalid



294
295
296
297
298
299
300
301
302
# File 'lib/mimi/core/manifest.rb', line 294

def self.validate_manifest_hash(manifest_hash)
  invalid_keys = manifest_hash.keys.reject { |k| k.is_a?(Symbol) }
  unless invalid_keys.empty?
    raise ArgumentError,
      "Invalid manifest keys, Symbols are expected: #{invalid_keys.join(', ')}"
  end

  manifest_hash.each { |n, p| validate_manifest_key_properties(n, p) }
end

.validate_manifest_key_properties(name, properties) ⇒ Object

Validates configurable parameter properties

Parameters:

  • name (Symbol)

    name of the parameter

  • properties (Hash)

    configurable parameter properties



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/mimi/core/manifest.rb', line 309

def self.validate_manifest_key_properties(name, properties)
  raise 'Hash as properties is expected' unless properties.is_a?(Hash)
  if properties[:desc]
    raise ArgumentError, 'String as :desc is expected' unless properties[:desc].is_a?(String)
  end
  if properties[:type]
    if properties[:type].is_a?(Array)
      if properties[:type].any? { |v| !v.is_a?(String) }
        raise ArgumentError, 'Array<String> is expected as enumeration :type'
      end
    elsif !ALLOWED_TYPES.include?(properties[:type].to_s)
      raise ArgumentError, "Unrecognised type '#{properties[:type]}'"
    end
  end
  if properties.keys.include?(:hidden)
    if !properties[:hidden].is_a?(TrueClass) && !properties[:hidden].is_a?(FalseClass)
      raise ArgumentError, 'Invalid type for :hidden, true or false is expected'
    end
  end
  if properties.keys.include?(:const)
    if !properties[:const].is_a?(TrueClass) && !properties[:const].is_a?(FalseClass)
      raise ArgumentError, 'Invalid type for :const, true or false is expected'
    end
  end
  if properties[:const] && !properties.keys.include?(:default)
    raise ArgumentError, ':default is required if :const is set'
  end
rescue ArgumentError => e
  raise ArgumentError, "Invalid manifest: invalid properties for '#{name}': #{e}"
end

Instance Method Details

#apply(values) ⇒ Hash<Symbol,Object>

Accepts the values, performs the validation and applies the manifest, responding with a Hash of parameters and processed values.

Performs the type coercion of values to the specified configurable parameter type.

  • type: :string, value: anything => String
  • type: :integer, value: 1 or '1' => 1
  • type: :decimal, value: 1, 1.0 (BigDecimal), '1' or '1.0' => 1.0 (BigDecimal)
  • type: :boolean, value: true or 'true' => true
  • type: :json, value: { 'id' => 123 } or '{"id":123}' => { 'id' => 123 }
  • type: ['a', 'b', 'c'] , value: 'a' => 'a'

Example:

manifest = Mimi::Core::Manifest.new(
  var1: {},
  var2: :integer,
  var3: :decimal,
  var4: :boolean,
  var5: :json,
  var6: ['a', 'b', 'c']
)

manifest.apply(
  var1: 'var1.value',
  var2: '2',
  var3: '3',
  var4: 'false',
  var5: '[{"name":"value"}]',
  var6: 'c'
)
# =>
# {
#   var1: 'var1.value', var2: 2, var3: 3.0, var4: false,
#   var5: [{ 'name' => 'value '}], var6: 'c'
# }

If :default is specified for the parameter and the value is not provided, the default value is returned, converted to corresponding type, if it is not nil

manifest = Mimi::Core::Manifest.new(var1: { default: nil })
manifest.apply({}) # => { var1: nil }

Values for parameters not defined in the manifest are ignored:

manifest = Mimi::Core::Manifest.new(var1: {})
manifest.apply(var1: '123', var2: '456') # => { var1: '123' }

Configurable parameters defined as :const cannot be changed by provided values:

manifest = Mimi::Core::Manifest.new(var1: { default: 1, const: true })
manifest.apply(var1: 2) # => { var1: 1 }

If a configurable parameter defined as required in the manifest (has no :default) and the provided values have no corresponding key, an ArgumentError is raised:

manifest = Mimi::Core::Manifest.new(var1: {})
manifest.apply({}) # => ArgumentError "Required value for 'var1' is missing"

If a value provided for the configurable parameter is incompatible (different type, wrong format etc), an ArgumentError is raised:

manifest = Mimi::Core::Manifest.new(var1: { type: :integer })
manifest.apply(var1: 'abc') # => ArgumentError "Invalid value provided for 'var1'"

During validation of provided values, all violations are detected and reported in a single ArgumentError:

manifest = Mimi::Core::Manifest.new(var1: { type: :integer }, var2: {})
manifest.apply(var1: 'abc') # =>
# ArgumentError "Invalid value provided for 'var1'. Required value for 'var2' is missing."

Parameters:

Returns:

  • (Hash<Symbol,Object>)

    where key is the parameter name, value is the parameter value

Raises:

  • (ArgumentError)

    on validation errors, missing values etc



280
281
282
283
284
# File 'lib/mimi/core/manifest.rb', line 280

def apply(values)
  raise ArgumentError, 'Hash is expected as values' unless values.is_a?(Hash)
  validate_values(values)
  process_values(values)
end

#keysArray<Symbol>

Returns a list of configurable parameter names

Returns:



149
150
151
# File 'lib/mimi/core/manifest.rb', line 149

def keys
  @manifest.keys
end

#merge(another) ⇒ Mimi::Core::Manifest

Returns a copy of current Manifest merged with another Hash or Manifest

Parameters:

Returns:



178
179
180
181
182
183
184
185
186
187
# File 'lib/mimi/core/manifest.rb', line 178

def merge(another)
  if !another.is_a?(Mimi::Core::Manifest) && !another.is_a?(Hash)
    raise ArgumentError 'Another Mimi::Core::Manifest or Hash is expected'
  end
  another_hash = another.is_a?(Hash) ? another.deep_dup : another.to_h.deep_dup
  new_manifest_hash = @manifest.deep_merge(another_hash)
  new_manifest_hash = manifest_hash_canonical(new_manifest_hash)
  self.class.validate_manifest_hash(new_manifest_hash)
  self.class.new(new_manifest_hash)
end

#merge!(another) ⇒ Object

Merges current Manifest with another Hash or Manifest, modifies current Manifest in-place

Parameters:



169
170
171
# File 'lib/mimi/core/manifest.rb', line 169

def merge!(another)
  @manifest = merge(another).to_h
end

#required?(name) ⇒ true, false

Returns true if the configurable parameter is a required one

Parameters:

  • name (Symbol)

    the name of configurable parameter

Returns:

  • (true, false)

Raises:

  • (ArgumentError)


158
159
160
161
162
163
# File 'lib/mimi/core/manifest.rb', line 158

def required?(name)
  raise ArgumentError, 'Symbol is expected as the parameter name' unless name.is_a?(Symbol)
  props = @manifest[name]
  return false unless props # parameter is not required if it is not declared
  !props.keys.include?(:default)
end

#to_hHash

Returns a Hash representation of the Manifest

Returns:



141
142
143
# File 'lib/mimi/core/manifest.rb', line 141

def to_h
  @manifest
end

#to_yamlString

Returns a YAML representation of the manifest

Returns:

  • (String)


359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/mimi/core/manifest.rb', line 359

def to_yaml
  out = []
  to_h.each do |k, v|
    next if v[:hidden]
    out << "#{k}:"
    vy = v[:desc].nil? ? '# nil' : v[:desc].inspect # value to yaml
    out << "  desc: #{vy}" if v.key?(:desc) && !v[:desc].empty?
    if v[:type].is_a?(Array)
      out << '  type:'
      v[:type].each { |t| out << "    - #{t}" }
    elsif v[:type] != :string
      out << "  type: #{v[:type]}"
    end
    out << '  const: true' if v[:const]
    vy = v[:default].nil? ? '# nil' : v[:default].inspect # value to yaml
    out << "  default: #{vy}" if v.key?(:default)
    out << ''
  end
  out.join("\n")
end