mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 18:30:26 -06:00
a84757fd91
Why this change? For a schema like this: ``` schema = { name: "section", properties: { category_property: { type: "categories", required: true, }, }, } ``` When the value of the property is set to an empty array, we are not raising an error which we should because the property is marked as required.
290 lines
7.4 KiB
Ruby
290 lines
7.4 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
|
|
|
|
def property_values_of_type(type)
|
|
fetch_property_values_of_type(@properties, @object, type)
|
|
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", "topic", "post", "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 "categories", "groups"
|
|
value.is_a?(Array) && value.all? { |id| id.is_a?(Integer) }
|
|
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", "upload", "post"
|
|
if !valid_ids(type).include?(value)
|
|
add_error(property_name, :"not_valid_#{type}_value")
|
|
return false
|
|
end
|
|
when "tags", "categories", "groups"
|
|
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, :"#{type}_value_not_valid_min", min:)
|
|
return false
|
|
end
|
|
|
|
if (max = validations&.dig(:max)) && value.length > max
|
|
add_error(property_name, :"#{type}_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].blank?
|
|
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 = {
|
|
"categories" => {
|
|
klass: Category,
|
|
},
|
|
"topic" => {
|
|
klass: Topic,
|
|
},
|
|
"post" => {
|
|
klass: Post,
|
|
},
|
|
"groups" => {
|
|
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
|