lib/dynamoid/type_casting.rb
# frozen_string_literal: true
module Dynamoid
# @private
module TypeCasting
def self.cast_attributes(attributes, attributes_options)
{}.tap do |h|
attributes.symbolize_keys.each do |attribute, value|
h[attribute] = cast_field(value, attributes_options[attribute])
end
end
end
def self.cast_field(value, options)
return value if options.nil?
return nil if value.nil?
type_caster = find_type_caster(options)
if type_caster.nil?
raise ArgumentError, "Unknown type #{options[:type]}"
end
type_caster.process(value)
end
def self.find_type_caster(options)
type_caster_class = case options[:type]
when :string then StringTypeCaster
when :integer then IntegerTypeCaster
when :number then NumberTypeCaster
when :set then SetTypeCaster
when :array then ArrayTypeCaster
when :map then MapTypeCaster
when :datetime then DateTimeTypeCaster
when :date then DateTypeCaster
when :raw then RawTypeCaster
when :serialized then SerializedTypeCaster
when :boolean then BooleanTypeCaster
when :binary then BinaryTypeCaster
when Class then CustomTypeCaster
end
if type_caster_class.present?
type_caster_class.new(options)
end
end
class Base
def initialize(options)
@options = options
end
def process(value)
value
end
end
class StringTypeCaster < Base
def process(value)
case value
when true
't'
when false
'f'
when String
value.dup
else
value.to_s
end
end
end
class IntegerTypeCaster < Base
def process(value)
# rubocop:disable Lint/DuplicateBranch
if value == true
1
elsif value == false
0
elsif value.is_a?(String) && value.blank?
nil
elsif value.is_a?(Float) && !value.finite?
nil
elsif !value.respond_to?(:to_i)
nil
else
value.to_i
end
# rubocop:enable Lint/DuplicateBranch
end
end
class NumberTypeCaster < Base
def process(value)
# rubocop:disable Lint/DuplicateBranch
if value == true
1
elsif value == false
0
elsif value.is_a?(Symbol)
value.to_s.to_d
elsif value.is_a?(String) && value.blank?
nil
elsif value.is_a?(Float) && !value.finite?
nil
elsif !value.respond_to?(:to_d)
nil
else
value.to_d
end
# rubocop:enable Lint/DuplicateBranch
end
end
class SetTypeCaster < Base
def process(value)
set = type_cast_to_set(value)
if set.present? && @options[:of].present?
process_typed_set(set)
else
set
end
end
private
def type_cast_to_set(value)
if value.is_a?(Set)
value.dup
elsif value.respond_to?(:to_set)
value.to_set
end
end
def process_typed_set(set)
type_caster = TypeCasting.find_type_caster(element_options)
if type_caster.nil?
raise ArgumentError, "Set element type #{element_type} isn't supported"
end
set.to_set { |el| type_caster.process(el) }
end
def element_type
if @options[:of].is_a?(Hash)
@options[:of].keys.first
else
@options[:of]
end
end
def element_options
if @options[:of].is_a?(Hash)
@options[:of][element_type].dup.tap do |options|
options[:type] = element_type
end
else
{ type: element_type }
end
end
end
class ArrayTypeCaster < Base
def process(value)
array = type_cast_to_array(value)
if array.present? && @options[:of].present?
process_typed_array(array)
else
array
end
end
private
def type_cast_to_array(value)
if value.is_a?(Array)
value.dup
elsif value.respond_to?(:to_a)
value.to_a
end
end
def process_typed_array(array)
type_caster = TypeCasting.find_type_caster(element_options)
if type_caster.nil?
raise ArgumentError, "Set element type #{element_type} isn't supported"
end
array.map { |el| type_caster.process(el) }
end
def element_type
if @options[:of].is_a?(Hash)
@options[:of].keys.first
else
@options[:of]
end
end
def element_options
if @options[:of].is_a?(Hash)
@options[:of][element_type].dup.tap do |options|
options[:type] = element_type
end
else
{ type: element_type }
end
end
end
class MapTypeCaster < Base
def process(value)
return nil if value.nil?
if value.is_a? Hash
value
elsif value.respond_to? :to_hash
value.to_hash
elsif value.respond_to? :to_h
value.to_h
end
end
end
class DateTimeTypeCaster < Base
def process(value)
if !value.respond_to?(:to_datetime)
nil
elsif value.is_a?(String)
dt = begin
DateTime.parse(value)
rescue StandardError
nil
end
if dt
seconds = string_utc_offset(value) || ApplicationTimeZone.utc_offset
offset = seconds_to_offset(seconds)
DateTime.new(dt.year, dt.mon, dt.mday, dt.hour, dt.min, dt.sec, offset)
end
else
value.to_datetime
end
end
private
def string_utc_offset(string)
Date._parse(string)[:offset]
end
# 3600 -> "+01:00"
def seconds_to_offset(seconds)
ActiveSupport::TimeZone.seconds_to_utc_offset(seconds)
end
end
class DateTypeCaster < Base
def process(value)
if value.respond_to?(:to_date)
begin
value.to_date
rescue StandardError
nil
end
end
end
end
class RawTypeCaster < Base
end
class SerializedTypeCaster < Base
end
class BooleanTypeCaster < Base
def process(value)
if value == ''
nil
else
![false, 'false', 'FALSE', 0, '0', 'f', 'F', 'off', 'OFF'].include? value
end
end
end
class BinaryTypeCaster < Base
def process(value)
if value.is_a? String
value.dup
else
value.to_s
end
end
end
class CustomTypeCaster < Base
end
end
end