module Rzo::App::ConfigValidation

Mix-in module providing configuration validation methods and safety checking. The goal is to provide useful feedback to the end user in the situation where ~/.rizzo.yaml is configured to point at directories which do not exist, have missing keys, etc… rubocop:disable Metrics/ModuleLength

Constants

CHECKS_PERSONAL_CONFIG

The checks to execute, in order. Each method must return nil if there are no issues found. Otherwise, the check should return either one, or an array of Issue instances.

CHECKS_REPO_CONFIG
RZO_PERSONAL_CONFIG_SCHEMA
Rizzo configuration schema for the personal configuration file at

~/.rizzo.yaml. Minimum necessary to load the complete configuration from all control repositories.

RZO_REPO_CONFIG_SCHEMA
Rizzo complete configuration schema.  This should move to a JSON file outside

the code.

Public Instance Methods

compute_issues(checks, config) click to toggle source

Compute Issues given a config map (base or complete), and an Array of methods to execute.

@param [Array<Symbol>] checks the method identifiers to execute, passing

config.  These methods must return nil (no issue found), an Issue
instance, or Array<Instance> for multiple issues found.

@param [Hash] config the config hash, either a base configuration or a

fully merged configuration.

@return [Array<Issue>] Array of issue instances, or an empty array if no

issues found with the config.
# File lib/rzo/app/config_validation.rb, line 150
def compute_issues(checks, config)
  ctx = self
  checks.each_with_object([]) do |mth, ary|
    debug "Checking config for #{mth} issues"
    if issue = ctx.send(mth, config)
      # May get back an Array<Issue> or one Issue
      ary.concat([*issue])
    end
  end
end
validate_complete_config!(config) click to toggle source

Validate a complete loaded configuration. This is distinct from a base configuration in that the YAML files in each control repository have already been merged, in order, on top of the base configuration originating at ~/.rizzo.yaml. This implements safety checking. These methods are expected to execute within the context of a Rzo::App::Subcommand instance, therefore log methods and the parsed configuration are assumed to be available.

The approach is to collect an Array of Issue instances. If issues are found, control is handed off to validate_inform! to inform the user of the issues and potentially abort the program.

@param [Hash] config the config hash, fully merged by load_config!

# File lib/rzo/app/config_validation.rb, line 190
def validate_complete_config!(config)
  issues = compute_issues(CHECKS_REPO_CONFIG, config)
  if issues.empty?
    debug 'No issues detected with the complete, merged configuration.'
  else
    validate_inform!(issues)
  end
end
validate_control_repos(config) click to toggle source

Validate the top level “control_repos” key, which should have a value of Array<String> where each string value is a fully qualified path.

@return [Issue,nil] Issue found, or nil if no issues found.

# File lib/rzo/app/config_validation.rb, line 242
def validate_control_repos(config)
  if repos = config['control_repos']
    return Issue.new('Top level key "control_repos" must have an Array value') unless repos.is_a? Array
  else
    return Issue.new('Top level key "control_repos" is not specified.  It must be an Array of paths to your control repos.')
  end
  repos.each_with_object([]) do |pth, ary|
    if issue = validate_existence(pth, '#/control_repos')
      ary << issue
    end
  end
end
validate_defaults_key(config) click to toggle source

Validate the configuration has a top level key named “defaults” and the value is a Hash map. rubocop:disable Metrics/MethodLength

@return [Issue,nil] Issue found, or nil if no issues found.

# File lib/rzo/app/config_validation.rb, line 220
def validate_defaults_key(config)
  if defaults = config['defaults']
    return Issue.new('Top level key "defaults" must have a Hash value') unless defaults.is_a? Hash
  else
    return Issue.new('Configuration does not contain top level "defaults" key')
  end
  if pth = defaults['bootstrap_repo_path']
    return Issue.new('#/defaults/bootstrap_repo_path is not a String') unless pth.is_a? String
  else
    return Issue.new 'Configuration "defaults" value does not contain a '\
      '"bootstrap_repo_path" key.  For example, '\
      '{"defaults":{"bootstrap_repo_path":"/tmp/foo"}}'
  end
  validate_existence(pth, '#/defaults/bootstrap_repo_path value of ')
end
validate_existence(path, prefix = '') click to toggle source

Given a string, validate it's a fully qualified path, readable, and a git directory.

@return [Issue,Array<Issue>,nil] nil if no issues found, or one or more

Issue instances.
# File lib/rzo/app/config_validation.rb, line 261
def validate_existence(path, prefix = '')
  pn = Pathname.new(path)
  git = pn + '.git'
  return Issue.new("#{prefix}#{pn} is not an absolute path.  It must be fully qualified, not relative") unless pn.absolute?
  return Issue.new("#{prefix}#{pn} is not a directory.  Has it been cloned?") unless pn.directory?
  return Issue.new("#{prefix}#{pn} is not readable.  Are permissions correct?") unless pn.readable?
  return Issue.new("#{prefix}#{git} does not exist.  Has #{git.dirname} been cloned properly?") unless git.directory?
end
validate_inform!(issues) click to toggle source

Inform the user about issues found and exit the program. The top level exception handler is not expected to display much information on validation errors. This method is expected to provide the helpful guidance.

@param [Array<Issue>] issues Array of issues. Each hash must have at least a key named `:message`

# File lib/rzo/app/config_validation.rb, line 293
def validate_inform!(issues)
  if opts[:validate]
    msg = "Validation issues found with #{opts[:config]}"
    exc = ErrorAndExit.new(msg, 2)
    exc.log_fatal = issues.each_with_object([]) { |i, a| a << i.to_s }
    raise exc
  else
    issues.each { |i| log.warn(i.to_s) }
  end
end
validate_personal_config!(config) click to toggle source

Validate a personal configuration, typically originating from ~/.rizzo.yaml. This configuration is necessary to build a complete control repo configuration using the top level control repo. This validation focuses on the minimum necessary configuration to bootstrap the complete configuration, primarily the repo locations and existence.

# File lib/rzo/app/config_validation.rb, line 167
def validate_personal_config!(config)
  issues = compute_issues(CHECKS_PERSONAL_CONFIG, config)
  if issues.empty?
    debug 'No issues detected with the personal configuration.'
  else
    validate_inform!(issues)
  end
end
validate_personal_schema(config) click to toggle source

Validate the personal configuration, focus on ensuring the rest of the configuration can load properly.

@return [Issue,Array<Issue>,nil] nil if no issues found, or one or more

Issue instances.
# File lib/rzo/app/config_validation.rb, line 276
def validate_personal_schema(config)
  if JSON::Validator.validate(RZO_PERSONAL_CONFIG_SCHEMA, config)
    debug 'No schema violations found in personal configuration file.'
    return nil
  else
    err_msgs = JSON::Validator.fully_validate(RZO_PERSONAL_CONFIG_SCHEMA, config)
    return err_msgs.map { |msg| Issue.new("Personal config problem: #{msg}") }
  end
end
validate_schema(config) click to toggle source

Validate using [json-schema](github.com/ruby-json-schema/json-schema)

@return [Issue,nil] Issue found, or nil if no issues found.

# File lib/rzo/app/config_validation.rb, line 204
def validate_schema(config)
  if JSON::Validator.validate(RZO_REPO_CONFIG_SCHEMA, config)
    debug 'No schema violations found in loaded config.'
    return nil
  else
    err_msgs = JSON::Validator.fully_validate(RZO_REPO_CONFIG_SCHEMA, config)
    return err_msgs.map { |msg| Issue.new("Schema violation: #{msg}") }
  end
end