221 lines
5.8 KiB
Ruby
221 lines
5.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "dry/schema"
|
|
require "dry/schema/messages"
|
|
require "dry/schema/path"
|
|
require "dry/schema/key_map"
|
|
|
|
require "dry/validation/constants"
|
|
require "dry/validation/macros"
|
|
require "dry/validation/schema_ext"
|
|
|
|
module Dry
|
|
module Validation
|
|
class Contract
|
|
# Contract's class interface
|
|
#
|
|
# @see Contract
|
|
#
|
|
# @api public
|
|
module ClassInterface
|
|
include Macros::Registrar
|
|
|
|
# @api private
|
|
def inherited(klass)
|
|
super
|
|
klass.instance_variable_set("@config", config.dup)
|
|
end
|
|
|
|
# Configuration
|
|
#
|
|
# @example
|
|
# class MyContract < Dry::Validation::Contract
|
|
# config.messages.backend = :i18n
|
|
# end
|
|
#
|
|
# @return [Config]
|
|
#
|
|
# @api public
|
|
def config
|
|
@config ||= Validation::Config.new
|
|
end
|
|
|
|
# Return macros registered for this class
|
|
#
|
|
# @return [Macros::Container]
|
|
#
|
|
# @api public
|
|
def macros
|
|
config.macros
|
|
end
|
|
|
|
# Define a params schema for your contract
|
|
#
|
|
# This type of schema is suitable for HTTP parameters
|
|
#
|
|
# @return [Dry::Schema::Params,NilClass]
|
|
# @see https://dry-rb.org/gems/dry-schema/params/
|
|
#
|
|
# @api public
|
|
def params(*external_schemas, &block)
|
|
define(:Params, external_schemas, &block)
|
|
end
|
|
|
|
# Define a JSON schema for your contract
|
|
#
|
|
# This type of schema is suitable for JSON data
|
|
#
|
|
# @return [Dry::Schema::JSON,NilClass]
|
|
# @see https://dry-rb.org/gems/dry-schema/json/
|
|
#
|
|
# @api public
|
|
def json(*external_schemas, &block)
|
|
define(:JSON, external_schemas, &block)
|
|
end
|
|
|
|
# Define a plain schema for your contract
|
|
#
|
|
# This type of schema does not offer coercion out of the box
|
|
#
|
|
# @return [Dry::Schema::Processor,NilClass]
|
|
# @see https://dry-rb.org/gems/dry-schema/
|
|
#
|
|
# @api public
|
|
def schema(*external_schemas, &block)
|
|
define(:schema, external_schemas, &block)
|
|
end
|
|
|
|
# Define a rule for your contract
|
|
#
|
|
# @example using a symbol
|
|
# rule(:age) do
|
|
# failure('must be at least 18') if values[:age] < 18
|
|
# end
|
|
#
|
|
# @example using a path to a value and a custom predicate
|
|
# rule('address.street') do
|
|
# failure('please provide a valid street address') if valid_street?(values[:street])
|
|
# end
|
|
#
|
|
# @return [Rule]
|
|
#
|
|
# @api public
|
|
def rule(*keys, &block)
|
|
ensure_valid_keys(*keys) if __schema__
|
|
|
|
Rule.new(keys: keys, block: block).tap do |rule|
|
|
rules << rule
|
|
end
|
|
end
|
|
|
|
# A shortcut that can be used to define contracts that won't be reused or inherited
|
|
#
|
|
# @example
|
|
# my_contract = Dry::Validation::Contract.build do
|
|
# params do
|
|
# required(:name).filled(:string)
|
|
# end
|
|
# end
|
|
#
|
|
# my_contract.call(name: "Jane")
|
|
#
|
|
# @return [Contract]
|
|
#
|
|
# @api public
|
|
def build(options = EMPTY_HASH, &block)
|
|
Class.new(self, &block).new(**options)
|
|
end
|
|
|
|
# @api private
|
|
def __schema__
|
|
@__schema__ if defined?(@__schema__)
|
|
end
|
|
|
|
# Return rules defined in this class
|
|
#
|
|
# @return [Array<Rule>]
|
|
#
|
|
# @api private
|
|
def rules
|
|
@rules ||= EMPTY_ARRAY
|
|
.dup
|
|
.concat(superclass.respond_to?(:rules) ? superclass.rules : EMPTY_ARRAY)
|
|
end
|
|
|
|
# Return messages configured for this class
|
|
#
|
|
# @return [Dry::Schema::Messages]
|
|
#
|
|
# @api private
|
|
def messages
|
|
@messages ||= Schema::Messages.setup(config.messages)
|
|
end
|
|
|
|
private
|
|
|
|
# @api private
|
|
def ensure_valid_keys(*keys)
|
|
valid_paths = key_map.to_dot_notation.map { |value| Schema::Path[value] }
|
|
|
|
invalid_keys = keys
|
|
.map { |key|
|
|
[key, Schema::Path[key]]
|
|
}
|
|
.flat_map { |(key, path)|
|
|
if (last = path.last).is_a?(Array)
|
|
last.map { |last_key|
|
|
path_key = [*path.to_a[0..-2], last_key]
|
|
[path_key, Schema::Path[path_key]]
|
|
}
|
|
else
|
|
[[key, path]]
|
|
end
|
|
}
|
|
.reject { |(_, path)|
|
|
valid_paths.any? { |valid_path| valid_path.include?(path) }
|
|
}
|
|
.map(&:first)
|
|
|
|
return if invalid_keys.empty?
|
|
|
|
raise InvalidKeysError, <<~STR.strip
|
|
#{name}.rule specifies keys that are not defined by the schema: #{invalid_keys.inspect}
|
|
STR
|
|
end
|
|
|
|
# @api private
|
|
def key_map
|
|
__schema__.key_map
|
|
end
|
|
|
|
# @api private
|
|
def core_schema_opts
|
|
{parent: superclass&.__schema__, config: config}
|
|
end
|
|
|
|
# @api private
|
|
def define(method_name, external_schemas, &block)
|
|
return __schema__ if external_schemas.empty? && block.nil?
|
|
|
|
unless __schema__.nil?
|
|
raise ::Dry::Validation::DuplicateSchemaError, "Schema has already been defined"
|
|
end
|
|
|
|
schema_opts = core_schema_opts
|
|
|
|
schema_opts.update(parent: external_schemas) if external_schemas.any?
|
|
|
|
case method_name
|
|
when :schema
|
|
@__schema__ = Schema.define(**schema_opts, &block)
|
|
when :Params
|
|
@__schema__ = Schema.Params(**schema_opts, &block)
|
|
when :JSON
|
|
@__schema__ = Schema.JSON(**schema_opts, &block)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|