Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 102 additions & 1 deletion lib/gourami/extensions/resources.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,107 @@ module Gourami
module Extensions
module Resources

# yield to the given block for each resource in the given namespace.
#
# @param resource_namespace [Symbol] The namespace of the resource (e.g. :users, :payments)
#
# @option offset [Integer] The offset of the resource (e.g. 0, 1, 2) for example in an update form, there may be existing items that already exist, however only the new items are sent to the form.
#
# @yield The block to execute, each time with a resource active.
#
# @example
# def validate
# with_each_resource(:social_broadcasts) do
# validate_presence(:title) # validates `attributes[:social_broadcasts][<EACH>][:title]`
# end
# end
#
# @example
# def validate
# with_each_resource(:items, offset: existing_items.count) do |item, key, index|
# # Standard validation methods are available, and will validate the attributes on the resource:
# validate_decimal_places(:price, max: 2)
#
# # You may reference the resource object directly to perform custom validation logic inline:
# append_error(:password_confirmation, :doesnt_match) if item["password"] != item["password_confirmation"]
#
# # If `items` is a Hash, `key` is the hash key of the resource.
# # If `items` is an Array, `key` is the index of the resource (`+ offset`, if an offset is given).
# validate_length(:name, max: 255) if key % 2 == 0
#
# # If `items` is a Hash, `index` is the hash key of the resource.
# # If `items` is an Array, `index` is the index of the resource (offset is NOT applied to index).
# append_error(:id, :is_invalid) if index > 500
# end
# end
def with_each_resource(resource_namespace, offset: nil, &_block)
resources = send(resource_namespace)
if resources.is_a?(Hash)
return resources.each_with_index do |(key, resource), index|
with_resource(resource_namespace, key, offset: offset) do
yield(resource, key, index)
end
end
end

send(resource_namespace).each_with_index do |resource, index|
with_resource(resource_namespace, index, offset: offset) do
yield(resource, offset ? index + offset : index, index)
end
end
end

# For the duration of the given block, all validations will be done on the given resource.
#
# @param resource_namespace [Symbol] The namespace of the resource (e.g. :users, :payments)
# @param resource_uid [String|Number] The uid of the resource (e.g. 0, 1, "123")
#
# @option offset [Integer] The offset of the resource (e.g. 0, 1, 2) for example in an update form, there may be existing items that already exist, however only the new items are sent to the form.
#
# @yield The block to execute with the resource active.
#
# @example
# def validate
# validate_presence(:title) # validates `attributes[:title]` of the form
#
# with_resource(:social_broadcasts, "facebook_page-41") do
# # Within this block all validations will be done on the resource.
# validate_presence(:title) # validates `attributes[:social_broadcasts]["facebook_page-41"][:title]`
# validate_presence(:trim_start_time) # validates `attributes[:social_broadcasts]["facebook_page-41"][:trim_start_time]`
# validate_presence(:trim_end_time) # validates `attributes[:social_broadcasts]["facebook_page-41"][:trim_end_time]`
# end
# end
def with_resource(resource_namespace, resource_uid, offset: nil, &_block)
@resource_namespace = resource_namespace
@resource_uid = resource_uid
@offset = offset
yield
ensure
@resource_namespace = nil
@resource_uid = nil
@offset = nil
end

# If a resource namespace is active (within with_resource block), find the resource using the namespace and uid.
# Otherwise, return the form object.
def current_resource
if @resource_namespace
send(@resource_namespace)[@resource_uid]
else
super
end
end

# If a resource namespace is active (within with_resource block), append the error to the resource.
# Otherwise, append the error to the form object.
def append_error(attribute_name, message)
if @resource_namespace
append_resource_error(@resource_namespace, @offset ? @resource_uid + @offset : @resource_uid, attribute_name, message)
else
super
end
end

# Return a deeply nested Hash which allows you to identify errors by resource.
#
# @return [Hash<Symbol>]
Expand Down Expand Up @@ -56,7 +157,7 @@ def resource_errors

# TODO: YARD
def resource_has_errors?(resource_namespace, resource_uid)
resource_errors[resource_namespace, resource_uid.to_s].values.map(&:flatten).any?
resource_errors[resource_namespace][resource_uid.to_s].values.map(&:flatten).any?
end

# TODO: YARD
Expand Down
79 changes: 69 additions & 10 deletions lib/gourami/validations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,45 @@ def any_errors?
# @param attribute_name [Symbol, nil] nil for base
# @param error [Symbol, String]
# The error identifier.
def append_error(attribute_name, error)
def append_root_error(attribute_name, error)
errors[attribute_name] << error
end

# NOTE: The following resource methods are to support the validate_* methods
# to cooperate with resource when using Extensions::Resources.

# Overridden and super invoked from Extensions::Resources
def current_resource
self
end

# Overridden and super invoked from Extensions::Resources
def append_error(attribute_name, message)
append_root_error(attribute_name, message)
end

def get_current_resource_attribute_value(attribute_name)
resource = current_resource
# If resource responds to the attribute, return the value. Otherwise, check if it's a hash and return the value for the attribute.
if resource.respond_to?(attribute_name)
resource.send(attribute_name)
elsif resource.respond_to?(:[])
if resource.key?(attribute_name.to_sym)
resource[attribute_name.to_sym]
elsif resource.key?(attribute_name.to_s)
resource[attribute_name.to_s]
end
else
nil
end
end

# Validate the presence of the attribute value. If the value is nil or
# false append the :cant_be_empty error to the attribute.
#
# @param attribute_name [Symbol]
def validate_presence(attribute_name, message = nil)
value = send(attribute_name)
value = get_current_resource_attribute_value(attribute_name)
if !value || value.to_s.strip.empty?
append_error(attribute_name, message || :cant_be_empty)
end
Expand All @@ -109,7 +138,7 @@ def validate_presence(attribute_name, message = nil)
# A block to determine if a given value is unique or not. It receives
# the value and returns true if the value is unique.
def validate_uniqueness(attribute_name, message = nil, &block)
value = send(attribute_name)
value = get_current_resource_attribute_value(attribute_name)
unless block.call(value)
append_error(attribute_name, message || :is_duplicated)
end
Expand Down Expand Up @@ -137,7 +166,7 @@ def validate_color_format(attribute_name, message = nil)
# @param attribute_name [Symbol]
# @param format [Regexp]
def validate_format(attribute_name, format, message = nil)
value = send(attribute_name)
value = get_current_resource_attribute_value(attribute_name)
if value && !(format =~ value)
append_error(attribute_name, message || :is_invalid)
end
Expand All @@ -157,7 +186,7 @@ def validate_length(attribute_name, options = {})

min = options.fetch(:min, nil)
max = options.fetch(:max, nil)
value = send(attribute_name)
value = get_current_resource_attribute_value(attribute_name)

return if options[:allow_blank] && value.blank?

Expand Down Expand Up @@ -185,7 +214,7 @@ def validate_length(attribute_name, options = {})
# @param attribute_name [Symbol]
# @param list [Array]
def validate_inclusion(attribute_name, list, message = nil)
value = send(attribute_name)
value = get_current_resource_attribute_value(attribute_name)
if value && !list.include?(value)
append_error(attribute_name, message || :isnt_listed)
end
Expand All @@ -194,7 +223,7 @@ def validate_inclusion(attribute_name, list, message = nil)
# Validate the presence of each object in attribute name within list. If the object
# is not included in the list, append the :not_listed error to the attribute.
def validate_inclusion_of_each(attribute_name, list, message = nil)
value = send(attribute_name)
value = get_current_resource_attribute_value(attribute_name)
value && value.each do |obj|
unless list.include?(obj)
append_error(attribute_name, message || "#{obj} isn't listed")
Expand All @@ -208,7 +237,7 @@ def validate_inclusion_of_each(attribute_name, list, message = nil)
#
# @param attribute_name [Symbol]
def validate_any(attribute_name, message = nil)
value = send(attribute_name)
value = get_current_resource_attribute_value(attribute_name)
if value && value.empty?
append_error(attribute_name, message || :cant_be_empty)
end
Expand All @@ -220,7 +249,7 @@ def validate_any(attribute_name, message = nil)
# @param attribute_name [Symbol]
# @param filetypes [Array<String>]
def validate_filetype(attribute_name, filetypes, message = nil)
value = send(attribute_name)
value = get_current_resource_attribute_value(attribute_name)
if value && !filetypes.include?(value[:type].to_s.split("/").first)
append_error(attribute_name, message || :is_invalid)
end
Expand All @@ -237,7 +266,7 @@ def validate_filetype(attribute_name, filetypes, message = nil)
# @option options [Integer] :max (nil)
# The maximum value the attribute can take, if nil, no validation is made.
def validate_range(attribute_name, options = {})
value = send(attribute_name)
value = get_current_resource_attribute_value(attribute_name)

return unless value

Expand All @@ -247,5 +276,35 @@ def validate_range(attribute_name, options = {})
append_error(attribute_name, options.fetch(:max_message, nil) || :greater_than_max) if max && value > max
end

# Ensure the provided numeric attribute has the correct number of decimal places within the given range.
#
# @param attribute_name [Symbol]
# @option options [Integer] :max (nil)
# The maximum number of decimal places the attribute can have.
# @option options [Integer] :min (0)
# The minimum number of decimal places the attribute can have.
# @option options [String] :max_message (nil)
# The error message to append if the attribute has too many decimal places.
# @option options [String] :min_message (nil)
# The error message to append if the attribute has too few decimal places.
#
# @example
# validate_decimal_places(:price, max: 2)
# validate_decimal_places(:price, min: 2, min_message: "Price must have at least 2 decimal places.")
def validate_decimal_places(attribute_name, max:, min: 0, max_message: nil, min_message: nil)
value = get_current_resource_attribute_value(attribute_name)&.to_s
return unless value

decimal_places = value.split(".", 2).last&.length || 0

if max && max > 0 && decimal_places > max
append_error(attribute_name, max_message || :too_many_decimal_places)
end

if min && min > 0 && decimal_places < min
append_error(attribute_name, min_message || :too_few_decimal_places)
end
end

end
end
2 changes: 1 addition & 1 deletion lib/gourami/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Gourami
VERSION = "1.4.0".freeze
VERSION = "2.0.0".freeze
end
Loading