class Dry::Types::Schema

The built-in Hash type can be defined in terms of keys and associated types its values can contain. Such definitions are named {Schema}s and defined as lists of {Key} types.

@see Dry::Types::Schema::Key

{Schema} evaluates default values for keys missing in input hash

@see Dry::Types::Default#evaluate @see Dry::Types::Default::Callable#evaluate

{Schema} implements Enumerable using its keys as collection.

@api public

Schema is a hash with explicit member types defined

@api public

Constants

NO_TRANSFORM
SYMBOLIZE_KEY

Attributes

keys[R]

@return [Array]

name_key_map[R]

@return [Hash[Symbol, Dry::Types::Schema::Key]]

transform_key[R]

@return [#call]

Public Class Methods

new(_primitive, **options) click to toggle source

@param [Class] _primitive @param [Hash] options

@option options [Array] :keys @option options [String] :key_transform_fn

@api private

Calls superclass method
# File lib/dry/types/schema.rb, line 41
def initialize(_primitive, **options)
  @keys = options.fetch(:keys)
  @name_key_map = keys.each_with_object({}) do |key, idx|
    idx[key.name] = key
  end

  key_fn = options.fetch(:key_transform_fn, NO_TRANSFORM)

  @transform_key = Dry::Types::FnContainer[key_fn]

  super
end

Public Instance Methods

apply(hash, options = EMPTY_HASH) click to toggle source

@param [Hash] hash

@option options [Boolean] :skip_missing If true don’t raise error if on missing keys @option options [Boolean] :resolve_defaults If false default value

won't be evaluated for missing key

@return [Hash{Symbol => Object}]

@api public

# File lib/dry/types/schema.rb, line 80
def apply(hash, options = EMPTY_HASH)
  call_unsafe(hash, options)
end
call_safe(hash, options = EMPTY_HASH) { || ... } click to toggle source

@param [Hash] hash

@return [Hash{Symbol => Object}]

@api private

# File lib/dry/types/schema.rb, line 68
def call_safe(hash, options = EMPTY_HASH)
  resolve_safe(coerce(hash) { return yield }, options) { return yield }
end
call_unsafe(hash, options = EMPTY_HASH) click to toggle source

@param [Hash] hash

@return [Hash{Symbol => Object}]

@api private

# File lib/dry/types/schema.rb, line 59
def call_unsafe(hash, options = EMPTY_HASH)
  resolve_unsafe(coerce(hash), options)
end
clear() click to toggle source

Empty schema with the same options

@return [Schema]

@api public

# File lib/dry/types/schema.rb, line 298
def clear
  with(keys: EMPTY_ARRAY)
end
constrained?() click to toggle source

@return [Boolean]

@api public

# File lib/dry/types/schema.rb, line 270
def constrained?
  true
end
each(&block) click to toggle source

Iterate over each key type

@return [Array<Dry::Types::Schema::Key>,Enumerator]

@api public

# File lib/dry/types/schema.rb, line 229
def each(&block)
  keys.each(&block)
end
key(name, fallback = Undefined, &block) click to toggle source

Fetch key type by a key name

Behaves as ::Hash#fetch

@overload key(name, fallback = Undefined)

@param [Symbol] name Key name
@param [Object] fallback Optional fallback, returned if key is missing
@return [Dry::Types::Schema::Key,Object] key type or fallback if key is not in schema

@overload key(name, &block)

@param [Symbol] name Key name
@param [Proc] block Fallback block, runs if key is missing
@return [Dry::Types::Schema::Key,Object] key type or block value if key is not in schema

@api public

# File lib/dry/types/schema.rb, line 259
def key(name, fallback = Undefined, &block)
  if Undefined.equal?(fallback)
    name_key_map.fetch(name, &block)
  else
    name_key_map.fetch(name, fallback)
  end
end
key?(name) click to toggle source

Whether the schema has the given key

@param [Symbol] name Key name

@return [Boolean]

@api public

# File lib/dry/types/schema.rb, line 240
def key?(name)
  name_key_map.key?(name)
end
lax() click to toggle source

@return [Lax]

@api public

# File lib/dry/types/schema.rb, line 277
def lax
  Lax.new(schema(keys.map(&:lax)))
end
merge(other) click to toggle source

Merge given schema keys into current schema

A new instance is returned.

@param other [Schema] schema @return [Schema]

@api public

# File lib/dry/types/schema.rb, line 289
def merge(other)
  schema(other.keys)
end
schema(keys_or_map) click to toggle source

@overload schema(type_map, meta = EMPTY_HASH)

@param [{Symbol => Dry::Types::Nominal}] type_map
@param [Hash] meta
@return [Dry::Types::Schema]

@overload schema(keys)

@param [Array<Dry::Types::Schema::Key>] key List of schema keys
@param [Hash] meta
@return [Dry::Types::Schema]

@api public

# File lib/dry/types/schema.rb, line 213
def schema(keys_or_map)
  if keys_or_map.is_a?(::Array)
    new_keys = keys_or_map
  else
    new_keys = build_keys(keys_or_map)
  end

  keys = merge_keys(self.keys, new_keys)
  Schema.new(primitive, **options, keys: keys, meta: meta)
end
strict(strict = true) click to toggle source

Make the schema intolerant to unknown keys

@return [Schema]

@api public

# File lib/dry/types/schema.rb, line 172
def strict(strict = true) # rubocop:disable Style/OptionalBooleanParameter
  with(strict: strict)
end
strict?() click to toggle source

Whether the schema rejects unknown keys

@return [Boolean]

@api public

# File lib/dry/types/schema.rb, line 163
def strict?
  options.fetch(:strict, false)
end
to_ast(meta: true) click to toggle source

@param meta [Boolean] Whether to dump the meta to the AST

@return [Array] An AST representation

@api public

# File lib/dry/types/schema.rb, line 149
def to_ast(meta: true)
  [
    :schema,
    [keys.map { |key| key.to_ast(meta: meta) },
     options.slice(:key_transform_fn, :type_transform_fn, :strict),
     meta ? self.meta : EMPTY_HASH]
  ]
end
transform_keys?() click to toggle source

Whether the schema transforms input keys

@return [Boolean]

@api public

# File lib/dry/types/schema.rb, line 198
def transform_keys?
  !options[:key_transform_fn].nil?
end
try(input) { |failure| ... } click to toggle source

@param input [Hash] hash

@yieldparam [Failure] failure @yieldreturn [Result]

@return [Logic::Result] @return [Object] if coercion fails and a block is given

@api public rubocop:disable Metrics/AbcSize rubocop:disable Metrics/PerceivedComplexity

# File lib/dry/types/schema.rb, line 95
def try(input)
  if primitive?(input)
    success = true
    output = {}
    result = {}

    input.each do |key, value|
      k = @transform_key.(key)
      type = @name_key_map[k]

      if type
        key_result = type.try(value)
        result[k] = key_result
        output[k] = key_result.input
        success &&= key_result.success?
      elsif strict?
        success = false
      end
    end

    if output.size < keys.size
      resolve_missing_keys(output, options) do
        success = false
      end
    end

    success &&= primitive?(output)

    if success
      failure = nil
    else
      error = CoercionError.new("#{input} doesn't conform schema", meta: result)
      failure = failure(output, error)
    end
  else
    failure = failure(input, CoercionError.new("#{input} must be a hash"))
  end

  if failure.nil?
    success(output)
  elsif block_given?
    yield(failure)
  else
    failure
  end
end
with_key_transform(proc = nil, &block) click to toggle source

Inject a key transformation function

@param [#call,nil] proc @param [#call,nil] block

@return [Schema]

@api public

# File lib/dry/types/schema.rb, line 184
def with_key_transform(proc = nil, &block)
  fn = proc || block

  raise ArgumentError, "a block or callable argument is required" if fn.nil?

  handle = Dry::Types::FnContainer.register(fn)
  with(key_transform_fn: handle)
end

Private Instance Methods

merge_keys(*keys) click to toggle source

@param [Array<Dry::Types::Schema::Keys>] keys

@return [Dry::Types::Schema]

@api private

# File lib/dry/types/schema.rb, line 309
def merge_keys(*keys)
  keys
    .flatten(1)
    .each_with_object({}) { |key, merged| merged[key.name] = key }
    .values
end
missing_key(key) click to toggle source

@return [MissingKeyError]

@api private

# File lib/dry/types/schema.rb, line 405
def missing_key(key)
  MissingKeyError.new(key)
end
resolve_missing_keys(hash, options) { || ... } click to toggle source

Try to add missing keys to the hash

@api private

# File lib/dry/types/schema.rb, line 373
def resolve_missing_keys(hash, options) # rubocop:disable Metrics/PerceivedComplexity
  skip_missing = options.fetch(:skip_missing, false)
  resolve_defaults = options.fetch(:resolve_defaults, true)

  keys.each do |key|
    next if hash.key?(key.name)

    if key.default? && resolve_defaults
      hash[key.name] = key.call_unsafe(Undefined)
    elsif key.required? && !skip_missing
      if block_given?
        return yield
      else
        raise missing_key(key.name)
      end
    end
  end
end
resolve_safe(hash, options = EMPTY_HASH) { || ... } click to toggle source

Validate and coerce a hash. Call a block and halt on any error

@api private

@return [Hash]

# File lib/dry/types/schema.rb, line 351
def resolve_safe(hash, options = EMPTY_HASH, &block)
  result = {}

  hash.each do |key, value|
    k = @transform_key.(key)
    type = @name_key_map[k]

    if type
      result[k] = type.call_safe(value, &block)
    elsif strict?
      yield
    end
  end

  resolve_missing_keys(result, options, &block) if result.size < keys.size

  result
end
resolve_unsafe(hash, options = EMPTY_HASH) click to toggle source

Validate and coerce a hash. Raise an exception on any error

@api private

@return [Hash]

# File lib/dry/types/schema.rb, line 321
def resolve_unsafe(hash, options = EMPTY_HASH)
  result = {}

  hash.each do |key, value|
    k = @transform_key.(key)
    type = @name_key_map[k]

    if type
      begin
        result[k] = type.call_unsafe(value)
      rescue ConstraintError => e
        raise SchemaError.new(type.name, value, e.result)
      rescue CoercionError => e
        raise SchemaError.new(type.name, value, e.message)
      end
    elsif strict?
      raise unexpected_keys(hash.keys)
    end
  end

  resolve_missing_keys(result, options) if result.size < keys.size

  result
end
unexpected_keys(hash_keys) click to toggle source

@param hash_keys [Array<Symbol>]

@return [UnknownKeysError]

@api private

# File lib/dry/types/schema.rb, line 397
def unexpected_keys(hash_keys)
  extra_keys = hash_keys.map(&transform_key) - name_key_map.keys
  UnknownKeysError.new(extra_keys)
end