mirror of
https://github.com/discourse/discourse-data-explorer.git
synced 2025-03-06 17:39:08 +00:00
The old float validation had several bugs. It will recognize strings like "a1.2" and "3.4b" as valid doubles, but will not recognize integers like "1234" as doubles. Also, since an empty string is not falsy in Ruby, it will recognize "Inf" as -Infinity. This commit fixes these issues
269 lines
8.3 KiB
Ruby
269 lines
8.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module ::DiscourseDataExplorer
|
|
class Parameter
|
|
attr_accessor :identifier, :type, :default, :nullable
|
|
|
|
def initialize(identifier, type, default, nullable, validate: true)
|
|
unless identifier
|
|
raise ValidationError.new("Parameter declaration error - identifier is missing")
|
|
end
|
|
|
|
raise ValidationError.new("Parameter declaration error - type is missing") unless type
|
|
|
|
# process aliases
|
|
type = type.to_sym
|
|
|
|
type = Parameter.type_aliases[type] if Parameter.type_aliases[type]
|
|
|
|
unless Parameter.types[type]
|
|
raise ValidationError.new("Parameter declaration error - unknown type #{type}")
|
|
end
|
|
|
|
@identifier = identifier
|
|
@type = type
|
|
@default = default
|
|
@nullable = nullable
|
|
begin
|
|
cast_to_ruby default if default.present? && validate
|
|
rescue ValidationError
|
|
raise ValidationError.new(
|
|
"Parameter declaration error - the default value is not a valid #{type}",
|
|
)
|
|
end
|
|
end
|
|
|
|
def to_hash
|
|
{ identifier: @identifier, type: @type, default: @default, nullable: @nullable }
|
|
end
|
|
|
|
def self.types
|
|
@types ||=
|
|
Enum.new(
|
|
# Normal types
|
|
:int,
|
|
:bigint,
|
|
:boolean,
|
|
:string,
|
|
:date,
|
|
:time,
|
|
:datetime,
|
|
:double,
|
|
# Selection help
|
|
:user_id,
|
|
:post_id,
|
|
:topic_id,
|
|
:category_id,
|
|
:group_id,
|
|
:badge_id,
|
|
# Arrays
|
|
:int_list,
|
|
:string_list,
|
|
:user_list,
|
|
:group_list,
|
|
)
|
|
end
|
|
|
|
def self.type_aliases
|
|
@type_aliases ||= { integer: :int, text: :string, timestamp: :datetime }
|
|
end
|
|
|
|
def self.create_from_sql(sql, opts = {})
|
|
in_params = false
|
|
ret_params = []
|
|
sql.lines.find do |line|
|
|
line.chomp!
|
|
|
|
if in_params
|
|
# -- (ident) :(ident) (= (ident))?
|
|
|
|
if line =~ /^\s*--\s*([a-zA-Z_ ]+)\s*:([a-z_]+)\s*(?:=\s+(.*)\s*)?$/
|
|
type = $1
|
|
ident = $2
|
|
default = $3
|
|
nullable = false
|
|
if type =~ /^(null)?(.*?)(null)?$/i
|
|
nullable = true if $1 || $3
|
|
type = $2
|
|
end
|
|
type = type.strip
|
|
|
|
begin
|
|
ret_params << Parameter.new(ident, type, default, nullable, validate: opts[:strict])
|
|
rescue StandardError
|
|
raise if opts[:strict]
|
|
end
|
|
|
|
false
|
|
elsif line =~ /^\s+$/
|
|
false
|
|
else
|
|
true
|
|
end
|
|
else
|
|
in_params = true if line =~ /^\s*--\s*\[params\]\s*$/
|
|
false
|
|
end
|
|
end
|
|
ret_params
|
|
end
|
|
|
|
def cast_to_ruby(string)
|
|
string = @default unless string
|
|
|
|
if string.blank?
|
|
if @nullable
|
|
return nil
|
|
else
|
|
raise ValidationError.new("Missing parameter #{identifier} of type #{type}")
|
|
end
|
|
end
|
|
return nil if string.downcase == "#null"
|
|
|
|
value = nil
|
|
|
|
case @type
|
|
when :int
|
|
invalid_format string, "Not an integer" unless string =~ /^-?\d+$/
|
|
value = string.to_i
|
|
invalid_format string, "Too large" unless Integer === value
|
|
when :bigint
|
|
invalid_format string, "Not an integer" unless string =~ /^-?\d+$/
|
|
value = string.to_i
|
|
when :boolean
|
|
value = !!(string =~ /t|true|y|yes|1/i)
|
|
when :string
|
|
value = string
|
|
when :time
|
|
begin
|
|
value = Time.parse string
|
|
rescue ArgumentError => e
|
|
invalid_format string, e.message
|
|
end
|
|
when :date
|
|
begin
|
|
value = Date.parse string
|
|
rescue ArgumentError => e
|
|
invalid_format string, e.message
|
|
end
|
|
when :datetime
|
|
begin
|
|
value = DateTime.parse string
|
|
rescue ArgumentError => e
|
|
invalid_format string, e.message
|
|
end
|
|
when :double
|
|
if string.strip =~ /^-?\d*\.?\d+$/
|
|
value = Float(string)
|
|
elsif string =~ /^(-?)Inf(inity)?$/i
|
|
if $1.present?
|
|
value = -Float::INFINITY
|
|
else
|
|
value = Float::INFINITY
|
|
end
|
|
elsif string =~ /^(-?)NaN$/i
|
|
if $1.present?
|
|
value = -Float::NAN
|
|
else
|
|
value = Float::NAN
|
|
end
|
|
else
|
|
invalid_format string
|
|
end
|
|
when :category_id
|
|
if string =~ %r{(.*)/(.*)}
|
|
parent_name = $1
|
|
child_name = $2
|
|
parent = Category.query_parent_category(parent_name)
|
|
invalid_format string, "Could not find category named #{parent_name}" unless parent
|
|
object = Category.query_category(child_name, parent)
|
|
if object.blank?
|
|
invalid_format string,
|
|
"Could not find subcategory of #{parent_name} named #{child_name}"
|
|
end
|
|
else
|
|
object =
|
|
Category.where(id: string.to_i).first || Category.where(slug: string).first ||
|
|
Category.where(name: string).first
|
|
invalid_format string, "Could not find category named #{string}" if object.blank?
|
|
end
|
|
|
|
value = object.id
|
|
when :user_id, :post_id, :topic_id, :group_id, :badge_id
|
|
if string.gsub(/[ _]/, "") =~ /^-?\d+$/
|
|
klass_name = (/^(.*)_id$/.match(type.to_s)[1].classify.to_sym)
|
|
begin
|
|
finder =
|
|
if type == :post_id || type == :topic_id
|
|
Object.const_get(klass_name).with_deleted
|
|
else
|
|
Object.const_get(klass_name)
|
|
end
|
|
object = finder.find(string.gsub(/[ _]/, "").to_i)
|
|
value = object.id
|
|
rescue ActiveRecord::RecordNotFound
|
|
invalid_format string, "The specified #{klass_name} was not found"
|
|
end
|
|
elsif type == :user_id
|
|
object = User.find_by_username_or_email(string)
|
|
invalid_format string, "The user named #{string} was not found" if object.blank?
|
|
value = object.id
|
|
elsif type == :post_id
|
|
if string =~ %r{/t/[^/]+/(\d+)(\?u=.*)?$}
|
|
object = Post.with_deleted.find_by(topic_id: $1, post_number: 1)
|
|
invalid_format string, "The first post for topic:#{$1} was not found" if object.blank?
|
|
value = object.id
|
|
elsif string =~ %r{(\d+)/(\d+)(\?u=.*)?$}
|
|
object = Post.with_deleted.find_by(topic_id: $1, post_number: $2)
|
|
if object.blank?
|
|
invalid_format string, "The post at topic:#{$1} post_number:#{$2} was not found"
|
|
end
|
|
value = object.id
|
|
end
|
|
elsif type == :topic_id
|
|
if string =~ %r{/t/[^/]+/(\d+)}
|
|
begin
|
|
object = Topic.with_deleted.find($1)
|
|
value = object.id
|
|
rescue ActiveRecord::RecordNotFound
|
|
invalid_format string, "The topic with id #{$1} was not found"
|
|
end
|
|
end
|
|
elsif type == :group_id
|
|
object = Group.where(name: string).first
|
|
invalid_format string, "The group named #{string} was not found" if object.blank?
|
|
value = object.id
|
|
else
|
|
invalid_format string
|
|
end
|
|
when :int_list
|
|
value = string.split(",").map { |s| s.downcase == "#null" ? nil : s.to_i }
|
|
invalid_format string, "can't be empty" if value.length == 0
|
|
when :string_list
|
|
value = string.split(",").map { |s| s.downcase == "#null" ? nil : s }
|
|
invalid_format string, "can't be empty" if value.length == 0
|
|
when :user_list
|
|
value = string.split(",").map { |s| User.find_by_username_or_email(s).id }
|
|
invalid_format string, "can't be empty" if value.length == 0
|
|
when :group_list
|
|
value = string.split(",").map { |s| Group.where(name: s).first.name }
|
|
invalid_format string, "The group with id #{string} was not found" if value.length == 0
|
|
else
|
|
raise TypeError.new("unknown parameter type??? should not get here")
|
|
end
|
|
|
|
value
|
|
end
|
|
|
|
private
|
|
|
|
def invalid_format(string, msg = nil)
|
|
if msg
|
|
raise ValidationError.new("'#{string}' is an invalid #{type} - #{msg}")
|
|
else
|
|
raise ValidationError.new("'#{string}' is an invalid value for #{type}")
|
|
end
|
|
end
|
|
end
|
|
end
|