Allow a symbol to be passed to attribute
, in place of a type object
The same is not true of `define_attribute`, which is meant to be the low level no-magic API that sits underneath. The differences between the two APIs are: - `attribute` - Lazy (the attribute will be defined after the schema has loaded) - Allows either a type object or a symbol - `define_attribute` - Runs immediately (might get trampled by schema loading) - Requires a type object This was the last blocker in terms of public interface requirements originally discussed for this feature back in May. All the implementation blockers have been cleared, so this feature is probably ready for release (pending one more look-over by me).
This commit is contained in:
parent
d731859916
commit
101c19f55f
@ -44,7 +44,7 @@ module ClassMethods # :nodoc:
|
||||
# store_listing.price_in_cents # => BigDecimal.new(10.1)
|
||||
#
|
||||
# class StoreListing < ActiveRecord::Base
|
||||
# attribute :price_in_cents, Type::Integer.new
|
||||
# attribute :price_in_cents, :integer
|
||||
# end
|
||||
#
|
||||
# # after
|
||||
@ -77,7 +77,10 @@ def attribute(name, cast_type, **options)
|
||||
name = name.to_s
|
||||
reload_schema_from_cache
|
||||
|
||||
self.attributes_to_define_after_schema_loads = attributes_to_define_after_schema_loads.merge(name => [cast_type, options])
|
||||
self.attributes_to_define_after_schema_loads =
|
||||
attributes_to_define_after_schema_loads.merge(
|
||||
name => [cast_type, options]
|
||||
)
|
||||
end
|
||||
|
||||
def define_attribute(
|
||||
@ -93,7 +96,11 @@ def define_attribute(
|
||||
def load_schema!
|
||||
super
|
||||
attributes_to_define_after_schema_loads.each do |name, (type, options)|
|
||||
define_attribute(name, type, **options)
|
||||
if type.is_a?(Symbol)
|
||||
type = connection.type_for_attribute_options(type, **options.except(:default))
|
||||
end
|
||||
|
||||
define_attribute(name, type, **options.slice(:default))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -134,8 +134,29 @@ def prepare_binds_for_database(binds) # :nodoc:
|
||||
binds.map(&:value_for_database)
|
||||
end
|
||||
|
||||
def type_for_attribute_options(type_name, **options)
|
||||
klass = type_classes_with_standard_constructor.fetch(type_name, Type::Value)
|
||||
klass.new(**options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def type_classes_with_standard_constructor
|
||||
{
|
||||
big_integer: Type::BigInteger,
|
||||
binary: Type::Binary,
|
||||
boolean: Type::Boolean,
|
||||
date: Type::Date,
|
||||
date_time: Type::DateTime,
|
||||
decimal: Type::Decimal,
|
||||
float: Type::Float,
|
||||
integer: Type::Integer,
|
||||
string: Type::String,
|
||||
text: Type::Text,
|
||||
time: Type::Time,
|
||||
}
|
||||
end
|
||||
|
||||
def types_which_need_no_typecasting
|
||||
[nil, Numeric, String]
|
||||
end
|
||||
|
@ -947,6 +947,10 @@ def cast_value(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def type_classes_with_standard_constructor
|
||||
super.merge(string: MysqlString, date_time: MysqlDateTime)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -48,6 +48,12 @@ def type_cast_for_database(value)
|
||||
end
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
other.is_a?(Array) &&
|
||||
subtype == other.subtype &&
|
||||
delimiter == other.delimiter
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def type_cast_array(value, method)
|
||||
|
@ -7,7 +7,7 @@ module OID # :nodoc:
|
||||
class Range < Type::Value # :nodoc:
|
||||
attr_reader :subtype, :type
|
||||
|
||||
def initialize(subtype, type)
|
||||
def initialize(subtype, type = :range)
|
||||
@subtype = subtype
|
||||
@type = type
|
||||
end
|
||||
@ -40,6 +40,12 @@ def type_cast_for_database(value)
|
||||
end
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
other.is_a?(Range) &&
|
||||
other.subtype == subtype &&
|
||||
other.type == type
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def type_cast_single(value)
|
||||
|
@ -65,6 +65,23 @@ def lookup_cast_type_from_column(column) # :nodoc:
|
||||
type_map.lookup(column.oid, column.fmod, column.sql_type)
|
||||
end
|
||||
|
||||
def type_for_attribute_options(
|
||||
type_name,
|
||||
array: false,
|
||||
range: false,
|
||||
**options
|
||||
)
|
||||
if array
|
||||
subtype = type_for_attribute_options(type_name, **options)
|
||||
OID::Array.new(subtype)
|
||||
elsif range
|
||||
subtype = type_for_attribute_options(type_name, **options)
|
||||
OID::Range.new(subtype)
|
||||
else
|
||||
super(type_name, **options)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def _quote(value)
|
||||
@ -103,6 +120,30 @@ def _type_cast(value)
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def type_classes_with_standard_constructor
|
||||
super.merge(
|
||||
bit: OID::Bit,
|
||||
bit_varying: OID::BitVarying,
|
||||
binary: OID::Bytea,
|
||||
cidr: OID::Cidr,
|
||||
date: OID::Date,
|
||||
date_time: OID::DateTime,
|
||||
decimal: OID::Decimal,
|
||||
enum: OID::Enum,
|
||||
float: OID::Float,
|
||||
hstore: OID::Hstore,
|
||||
inet: OID::Inet,
|
||||
json: OID::Json,
|
||||
jsonb: OID::Jsonb,
|
||||
money: OID::Money,
|
||||
point: OID::Point,
|
||||
time: OID::Time,
|
||||
uuid: OID::Uuid,
|
||||
vector: OID::Vector,
|
||||
xml: OID::Xml,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -240,6 +240,10 @@ def _type_cast(value) # :nodoc:
|
||||
end
|
||||
end
|
||||
|
||||
def type_classes_with_standard_constructor
|
||||
super.merge(binary: SQLite3Binary)
|
||||
end
|
||||
|
||||
def quote_string(s) #:nodoc:
|
||||
@connection.class.quote(s)
|
||||
end
|
||||
|
@ -51,7 +51,7 @@ def type_cast_from_user(value)
|
||||
end
|
||||
|
||||
test "undecorated columns are not touched" do
|
||||
Model.attribute :another_string, Type::String.new, default: 'something or other'
|
||||
Model.attribute :another_string, :string, default: 'something or other'
|
||||
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
|
||||
|
||||
assert_equal 'something or other', Model.new.another_string
|
||||
@ -86,7 +86,7 @@ def type_cast_from_user(value)
|
||||
end
|
||||
|
||||
test "decorating attributes does not modify parent classes" do
|
||||
Model.attribute :another_string, Type::String.new, default: 'whatever'
|
||||
Model.attribute :another_string, :string, default: 'whatever'
|
||||
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
|
||||
child_class = Class.new(Model)
|
||||
child_class.decorate_attribute_type(:another_string, :test) { |t| StringDecorator.new(t) }
|
||||
@ -110,7 +110,7 @@ def type_cast_from_user(value)
|
||||
end
|
||||
|
||||
test "decorating with a proc" do
|
||||
Model.attribute :an_int, Type::Integer.new
|
||||
Model.attribute :an_int, :integer
|
||||
type_is_integer = proc { |_, type| type.type == :integer }
|
||||
Model.decorate_matching_attribute_types type_is_integer, :multiplier do |type|
|
||||
Multiplier.new(type)
|
||||
|
@ -1,17 +1,17 @@
|
||||
require 'cases/helper'
|
||||
|
||||
class OverloadedType < ActiveRecord::Base
|
||||
attribute :overloaded_float, Type::Integer.new
|
||||
attribute :overloaded_string_with_limit, Type::String.new(limit: 50)
|
||||
attribute :non_existent_decimal, Type::Decimal.new
|
||||
attribute :string_with_default, Type::String.new, default: 'the overloaded default'
|
||||
attribute :overloaded_float, :integer
|
||||
attribute :overloaded_string_with_limit, :string, limit: 50
|
||||
attribute :non_existent_decimal, :decimal
|
||||
attribute :string_with_default, :string, default: 'the overloaded default'
|
||||
end
|
||||
|
||||
class ChildOfOverloadedType < OverloadedType
|
||||
end
|
||||
|
||||
class GrandchildOfOverloadedType < ChildOfOverloadedType
|
||||
attribute :overloaded_float, Type::Float.new
|
||||
attribute :overloaded_float, :float
|
||||
end
|
||||
|
||||
class UnoverloadedType < ActiveRecord::Base
|
||||
@ -124,5 +124,37 @@ def type_cast_from_database(*)
|
||||
|
||||
assert_equal "from user", model.wibble
|
||||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter)
|
||||
test "arrays types can be specified" do
|
||||
klass = Class.new(OverloadedType) do
|
||||
attribute :my_array, :string, limit: 50, array: true
|
||||
attribute :my_int_array, :integer, array: true
|
||||
end
|
||||
|
||||
string_array = ConnectionAdapters::PostgreSQL::OID::Array.new(
|
||||
Type::String.new(limit: 50))
|
||||
int_array = ConnectionAdapters::PostgreSQL::OID::Array.new(
|
||||
Type::Integer.new)
|
||||
refute_equal string_array, int_array
|
||||
assert_equal string_array, klass.type_for_attribute("my_array")
|
||||
assert_equal int_array, klass.type_for_attribute("my_int_array")
|
||||
end
|
||||
|
||||
test "range types can be specified" do
|
||||
klass = Class.new(OverloadedType) do
|
||||
attribute :my_range, :string, limit: 50, range: true
|
||||
attribute :my_int_range, :integer, range: true
|
||||
end
|
||||
|
||||
string_range = ConnectionAdapters::PostgreSQL::OID::Range.new(
|
||||
Type::String.new(limit: 50))
|
||||
int_range = ConnectionAdapters::PostgreSQL::OID::Range.new(
|
||||
Type::Integer.new)
|
||||
refute_equal string_range, int_range
|
||||
assert_equal string_range, klass.type_for_attribute("my_range")
|
||||
assert_equal int_range, klass.type_for_attribute("my_int_range")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -903,8 +903,8 @@ def test_default
|
||||
class NumericData < ActiveRecord::Base
|
||||
self.table_name = 'numeric_data'
|
||||
|
||||
attribute :my_house_population, Type::Integer.new
|
||||
attribute :atoms_in_universe, Type::Integer.new
|
||||
attribute :my_house_population, :integer
|
||||
attribute :atoms_in_universe, :integer
|
||||
end
|
||||
|
||||
def test_big_decimal_conditions
|
||||
|
@ -15,9 +15,9 @@
|
||||
class NumericData < ActiveRecord::Base
|
||||
self.table_name = 'numeric_data'
|
||||
|
||||
attribute :world_population, Type::Integer.new
|
||||
attribute :my_house_population, Type::Integer.new
|
||||
attribute :atoms_in_universe, Type::Integer.new
|
||||
attribute :world_population, :integer
|
||||
attribute :my_house_population, :integer
|
||||
attribute :atoms_in_universe, :integer
|
||||
end
|
||||
|
||||
class CalculationsTest < ActiveRecord::TestCase
|
||||
|
@ -711,7 +711,7 @@ def type_cast_for_database(value)
|
||||
test "attribute_will_change! doesn't try to save non-persistable attributes" do
|
||||
klass = Class.new(ActiveRecord::Base) do
|
||||
self.table_name = 'people'
|
||||
attribute :non_persisted_attribute, ActiveRecord::Type::String.new
|
||||
attribute :non_persisted_attribute, :string
|
||||
end
|
||||
|
||||
record = klass.new(first_name: "Sean")
|
||||
|
@ -15,9 +15,9 @@
|
||||
|
||||
class BigNumber < ActiveRecord::Base
|
||||
unless current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter)
|
||||
attribute :value_of_e, Type::Integer.new
|
||||
attribute :value_of_e, :integer
|
||||
end
|
||||
attribute :my_house_population, Type::Integer.new
|
||||
attribute :my_house_population, :integer
|
||||
end
|
||||
|
||||
class Reminder < ActiveRecord::Base; end
|
||||
|
@ -78,6 +78,11 @@ def test_types_line_up
|
||||
end
|
||||
end.compact
|
||||
|
||||
if lengths.uniq.length != 1
|
||||
p lengths.uniq.length
|
||||
puts column_set
|
||||
end
|
||||
|
||||
assert_equal 1, lengths.uniq.length
|
||||
end
|
||||
end
|
||||
|
@ -113,7 +113,7 @@ class IntegerTest < ActiveRecord::TestCase
|
||||
test "values which are out of range can be re-assigned" do
|
||||
klass = Class.new(ActiveRecord::Base) do
|
||||
self.table_name = 'posts'
|
||||
attribute :foo, Type::Integer.new
|
||||
attribute :foo, :integer
|
||||
end
|
||||
model = klass.new
|
||||
|
||||
|
@ -150,7 +150,7 @@ def test_validators
|
||||
|
||||
def test_numericality_validation_with_mutation
|
||||
Topic.class_eval do
|
||||
attribute :wibble, ActiveRecord::Type::String.new
|
||||
attribute :wibble, :string
|
||||
validates_numericality_of :wibble, only_integer: true
|
||||
end
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user