mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-15 15:29:48 +08:00
This change adds the ability to validate more complex structures in the Ruby service contracts. Contracts were limited to flat structures, which is fine most of the time, but it can become tedious when managing lots of attributes. With this new feature, contracts like this one can be defined: ```ruby attribute :channel_id, :integer attribute :record, :hash do attribute :id, :integer attribute :created_at, :datetime attribute :enabled, :boolean end attribute :user, :hash do attribute :username, :string attribute :age, :integer validates :username, presence: true end attribute :items, :array do attribute :name, :string validates :name, presence: true end validates :channel_id, presence: true ``` Two nested types are available: `hash` and `array`. Each block creates a new contract, meaning coercions, validations and callbacks are available as usual.
83 lines
2 KiB
Ruby
83 lines
2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Service::ContractBase
|
|
include ActiveModel::API
|
|
include ActiveModel::Attributes
|
|
include ActiveModel::AttributeMethods
|
|
include ActiveModel::Validations::Callbacks
|
|
|
|
delegate :slice, :merge, to: :to_hash
|
|
|
|
class << self
|
|
def attribute(name, cast_type = nil, **options, &block)
|
|
return super(name, cast_type, **options) unless block_given?
|
|
|
|
nested_contract_class =
|
|
Class.new(Service::ContractBase) do
|
|
define_singleton_method(:name) { "#{name}_contract".classify }
|
|
class_eval(&block)
|
|
end
|
|
super(
|
|
name,
|
|
Service::NestedContractType.new(
|
|
contract_class: nested_contract_class,
|
|
nested_type: cast_type || :hash,
|
|
),
|
|
**options,
|
|
)
|
|
end
|
|
end
|
|
|
|
def initialize(*args, options: nil, **kwargs)
|
|
@__options__ = options
|
|
kwargs.deep_symbolize_keys!.slice!(*self.class.attribute_names.map(&:to_sym))
|
|
super(*args, **kwargs)
|
|
end
|
|
|
|
def options
|
|
@__options__
|
|
end
|
|
|
|
def to_hash
|
|
attributes.symbolize_keys.deep_transform_values do
|
|
_1.is_a?(Service::ContractBase) ? _1.to_hash : _1
|
|
end
|
|
end
|
|
|
|
def raw_attributes
|
|
@attributes.values_before_type_cast
|
|
end
|
|
|
|
def valid?(context = nil)
|
|
[super, nested_attributes_valid?].all?
|
|
end
|
|
|
|
private
|
|
|
|
def nested_attributes_valid?
|
|
nested_attributes.map(&method(:validate_nested)).all?
|
|
end
|
|
|
|
def nested_attributes
|
|
@attributes.each_value.select { _1.type.is_a?(Service::NestedContractType) && _1.value }
|
|
end
|
|
|
|
def validate_nested(attribute)
|
|
Array
|
|
.wrap(attribute.value)
|
|
.map
|
|
.with_index do |contract, index|
|
|
next true if contract.valid?
|
|
import_nested_errors(contract, attribute, index)
|
|
false
|
|
end
|
|
.all?
|
|
end
|
|
|
|
def import_nested_errors(contract, attribute, index)
|
|
array_index = "[#{index}]" if attribute.value.is_a?(Array)
|
|
contract.errors.each do |error|
|
|
errors.import(error, attribute: :"#{attribute.name}#{array_index}.#{error.attribute}")
|
|
end
|
|
end
|
|
end
|