mirror of
https://github.com/discourse/discourse.git
synced 2025-02-05 19:11:13 +00:00
86b2e3aa3e
Why this change? While working on the tag selector for the theme object editor, I realised that there is an extremely high possibility that users might want to select more than one tag. By supporting the ability to select more than one tag, it also means that we get support for a single tag for free as well. What does this change do? 1. Change `type: tag` to `type: tags` and support `min` and `max` validations for `type: tags`. 2. Fix the `<SchemaThemeSetting::Types::Tags>` component to support the `min` and `max` validations
284 lines
7.2 KiB
Ruby
284 lines
7.2 KiB
Ruby
# 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
|