# frozen_string_literal: truerequire'dry/initializer'require'dry/schema/constants'require'dry/schema/config'require'dry/schema/compiler'require'dry/schema/types'require'dry/schema/macros'require'dry/schema/processor'require'dry/schema/key_map'require'dry/schema/key_coercer'require'dry/schema/value_coercer'require'dry/schema/rule_applier'moduleDrymoduleSchema# The schema definition DSL class## The DSL is exposed by:# - `Schema.define`# - `Schema.Params`# - `Schema.JSON`# - `Schema::Params.define` - use with sub-classes# - `Schema::JSON.define` - use with sub-classes## @example class-based definition# class UserSchema < Dry::Schema::Params# define do# required(:name).filled# required(:age).filled(:integer, gt: 18)# end# end## user_schema = UserSchema.new# user_schema.(name: 'Jame', age: 21)## @example instance-based definition shortcut# UserSchema = Dry::Schema.Params do# required(:name).filled# required(:age).filled(:integer, gt: 18)# end## UserSchema.(name: 'Jame', age: 21)## @api publicclassDSLTypes=Schema::TypesextendDry::Initializerinclude::Dry::Equalizer(:options)# @return [Compiler] The rule compiler objectoption:compiler,default: ->{Compiler.new}# @return [Compiler] The type of the processor (Params, JSON, or a custom sub-class)option:processor_type,default: ->{Processor}# @return [Array] An array with macros defined within the DSLoption:macros,default: ->{EMPTY_ARRAY.dup}# @return [Compiler] A key=>type map defined within the DSLoption:types,default: ->{EMPTY_HASH.dup}# @return [DSL] An optional parent DSL object that will be used to merge keys and rulesoption:parent,optional: true# @return [Config] Configuration object exposed via `#configure` methodoption:config,optional: true,default: proc{parent?parent.config.dup:Config.new}# Build a new DSL object and evaluate provided block## @param [Hash] options# @option options [Class] :processor The processor type (`Params`, `JSON` or a custom sub-class)# @option options [Compiler] :compiler An instance of a rule compiler (must be compatible with `Schema::Compiler`) (optional)# @option options [DSL] :parent An instance of the parent DSL (optional)# @option options [Config] :config A configuration object (optional)## @see Schema.define# @see Schema.Params# @see Schema.JSON# @see Processor.define## @return [DSL]## @api publicdefself.new(**options,&block)dsl=superdsl.instance_eval(&block)ifblockdslend# Provide customized configuration for your schema## @example# Dry::Schema.define do# configure do |config|# config.messages.backend = :i18n# end# end## @see Config## @return [DSL]## @api publicdefconfigure(&block)config.configure(&block)selfend# Return a macro with the provided name## @param [Symbol] name## @return [Macros::Core]## @api publicdef[](name)macros.detect{|macro|macro.name.equal?(name)}end# Define a required key## @example# required(:name).filled## required(:age).value(:integer)## required(:user_limit).value(:integer, gt?: 0)## required(:tags).filled { array? | str? }## @param [Symbol] name The key name## @return [Macros::Required]## @api publicdefrequired(name,&block)key(name,macro: Macros::Required,&block)end# Define an optional key## This works exactly the same as `required` except that if a key is not present# rules will not be applied## @see DSL#required## @param [Symbol] name The key name## @return [Macros::Optional]## @api publicdefoptional(name,&block)key(name,macro: Macros::Optional,&block)end# A generic method for defining keys## @param [Symbol] name The key name# @param [Class] macro The macro sub-class (ie `Macros::Required` or any other `Macros::Key` subclass)## @return [Macros::Key]## @api publicdefkey(name,macro:,&block)raiseArgumentError,"Key +#{name}+ is not a symbol"unlessname.is_a?(::Symbol)set_type(name,Types::Any)macro=macro.new(name: name,compiler: compiler,schema_dsl: self,filter_schema_dsl: filter_schema_dsl)macro.value(&block)ifblockmacros<<macromacroend# Build a processor based on DSL's definitions## @return [Processor, Params, JSON]## @api privatedefcallsteps=[key_coercer]steps<<filter_schema.rule_applieriffilter_rules?steps<<value_coercer<<rule_applierprocessor_type.new(schema_dsl: self,steps: steps)end# Cast this DSL into a rule object## @return [RuleApplier]defto_rulecall.to_ruleend# A shortcut for defining an array type with a member## @example# required(:tags).filled(array[:string])## @return [Dry::Types::Array::Member]## @api publicdefarray->member_type{type_registry['array'].of(resolve_type(member_type))}end# Return type schema used by the value coercer## @return [Dry::Types::Safe]## @api privatedeftype_schemaschema=type_registry['hash'].schema(types).laxparent?parent.type_schema.schema(schema.to_a):schemaend# Return a new DSL instance using the same processor type## @return [Dry::Types::Safe]## @api privatedefnew(options=EMPTY_HASH,&block)self.class.new(options.merge(processor_type: processor_type,config: config),&block)end# Set a type for the given key name## @param [Symbol] name The key name# @param [Symbol, Array<Symbol>, Dry::Types::Type] spec The type spec or a type object## @return [Dry::Types::Safe]## @api privatedefset_type(name,spec)type=resolve_type(spec)meta={required: false,maybe: type.optional?}types[name]=type.meta(meta)end# Resolve type object from the provided spec## @param [Symbol, Array<Symbol>, Dry::Types::Type] spec## @return [Dry::Types::Type]## @api privatedefresolve_type(spec)casespecwhen::Dry::Types::Typethenspecwhen::Arraythenspec.map{|s|resolve_type(s)}.reduce(:|)elsetype_registry[spec]endend# @api privatedeffilter_schemafilter_schema_dsl.callend# Build an input schema DSL used by `filter` API## @see Macros::Value#filter## @api privatedeffilter_schema_dsl@filter_schema_dsl||=new(parent: parent_filter_schema)end# Check if any filter rules were defined## @api privatedeffilter_rules?(instance_variable_defined?('@filter_schema_dsl')&&!filter_schema_dsl.macros.empty?)||parent&.filter_rules?endprotected# Build a rule applier## @return [RuleApplier]## @api protecteddefrule_applierRuleApplier.new(rules,config: config.finalize!)end# Build rules from defined macros## @see #rule_applier## @api protecteddefrulesparent_rules.merge(macros.map{|m|[m.name,m.to_rule]}.to_h.compact)end# Build a key map from defined types## @api protecteddefkey_map(types=self.types)keys=types.map{|key,type|key_spec(key,type)}km=KeyMap.new(keys)ifkey_map_typekm.public_send(key_map_type)elsekmendendprivate# @api privatedefparent_filter_schemareturnunlessparentparent.filter_schemaifparent.filter_rules?end# Build a key coercer## @return [KeyCoercer]## @api privatedefkey_coercerKeyCoercer.symbolized(key_map+parent_key_map)end# Build a value coercer## @return [ValueCoercer]## @api privatedefvalue_coercerValueCoercer.new(type_schema)end# Return type registry configured by the processor type## @api privatedeftype_registry@type_registry||=TypeRegistry.new(config.types,processor_type.config.type_registry_namespace)end# Return key map type configured by the processor type## @api privatedefkey_map_typeprocessor_type.config.key_map_typeend# Build a key spec needed by the key map## @api privatedefkey_spec(name,type)iftype.respond_to?(:keys){name=>key_map(type.name_key_map)}elsiftype.respond_to?(:member)kv=key_spec(name,type.member)kv.equal?(name)?name:kv.flatten(1)elsenameendend# @api privatedefparent_rulesparent&.rules||EMPTY_HASHend# @api privatedefparent_key_mapparent&.key_map||EMPTY_ARRAYendendendend