From 9cc8c6f3730df3d94c81a55be9ee1b7b4ffd29f6 Mon Sep 17 00:00:00 2001 From: Kir Shatrov Date: Mon, 7 Sep 2015 21:20:15 +0300 Subject: Move ActiveRecord::Type to ActiveModel The first step of bringing typecasting to ActiveModel --- activemodel/lib/active_model/type.rb | 59 ++++++++ activemodel/lib/active_model/type/big_integer.rb | 13 ++ activemodel/lib/active_model/type/binary.rb | 50 +++++++ activemodel/lib/active_model/type/boolean.rb | 21 +++ activemodel/lib/active_model/type/date.rb | 50 +++++++ activemodel/lib/active_model/type/date_time.rb | 44 ++++++ activemodel/lib/active_model/type/decimal.rb | 50 +++++++ .../lib/active_model/type/decimal_without_scale.rb | 11 ++ activemodel/lib/active_model/type/float.rb | 25 ++++ .../lib/active_model/type/hash_lookup_type_map.rb | 23 ++++ activemodel/lib/active_model/type/helpers.rb | 4 + .../type/helpers/accepts_multiparameter_time.rb | 27 ++++ .../lib/active_model/type/helpers/mutable.rb | 18 +++ .../lib/active_model/type/helpers/numeric.rb | 34 +++++ .../lib/active_model/type/helpers/time_value.rb | 69 ++++++++++ activemodel/lib/active_model/type/integer.rb | 66 +++++++++ activemodel/lib/active_model/type/registry.rb | 151 +++++++++++++++++++++ activemodel/lib/active_model/type/string.rb | 36 +++++ activemodel/lib/active_model/type/text.rb | 11 ++ activemodel/lib/active_model/type/time.rb | 42 ++++++ activemodel/lib/active_model/type/type_map.rb | 64 +++++++++ .../lib/active_model/type/unsigned_integer.rb | 15 ++ activemodel/lib/active_model/type/value.rb | 104 ++++++++++++++ 23 files changed, 987 insertions(+) create mode 100644 activemodel/lib/active_model/type.rb create mode 100644 activemodel/lib/active_model/type/big_integer.rb create mode 100644 activemodel/lib/active_model/type/binary.rb create mode 100644 activemodel/lib/active_model/type/boolean.rb create mode 100644 activemodel/lib/active_model/type/date.rb create mode 100644 activemodel/lib/active_model/type/date_time.rb create mode 100644 activemodel/lib/active_model/type/decimal.rb create mode 100644 activemodel/lib/active_model/type/decimal_without_scale.rb create mode 100644 activemodel/lib/active_model/type/float.rb create mode 100644 activemodel/lib/active_model/type/hash_lookup_type_map.rb create mode 100644 activemodel/lib/active_model/type/helpers.rb create mode 100644 activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb create mode 100644 activemodel/lib/active_model/type/helpers/mutable.rb create mode 100644 activemodel/lib/active_model/type/helpers/numeric.rb create mode 100644 activemodel/lib/active_model/type/helpers/time_value.rb create mode 100644 activemodel/lib/active_model/type/integer.rb create mode 100644 activemodel/lib/active_model/type/registry.rb create mode 100644 activemodel/lib/active_model/type/string.rb create mode 100644 activemodel/lib/active_model/type/text.rb create mode 100644 activemodel/lib/active_model/type/time.rb create mode 100644 activemodel/lib/active_model/type/type_map.rb create mode 100644 activemodel/lib/active_model/type/unsigned_integer.rb create mode 100644 activemodel/lib/active_model/type/value.rb (limited to 'activemodel') diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb new file mode 100644 index 0000000000..64346bcab2 --- /dev/null +++ b/activemodel/lib/active_model/type.rb @@ -0,0 +1,59 @@ +require 'active_model/type/helpers' +require 'active_model/type/value' + +require 'active_model/type/big_integer' +require 'active_model/type/binary' +require 'active_model/type/boolean' +require 'active_model/type/date' +require 'active_model/type/date_time' +require 'active_model/type/decimal' +require 'active_model/type/decimal_without_scale' +require 'active_model/type/float' +require 'active_model/type/integer' +require 'active_model/type/string' +require 'active_model/type/text' +require 'active_model/type/time' +require 'active_model/type/unsigned_integer' + +require 'active_model/type/registry' +require 'active_model/type/type_map' +require 'active_model/type/hash_lookup_type_map' + +module ActiveModel + module Type + @registry = Registry.new + + class << self + attr_accessor :registry # :nodoc: + delegate :add_modifier, to: :registry + + # Add a new type to the registry, allowing it to be referenced as a + # symbol by ActiveModel::Attributes::ClassMethods#attribute. If your + # type is only meant to be used with a specific database adapter, you can + # do so by passing +adapter: :postgresql+. If your type has the same + # name as a native type for the current adapter, an exception will be + # raised unless you specify an +:override+ option. +override: true+ will + # cause your type to be used instead of the native type. +override: + # false+ will cause the native type to be used over yours if one exists. + def register(type_name, klass = nil, **options, &block) + registry.register(type_name, klass, **options, &block) + end + + def lookup(*args, **kwargs) # :nodoc: + registry.lookup(*args, **kwargs) + end + end + + register(:big_integer, Type::BigInteger, override: false) + register(:binary, Type::Binary, override: false) + register(:boolean, Type::Boolean, override: false) + register(:date, Type::Date, override: false) + register(:date_time, Type::DateTime, override: false) + register(:decimal, Type::Decimal, override: false) + register(:float, Type::Float, override: false) + register(:integer, Type::Integer, override: false) + register(:string, Type::String, override: false) + register(:text, Type::Text, override: false) + register(:time, Type::Time, override: false) + end +end diff --git a/activemodel/lib/active_model/type/big_integer.rb b/activemodel/lib/active_model/type/big_integer.rb new file mode 100644 index 0000000000..4168cbfce7 --- /dev/null +++ b/activemodel/lib/active_model/type/big_integer.rb @@ -0,0 +1,13 @@ +require 'active_model/type/integer' + +module ActiveModel + module Type + class BigInteger < Integer # :nodoc: + private + + def max_value + ::Float::INFINITY + end + end + end +end diff --git a/activemodel/lib/active_model/type/binary.rb b/activemodel/lib/active_model/type/binary.rb new file mode 100644 index 0000000000..a0cc45b4c3 --- /dev/null +++ b/activemodel/lib/active_model/type/binary.rb @@ -0,0 +1,50 @@ +module ActiveModel + module Type + class Binary < Value # :nodoc: + def type + :binary + end + + def binary? + true + end + + def cast(value) + if value.is_a?(Data) + value.to_s + else + super + end + end + + def serialize(value) + return if value.nil? + Data.new(super) + end + + def changed_in_place?(raw_old_value, value) + old_value = deserialize(raw_old_value) + old_value != value + end + + class Data # :nodoc: + def initialize(value) + @value = value.to_s + end + + def to_s + @value + end + alias_method :to_str, :to_s + + def hex + @value.unpack('H*')[0] + end + + def ==(other) + other == to_s || super + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb new file mode 100644 index 0000000000..c1bce98c87 --- /dev/null +++ b/activemodel/lib/active_model/type/boolean.rb @@ -0,0 +1,21 @@ +module ActiveModel + module Type + class Boolean < Value # :nodoc: + FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set + + def type + :boolean + end + + private + + def cast_value(value) + if value == '' + nil + else + !FALSE_VALUES.include?(value) + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb new file mode 100644 index 0000000000..f74243a22c --- /dev/null +++ b/activemodel/lib/active_model/type/date.rb @@ -0,0 +1,50 @@ +module ActiveModel + module Type + class Date < Value # :nodoc: + include Helpers::AcceptsMultiparameterTime.new + + def type + :date + end + + def type_cast_for_schema(value) + "'#{value.to_s(:db)}'" + end + + private + + def cast_value(value) + if value.is_a?(::String) + return if value.empty? + fast_string_to_date(value) || fallback_string_to_date(value) + elsif value.respond_to?(:to_date) + value.to_date + else + value + end + end + + ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ + def fast_string_to_date(string) + if string =~ ISO_DATE + new_date $1.to_i, $2.to_i, $3.to_i + end + end + + def fallback_string_to_date(string) + new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) + end + + def new_date(year, mon, mday) + if year && year != 0 + ::Date.new(year, mon, mday) rescue nil + end + end + + def value_from_multiparameter_assignment(*) + time = super + time && time.to_date + end + end + end +end diff --git a/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb new file mode 100644 index 0000000000..b068cfc672 --- /dev/null +++ b/activemodel/lib/active_model/type/date_time.rb @@ -0,0 +1,44 @@ +module ActiveModel + module Type + class DateTime < Value # :nodoc: + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 4 => 0, 5 => 0 } + ) + + def type + :datetime + end + + private + + def cast_value(string) + return string unless string.is_a?(::String) + return if string.empty? + + fast_string_to_time(string) || fallback_string_to_time(string) + end + + # '0.123456' -> 123456 + # '1.123456' -> 123456 + def microseconds(time) + time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 + end + + def fallback_string_to_time(string) + time_hash = ::Date._parse(string) + time_hash[:sec_fraction] = microseconds(time_hash) + + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) + end + + def value_from_multiparameter_assignment(values_hash) + missing_parameter = (1..3).detect { |key| !values_hash.key?(key) } + if missing_parameter + raise ArgumentError, missing_parameter + end + super + end + end + end +end diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb new file mode 100644 index 0000000000..83a16018ea --- /dev/null +++ b/activemodel/lib/active_model/type/decimal.rb @@ -0,0 +1,50 @@ +module ActiveModel + module Type + class Decimal < Value # :nodoc: + include Helpers::Numeric + + def type + :decimal + end + + def type_cast_for_schema(value) + value.to_s.inspect + end + + private + + def cast_value(value) + casted_value = case value + when ::Float + convert_float_to_big_decimal(value) + when ::Numeric, ::String + BigDecimal(value, precision.to_i) + else + if value.respond_to?(:to_d) + value.to_d + else + cast_value(value.to_s) + end + end + + scale ? casted_value.round(scale) : casted_value + end + + def convert_float_to_big_decimal(value) + if precision + BigDecimal(value, float_precision) + else + value.to_d + end + end + + def float_precision + if precision.to_i > ::Float::DIG + 1 + ::Float::DIG + 1 + else + precision.to_i + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/decimal_without_scale.rb b/activemodel/lib/active_model/type/decimal_without_scale.rb new file mode 100644 index 0000000000..129baa0c10 --- /dev/null +++ b/activemodel/lib/active_model/type/decimal_without_scale.rb @@ -0,0 +1,11 @@ +require 'active_model/type/big_integer' + +module ActiveModel + module Type + class DecimalWithoutScale < BigInteger # :nodoc: + def type + :decimal + end + end + end +end diff --git a/activemodel/lib/active_model/type/float.rb b/activemodel/lib/active_model/type/float.rb new file mode 100644 index 0000000000..0f925bc7e1 --- /dev/null +++ b/activemodel/lib/active_model/type/float.rb @@ -0,0 +1,25 @@ +module ActiveModel + module Type + class Float < Value # :nodoc: + include Helpers::Numeric + + def type + :float + end + + alias serialize cast + + private + + def cast_value(value) + case value + when ::Float then value + when "Infinity" then ::Float::INFINITY + when "-Infinity" then -::Float::INFINITY + when "NaN" then ::Float::NAN + else value.to_f + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/hash_lookup_type_map.rb b/activemodel/lib/active_model/type/hash_lookup_type_map.rb new file mode 100644 index 0000000000..45d3515aad --- /dev/null +++ b/activemodel/lib/active_model/type/hash_lookup_type_map.rb @@ -0,0 +1,23 @@ +module ActiveModel + module Type + class HashLookupTypeMap < TypeMap # :nodoc: + def alias_type(type, alias_type) + register_type(type) { |_, *args| lookup(alias_type, *args) } + end + + def key?(key) + @mapping.key?(key) + end + + def keys + @mapping.keys + end + + private + + def perform_fetch(type, *args, &block) + @mapping.fetch(type, block).call(type, *args) + end + end + end +end diff --git a/activemodel/lib/active_model/type/helpers.rb b/activemodel/lib/active_model/type/helpers.rb new file mode 100644 index 0000000000..a805a359ab --- /dev/null +++ b/activemodel/lib/active_model/type/helpers.rb @@ -0,0 +1,4 @@ +require 'active_model/type/helpers/accepts_multiparameter_time' +require 'active_model/type/helpers/numeric' +require 'active_model/type/helpers/mutable' +require 'active_model/type/helpers/time_value' diff --git a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb new file mode 100644 index 0000000000..fa1ccd057e --- /dev/null +++ b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb @@ -0,0 +1,27 @@ +module ActiveModel + module Type + module Helpers + class AcceptsMultiparameterTime < Module # :nodoc: + def initialize(defaults: {}) + define_method(:cast) do |value| + if value.is_a?(Hash) + value_from_multiparameter_assignment(value) + else + super(value) + end + end + + define_method(:value_from_multiparameter_assignment) do |values_hash| + defaults.each do |k, v| + values_hash[k] ||= v + end + return unless values_hash[1] && values_hash[2] && values_hash[3] + values = values_hash.sort.map(&:last) + ::Time.send(default_timezone, *values) + end + private :value_from_multiparameter_assignment + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/helpers/mutable.rb b/activemodel/lib/active_model/type/helpers/mutable.rb new file mode 100644 index 0000000000..4dddbe4e5e --- /dev/null +++ b/activemodel/lib/active_model/type/helpers/mutable.rb @@ -0,0 +1,18 @@ +module ActiveModel + module Type + module Helpers + module Mutable # :nodoc: + def cast(value) + deserialize(serialize(value)) + end + + # +raw_old_value+ will be the `_before_type_cast` version of the + # value (likely a string). +new_value+ will be the current, type + # cast value. + def changed_in_place?(raw_old_value, new_value) + raw_old_value != serialize(new_value) + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb new file mode 100644 index 0000000000..c883010506 --- /dev/null +++ b/activemodel/lib/active_model/type/helpers/numeric.rb @@ -0,0 +1,34 @@ +module ActiveModel + module Type + module Helpers + module Numeric # :nodoc: + def cast(value) + value = case value + when true then 1 + when false then 0 + when ::String then value.presence + else value + end + super(value) + end + + def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: + super || number_to_non_number?(old_value, new_value_before_type_cast) + end + + private + + def number_to_non_number?(old_value, new_value_before_type_cast) + old_value != nil && non_numeric_string?(new_value_before_type_cast) + end + + def non_numeric_string?(value) + # 'wibble'.to_i will give zero, we want to make sure + # that we aren't marking int zero to string zero as + # changed. + value.to_s !~ /\A-?\d+\.?\d*\z/ + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb new file mode 100644 index 0000000000..5a899d43ec --- /dev/null +++ b/activemodel/lib/active_model/type/helpers/time_value.rb @@ -0,0 +1,69 @@ +module ActiveModel + module Type + module Helpers + module TimeValue # :nodoc: + def serialize(value) + if precision && value.respond_to?(:usec) + number_of_insignificant_digits = 6 - precision + round_power = 10 ** number_of_insignificant_digits + value = value.change(usec: value.usec / round_power * round_power) + end + + if value.acts_like?(:time) + zone_conversion_method = is_utc? ? :getutc : :getlocal + + if value.respond_to?(zone_conversion_method) + value = value.send(zone_conversion_method) + end + end + + value + end + + def is_utc? + ::Time.zone_default =~ 'UTC' + end + + def default_timezone + ::Time.zone_default + end + + def type_cast_for_schema(value) + "'#{value.to_s(:db)}'" + end + + def user_input_in_time_zone(value) + value.in_time_zone + end + + private + + def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) + # Treat 0000-00-00 00:00:00 as nil. + return if year.nil? || (year == 0 && mon == 0 && mday == 0) + + + if offset + time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil + return unless time + + time -= offset + is_utc? ? time : time.getlocal + else + ::Time.public_send(default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + end + end + + ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ + + # Doesn't handle time zones. + def fast_string_to_time(string) + if string =~ ISO_DATETIME + microsec = ($7.to_r * 1_000_000).to_i + new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec + end + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb new file mode 100644 index 0000000000..2f73ede009 --- /dev/null +++ b/activemodel/lib/active_model/type/integer.rb @@ -0,0 +1,66 @@ +module ActiveModel + module Type + class Integer < Value # :nodoc: + include Helpers::Numeric + + # Column storage size in bytes. + # 4 bytes means a MySQL int or Postgres integer as opposed to smallint etc. + DEFAULT_LIMIT = 4 + + def initialize(*) + super + @range = min_value...max_value + end + + def type + :integer + end + + def deserialize(value) + return if value.nil? + value.to_i + end + + def serialize(value) + result = cast(value) + if result + ensure_in_range(result) + end + result + end + + protected + + attr_reader :range + + private + + def cast_value(value) + case value + when true then 1 + when false then 0 + else + value.to_i rescue nil + end + end + + def ensure_in_range(value) + unless range.cover?(value) + raise RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}" + end + end + + def max_value + 1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign + end + + def min_value + -max_value + end + + def _limit + self.limit || DEFAULT_LIMIT + end + end + end +end diff --git a/activemodel/lib/active_model/type/registry.rb b/activemodel/lib/active_model/type/registry.rb new file mode 100644 index 0000000000..8070a4bd38 --- /dev/null +++ b/activemodel/lib/active_model/type/registry.rb @@ -0,0 +1,151 @@ +module ActiveModel + # :stopdoc: + module Type + class Registry + def initialize + @registrations = [] + end + + def register(type_name, klass = nil, **options, &block) + block ||= proc { |_, *args| klass.new(*args) } + registrations << registration_klass.new(type_name, block, **options) + end + + def lookup(symbol, *args) + registration = registrations + .select { |r| r.matches?(symbol, *args) } + .max + + if registration + registration.call(self, symbol, *args) + else + raise ArgumentError, "Unknown type #{symbol.inspect}" + end + end + + def add_modifier(options, klass, **args) + registrations << decoration_registration_klass.new(options, klass, **args) + end + + protected + + attr_reader :registrations + + private + + def registration_klass + Registration + end + + def decoration_registration_klass + DecorationRegistration + end + end + + class Registration + def initialize(name, block, override: nil) + @name = name + @block = block + @override = override + end + + def call(_registry, *args, **kwargs) + if kwargs.any? # https://bugs.ruby-lang.org/issues/10856 + block.call(*args, **kwargs) + else + block.call(*args) + end + end + + def matches?(type_name, *args, **kwargs) + type_name == name# && matches_adapter?(**kwargs) + end + + def <=>(other) + # if conflicts_with?(other) + # raise TypeConflictError.new("Type #{name} was registered for all + # adapters, but shadows a native type with + # the same name for #{other.adapter}".squish) + # end + priority <=> other.priority + end + + protected + + attr_reader :name, :block, :override + + def priority + result = 0 + # if adapter + # result |= 1 + # end + if override + result |= 2 + end + result + end + + # def priority_except_adapter + # priority & 0b111111100 + # end + + private + + # def matches_adapter?(adapter: nil, **) + # (self.adapter.nil? || adapter == self.adapter) + # end + + # def conflicts_with?(other) + # same_priority_except_adapter?(other) && + # has_adapter_conflict?(other) + # end + + # def same_priority_except_adapter?(other) + # priority_except_adapter == other.priority_except_adapter + # end + + # def has_adapter_conflict?(other) + # (override.nil? && other.adapter) || + # (adapter && other.override.nil?) + # end + end + + class DecorationRegistration < Registration + def initialize(options, klass) + @options = options + @klass = klass + # @adapter = adapter + end + + def call(registry, *args, **kwargs) + subtype = registry.lookup(*args, **kwargs.except(*options.keys)) + klass.new(subtype) + end + + def matches?(*args, **kwargs) + matches_options?(**kwargs) + end + + def priority + super | 4 + end + + protected + + attr_reader :options, :klass + + private + + def matches_options?(**kwargs) + options.all? do |key, value| + kwargs[key] == value + end + end + end + end + + class TypeConflictError < StandardError + end + + # :startdoc: +end diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb new file mode 100644 index 0000000000..fd1630c751 --- /dev/null +++ b/activemodel/lib/active_model/type/string.rb @@ -0,0 +1,36 @@ +module ActiveModel + module Type + class String < Value # :nodoc: + def type + :string + end + + def changed_in_place?(raw_old_value, new_value) + if new_value.is_a?(::String) + raw_old_value != new_value + end + end + + def serialize(value) + case value + when ::Numeric, ActiveSupport::Duration then value.to_s + when ::String then ::String.new(value) + when true then "t" + when false then "f" + else super + end + end + + private + + def cast_value(value) + case value + when true then "t" + when false then "f" + # String.new is slightly faster than dup + else ::String.new(value.to_s) + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/text.rb b/activemodel/lib/active_model/type/text.rb new file mode 100644 index 0000000000..1ad04daba4 --- /dev/null +++ b/activemodel/lib/active_model/type/text.rb @@ -0,0 +1,11 @@ +require 'active_model/type/string' + +module ActiveModel + module Type + class Text < String # :nodoc: + def type + :text + end + end + end +end diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb new file mode 100644 index 0000000000..7101bad566 --- /dev/null +++ b/activemodel/lib/active_model/type/time.rb @@ -0,0 +1,42 @@ +module ActiveModel + module Type + class Time < Value # :nodoc: + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 } + ) + + def type + :time + end + + def user_input_in_time_zone(value) + return unless value.present? + + case value + when ::String + value = "2000-01-01 #{value}" + when ::Time + value = value.change(year: 2000, day: 1, month: 1) + end + + super(value) + end + + private + + def cast_value(value) + return value unless value.is_a?(::String) + return if value.empty? + + dummy_time_value = "2000-01-01 #{value}" + + fast_string_to_time(dummy_time_value) || begin + time_hash = ::Date._parse(dummy_time_value) + return if time_hash[:hour].nil? + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/type_map.rb b/activemodel/lib/active_model/type/type_map.rb new file mode 100644 index 0000000000..402b660a71 --- /dev/null +++ b/activemodel/lib/active_model/type/type_map.rb @@ -0,0 +1,64 @@ +require 'concurrent' + +module ActiveModel + module Type + class TypeMap # :nodoc: + def initialize + @mapping = {} + @cache = Concurrent::Map.new do |h, key| + h.fetch_or_store(key, Concurrent::Map.new) + end + end + + def lookup(lookup_key, *args) + fetch(lookup_key, *args) { default_value } + end + + def fetch(lookup_key, *args, &block) + @cache[lookup_key].fetch_or_store(args) do + perform_fetch(lookup_key, *args, &block) + end + end + + def register_type(key, value = nil, &block) + raise ::ArgumentError unless value || block + @cache.clear + + if block + @mapping[key] = block + else + @mapping[key] = proc { value } + end + end + + def alias_type(key, target_key) + register_type(key) do |sql_type, *args| + metadata = sql_type[/\(.*\)/, 0] + lookup("#{target_key}#{metadata}", *args) + end + end + + def clear + @mapping.clear + end + + private + + def perform_fetch(lookup_key, *args) + matching_pair = @mapping.reverse_each.detect do |key, _| + key === lookup_key + end + + if matching_pair + matching_pair.last.call(lookup_key, *args) + else + yield lookup_key, *args + end + end + + def default_value + @default_value ||= Value.new + end + end + end +end diff --git a/activemodel/lib/active_model/type/unsigned_integer.rb b/activemodel/lib/active_model/type/unsigned_integer.rb new file mode 100644 index 0000000000..3f49f9f5f7 --- /dev/null +++ b/activemodel/lib/active_model/type/unsigned_integer.rb @@ -0,0 +1,15 @@ +module ActiveModel + module Type + class UnsignedInteger < Integer # :nodoc: + private + + def max_value + super * 2 + end + + def min_value + 0 + end + end + end +end diff --git a/activemodel/lib/active_model/type/value.rb b/activemodel/lib/active_model/type/value.rb new file mode 100644 index 0000000000..c7d1197d69 --- /dev/null +++ b/activemodel/lib/active_model/type/value.rb @@ -0,0 +1,104 @@ +module ActiveModel + module Type + class Value + attr_reader :precision, :scale, :limit + + def initialize(precision: nil, limit: nil, scale: nil) + @precision = precision + @scale = scale + @limit = limit + end + + def type # :nodoc: + end + + # Converts a value from database input to the appropriate ruby type. The + # return value of this method will be returned from + # ActiveRecord::AttributeMethods::Read#read_attribute. The default + # implementation just calls Value#cast. + # + # +value+ The raw input, as provided from the database. + def deserialize(value) + cast(value) + end + + # Type casts a value from user input (e.g. from a setter). This value may + # be a string from the form builder, or a ruby object passed to a setter. + # There is currently no way to differentiate between which source it came + # from. + # + # The return value of this method will be returned from + # ActiveRecord::AttributeMethods::Read#read_attribute. See also: + # Value#cast_value. + # + # +value+ The raw input, as provided to the attribute setter. + def cast(value) + cast_value(value) unless value.nil? + end + + # Casts a value from the ruby type to a type that the database knows how + # to understand. The returned value from this method should be a + # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or + # +nil+. + def serialize(value) + value + end + + # Type casts a value for schema dumping. This method is private, as we are + # hoping to remove it entirely. + def type_cast_for_schema(value) # :nodoc: + value.inspect + end + + # These predicates are not documented, as I need to look further into + # their use, and see if they can be removed entirely. + def binary? # :nodoc: + false + end + + # Determines whether a value has changed for dirty checking. +old_value+ + # and +new_value+ will always be type-cast. Types should not need to + # override this method. + def changed?(old_value, new_value, _new_value_before_type_cast) + old_value != new_value + end + + # Determines whether the mutable value has been modified since it was + # read. Returns +false+ by default. If your type returns an object + # which could be mutated, you should override this method. You will need + # to either: + # + # - pass +new_value+ to Value#serialize and compare it to + # +raw_old_value+ + # + # or + # + # - pass +raw_old_value+ to Value#deserialize and compare it to + # +new_value+ + # + # +raw_old_value+ The original value, before being passed to + # +deserialize+. + # + # +new_value+ The current value, after type casting. + def changed_in_place?(raw_old_value, new_value) + false + end + + def ==(other) + self.class == other.class && + precision == other.precision && + scale == other.scale && + limit == other.limit + end + + private + + # Convenience method for types which do not need separate type casting + # behavior for user and database inputs. Called by Value#cast for + # values except +nil+. + def cast_value(value) # :doc: + value + end + end + end +end -- cgit v1.2.3 From e467deb6c66d9b45a0c596706284c140f8e1f1b3 Mon Sep 17 00:00:00 2001 From: Sean Griffin Date: Mon, 21 Sep 2015 09:01:34 -0600 Subject: `TypeMap` and `HashLookupTypeMap` shouldn't be in Active Model These are used by the connection adapters to convert SQL type information into the appropriate type object, and makes no sense outside of the context of Active Record --- activemodel/lib/active_model/type.rb | 2 - .../lib/active_model/type/hash_lookup_type_map.rb | 23 -------- activemodel/lib/active_model/type/type_map.rb | 64 ---------------------- 3 files changed, 89 deletions(-) delete mode 100644 activemodel/lib/active_model/type/hash_lookup_type_map.rb delete mode 100644 activemodel/lib/active_model/type/type_map.rb (limited to 'activemodel') diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb index 64346bcab2..e45354f417 100644 --- a/activemodel/lib/active_model/type.rb +++ b/activemodel/lib/active_model/type.rb @@ -16,8 +16,6 @@ require 'active_model/type/time' require 'active_model/type/unsigned_integer' require 'active_model/type/registry' -require 'active_model/type/type_map' -require 'active_model/type/hash_lookup_type_map' module ActiveModel module Type diff --git a/activemodel/lib/active_model/type/hash_lookup_type_map.rb b/activemodel/lib/active_model/type/hash_lookup_type_map.rb deleted file mode 100644 index 45d3515aad..0000000000 --- a/activemodel/lib/active_model/type/hash_lookup_type_map.rb +++ /dev/null @@ -1,23 +0,0 @@ -module ActiveModel - module Type - class HashLookupTypeMap < TypeMap # :nodoc: - def alias_type(type, alias_type) - register_type(type) { |_, *args| lookup(alias_type, *args) } - end - - def key?(key) - @mapping.key?(key) - end - - def keys - @mapping.keys - end - - private - - def perform_fetch(type, *args, &block) - @mapping.fetch(type, block).call(type, *args) - end - end - end -end diff --git a/activemodel/lib/active_model/type/type_map.rb b/activemodel/lib/active_model/type/type_map.rb deleted file mode 100644 index 402b660a71..0000000000 --- a/activemodel/lib/active_model/type/type_map.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'concurrent' - -module ActiveModel - module Type - class TypeMap # :nodoc: - def initialize - @mapping = {} - @cache = Concurrent::Map.new do |h, key| - h.fetch_or_store(key, Concurrent::Map.new) - end - end - - def lookup(lookup_key, *args) - fetch(lookup_key, *args) { default_value } - end - - def fetch(lookup_key, *args, &block) - @cache[lookup_key].fetch_or_store(args) do - perform_fetch(lookup_key, *args, &block) - end - end - - def register_type(key, value = nil, &block) - raise ::ArgumentError unless value || block - @cache.clear - - if block - @mapping[key] = block - else - @mapping[key] = proc { value } - end - end - - def alias_type(key, target_key) - register_type(key) do |sql_type, *args| - metadata = sql_type[/\(.*\)/, 0] - lookup("#{target_key}#{metadata}", *args) - end - end - - def clear - @mapping.clear - end - - private - - def perform_fetch(lookup_key, *args) - matching_pair = @mapping.reverse_each.detect do |key, _| - key === lookup_key - end - - if matching_pair - matching_pair.last.call(lookup_key, *args) - else - yield lookup_key, *args - end - end - - def default_value - @default_value ||= Value.new - end - end - end -end -- cgit v1.2.3 From 22cc2b86f790b08bf58311bb6eb3bb148cb1dea9 Mon Sep 17 00:00:00 2001 From: Sean Griffin Date: Mon, 21 Sep 2015 09:24:39 -0600 Subject: Various stylistic nitpicks We do not need to require each file from AM individually, the type module does that for us. Even if the classes are extremely small right now, I'd rather keep any custom classes needed by AR in their own files, as they can easily have more complex changes in the future. --- activemodel/lib/active_model/type/helpers/time_value.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'activemodel') diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb index 5a899d43ec..c55bb1e136 100644 --- a/activemodel/lib/active_model/type/helpers/time_value.rb +++ b/activemodel/lib/active_model/type/helpers/time_value.rb @@ -19,11 +19,11 @@ module ActiveModel value end - + def is_utc? ::Time.zone_default =~ 'UTC' end - + def default_timezone ::Time.zone_default end @@ -41,7 +41,6 @@ module ActiveModel def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) # Treat 0000-00-00 00:00:00 as nil. return if year.nil? || (year == 0 && mon == 0 && mday == 0) - if offset time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil @@ -53,7 +52,7 @@ module ActiveModel ::Time.public_send(default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil end end - + ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ # Doesn't handle time zones. -- cgit v1.2.3 From 4590d7729e241cb7f66e018a2a9759cb3baa36e5 Mon Sep 17 00:00:00 2001 From: Sean Griffin Date: Mon, 21 Sep 2015 09:40:04 -0600 Subject: Simplify the implementation of Active Model's type registry Things like decorations, overrides, and priorities only matter for Active Record, so the Active Model registry can be implemented much more simply. At this point, I wonder if having Active Record's registry inherit from Active Model's is even worth the trouble? The Active Model class was also missing test cases, which have been backfilled. This removes the error when two types are registered with the same name, but given that Active Model is meant to be significantly more generic, I do not think this is an issue for now. If we want, we can raise an error at the point that someone tries to register it. --- activemodel/lib/active_model/type/registry.rb | 107 +++----------------------- activemodel/test/cases/type/registry_test.rb | 39 ++++++++++ 2 files changed, 49 insertions(+), 97 deletions(-) create mode 100644 activemodel/test/cases/type/registry_test.rb (limited to 'activemodel') diff --git a/activemodel/lib/active_model/type/registry.rb b/activemodel/lib/active_model/type/registry.rb index 8070a4bd38..adc88eb624 100644 --- a/activemodel/lib/active_model/type/registry.rb +++ b/activemodel/lib/active_model/type/registry.rb @@ -12,9 +12,7 @@ module ActiveModel end def lookup(symbol, *args) - registration = registrations - .select { |r| r.matches?(symbol, *args) } - .max + registration = find_registration(symbol, *args) if registration registration.call(self, symbol, *args) @@ -23,30 +21,26 @@ module ActiveModel end end - def add_modifier(options, klass, **args) - registrations << decoration_registration_klass.new(options, klass, **args) - end - protected attr_reader :registrations - + private - + def registration_klass Registration end - - def decoration_registration_klass - DecorationRegistration + + def find_registration(symbol, *args) + registrations.find { |r| r.matches?(symbol, *args) } end end class Registration - def initialize(name, block, override: nil) + # Options must be taken because of https://bugs.ruby-lang.org/issues/10856 + def initialize(name, block, **) @name = name @block = block - @override = override end def call(_registry, *args, **kwargs) @@ -58,94 +52,13 @@ module ActiveModel end def matches?(type_name, *args, **kwargs) - type_name == name# && matches_adapter?(**kwargs) - end - - def <=>(other) - # if conflicts_with?(other) - # raise TypeConflictError.new("Type #{name} was registered for all - # adapters, but shadows a native type with - # the same name for #{other.adapter}".squish) - # end - priority <=> other.priority + type_name == name end protected - attr_reader :name, :block, :override - - def priority - result = 0 - # if adapter - # result |= 1 - # end - if override - result |= 2 - end - result - end - - # def priority_except_adapter - # priority & 0b111111100 - # end - - private - - # def matches_adapter?(adapter: nil, **) - # (self.adapter.nil? || adapter == self.adapter) - # end - - # def conflicts_with?(other) - # same_priority_except_adapter?(other) && - # has_adapter_conflict?(other) - # end - - # def same_priority_except_adapter?(other) - # priority_except_adapter == other.priority_except_adapter - # end - - # def has_adapter_conflict?(other) - # (override.nil? && other.adapter) || - # (adapter && other.override.nil?) - # end + attr_reader :name, :block end - - class DecorationRegistration < Registration - def initialize(options, klass) - @options = options - @klass = klass - # @adapter = adapter - end - - def call(registry, *args, **kwargs) - subtype = registry.lookup(*args, **kwargs.except(*options.keys)) - klass.new(subtype) - end - - def matches?(*args, **kwargs) - matches_options?(**kwargs) - end - - def priority - super | 4 - end - - protected - - attr_reader :options, :klass - - private - - def matches_options?(**kwargs) - options.all? do |key, value| - kwargs[key] == value - end - end - end - end - - class TypeConflictError < StandardError end - # :startdoc: end diff --git a/activemodel/test/cases/type/registry_test.rb b/activemodel/test/cases/type/registry_test.rb new file mode 100644 index 0000000000..2a48998a62 --- /dev/null +++ b/activemodel/test/cases/type/registry_test.rb @@ -0,0 +1,39 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + class RegistryTest < ActiveModel::TestCase + test "a class can be registered for a symbol" do + registry = Type::Registry.new + registry.register(:foo, ::String) + registry.register(:bar, ::Array) + + assert_equal "", registry.lookup(:foo) + assert_equal [], registry.lookup(:bar) + end + + test "a block can be registered" do + registry = Type::Registry.new + registry.register(:foo) do |*args| + [*args, "block for foo"] + end + registry.register(:bar) do |*args| + [*args, "block for bar"] + end + + assert_equal [:foo, 1, "block for foo"], registry.lookup(:foo, 1) + assert_equal [:foo, 2, "block for foo"], registry.lookup(:foo, 2) + assert_equal [:bar, 1, 2, 3, "block for bar"], registry.lookup(:bar, 1, 2, 3) + end + + test "a reasonable error is given when no type is found" do + registry = Type::Registry.new + + e = assert_raises(ArgumentError) do + registry.lookup(:foo) + end + + assert_equal "Unknown type :foo", e.message + end + end +end -- cgit v1.2.3 From 858a7b04291b447edc541c6e17eec81f0cce34e5 Mon Sep 17 00:00:00 2001 From: Sean Griffin Date: Mon, 21 Sep 2015 10:09:29 -0600 Subject: Move the appropriate type tests to the Active Model suite Any tests for a type which is not overridden by Active Record, and does not test the specifics of the attributes API interacting in more complex ways have no reason to be in the Active Record suite. Doing this revealed that the implementation of the date and time types in AM was actually completely broken, and incapable of returning any value other than `nil`. --- .../lib/active_model/type/helpers/time_value.rb | 10 +- activemodel/test/cases/type/decimal_test.rb | 57 ++++++++++ activemodel/test/cases/type/integer_test.rb | 108 ++++++++++++++++++ activemodel/test/cases/type/string_test.rb | 20 ++++ .../test/cases/type/unsigned_integer_test.rb | 18 +++ activemodel/test/cases/types_test.rb | 121 +++++++++++++++++++++ 6 files changed, 332 insertions(+), 2 deletions(-) create mode 100644 activemodel/test/cases/type/decimal_test.rb create mode 100644 activemodel/test/cases/type/integer_test.rb create mode 100644 activemodel/test/cases/type/string_test.rb create mode 100644 activemodel/test/cases/type/unsigned_integer_test.rb create mode 100644 activemodel/test/cases/types_test.rb (limited to 'activemodel') diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb index c55bb1e136..0a0e58654b 100644 --- a/activemodel/lib/active_model/type/helpers/time_value.rb +++ b/activemodel/lib/active_model/type/helpers/time_value.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/time/zones" + module ActiveModel module Type module Helpers @@ -21,11 +23,15 @@ module ActiveModel end def is_utc? - ::Time.zone_default =~ 'UTC' + ::Time.zone_default.nil? || ::Time.zone_default =~ 'UTC' end def default_timezone - ::Time.zone_default + if is_utc? + :utc + else + :local + end end def type_cast_for_schema(value) diff --git a/activemodel/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb new file mode 100644 index 0000000000..353dbf84ad --- /dev/null +++ b/activemodel/test/cases/type/decimal_test.rb @@ -0,0 +1,57 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + module Type + class DecimalTest < ActiveModel::TestCase + def test_type_cast_decimal + type = Decimal.new + assert_equal BigDecimal.new("0"), type.cast(BigDecimal.new("0")) + assert_equal BigDecimal.new("123"), type.cast(123.0) + assert_equal BigDecimal.new("1"), type.cast(:"1") + end + + def test_type_cast_decimal_from_float_with_large_precision + type = Decimal.new(precision: ::Float::DIG + 2) + assert_equal BigDecimal.new("123.0"), type.cast(123.0) + end + + def test_type_cast_from_float_with_unspecified_precision + type = Decimal.new + assert_equal 22.68.to_d, type.cast(22.68) + end + + def test_type_cast_decimal_from_rational_with_precision + type = Decimal.new(precision: 2) + assert_equal BigDecimal("0.33"), type.cast(Rational(1, 3)) + end + + def test_type_cast_decimal_from_rational_with_precision_and_scale + type = Decimal.new(precision: 4, scale: 2) + assert_equal BigDecimal("0.33"), type.cast(Rational(1, 3)) + end + + def test_type_cast_decimal_from_rational_without_precision_defaults_to_18_36 + type = Decimal.new + assert_equal BigDecimal("0.333333333333333333E0"), type.cast(Rational(1, 3)) + end + + def test_type_cast_decimal_from_object_responding_to_d + value = Object.new + def value.to_d + BigDecimal.new("1") + end + type = Decimal.new + assert_equal BigDecimal("1"), type.cast(value) + end + + def test_changed? + type = Decimal.new + + assert type.changed?(5.0, 5.0, '5.0wibble') + assert_not type.changed?(5.0, 5.0, '5.0') + assert_not type.changed?(-5.0, -5.0, '-5.0') + end + end + end +end diff --git a/activemodel/test/cases/type/integer_test.rb b/activemodel/test/cases/type/integer_test.rb new file mode 100644 index 0000000000..dac922db42 --- /dev/null +++ b/activemodel/test/cases/type/integer_test.rb @@ -0,0 +1,108 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + module Type + class IntegerTest < ActiveModel::TestCase + test "simple values" do + type = Type::Integer.new + assert_equal 1, type.cast(1) + assert_equal 1, type.cast('1') + assert_equal 1, type.cast('1ignore') + assert_equal 0, type.cast('bad1') + assert_equal 0, type.cast('bad') + assert_equal 1, type.cast(1.7) + assert_equal 0, type.cast(false) + assert_equal 1, type.cast(true) + assert_nil type.cast(nil) + end + + test "random objects cast to nil" do + type = Type::Integer.new + assert_nil type.cast([1,2]) + assert_nil type.cast({1 => 2}) + assert_nil type.cast(1..2) + end + + test "casting objects without to_i" do + type = Type::Integer.new + assert_nil type.cast(::Object.new) + end + + test "casting nan and infinity" do + type = Type::Integer.new + assert_nil type.cast(::Float::NAN) + assert_nil type.cast(1.0/0.0) + end + + test "casting booleans for database" do + type = Type::Integer.new + assert_equal 1, type.serialize(true) + assert_equal 0, type.serialize(false) + end + + test "changed?" do + type = Type::Integer.new + + assert type.changed?(5, 5, '5wibble') + assert_not type.changed?(5, 5, '5') + assert_not type.changed?(5, 5, '5.0') + assert_not type.changed?(-5, -5, '-5') + assert_not type.changed?(-5, -5, '-5.0') + assert_not type.changed?(nil, nil, nil) + end + + test "values below int min value are out of range" do + assert_raises(::RangeError) do + Integer.new.serialize(-2147483649) + end + end + + test "values above int max value are out of range" do + assert_raises(::RangeError) do + Integer.new.serialize(2147483648) + end + end + + test "very small numbers are out of range" do + assert_raises(::RangeError) do + Integer.new.serialize(-9999999999999999999999999999999) + end + end + + test "very large numbers are out of range" do + assert_raises(::RangeError) do + Integer.new.serialize(9999999999999999999999999999999) + end + end + + test "normal numbers are in range" do + type = Integer.new + assert_equal(0, type.serialize(0)) + assert_equal(-1, type.serialize(-1)) + assert_equal(1, type.serialize(1)) + end + + test "int max value is in range" do + assert_equal(2147483647, Integer.new.serialize(2147483647)) + end + + test "int min value is in range" do + assert_equal(-2147483648, Integer.new.serialize(-2147483648)) + end + + test "columns with a larger limit have larger ranges" do + type = Integer.new(limit: 8) + + assert_equal(9223372036854775807, type.serialize(9223372036854775807)) + assert_equal(-9223372036854775808, type.serialize(-9223372036854775808)) + assert_raises(::RangeError) do + type.serialize(-9999999999999999999999999999999) + end + assert_raises(::RangeError) do + type.serialize(9999999999999999999999999999999) + end + end + end + end +end diff --git a/activemodel/test/cases/type/string_test.rb b/activemodel/test/cases/type/string_test.rb new file mode 100644 index 0000000000..8ec771ea42 --- /dev/null +++ b/activemodel/test/cases/type/string_test.rb @@ -0,0 +1,20 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + class StringTypeTest < ActiveModel::TestCase + test "type casting" do + type = Type::String.new + assert_equal "t", type.cast(true) + assert_equal "f", type.cast(false) + assert_equal "123", type.cast(123) + end + + test "values are duped coming out" do + s = "foo" + type = Type::String.new + assert_not_same s, type.cast(s) + assert_not_same s, type.deserialize(s) + end + end +end diff --git a/activemodel/test/cases/type/unsigned_integer_test.rb b/activemodel/test/cases/type/unsigned_integer_test.rb new file mode 100644 index 0000000000..16301b3ac0 --- /dev/null +++ b/activemodel/test/cases/type/unsigned_integer_test.rb @@ -0,0 +1,18 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + module Type + class UnsignedIntegerTest < ActiveModel::TestCase + test "unsigned int max value is in range" do + assert_equal(4294967295, UnsignedInteger.new.serialize(4294967295)) + end + + test "minus value is out of range" do + assert_raises(::RangeError) do + UnsignedInteger.new.serialize(-1) + end + end + end + end +end diff --git a/activemodel/test/cases/types_test.rb b/activemodel/test/cases/types_test.rb new file mode 100644 index 0000000000..0bb971fa39 --- /dev/null +++ b/activemodel/test/cases/types_test.rb @@ -0,0 +1,121 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + class TypesTest < ActiveModel::TestCase + def test_type_cast_boolean + type = Type::Boolean.new + assert type.cast('').nil? + assert type.cast(nil).nil? + + assert type.cast(true) + assert type.cast(1) + assert type.cast('1') + assert type.cast('t') + assert type.cast('T') + assert type.cast('true') + assert type.cast('TRUE') + assert type.cast('on') + assert type.cast('ON') + assert type.cast(' ') + assert type.cast("\u3000\r\n") + assert type.cast("\u0000") + assert type.cast('SOMETHING RANDOM') + + # explicitly check for false vs nil + assert_equal false, type.cast(false) + assert_equal false, type.cast(0) + assert_equal false, type.cast('0') + assert_equal false, type.cast('f') + assert_equal false, type.cast('F') + assert_equal false, type.cast('false') + assert_equal false, type.cast('FALSE') + assert_equal false, type.cast('off') + assert_equal false, type.cast('OFF') + end + + def test_type_cast_float + type = Type::Float.new + assert_equal 1.0, type.cast("1") + end + + def test_changing_float + type = Type::Float.new + + assert type.changed?(5.0, 5.0, '5wibble') + assert_not type.changed?(5.0, 5.0, '5') + assert_not type.changed?(5.0, 5.0, '5.0') + assert_not type.changed?(nil, nil, nil) + end + + def test_type_cast_binary + type = Type::Binary.new + assert_equal nil, type.cast(nil) + assert_equal "1", type.cast("1") + assert_equal 1, type.cast(1) + end + + def test_type_cast_time + type = Type::Time.new + assert_equal nil, type.cast(nil) + assert_equal nil, type.cast('') + assert_equal nil, type.cast('ABC') + + time_string = Time.now.utc.strftime("%T") + assert_equal time_string, type.cast(time_string).strftime("%T") + end + + def test_type_cast_datetime_and_timestamp + type = Type::DateTime.new + assert_equal nil, type.cast(nil) + assert_equal nil, type.cast('') + assert_equal nil, type.cast(' ') + assert_equal nil, type.cast('ABC') + + datetime_string = Time.now.utc.strftime("%FT%T") + assert_equal datetime_string, type.cast(datetime_string).strftime("%FT%T") + end + + def test_type_cast_date + type = Type::Date.new + assert_equal nil, type.cast(nil) + assert_equal nil, type.cast('') + assert_equal nil, type.cast(' ') + assert_equal nil, type.cast('ABC') + + date_string = Time.now.utc.strftime("%F") + assert_equal date_string, type.cast(date_string).strftime("%F") + end + + def test_type_cast_duration_to_integer + type = Type::Integer.new + assert_equal 1800, type.cast(30.minutes) + assert_equal 7200, type.cast(2.hours) + end + + def test_string_to_time_with_timezone + ["UTC", "US/Eastern"].each do |zone| + with_timezone_config default: zone do + type = Type::DateTime.new + assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.cast("Wed, 04 Sep 2013 03:00:00 EAT") + end + end + end + + def test_type_equality + assert_equal Type::Value.new, Type::Value.new + assert_not_equal Type::Value.new, Type::Integer.new + assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2) + end + + private + + def with_timezone_config(default:) + old_zone_default = ::Time.zone_default + ::Time.zone_default = ::Time.find_zone(default) + yield + ensure + ::Time.zone_default = old_zone_default + end + end +end -- cgit v1.2.3