aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activerecord/lib/active_record/type.rb25
-rw-r--r--activerecord/lib/active_record/type/adapter_specific_registry.rb139
-rw-r--r--activerecord/test/cases/type/adapter_specific_registry_test.rb133
-rw-r--r--activerecord/test/cases/type_test.rb39
4 files changed, 336 insertions, 0 deletions
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
index f18b076d58..7ce0370d8b 100644
--- a/activerecord/lib/active_record/type.rb
+++ b/activerecord/lib/active_record/type.rb
@@ -16,5 +16,30 @@ require 'active_record/type/text'
require 'active_record/type/time'
require 'active_record/type/unsigned_integer'
+require 'active_record/type/adapter_specific_registry'
require 'active_record/type/type_map'
require 'active_record/type/hash_lookup_type_map'
+
+module ActiveRecord
+ module Type
+ @registry = AdapterSpecificRegistry.new
+
+ class << self
+ attr_accessor :registry # :nodoc:
+
+ def register(*args)
+ registry.register(*args)
+ end
+
+ def lookup(*args, adapter: current_adapter_name, **kwargs)
+ registry.lookup(*args, adapter: adapter, **kwargs)
+ end
+
+ private
+
+ def current_adapter_name
+ ActiveRecord::Base.connection.adapter_name.downcase.to_sym
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb
new file mode 100644
index 0000000000..ce9ff6a557
--- /dev/null
+++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb
@@ -0,0 +1,139 @@
+module ActiveRecord
+ module Type
+ class AdapterSpecificRegistry # :nodoc:
+ 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
+ end
+
+ def add_modifier(options, klass, **args)
+ registrations << DecorationRegistration.new(options, klass, **args)
+ end
+
+ protected
+
+ attr_reader :registrations
+ end
+
+ class Registration
+ def initialize(name, block, adapter: nil, override: nil)
+ @name = name
+ @block = block
+ @adapter = adapter
+ @override = override
+ end
+
+ def call(_registry, *args, adapter: nil, **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, :adapter, :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, adapter: nil)
+ @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_adapter?(**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
+end
diff --git a/activerecord/test/cases/type/adapter_specific_registry_test.rb b/activerecord/test/cases/type/adapter_specific_registry_test.rb
new file mode 100644
index 0000000000..8b836b4793
--- /dev/null
+++ b/activerecord/test/cases/type/adapter_specific_registry_test.rb
@@ -0,0 +1,133 @@
+require "cases/helper"
+
+module ActiveRecord
+ class AdapterSpecificRegistryTest < ActiveRecord::TestCase
+ test "a class can be registered for a symbol" do
+ registry = Type::AdapterSpecificRegistry.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::AdapterSpecificRegistry.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 "filtering by adapter" do
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, String, adapter: :sqlite3)
+ registry.register(:foo, Array, adapter: :postgresql)
+
+ assert_equal "", registry.lookup(:foo, adapter: :sqlite3)
+ assert_equal [], registry.lookup(:foo, adapter: :postgresql)
+ end
+
+ test "an error is raised if both a generic and adapter specific type match" do
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, String)
+ registry.register(:foo, Array, adapter: :postgresql)
+
+ assert_raises TypeConflictError do
+ registry.lookup(:foo, adapter: :postgresql)
+ end
+ assert_equal "", registry.lookup(:foo, adapter: :sqlite3)
+ end
+
+ test "a generic type can explicitly override an adapter specific type" do
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, String, override: true)
+ registry.register(:foo, Array, adapter: :postgresql)
+
+ assert_equal "", registry.lookup(:foo, adapter: :postgresql)
+ assert_equal "", registry.lookup(:foo, adapter: :sqlite3)
+ end
+
+ test "a generic type can explicitly allow an adapter type to be used instead" do
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, String, override: false)
+ registry.register(:foo, Array, adapter: :postgresql)
+
+ assert_equal [], registry.lookup(:foo, adapter: :postgresql)
+ assert_equal "", registry.lookup(:foo, adapter: :sqlite3)
+ end
+
+ test "a reasonable error is given when no type is found" do
+ registry = Type::AdapterSpecificRegistry.new
+
+ e = assert_raises(ArgumentError) do
+ registry.lookup(:foo)
+ end
+
+ assert_equal "Unknown type :foo", e.message
+ end
+
+ test "construct args are passed to the type" do
+ type = Struct.new(:args)
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, type)
+
+ assert_equal type.new, registry.lookup(:foo)
+ assert_equal type.new(:ordered_arg), registry.lookup(:foo, :ordered_arg)
+ assert_equal type.new(keyword: :arg), registry.lookup(:foo, keyword: :arg)
+ assert_equal type.new(keyword: :arg), registry.lookup(:foo, keyword: :arg, adapter: :postgresql)
+ end
+
+ test "registering a modifier" do
+ decoration = Struct.new(:value)
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, String)
+ registry.register(:bar, Hash)
+ registry.add_modifier({ array: true }, decoration)
+
+ assert_equal decoration.new(""), registry.lookup(:foo, array: true)
+ assert_equal decoration.new({}), registry.lookup(:bar, array: true)
+ assert_equal "", registry.lookup(:foo)
+ end
+
+ test "registering multiple modifiers" do
+ decoration = Struct.new(:value)
+ other_decoration = Struct.new(:value)
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, String)
+ registry.add_modifier({ array: true }, decoration)
+ registry.add_modifier({ range: true }, other_decoration)
+
+ assert_equal "", registry.lookup(:foo)
+ assert_equal decoration.new(""), registry.lookup(:foo, array: true)
+ assert_equal other_decoration.new(""), registry.lookup(:foo, range: true)
+ assert_equal(
+ decoration.new(other_decoration.new("")),
+ registry.lookup(:foo, array: true, range: true)
+ )
+ end
+
+ test "registering adapter specific modifiers" do
+ decoration = Struct.new(:value)
+ type = Struct.new(:args)
+ registry = Type::AdapterSpecificRegistry.new
+ registry.register(:foo, type)
+ registry.add_modifier({ array: true }, decoration, adapter: :postgresql)
+
+ assert_equal(
+ decoration.new(type.new(keyword: :arg)),
+ registry.lookup(:foo, array: true, adapter: :postgresql, keyword: :arg)
+ )
+ assert_equal(
+ type.new(array: true),
+ registry.lookup(:foo, array: true, adapter: :sqlite3)
+ )
+ end
+ end
+end
diff --git a/activerecord/test/cases/type_test.rb b/activerecord/test/cases/type_test.rb
new file mode 100644
index 0000000000..d45a9b3141
--- /dev/null
+++ b/activerecord/test/cases/type_test.rb
@@ -0,0 +1,39 @@
+require "cases/helper"
+
+class TypeTest < ActiveRecord::TestCase
+ setup do
+ @old_registry = ActiveRecord::Type.registry
+ ActiveRecord::Type.registry = ActiveRecord::Type::AdapterSpecificRegistry.new
+ end
+
+ teardown do
+ ActiveRecord::Type.registry = @old_registry
+ end
+
+ test "registering a new type" do
+ type = Struct.new(:args)
+ ActiveRecord::Type.register(:foo, type)
+
+ assert_equal type.new(:arg), ActiveRecord::Type.lookup(:foo, :arg)
+ end
+
+ test "looking up a type for a specific adapter" do
+ type = Struct.new(:args)
+ pgtype = Struct.new(:args)
+ ActiveRecord::Type.register(:foo, type, override: false)
+ ActiveRecord::Type.register(:foo, pgtype, adapter: :postgresql)
+
+ assert_equal type.new, ActiveRecord::Type.lookup(:foo, adapter: :sqlite)
+ assert_equal pgtype.new, ActiveRecord::Type.lookup(:foo, adapter: :postgresql)
+ end
+
+ test "lookup defaults to the current adapter" do
+ current_adapter = ActiveRecord::Base.connection.adapter_name.downcase.to_sym
+ type = Struct.new(:args)
+ adapter_type = Struct.new(:args)
+ ActiveRecord::Type.register(:foo, type, override: false)
+ ActiveRecord::Type.register(:foo, adapter_type, adapter: current_adapter)
+
+ assert_equal adapter_type.new, ActiveRecord::Type.lookup(:foo)
+ end
+end