Reputation: 2732
How can I validate a model with locale or fallthrough accessors with mobility gem using the accessors on creation or updating?
Using mobility gem: v1.0.5
c = Category.new(name_en: 'Cat. 1', name_de: 'Kat. 1')
c.valid?
# => false
c.errors
# Should output the individual validation errors for every single locale accessor.
Example:
=> #<ActiveModel::Errors:0x00007fb75c488ee0 ...
@messages={:name_en=>["has already been taken"]},
@details={:name_en=>[{:error=>:taken, :value=>"Cat. 1"}]}
Upvotes: 3
Views: 896
Reputation: 2732
Finally I have coded a plugin called ValidatesAccessors
. Feedback are welcome.
Config mobility
# config/initializers/mobility.rb
Mobility.configure do
# PLUGINS
plugins do
# ...
# One of both must be set.
fallthrough_accessors
locale_accessors
# Add ValidatesAccessors plugin
validates_accessors
end
end
Add mobility plugin code
# lib/mobility/plugins/validates_accessors.rb
module Mobility
# Mobility plugins.
module Plugins
# Adds accessor validation so you can easily validate accessor attributes like +name_en+.
module ValidatesAccessors
extend Plugin
default true
initialize_hook do |*names|
if options[:validates_accessors] &&
!options[:fallthrough_accessors] &&
!options[:locale_accessors]
warn 'The Validates Accessors plugin depends on Fallthrough Accessors or Locale '\
'Accessors being enabled, but both options are falsey'
else
options[:validates_accessors] = {} unless options[:validates_accessors].is_a?(Hash)
options[:validates_accessors].reverse_merge!(locales: Mobility.available_locales)
names.each do |name|
define_accessors_locales(name, options[:validates_accessors])
end
include Mobility::Validations::Concern
end
end
private
def define_accessors_locales(name, options)
module_eval <<~RUBY, __FILE__, __LINE__ + 1
def mobility_accessors_locales_#{name}
#{options[:locales].map(&:to_sym)}
end
RUBY
end
end
register_plugin(:validates_accessors, ValidatesAccessors)
end
end
Add mobility model concern code
# lib/mobility/validations/concern.rb
module Mobility
module Validations
# Mobility validations concern module.
module Concern
extend ActiveSupport::Concern
private
# This validation will perform a validation round against each mobility locales and add
# errors for mobility attributes names.
def validates_mobility_attributes
# Only validates mobility attributes from the admin locale.
return unless Mobility.locale.to_sym == I18n.locale.to_sym
mobility_errors = mobility_errors_for_attributes(self.class.mobility_attributes)
# Add translated attributes errors back to the object.
mobility_errors.each do |attribute, attribute_errors|
attribute_errors[:messages].zip(attribute_errors[:details]).each do |attribute_error|
errors.add(attribute,
attribute_error.second.delete(:error),
message: attribute_error.first, **attribute_error.second)
end
end
end
# Return all translated attributes with errors for the given locales, including their error
# messages.
def mobility_errors_for_attributes(attribute_names)
{}.tap do |mobility_errors|
locales = mobility_accessors_locales(attribute_names)
additional_locales = locales - [I18n.locale.to_sym]
# Track errors for current locale.
if locales.include?(I18n.locale.to_sym)
mobility_errors.merge!(mobility_errors_for_locale(attribute_names, I18n.locale.to_sym))
end
# Validates the given object against each locale except the current one and track their
# errors.
additional_locales.each do |locale|
Mobility.with_locale(locale) do
if invalid?
mobility_errors.merge!(mobility_errors_for_locale(attribute_names,
locale))
end
end
end
end
end
# Return all translated attributes with errors for the given locale, including their error
# messages.
def mobility_errors_for_locale(attribute_names, locale)
{}.tap do |mobility_errors|
attribute_names.each do |attribute|
next unless mobility_accessors_locales_for(attribute).include?(locale) &&
(messages = errors.messages.delete(attribute.to_sym)).present?
mobility_errors["#{attribute}_#{locale.to_s.underscore}".to_sym] = {
messages: messages,
details: errors.details.delete(attribute.to_sym)
}
end
end
end
# Define which locales to validate against all translated attribute.
def mobility_accessors_locales(attribute_names)
locales = []
attribute_names.each { |attribute| locales |= mobility_accessors_locales_for(attribute) }
locales
end
# Define which locales to validate against for a single translated attribute.
def mobility_accessors_locales_for(attribute)
locales = if try("mobility_accessors_locales_#{attribute}").respond_to?(:call)
try("mobility_accessors_locales_#{attribute}").call(self)
else
try("mobility_accessors_locales_#{attribute}")
end
locales || []
end
end
end
end
Use the plugin
class Category < ApplicationRecord
extend Mobility
translates :name # without config.
translates :name, validates_accessors: { locales: %i[en de fr] } # specifying locales to validate.
translates :description, validates_accessors: { locales: %i[en de] } # using another attribute.
translates :name, :description, validates_accessors: { locales: %i[en de fr] } # same config for both attributes.
# Add here your translated attributes validations.
validates :name, presence: true,
uniqueness: true,
length: { maximum: 5 }
# After validations add mobility ValidatesAccessors validation.
validate :validates_mobility_attributes
# ...
end
Example:
=> #<ActiveModel::Errors:0x00007fb75c488ee0 ...
@messages={:name_en=>["has already been taken"]},
@details={:name_en=>[{:error=>:taken, :value=>"Cat. 1"}]}
Upvotes: 2