diff --git a/README.md b/README.md index 20c8d6a..979c8c2 100644 --- a/README.md +++ b/README.md @@ -199,17 +199,63 @@ end Using a class: ```ruby -GlobalID::Locator.use :bar, BarLocator.new class BarLocator def locate(gid, options = {}) @search_client.search name: gid.model_name, id: gid.model_id end end + +GlobalID::Locator.use :bar, BarLocator.new +``` + +It's recommended to inherit from `GlobalID::Locator::BaseLocator` (or `GlobalID::Locator::UnscopedLocator` for Active Record models) to get default implementations of `model_class` and `locate_many`: + +```ruby +class BarLocator < GlobalID::Locator::BaseLocator + def locate(gid, options = {}) + @search_client.search name: gid.model_name, id: gid.model_id + end +end + +GlobalID::Locator.use :bar, BarLocator.new ``` After defining locators as above, URIs like `gid://foo/Person/1` and `gid://bar/Person/1` will now use the foo block locator and `BarLocator` respectively. Other apps will still keep using the default locator. +#### Custom Model Class Derivation + +By default, GlobalID derives the model class by calling `constantize` on the model name from the GID. Custom locators can override this behavior by implementing a `model_class` method. This is useful when the model name in the GID doesn't match the actual class name, or when you want to redirect to a different model. + +Inherit from `BaseLocator` and override `model_class`: + +```ruby +class RemoteLocator < GlobalID::Locator::BaseLocator + def model_class(gid) + # Map remote model names to local models + case gid.model_name + when 'User' + RemoteUser + when 'Profile' + RemoteProfile + else + super # Fall back to default constantize behavior + end + end + + def locate(gid, options = {}) + # Use the mapped model class to find the record + model_class(gid).find_by(remote_id: gid.model_id) + end +end + +GlobalID::Locator.use :remote, RemoteLocator.new +``` + +This allows you to work with Global IDs that reference models that don't exist in your application, redirecting them to the appropriate local models. + +**Note**: For backward compatibility, if a custom locator doesn't implement `model_class`, GlobalID will fall back to the default behavior (`constantize`) but will emit a deprecation warning. To avoid this, inherit from `GlobalID::Locator::BaseLocator` or `GlobalID::Locator::UnscopedLocator`. + ### Custom Default Locator A custom default locator can be set for an app by calling `GlobalID::Locator.default_locator=` and providing a default locator to use for that app. @@ -221,7 +267,7 @@ class MyCustomLocator < UnscopedLocator super(gid, options) end end - + def locate_many(gids, options = {}) ActiveRecord::Base.connected_to(role: :reading) do super(gids, options) diff --git a/lib/global_id/global_id.rb b/lib/global_id/global_id.rb index 9bf961f..9791273 100644 --- a/lib/global_id/global_id.rb +++ b/lib/global_id/global_id.rb @@ -55,7 +55,17 @@ def find(options = {}) def model_class @model_class ||= begin - model = model_name.constantize + locator = Locator.locator_for(self) + model = if locator.respond_to?(:model_class) + locator.model_class(self) + else + GlobalID.deprecator.warn <<~MSG.squish + Your locator #{locator.class.name} does not implement the + `model_class` method. Please add a `model_class(gid)` method + to your locator or inherit from `GlobalID::Locator::BaseLocator`. + MSG + model_name.constantize + end if model <= GlobalID raise ArgumentError, "GlobalID and SignedGlobalID cannot be used as model_class." diff --git a/lib/global_id/locator.rb b/lib/global_id/locator.rb index c9edc5e..b2456ac 100644 --- a/lib/global_id/locator.rb +++ b/lib/global_id/locator.rb @@ -135,11 +135,11 @@ def use(app, locator = nil, &locator_block) @locators[normalize_app(app)] = locator || BlockLocator.new(locator_block) end - private - def locator_for(gid) - @locators.fetch(normalize_app(gid.app)) { default_locator } - end + def locator_for(gid) + @locators.fetch(normalize_app(gid.app)) { default_locator } + end + private def find_allowed?(model_class, only = nil) only ? Array(only).any? { |c| model_class <= c } : true end @@ -157,6 +157,10 @@ def normalize_app(app) @locators = {} class BaseLocator + def model_class(gid) + gid.model_name.constantize + end + def locate(gid, options = {}) return unless model_id_is_valid?(gid) model_class = gid.model_class @@ -234,6 +238,10 @@ def initialize(block) @locator = block end + def model_class(gid) + gid.model_name.constantize + end + def locate(gid, options = {}) @locator.call(gid, options) end diff --git a/test/cases/global_locator_test.rb b/test/cases/global_locator_test.rb index 6775d2d..1dbf5db 100644 --- a/test/cases/global_locator_test.rb +++ b/test/cases/global_locator_test.rb @@ -281,6 +281,7 @@ class GlobalLocatorTest < ActiveSupport::TestCase test 'use locator with class' do class BarLocator + def model_class(gid); gid.model_name.constantize; end def locate(gid, options = {}); :bar; end def locate_many(gids, options = {}); gids.map(&:model_id); end end @@ -295,6 +296,7 @@ def locate_many(gids, options = {}); gids.map(&:model_id); end test 'use locator with class and single argument' do class DeprecatedBarLocator + def model_class(gid); gid.model_name.constantize; end def locate(gid); :deprecated; end def locate_many(gids, options = {}); gids.map(&:model_id); end end @@ -325,6 +327,39 @@ def locate_many(gids, options = {}); gids.map(&:model_id); end end end + test 'locator with custom model_class derivation' do + class CustomModelLocator < GlobalID::Locator::BaseLocator + def model_class(_gid); Person; end + end + + GlobalID::Locator.use :custom, CustomModelLocator.new + + with_app 'custom' do + gid = GlobalID.new('gid://custom/Folk/5') + + found = GlobalID::Locator.locate(gid) + assert_kind_of Person, found + assert_equal '5', found.id + end + end + + test 'locator without model_class method shows deprecation warning' do + class LegacyLocator + # Intentionally doesn't implement model_class + def locate(gid, options = {}); Person.find(gid.model_id); end + end + + GlobalID::Locator.use :legacy, LegacyLocator.new + + with_app 'legacy' do + gid = Person.new('5').to_gid + + assert_deprecated(nil, GlobalID.deprecator) do + assert_equal Person, gid.model_class + end + end + end + test "by valid purpose returns right model" do instance = Person.new login_sgid = instance.to_signed_global_id(for: 'login') @@ -387,7 +422,7 @@ def locate_many(gids, options = {}); gids.map(&:model_id); end end test "can set default_locator" do - class MyLocator + class MyLocator < GlobalID::Locator::BaseLocator def locate(gid, options = {}); :my_locator; end end