Module: Insika::Plugin::Loader::ConfigSchema

Defined in:
lib/insika/plugin/loader.rb

Overview

Subset JSON Schema validator (no gem; swappable). validate(schema, value) -> [String] (empty = valid). An invalid schema AND a config that fails validation land in the same list (fail-closed per plugin).

Constant Summary collapse

KEYWORDS =
%w[type properties required additionalProperties enum].freeze
TYPES =
{
  "object" => [Hash], "array" => [Array], "string" => [String],
  "integer" => [Integer], "number" => [Numeric],
  "boolean" => [TrueClass, FalseClass], "null" => [NilClass]
}.freeze

Class Method Summary collapse

Class Method Details

.check_enum(schema, value, path) ⇒ Object



328
329
330
331
332
333
# File 'lib/insika/plugin/loader.rb', line 328

def self.check_enum(schema, value, path)
  return [] unless schema.key?("enum")
  return [] if Array(schema["enum"]).include?(value)

  ["#{path}: value #{value.inspect} not in enum"]
end

.check_object(schema, value, path) ⇒ Object



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/insika/plugin/loader.rb', line 335

def self.check_object(schema, value, path)
  return [] unless schema.key?("properties") || schema.key?("required") ||
                   schema.key?("additionalProperties")

  props = schema["properties"] || {}
  return ["#{path}: properties must be a Hash"] unless props.is_a?(Hash)
  return [] unless value.is_a?(Hash) # object keywords only apply to a Hash

  errors = []
  props.each { |k, sub| errors.concat(validate(sub, value[k], "#{path}.#{k}")) if value.key?(k) }
  Array(schema["required"]).each do |req|
    errors << "#{path}: missing required key: #{req}" unless value.key?(req)
  end
  if schema["additionalProperties"] == false && !(extra = value.keys - props.keys).empty?
    errors << "#{path}: keys not allowed: #{extra.join(', ')}"
  end
  errors
end

.check_type(schema, value, path) ⇒ Object



318
319
320
321
322
323
324
325
326
# File 'lib/insika/plugin/loader.rb', line 318

def self.check_type(schema, value, path)
  return [] unless schema.key?("type")

  klasses = TYPES[schema["type"]]
  return ["#{path}: unknown type: #{schema['type'].inspect}"] if klasses.nil?
  return [] if klasses.any? { |k| value.is_a?(k) }

  ["#{path}: expected #{schema['type']}, got #{value.class}"]
end

.validate(schema, value, path = "config") ⇒ Object



306
307
308
309
310
311
312
313
314
315
316
# File 'lib/insika/plugin/loader.rb', line 306

def self.validate(schema, value, path = "config")
  return ["#{path}: schema must be a Hash"] unless schema.is_a?(Hash)

  errors = []
  unknown = schema.keys - KEYWORDS
  errors << "#{path}: unsupported keyword(s): #{unknown.join(', ')}" unless unknown.empty?
  errors.concat(check_type(schema, value, path))
  errors.concat(check_enum(schema, value, path))
  errors.concat(check_object(schema, value, path))
  errors
end