Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down
12 changes: 11 additions & 1 deletion lib/global_id/global_id.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
16 changes: 12 additions & 4 deletions lib/global_id/locator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
37 changes: 36 additions & 1 deletion test/cases/global_locator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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

Expand Down
Loading