class Frise::Validator

Checks if a pre-loaded config object conforms to a schema file.

The validate and validate_at static methods read schema files and validates config objects against the parsed schema. They can optionally be initialized with a set of user-defined validators that can be used in the schema files for custom validations.

Attributes

errors[R]

Public Class Methods

new(root, validators = nil) click to toggle source
# File lib/frise/validator.rb, line 16
def initialize(root, validators = nil)
  @root = root
  @validators = validators
  @errors = []
end
parse_symbols(obj) click to toggle source
# File lib/frise/validator.rb, line 156
def self.parse_symbols(obj)
  case obj
  when Array then obj.map { |e| parse_symbols(e) }
  when Hash then Hash[obj.map { |k, v| [parse_symbols(k), parse_symbols(v)] }]
  when String then obj.start_with?('$') ? obj[1..-1].to_sym : obj
  else obj
  end
end
validate(config, schema_file, options = {}) click to toggle source
# File lib/frise/validator.rb, line 190
def self.validate(config, schema_file, options = {})
  validate_obj_at(config, [], Parser.parse(schema_file) || { allow_unknown_keys: true }, **options)
end
validate_at(config, at_path, schema_file, options = {}) click to toggle source
# File lib/frise/validator.rb, line 194
def self.validate_at(config, at_path, schema_file, options = {})
  validate_obj_at(config, at_path, Parser.parse(schema_file) || { allow_unknown_keys: true }, **options)
end
validate_obj(config, schema, options = {}) click to toggle source
# File lib/frise/validator.rb, line 165
def self.validate_obj(config, schema, options = {})
  validate_obj_at(config, [], schema, **options)
end
validate_obj_at(config, at_path, schema, path_prefix: nil, validators: nil, print: nil, fatal: nil, raise_error: nil) click to toggle source
# File lib/frise/validator.rb, line 169
def self.validate_obj_at(config, at_path, schema, path_prefix: nil, validators: nil, print: nil, fatal: nil, raise_error: nil)
  schema = parse_symbols(schema)
  at_path.reverse.each { |key| schema = { key => schema, :allow_unknown_keys => true } }

  validator = Validator.new(config, validators)
  validator.validate_object((path_prefix || []).join('.'), config, schema)

  if validator.errors.any?
    if print
      puts "#{validator.errors.length} config error(s) found:"
      validator.errors.each do |error|
        puts " - #{error}"
      end
    end

    exit 1 if fatal
    raise ValidationError.new(validator.errors), 'Invalid configuration' if raise_error
  end
  validator.errors
end

Public Instance Methods

add_validation_error(path, msg) click to toggle source
# File lib/frise/validator.rb, line 29
def add_validation_error(path, msg)
  logged_path = path.empty? ? '<root>' : path
  @errors << "At #{logged_path}: #{msg}"
end
get_expected_types(full_schema) click to toggle source
# File lib/frise/validator.rb, line 64
def get_expected_types(full_schema)
  type_key = full_schema.fetch(:type, 'Hash')
  allowed_types = %w[Hash Array String Integer Float Object]
  return [Object.const_get(type_key)] if allowed_types.include?(type_key)
  return [TrueClass, FalseClass] if type_key == 'Boolean'
  raise "Invalid expected type in schema: #{type_key}"
end
get_full_schema(schema) click to toggle source
# File lib/frise/validator.rb, line 34
def get_full_schema(schema)
  case schema
  when Hash then
    default_type = schema[:enum] || schema[:one_of] ? 'Object' : 'Hash'
    { type: default_type }.merge(schema)
  when Symbol then { type: 'Object', validate: schema }
  when Array
    if schema.size == 1
      { type: 'Array', all: schema[0] }
    else
      (raise "Invalid schema: #{schema.inspect}")
    end
  when String
    if schema.end_with?('?')
      { type: schema[0..-2], optional: true }
    else
      { type: schema }
    end
  else raise "Invalid schema: #{schema.inspect}"
  end
end
validate_custom(full_schema, obj, path) click to toggle source
# File lib/frise/validator.rb, line 82
def validate_custom(full_schema, obj, path)
  if full_schema[:validate]
    begin
      @validators.method(full_schema[:validate]).call(@root, obj)
    rescue StandardError => e
      add_validation_error(path, e.message)
    end
  end
  true
end
validate_enum(full_schema, obj, path) click to toggle source
# File lib/frise/validator.rb, line 93
def validate_enum(full_schema, obj, path)
  if full_schema[:enum] && !full_schema[:enum].include?(obj)
    add_validation_error(path, "invalid value #{obj.inspect}. " \
      "Accepted values are #{full_schema[:enum].map(&:inspect).join(', ')}")
    return false
  end
  true
end
validate_object(path, obj, schema) click to toggle source
# File lib/frise/validator.rb, line 142
def validate_object(path, obj, schema)
  full_schema = get_full_schema(schema)

  return unless validate_optional(full_schema, obj, path)
  return unless validate_type(full_schema, obj, path)
  return unless validate_custom(full_schema, obj, path)
  return unless validate_enum(full_schema, obj, path)
  return unless validate_one_of(full_schema, obj, path)

  processed_keys = Set.new
  return unless validate_spec_keys(full_schema, obj, path, processed_keys)
  validate_remaining_keys(full_schema, obj, path, processed_keys)
end
validate_one_of(full_schema, obj, path) click to toggle source
# File lib/frise/validator.rb, line 102
def validate_one_of(full_schema, obj, path)
  if full_schema[:one_of]
    full_schema[:one_of].each do |schema_opt|
      opt_validator = Validator.new(@root, @validators)
      opt_validator.validate_object(path, obj, schema_opt)
      return true if opt_validator.errors.empty?
    end
    add_validation_error(path, "#{obj.inspect} does not match any of the possible schemas")
    return false
  end
  true
end
validate_optional(full_schema, obj, path) click to toggle source
# File lib/frise/validator.rb, line 56
def validate_optional(full_schema, obj, path)
  if obj.nil?
    add_validation_error(path, 'missing required value') unless full_schema[:optional]
    return false
  end
  true
end
validate_remaining_keys(full_schema, obj, path, processed_keys) click to toggle source
# File lib/frise/validator.rb, line 124
def validate_remaining_keys(full_schema, obj, path, processed_keys)
  expected_types = get_expected_types(full_schema)
  if expected_types.size == 1 && expected_types[0].ancestors.member?(Enumerable)
    hash = obj.is_a?(Hash) ? obj : Hash[obj.map.with_index { |x, i| [i, x] }]
    hash.each do |key, value|
      validate_object(path, key, full_schema[:all_keys]) if full_schema[:all_keys] && !key.is_a?(Symbol)

      next if processed_keys.member? key
      if full_schema[:all]
        validate_object(path.empty? ? key : "#{path}.#{key}", value, full_schema[:all])
      elsif !full_schema[:allow_unknown_keys]
        add_validation_error(path, "unknown key: #{key}")
      end
    end
  end
  true
end
validate_spec_keys(full_schema, obj, path, processed_keys) click to toggle source
# File lib/frise/validator.rb, line 115
def validate_spec_keys(full_schema, obj, path, processed_keys)
  full_schema.each do |spec_key, spec_value|
    next if spec_key.is_a?(Symbol)
    validate_object(path.empty? ? spec_key : "#{path}.#{spec_key}", obj[spec_key], spec_value)
    processed_keys << spec_key
  end
  true
end
validate_type(full_schema, obj, path) click to toggle source
# File lib/frise/validator.rb, line 72
def validate_type(full_schema, obj, path)
  expected_types = get_expected_types(full_schema)
  unless expected_types.any? { |typ| obj.is_a?(typ) }
    type_key = full_schema.fetch(:type, 'Hash')
    add_validation_error(path, "expected #{type_key}, found #{widened_class(obj)}")
    return false
  end
  true
end
widened_class(obj) click to toggle source
# File lib/frise/validator.rb, line 22
def widened_class(obj)
  class_name = obj.class.to_s
  return 'Boolean' if %w[TrueClass FalseClass].include? class_name
  return 'Integer' if %w[Fixnum Bignum].include? class_name
  class_name
end