# frozen_string_literal: true module ActiveDryForm class BaseForm attr_accessor :data, :parent_form, :errors, :base_errors attr_reader :record, :validator, :attributes def initialize(record: nil, params: nil) @attributes = {} self.params = params if params self.record = record if record @errors = {} @base_errors = [] end def record=(value) @record = if value.is_a?(Hash) hr = HashRecord.new hr.replace(value) hr.define_methods hr else value end end def params=(params) param_key = self.class::NAMESPACE.param_key form_params = params[param_key] || params[param_key.to_sym] || params if form_params.is_a?(::ActionController::Parameters) unless ActiveDryForm.config.allow_action_controller_params message = 'in `params` use `request.parameters` instead of `params` or set `allow_action_controller_params` to `true` in config' raise ParamsNotAllowedError, message end form_params = form_params.to_unsafe_h end self.attributes = form_params end def attributes=(attrs) attrs.each do |attr, v| next if !ActiveDryForm.config.strict_param_keys && !respond_to?(:"#{attr}=") public_send(:"#{attr}=", v) end end def errors_full_messages return if errors.blank? errors.flat_map do |field, errors| case errors when Array "#{t(model_name.i18n_key, field)}: #{errors.join(',')}" when Hash errors.map do |k, v| case k when Integer nested_key, nested_errors = v.to_a.first "#{t(field, nested_key)} (#{k + 1}): #{nested_errors.join(',')}" when Symbol "#{t(field, k)}: #{v.join(',')}" end end end end end def t(*keys) str_keys = keys.join('.') I18n.t("helpers.label.#{str_keys}", default: :"activerecord.attributes.#{str_keys}") end def persisted? record&.persisted? end def model_name self.class::NAMESPACE end def info(sub_key) { type: self.class::FIELDS_INFO.dig(:properties, sub_key, :format) || self.class::FIELDS_INFO.dig(:properties, sub_key, :type), required: self.class::FIELDS_INFO[:required].include?(sub_key.to_s), } end # ActionView::Helpers::Tags::Translator#human_attribute_name def to_model self end def to_key key = id [key] if key end # hidden field for nested association def id record&.id end # используется при генерации URL, когда record.persisted? def to_param id.to_s end def validate @validator = self.class::CURRENT_CONTRACT.call(attributes, { form: self, record: record }) @data = @validator.values.data @errors = @validator.errors.to_h @base_errors = @validator.errors.filter(:base?).map(&:to_s) @is_valid = @base_errors.empty? && @errors.empty? _deep_validate_nested end def valid? @is_valid end def self.human_attribute_name(field) I18n.t(field, scope: :"activerecord.attributes.#{self::NAMESPACE.i18n_key}") end def self.wrap(object) return object if object.is_a?(BaseForm) form = new form.attributes = object if object form end def [](key) public_send(key) end def []=(key, value) public_send(:"#{key}=", value) end def self.define_methods const_set :NESTED_FORM_KEYS, [] self::FIELDS_INFO[:properties].each do |key, value| define_method :"#{key}=" do |v| attributes[key] = _deep_transform_values_in_params!(v) end sub_klass = if value[:properties] || value.dig(:items, :properties) Class.new(BaseForm).tap do |klass| klass.const_set :NAMESPACE, ActiveModel::Name.new(nil, nil, key.to_s) klass.const_set :FIELDS_INFO, value[:items] || value klass.define_methods end elsif const_defined?(:CURRENT_CONTRACT) dry_type = self::CURRENT_CONTRACT.schema.schema_dsl.types[key] dry_type = dry_type.member if dry_type.respond_to?(:member) dry_type.primitive if dry_type.respond_to?(:primitive) end if sub_klass && sub_klass < BaseForm self::NESTED_FORM_KEYS << { type: sub_klass.const_defined?(:CURRENT_CONTRACT) ? :instance : :hash, namespace: key, is_array: value[:type] == 'array', } nested_key = key end if nested_key && value[:type] == 'array' define_method nested_key do nested_records = record.try(nested_key) || [] if attributes.key?(nested_key) attributes[nested_key].each_with_index do |nested_params, idx| attributes[nested_key][idx] = sub_klass.wrap(nested_params) attributes[nested_key][idx].record = nested_records[idx] attributes[nested_key][idx].parent_form = self attributes[nested_key][idx] end else attributes[nested_key] = nested_records.map do |nested_record| nested_form = sub_klass.new nested_form.record = nested_record nested_form.parent_form = self nested_form end end attributes[nested_key] end elsif nested_key define_method nested_key do attributes[nested_key] = sub_klass.wrap(attributes[nested_key]) attributes[nested_key].record = record.try(nested_key) attributes[nested_key].parent_form = self attributes[nested_key] end else define_method key do (data || attributes).fetch(key) { record.try(key) } end end end end private def _deep_transform_values_in_params!(object) return object if object.is_a?(BaseForm) case object when String object.strip.presence when Hash object.transform_values! { |value| _deep_transform_values_in_params!(value) } when Array object.filter_map { |e| _deep_transform_values_in_params!(e) } else object end end private def _deep_validate_nested self.class::NESTED_FORM_KEYS.each do |nested_info| namespace, type, is_array = nested_info.values_at(:namespace, :type, :is_array) next unless attributes.key?(namespace) nested_data = public_send(namespace) if type == :hash && is_array nested_data.each_with_index do |nested_form, idx| nested_form.errors = @errors.dig(namespace, idx) || {} nested_form.data = @data.dig(namespace, idx) end elsif type == :hash nested_data.errors = @errors[namespace] || {} nested_data.data = @data[namespace] elsif type == :instance && is_array @data[namespace] = [] nested_data.each_with_index do |nested_form, idx| nested_form.validate @data[namespace][idx] = nested_form.data @base_errors += nested_form.base_errors @is_valid &= nested_form.valid? end else nested_data.validate @data[namespace] = nested_data.data @base_errors += nested_data.base_errors @is_valid &= nested_data.valid? end end end class HashRecord < Hash def persisted? false end def id self[:id] || self['id'] end def define_methods keys.each do |key| define_singleton_method(key) { fetch(key) } end end end end end