# frozen_string_literal: true class ThemeSettingsObjectValidator class << self def validate_objects(schema:, objects:) error_messages = [] objects.each_with_index do |object, index| humanize_error_messages( self.new(schema: schema, object: object).validate, index:, error_messages:, ) end error_messages end private def humanize_error_messages(errors, index:, error_messages:) errors.each do |property_json_pointer, error_details| error_messages.push(*error_details.humanize_messages("/#{index}#{property_json_pointer}")) end end end class ThemeSettingsObjectErrors def initialize @errors = [] end def add_error(error, i18n_opts = {}) @errors << ThemeSettingsObjectError.new(error, i18n_opts) end def humanize_messages(property_json_pointer) @errors.map { |error| error.humanize_messages(property_json_pointer) } end def full_messages @errors.map(&:error_message) end end class ThemeSettingsObjectError def initialize(error, i18n_opts = {}) @error = error @i18n_opts = i18n_opts end def humanize_messages(property_json_pointer) I18n.t( "themes.settings_errors.objects.humanize_#{@error}", @i18n_opts.merge(property_json_pointer:), ) end def error_message I18n.t("themes.settings_errors.objects.#{@error}", @i18n_opts) end end def initialize(schema:, object:, json_pointer_prefix: "", errors: {}, valid_ids_lookup: {}) @object = object.with_indifferent_access @schema_name = schema[:name] @properties = schema[:properties] @errors = errors @json_pointer_prefix = json_pointer_prefix @valid_ids_lookup = valid_ids_lookup end def validate @properties.each do |property_name, property_attributes| if property_attributes[:type] == "objects" validate_child_objects( @object[property_name], property_name:, schema: property_attributes[:schema], ) else validate_property(property_name, property_attributes) end end @errors end private def validate_child_objects(objects, property_name:, schema:) return if objects.blank? objects.each_with_index do |object, index| self .class .new( schema:, object:, valid_ids_lookup:, json_pointer_prefix: "#{@json_pointer_prefix}#{property_name}/#{index}/", errors: @errors, ) .validate end end def validate_property(property_name, property_attributes) return if property_attributes[:required] && !is_property_present?(property_name) return if !has_valid_property_value_type?(property_attributes, property_name) !has_valid_property_value?(property_attributes, property_name) end def has_valid_property_value_type?(property_attributes, property_name) value = @object[property_name] type = property_attributes[:type] return true if (value.nil? && type != "enum") is_value_valid = case type when "string" value.is_a?(String) when "integer", "category", "topic", "post", "group", "upload" value.is_a?(Integer) when "float" value.is_a?(Float) || value.is_a?(Integer) when "boolean" [true, false].include?(value) when "enum" property_attributes[:choices].include?(value) when "tags" value.is_a?(Array) && value.all? { |tag| tag.is_a?(String) } else add_error(property_name, :invalid_type, type:) return false end if is_value_valid true else add_error(property_name, "not_valid_#{type}_value", property_attributes) false end end def has_valid_property_value?(property_attributes, property_name) validations = property_attributes[:validations] type = property_attributes[:type] value = @object[property_name] return true if value.nil? case type when "topic", "category", "upload", "post", "group" if !valid_ids(type).include?(value) add_error(property_name, :"not_valid_#{type}_value") return false end when "tags" if !Array(value).to_set.subset?(valid_ids(type)) add_error(property_name, :"not_valid_#{type}_value") return false end if (min = validations&.dig(:min)) && value.length < min add_error(property_name, :tags_value_not_valid_min, min:) return false end if (max = validations&.dig(:max)) && value.length > max add_error(property_name, :tags_value_not_valid_max, max:) return false end when "string" if (min = validations&.dig(:min_length)) && value.length < min add_error(property_name, :string_value_not_valid_min, min:) return false end if (max = validations&.dig(:max_length)) && value.length > max add_error(property_name, :string_value_not_valid_max, max:) return false end if validations&.dig(:url) && !UrlHelper.relaxed_parse(value) add_error(property_name, :string_value_not_valid_url) return false end when "integer", "float" if (min = validations&.dig(:min)) && value < min add_error(property_name, :number_value_not_valid_min, min:) return false end if (max = validations&.dig(:max)) && value > max add_error(property_name, :number_value_not_valid_max, max:) return false end end true end def is_property_present?(property_name) if @object[property_name].nil? add_error(property_name, :required) false else true end end def add_error(property_name, key, i18n_opts = {}) pointer = json_pointer(property_name) @errors[pointer] ||= ThemeSettingsObjectErrors.new @errors[pointer].add_error(key, i18n_opts) end def json_pointer(property_name) "/#{@json_pointer_prefix}#{property_name}" end def valid_ids_lookup @valid_ids_lookup ||= {} end TYPE_TO_MODEL_MAP = { "category" => { klass: Category, }, "topic" => { klass: Topic, }, "post" => { klass: Post, }, "group" => { klass: Group, }, "upload" => { klass: Upload, }, "tags" => { klass: Tag, column: :name, }, } private_constant :TYPE_TO_MODEL_MAP def valid_ids(type) valid_ids_lookup[type] ||= begin column = TYPE_TO_MODEL_MAP[type][:column] || :id Set.new( TYPE_TO_MODEL_MAP[type][:klass].where( column => fetch_property_values_of_type(@properties, @object, type), ).pluck(column), ) end end def fetch_property_values_of_type(properties, object, type) values = Set.new properties.each do |property_name, property_attributes| if property_attributes[:type] == type values.merge(Array(object[property_name])) elsif property_attributes[:type] == "objects" object[property_name]&.each do |child_object| values.merge( fetch_property_values_of_type( property_attributes[:schema][:properties], child_object, type, ), ) end end end values end end