module Nakischema

Constants

Error

Public Class Methods

validate(object, schema, path = []) click to toggle source
# File lib/nakischema.rb, line 4
def self.validate object, schema, path = []
  raise_with_path = lambda do |msg, _path = path|
    raise Error.new "#{msg}#{" (at #{_path})" unless _path.empty?}"
  end
  # TODO: maybe move '(at ...)' to the beginning
  case schema
  when NilClass, TrueClass, FalseClass, String, Symbol ; raise_with_path.call "expected #{schema.inspect} != #{object.inspect}" unless schema == object
  # TODO: maybe deprecate the NilClass, TrueClass, FalseClass since they can be asserted via the next case branch
  when Class                                           ; raise_with_path.call "expected #{schema        } != #{object.class  }" unless schema === object
  when Regexp                                          ; raise_with_path.call "expected #{schema        } != #{object.inspect}" unless schema === object
  when Range                                           ; raise_with_path.call "expected #{schema        } != #{object        }" unless schema.include? object
  when Hash
    raise_with_path.call "expected Hash != #{object.class}" unless object.is_a? Hash unless (schema.keys & %i{ keys each_key each_value }).empty?
    raise_with_path.call "expected Array != #{object.class}" unless object.is_a? Array unless (schema.keys & %i{ size }).empty?
    schema.each do |k, v|
      case k
      when :size ; raise_with_path.call "expected explicit size #{v} != #{object.size}" unless v.include? object.size
      # when Fixnum
      #   raise_with_path.call "expected Array != #{object.class}" unless object.is_a? Array
      #   validate object[k], v, [*path, :"##{k}"]
      when :keys ; validate object.keys, v, [*path, :keys]
      when :values ; validate object.values, v, [*path, :values]
      when :keys_sorted ; validate object.keys.sort, v, [*path, :keys_sorted]
      when :hash_opt ; v.each{ |k, v| validate object.fetch(k), v, [*path, k] if object.key? k }
      when :hash_req ; v.each{ |k, v| validate object.fetch(k), v, [*path, k] }
      when :hash     ; raise_with_path.call "expected Hash != #{object.class}" unless object.is_a? Hash
                       raise_with_path.call "expected implicit keys #{v.keys} != #{object.keys.sort}" unless v.keys.sort == object.keys.sort
                       v.each{ |k, v| validate object.fetch(k), v, [*path, k] }
      when :each_key ; object.keys.each_with_index{ |k, i| validate k, v, [*path, :"key##{i}"] }
      when :each_value ; object.values.each_with_index{ |v_, i| validate v_, v, [*path, :"value##{i}"] }
      when :each
        raise_with_path.call "expected iterable != #{object.class}" unless object.respond_to? :each_with_index
        object.each_with_index{ |e, i| validate e, v, [*path, :"##{i}"] }
      # when :case
      #   raise_with_path.call "expected at least one of #{v.size} cases to match the #{object.inspect}" if v.map.with_index do |(k, v), i|
      #     next if begin
      #       validate object, k
      #       nil
      #     rescue Error => e
      #       e
      #     end
      #     validate object, v, [*path, :"case##{i}"]
      #     true
      #   end.none?
      when :assertions
        v.each_with_index do |assertion, i|
          begin
            raise Error.new "custom assertion failed" unless assertion.call object, [*path, :"assertion##{i}"]
          rescue Error => e
            raise_with_path.call e, [*path, :"assertion##{i}"]
          end
        end
      else
        raise_with_path.call "unsupported rule #{k.inspect}"
      end
    end
  when Array
    if schema.map(&:class) == [Array]
      raise_with_path.call "expected Array != #{object.class}" unless object.is_a? Array
      raise_with_path.call "expected implicit size #{schema[0].size} != #{object.size} for #{object.inspect}" unless schema[0].size == object.size
      object.zip(schema[0]).each_with_index{ |(o, v), i| validate o, v, [*path, :"##{i}"] }
    else
      results = schema.lazy.with_index.map do |v, i|
        # raise_with_path.call "unsupported nested Array" if v.is_a? Array
        begin
          validate object, v, [*path, :"variant##{i}"]
          nil
        rescue Error => e
          e
        end
      end
      raise Error.new "expected at least one of #{schema.size} rules to match the #{object.inspect}, errors:\n" +
        results.force.compact.map{ |_| _.to_s.gsub(/^/, "  ") }.join("\n") if results.all?
    end
  else
    raise_with_path.call "unsupported rule class #{schema.class}"
  end
end
validate_oga_xml(object, schema, path = []) click to toggle source
# File lib/nakischema.rb, line 83
def self.validate_oga_xml object, schema, path = []
  raise_with_path = lambda do |msg, _path = path|
    raise Error.new "#{msg}#{" (at #{_path})" unless _path.empty?}"
  end
  case schema
  when String, Regexp ; raise_with_path.call "expected #{schema.inspect} != #{object.inspect}" unless schema === object
  when Hash
    schema.each do |k, v|
      case k
      when :size ; raise_with_path.call "expected explicit size #{v} != #{object.size}" unless v.include? object.size
      when :text ; raise_with_path.call "expected text #{v.inspect} != #{object.text.inspect}" unless v == object.text
      when :each ; raise_with_path.call "expected iterable != #{object.class}" unless object.respond_to? :each_with_index
                   object.each_with_index{ |e, i| validate_oga_xml e, v, [*path, :"##{i}"] }
      when :exact ; children = object.xpath "./*"
                    names = children.map(&:name).uniq
                    raise_with_path.call "expected implicit children #{v.keys} != #{names}" unless v.keys == names
                    v.each{ |k, v| validate_oga_xml children.select{ |_| _.name == k }, v, [*path, k] }
      when :children   ; v.each{ |k, v| validate_oga_xml object.xpath(k.start_with?("./") ? k : "./#{k}"), v, [*path, k] }
      when :attr_exact ; names = object.attributes.map &:name
                         raise_with_path.call "expected implicit attributes #{v.keys} != #{names}" unless v.keys == names
                         v.each{ |k, v| validate_oga_xml object[k], v, [*path, k] }
      when :attr_req   ; v.each{ |k, v| validate_oga_xml object[k], v, [*path, k] }
      when :assertions
        v.each_with_index do |assertion, i|
          begin
            raise Error.new "custom assertion failed" unless assertion.call object, [*path, :"assertion##{i}"]
          rescue Error => e
            raise_with_path.call e, [*path, :"assertion##{i}"]
          end
        end
      else
        raise_with_path.call "unsupported rule #{k.inspect}"
      end
    end
  when Array
    if schema.map(&:class) == [Array]
      raise_with_path.call "expected implicit size #{schema[0].size} != #{object.size} for #{object.inspect}" unless schema[0].size == object.size
      object.zip(schema[0]).each_with_index{ |(o, v), i| validate_oga_xml o, v, [*path, :"##{i}"] }
    else
      results = schema.lazy.with_index.map do |v, i|
        begin
          validate_oga_xml object, v, [*path, :"variant##{i}"]
          nil
        rescue Error => e
          e
        end
      end
      raise Error.new "expected at least one of #{schema.size} rules to match the #{object.inspect}, errors:\n" +
        results.force.compact.map{ |_| _.to_s.gsub(/^/, "  ") }.join("\n") if results.all?
    end
  else
    raise_with_path.call "unsupported rule class #{schema.class}"
  end
end