Class: Frise::Validator

Inherits:
Object
  • Object
show all
Defined in:
lib/frise/validator.rb

Overview

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.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root, validators = nil) ⇒ Validator

Returns a new instance of Validator.



16
17
18
19
20
21
22
# File 'lib/frise/validator.rb', line 16

def initialize(root, validators = nil)
  super()

  @root = root
  @validators = validators
  @errors = []
end

Instance Attribute Details

#errorsObject (readonly)

Returns the value of attribute errors.



14
15
16
# File 'lib/frise/validator.rb', line 14

def errors
  @errors
end

Class Method Details

.parse_symbols(obj) ⇒ Object



167
168
169
170
171
172
173
174
# File 'lib/frise/validator.rb', line 167

def self.parse_symbols(obj)
  case obj
  when Array then obj.map { |e| parse_symbols(e) }
  when Hash then obj.to_h { |k, v| [parse_symbols(k), parse_symbols(v)] }
  when String then obj.start_with?('$') ? obj[1..].to_sym : obj
  else obj
  end
end

.validate(config, schema_file, options = {}) ⇒ Object



211
212
213
# File 'lib/frise/validator.rb', line 211

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 = {}) ⇒ Object



215
216
217
# File 'lib/frise/validator.rb', line 215

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 = {}) ⇒ Object



176
177
178
# File 'lib/frise/validator.rb', line 176

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) ⇒ Object



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/frise/validator.rb', line 180

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

Instance Method Details

#add_validation_error(path, msg) ⇒ Object



31
32
33
34
# File 'lib/frise/validator.rb', line 31

def add_validation_error(path, msg)
  logged_path = path.empty? ? '<root>' : path
  @errors << "At #{logged_path}: #{msg}"
end

#get_expected_types(full_schema) ⇒ Object



66
67
68
69
70
71
72
# File 'lib/frise/validator.rb', line 66

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) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/frise/validator.rb', line 36

def get_full_schema(schema)
  case schema
  when Hash
    default_type = schema[:enum] || schema[:one_of] || schema.key?(:constant) ? '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_constant(full_schema, obj, path) ⇒ Object



117
118
119
120
121
122
123
# File 'lib/frise/validator.rb', line 117

def validate_constant(full_schema, obj, path)
  if full_schema.key?(:constant) && full_schema[:constant] != obj
    add_validation_error(path, "invalid value #{obj.inspect}. " \
                               "The only accepted value is #{full_schema[:constant]}")
  end
  true
end

#validate_custom(full_schema, obj, path) ⇒ Object



84
85
86
87
88
89
90
91
92
93
# File 'lib/frise/validator.rb', line 84

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) ⇒ Object



95
96
97
98
99
100
101
102
# File 'lib/frise/validator.rb', line 95

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) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/frise/validator.rb', line 152

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)
  return unless validate_constant(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) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/frise/validator.rb', line 104

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) ⇒ Object



58
59
60
61
62
63
64
# File 'lib/frise/validator.rb', line 58

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) ⇒ Object



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/frise/validator.rb', line 134

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 : obj.map.with_index { |x, i| [i, x] }.to_h
    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) ⇒ Object



125
126
127
128
129
130
131
132
# File 'lib/frise/validator.rb', line 125

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) ⇒ Object



74
75
76
77
78
79
80
81
82
# File 'lib/frise/validator.rb', line 74

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) ⇒ Object



24
25
26
27
28
29
# File 'lib/frise/validator.rb', line 24

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