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 ++++++++++++++ .../lib/active_record/attribute_methods/query.rb | 2 +- .../active_record/connection_adapters/column.rb | 7 - activerecord/lib/active_record/type.rb | 66 ++++++--- .../type/adapter_specific_registry.rb | 39 ++---- activerecord/lib/active_record/type/big_integer.rb | 13 -- activerecord/lib/active_record/type/binary.rb | 50 ------- activerecord/lib/active_record/type/boolean.rb | 19 --- activerecord/lib/active_record/type/date.rb | 49 ------- activerecord/lib/active_record/type/date_time.rb | 44 ------ activerecord/lib/active_record/type/decimal.rb | 50 ------- .../active_record/type/decimal_without_scale.rb | 11 -- activerecord/lib/active_record/type/float.rb | 25 ---- .../lib/active_record/type/hash_lookup_type_map.rb | 23 ---- activerecord/lib/active_record/type/helpers.rb | 4 - .../type/helpers/accepts_multiparameter_time.rb | 30 ---- .../lib/active_record/type/helpers/mutable.rb | 18 --- .../lib/active_record/type/helpers/numeric.rb | 34 ----- .../lib/active_record/type/helpers/time_value.rb | 58 -------- activerecord/lib/active_record/type/integer.rb | 66 --------- .../active_record/type/internal/abstract_json.rb | 4 +- .../lib/active_record/type/internal/timezone.rb | 15 ++ activerecord/lib/active_record/type/serialized.rb | 4 +- activerecord/lib/active_record/type/string.rb | 36 ----- activerecord/lib/active_record/type/text.rb | 11 -- activerecord/lib/active_record/type/time.rb | 42 ------ activerecord/lib/active_record/type/type_map.rb | 64 --------- .../lib/active_record/type/unsigned_integer.rb | 15 -- activerecord/lib/active_record/type/value.rb | 104 -------------- 51 files changed, 1065 insertions(+), 825 deletions(-) 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 delete mode 100644 activerecord/lib/active_record/type/big_integer.rb delete mode 100644 activerecord/lib/active_record/type/binary.rb delete mode 100644 activerecord/lib/active_record/type/boolean.rb delete mode 100644 activerecord/lib/active_record/type/date.rb delete mode 100644 activerecord/lib/active_record/type/date_time.rb delete mode 100644 activerecord/lib/active_record/type/decimal.rb delete mode 100644 activerecord/lib/active_record/type/decimal_without_scale.rb delete mode 100644 activerecord/lib/active_record/type/float.rb delete mode 100644 activerecord/lib/active_record/type/hash_lookup_type_map.rb delete mode 100644 activerecord/lib/active_record/type/helpers.rb delete mode 100644 activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb delete mode 100644 activerecord/lib/active_record/type/helpers/mutable.rb delete mode 100644 activerecord/lib/active_record/type/helpers/numeric.rb delete mode 100644 activerecord/lib/active_record/type/helpers/time_value.rb delete mode 100644 activerecord/lib/active_record/type/integer.rb create mode 100644 activerecord/lib/active_record/type/internal/timezone.rb delete mode 100644 activerecord/lib/active_record/type/string.rb delete mode 100644 activerecord/lib/active_record/type/text.rb delete mode 100644 activerecord/lib/active_record/type/time.rb delete mode 100644 activerecord/lib/active_record/type/type_map.rb delete mode 100644 activerecord/lib/active_record/type/unsigned_integer.rb delete mode 100644 activerecord/lib/active_record/type/value.rb 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 diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 553122a5fc..10498f4322 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -19,7 +19,7 @@ module ActiveRecord if Numeric === value || value !~ /[^0-9]/ !value.to_i.zero? else - return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) + return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value) !value.blank? end elsif value.respond_to?(:zero?) diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 8222811e0c..5e31efec4a 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -5,13 +5,6 @@ module ActiveRecord module ConnectionAdapters # An abstract definition of a column in a table. class Column - FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set - - module Format - ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ - ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ - end - attr_reader :name, :null, :sql_type_metadata, :default, :default_function, :collation delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 53f3b53bec..165043021b 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -1,26 +1,28 @@ -require 'active_record/type/helpers' -require 'active_record/type/value' +require 'active_model/type/helpers' +require 'active_model/type/value' -require 'active_record/type/big_integer' -require 'active_record/type/binary' -require 'active_record/type/boolean' -require 'active_record/type/date' -require 'active_record/type/date_time' -require 'active_record/type/decimal' -require 'active_record/type/decimal_without_scale' -require 'active_record/type/float' -require 'active_record/type/integer' -require 'active_record/type/serialized' -require 'active_record/type/string' -require 'active_record/type/text' -require 'active_record/type/time' -require 'active_record/type/unsigned_integer' +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_record/type/adapter_specific_registry' -require 'active_record/type/type_map' -require 'active_record/type/hash_lookup_type_map' +require 'active_model/type/registry' +require 'active_model/type/type_map' +require 'active_model/type/hash_lookup_type_map' require 'active_record/type/internal/abstract_json' +require 'active_record/type/internal/timezone' +require 'active_record/type/serialized' +require 'active_record/type/adapter_specific_registry' module ActiveRecord module Type @@ -53,6 +55,32 @@ module ActiveRecord end end + class Date < ActiveModel::Type::Date + include Internal::Timezone + end + + class DateTime < ActiveModel::Type::DateTime + include Internal::Timezone + end + class Time < ActiveModel::Type::Time + include Internal::Timezone + end + + Helpers = ActiveModel::Type::Helpers + BigInteger = ActiveModel::Type::BigInteger + Binary = ActiveModel::Type::Binary + Boolean = ActiveModel::Type::Boolean + Decimal = ActiveModel::Type::Decimal + DecimalWithoutScale = ActiveModel::Type::DecimalWithoutScale + Float = ActiveModel::Type::Float + Integer = ActiveModel::Type::Integer + String = ActiveModel::Type::String + Text = ActiveModel::Type::Text + UnsignedInteger = ActiveModel::Type::UnsignedInteger + Value = ActiveModel::Type::Value + TypeMap = ActiveModel::Type::TypeMap + HashLookupTypeMap = ActiveModel::Type::HashLookupTypeMap + register(:big_integer, Type::BigInteger, override: false) register(:binary, Type::Binary, override: false) register(:boolean, Type::Boolean, override: false) diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb index 5f71b3cb94..3509429058 100644 --- a/activerecord/lib/active_record/type/adapter_specific_registry.rb +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -1,35 +1,18 @@ +require 'active_model/type/registry' + module ActiveRecord # :stopdoc: module Type - class AdapterSpecificRegistry - def initialize - @registrations = [] - end - - def register(type_name, klass = nil, **options, &block) - block ||= proc { |_, *args| klass.new(*args) } - registrations << Registration.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 + class AdapterSpecificRegistry < ActiveModel::Type::Registry + private + + def registration_klass + Registration end - - def add_modifier(options, klass, **args) - registrations << DecorationRegistration.new(options, klass, **args) + + def decoration_registration_klass + DecorationRegistration end - - protected - - attr_reader :registrations end class Registration @@ -135,7 +118,7 @@ module ActiveRecord end end - class TypeConflictError < StandardError + class TypeConflictError < ::ActiveModel::TypeConflictError end # :startdoc: diff --git a/activerecord/lib/active_record/type/big_integer.rb b/activerecord/lib/active_record/type/big_integer.rb deleted file mode 100644 index 0c72d8914f..0000000000 --- a/activerecord/lib/active_record/type/big_integer.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'active_record/type/integer' - -module ActiveRecord - module Type - class BigInteger < Integer # :nodoc: - private - - def max_value - ::Float::INFINITY - end - end - end -end diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb deleted file mode 100644 index 0baf8c63ad..0000000000 --- a/activerecord/lib/active_record/type/binary.rb +++ /dev/null @@ -1,50 +0,0 @@ -module ActiveRecord - 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/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb deleted file mode 100644 index f6a75512fd..0000000000 --- a/activerecord/lib/active_record/type/boolean.rb +++ /dev/null @@ -1,19 +0,0 @@ -module ActiveRecord - module Type - class Boolean < Value # :nodoc: - def type - :boolean - end - - private - - def cast_value(value) - if value == '' - nil - else - !ConnectionAdapters::Column::FALSE_VALUES.include?(value) - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb deleted file mode 100644 index 3ceab59ebb..0000000000 --- a/activerecord/lib/active_record/type/date.rb +++ /dev/null @@ -1,49 +0,0 @@ -module ActiveRecord - 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 - - def fast_string_to_date(string) - if string =~ ConnectionAdapters::Column::Format::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/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb deleted file mode 100644 index a5199959b9..0000000000 --- a/activerecord/lib/active_record/type/date_time.rb +++ /dev/null @@ -1,44 +0,0 @@ -module ActiveRecord - 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/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb deleted file mode 100644 index f5b145230d..0000000000 --- a/activerecord/lib/active_record/type/decimal.rb +++ /dev/null @@ -1,50 +0,0 @@ -module ActiveRecord - 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/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb deleted file mode 100644 index ff5559e300..0000000000 --- a/activerecord/lib/active_record/type/decimal_without_scale.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'active_record/type/big_integer' - -module ActiveRecord - module Type - class DecimalWithoutScale < BigInteger # :nodoc: - def type - :decimal - end - end - end -end diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb deleted file mode 100644 index d88482b85d..0000000000 --- a/activerecord/lib/active_record/type/float.rb +++ /dev/null @@ -1,25 +0,0 @@ -module ActiveRecord - 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/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb deleted file mode 100644 index 3b01e3f8ca..0000000000 --- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb +++ /dev/null @@ -1,23 +0,0 @@ -module ActiveRecord - 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/activerecord/lib/active_record/type/helpers.rb b/activerecord/lib/active_record/type/helpers.rb deleted file mode 100644 index 634d417d13..0000000000 --- a/activerecord/lib/active_record/type/helpers.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'active_record/type/helpers/accepts_multiparameter_time' -require 'active_record/type/helpers/numeric' -require 'active_record/type/helpers/mutable' -require 'active_record/type/helpers/time_value' diff --git a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb deleted file mode 100644 index be571fc1c7..0000000000 --- a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb +++ /dev/null @@ -1,30 +0,0 @@ -module ActiveRecord - 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( - ActiveRecord::Base.default_timezone, - *values - ) - end - private :value_from_multiparameter_assignment - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/helpers/mutable.rb b/activerecord/lib/active_record/type/helpers/mutable.rb deleted file mode 100644 index 88a9099277..0000000000 --- a/activerecord/lib/active_record/type/helpers/mutable.rb +++ /dev/null @@ -1,18 +0,0 @@ -module ActiveRecord - 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/activerecord/lib/active_record/type/helpers/numeric.rb b/activerecord/lib/active_record/type/helpers/numeric.rb deleted file mode 100644 index a755a02a59..0000000000 --- a/activerecord/lib/active_record/type/helpers/numeric.rb +++ /dev/null @@ -1,34 +0,0 @@ -module ActiveRecord - 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/activerecord/lib/active_record/type/helpers/time_value.rb b/activerecord/lib/active_record/type/helpers/time_value.rb deleted file mode 100644 index 7eb41557cb..0000000000 --- a/activerecord/lib/active_record/type/helpers/time_value.rb +++ /dev/null @@ -1,58 +0,0 @@ -module ActiveRecord - 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 = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal - - if value.respond_to?(zone_conversion_method) - value = value.send(zone_conversion_method) - end - end - - value - 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 - Base.default_timezone == :utc ? time : time.getlocal - else - ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ ConnectionAdapters::Column::Format::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/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb deleted file mode 100644 index c5040c6d3b..0000000000 --- a/activerecord/lib/active_record/type/integer.rb +++ /dev/null @@ -1,66 +0,0 @@ -module ActiveRecord - 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/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb index 963a8245d0..097d1bd363 100644 --- a/activerecord/lib/active_record/type/internal/abstract_json.rb +++ b/activerecord/lib/active_record/type/internal/abstract_json.rb @@ -1,8 +1,8 @@ module ActiveRecord module Type module Internal # :nodoc: - class AbstractJson < Type::Value # :nodoc: - include Type::Helpers::Mutable + class AbstractJson < ActiveModel::Type::Value # :nodoc: + include ActiveModel::Type::Helpers::Mutable def type :json diff --git a/activerecord/lib/active_record/type/internal/timezone.rb b/activerecord/lib/active_record/type/internal/timezone.rb new file mode 100644 index 0000000000..07dbb0b2c6 --- /dev/null +++ b/activerecord/lib/active_record/type/internal/timezone.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module Type + module Internal + module Timezone + def is_utc? + ActiveRecord::Base.default_timezone == :utc + end + + def default_timezone + ActiveRecord::Base.default_timezone + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index ea3e0d6a45..203a395415 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -1,7 +1,7 @@ module ActiveRecord module Type - class Serialized < DelegateClass(Type::Value) # :nodoc: - include Helpers::Mutable + class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc: + include ActiveModel::Type::Helpers::Mutable attr_reader :subtype, :coder diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb deleted file mode 100644 index 2662b7e874..0000000000 --- a/activerecord/lib/active_record/type/string.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActiveRecord - 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/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb deleted file mode 100644 index 26f980f060..0000000000 --- a/activerecord/lib/active_record/type/text.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'active_record/type/string' - -module ActiveRecord - module Type - class Text < String # :nodoc: - def type - :text - end - end - end -end diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb deleted file mode 100644 index 19a10021bc..0000000000 --- a/activerecord/lib/active_record/type/time.rb +++ /dev/null @@ -1,42 +0,0 @@ -module ActiveRecord - 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/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb deleted file mode 100644 index 8ce36cc9af..0000000000 --- a/activerecord/lib/active_record/type/type_map.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'concurrent' - -module ActiveRecord - 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/activerecord/lib/active_record/type/unsigned_integer.rb b/activerecord/lib/active_record/type/unsigned_integer.rb deleted file mode 100644 index ed3e527483..0000000000 --- a/activerecord/lib/active_record/type/unsigned_integer.rb +++ /dev/null @@ -1,15 +0,0 @@ -module ActiveRecord - module Type - class UnsignedInteger < Integer # :nodoc: - private - - def max_value - super * 2 - end - - def min_value - 0 - end - end - end -end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb deleted file mode 100644 index 6b9d147ecc..0000000000 --- a/activerecord/lib/active_record/type/value.rb +++ /dev/null @@ -1,104 +0,0 @@ -module ActiveRecord - 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