tomdalling/rschema

View on GitHub
lib/rschema/schemas/variable_hash.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

module RSchema
  module Schemas
    #
    # A schema that matches variable-sized `Hash` objects, where the keys are
    # _not_ known ahead of time.
    #
    # @example A hash of integers to strings
    #     schema = RSchema.define { variable_hash(_Integer => _String) }
    #     schema.valid?({ 5 => "hello", 7 => "world" }) #=> true
    #     schema.valid?({}) #=> true
    #
    class VariableHash
      attr_reader :key_schema, :value_schema

      def initialize(key_schema, value_schema)
        @key_schema = key_schema
        @value_schema = value_schema
      end

      def call(value, options)
        return not_a_hash_result(value) unless value.is_a?(Hash)

        accumulate_elements(value, options).to_result
      end

      def with_wrapped_subschemas(wrapper)
        self.class.new(
          wrapper.wrap(key_schema),
          wrapper.wrap(value_schema),
        )
      end

      private

      def not_a_hash_result(value)
        Result.failure(
          Error.new(
            schema: self,
            value: value,
            symbolic_name: :not_a_hash,
          ),
        )
      end

      def accumulate_elements(value, options)
        Accumulation.new.tap do |accumulation|
          value.each do |key, subvalue|
            key_result = key_schema.call(key, options)
            subvalue_result = value_schema.call(subvalue, options)
            accumulation.merge!(key, key_result, subvalue_result)
            break if options.fail_fast? && accumulation.failed?
          end
        end
      end

      # @!visibility private
      class Accumulation
        def initialize
          @key_errors = {}
          @value_errors = {}
          @validated_hash = {}
          @failed = false
        end

        def merge!(key, key_result, value_result)
          if key_result.invalid?
            @key_errors[key] = key_result.error
            @failed = true
          elsif value_result.invalid?
            @value_errors[key] = value_result.error
            @failed = true
          else
            @validated_hash[key_result.value] = value_result.value
          end
        end

        def failed?
          @failed
        end

        def to_result
          if failed?
            Result.failure(keys: @key_errors, values: @value_errors)
          else
            Result.success(@validated_hash)
          end
        end
      end
    end
  end
end