Move ActiveRecord::Type to ActiveModel

The first step of bringing typecasting to ActiveModel
This commit is contained in:
Kir Shatrov 2015-09-07 21:20:15 +03:00 committed by Sean Griffin
parent b223d729d8
commit 9cc8c6f373
31 changed files with 336 additions and 96 deletions

@ -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

@ -1,6 +1,6 @@
require 'active_record/type/integer'
require 'active_model/type/integer'
module ActiveRecord
module ActiveModel
module Type
class BigInteger < Integer # :nodoc:
private

@ -1,4 +1,4 @@
module ActiveRecord
module ActiveModel
module Type
class Binary < Value # :nodoc:
def type

@ -1,6 +1,8 @@
module ActiveRecord
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
@ -11,7 +13,7 @@ def cast_value(value)
if value == ''
nil
else
!ConnectionAdapters::Column::FALSE_VALUES.include?(value)
!FALSE_VALUES.include?(value)
end
end
end

@ -1,4 +1,4 @@
module ActiveRecord
module ActiveModel
module Type
class Date < Value # :nodoc:
include Helpers::AcceptsMultiparameterTime.new
@ -24,8 +24,9 @@ def cast_value(value)
end
end
ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
def fast_string_to_date(string)
if string =~ ConnectionAdapters::Column::Format::ISO_DATE
if string =~ ISO_DATE
new_date $1.to_i, $2.to_i, $3.to_i
end
end

@ -1,4 +1,4 @@
module ActiveRecord
module ActiveModel
module Type
class DateTime < Value # :nodoc:
include Helpers::TimeValue

@ -1,4 +1,4 @@
module ActiveRecord
module ActiveModel
module Type
class Decimal < Value # :nodoc:
include Helpers::Numeric

@ -1,6 +1,6 @@
require 'active_record/type/big_integer'
require 'active_model/type/big_integer'
module ActiveRecord
module ActiveModel
module Type
class DecimalWithoutScale < BigInteger # :nodoc:
def type

@ -1,4 +1,4 @@
module ActiveRecord
module ActiveModel
module Type
class Float < Value # :nodoc:
include Helpers::Numeric

@ -1,4 +1,4 @@
module ActiveRecord
module ActiveModel
module Type
class HashLookupTypeMap < TypeMap # :nodoc:
def alias_type(type, alias_type)

@ -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'

@ -1,4 +1,4 @@
module ActiveRecord
module ActiveModel
module Type
module Helpers
class AcceptsMultiparameterTime < Module # :nodoc:
@ -17,10 +17,7 @@ def initialize(defaults: {})
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
)
::Time.send(default_timezone, *values)
end
private :value_from_multiparameter_assignment
end

@ -1,4 +1,4 @@
module ActiveRecord
module ActiveModel
module Type
module Helpers
module Mutable # :nodoc:

@ -1,4 +1,4 @@
module ActiveRecord
module ActiveModel
module Type
module Helpers
module Numeric # :nodoc:

@ -1,4 +1,4 @@
module ActiveRecord
module ActiveModel
module Type
module Helpers
module TimeValue # :nodoc:
@ -10,7 +10,7 @@ def serialize(value)
end
if value.acts_like?(:time)
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
zone_conversion_method = is_utc? ? :getutc : :getlocal
if value.respond_to?(zone_conversion_method)
value = value.send(zone_conversion_method)
@ -20,6 +20,14 @@ def serialize(value)
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
@ -34,20 +42,23 @@ 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
is_utc? ? time : time.getlocal
else
::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
::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 =~ ConnectionAdapters::Column::Format::ISO_DATETIME
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

@ -1,4 +1,4 @@
module ActiveRecord
module ActiveModel
module Type
class Integer < Value # :nodoc:
include Helpers::Numeric

@ -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

@ -1,4 +1,4 @@
module ActiveRecord
module ActiveModel
module Type
class String < Value # :nodoc:
def type

@ -1,6 +1,6 @@
require 'active_record/type/string'
require 'active_model/type/string'
module ActiveRecord
module ActiveModel
module Type
class Text < String # :nodoc:
def type

@ -1,4 +1,4 @@
module ActiveRecord
module ActiveModel
module Type
class Time < Value # :nodoc:
include Helpers::TimeValue

@ -1,6 +1,6 @@
require 'concurrent'
module ActiveRecord
module ActiveModel
module Type
class TypeMap # :nodoc:
def initialize

@ -1,4 +1,4 @@
module ActiveRecord
module ActiveModel
module Type
class UnsignedInteger < Integer # :nodoc:
private

@ -1,4 +1,4 @@
module ActiveRecord
module ActiveModel
module Type
class Value
attr_reader :precision, :scale, :limit

@ -19,7 +19,7 @@ def query_attribute(attr_name)
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?)

@ -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

@ -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 @@ def current_adapter_name
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)

@ -1,35 +1,18 @@
require 'active_model/type/registry'
module ActiveRecord
# :stopdoc:
module Type
class AdapterSpecificRegistry
def initialize
@registrations = []
class AdapterSpecificRegistry < ActiveModel::Type::Registry
private
def registration_klass
Registration
end
def register(type_name, klass = nil, **options, &block)
block ||= proc { |_, *args| klass.new(*args) }
registrations << Registration.new(type_name, block, **options)
def decoration_registration_klass
DecorationRegistration
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 << DecorationRegistration.new(options, klass, **args)
end
protected
attr_reader :registrations
end
class Registration
@ -135,7 +118,7 @@ def matches_options?(**kwargs)
end
end
class TypeConflictError < StandardError
class TypeConflictError < ::ActiveModel::TypeConflictError
end
# :startdoc:

@ -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'

@ -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

@ -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

@ -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