Custom locators can override model class derivation
GlobalID now lets custom locators define a model_class(gid) method, giving developers control over how a GID is mapped to a Ruby class. This change removes the assumption that the model name in a GID must match a top‑level constant, while retaining compatibility for existing locators.
背景
Historically GlobalID derived the model class by constantizing the model name extracted from the GID, which forced a one‑to‑one naming convention between GIDs and Ruby classes. Issue #80 highlighted a real‑world scenario where services are being split and GIDs refer to namespaced models that no longer exist in the top‑level namespace. Developers needed a way to map those names to the appropriate local classes without renaming or duplicating constants. By allowing locators to override the derivation logic, the library now supports more flexible service‑oriented architectures.
技術的な変更
The core change adds a hook for locators to provide their own model_class(gid) implementation, while preserving the previous constantize behavior as a fallback. This hook is invoked from GlobalID#model_class, which now consults the associated locator before falling back.
@@
def model_class
@model_class ||= begin
- model = model_name.constantize
+ locator = Locator.locator_for(self)
+ model = begin
+ locator.model_class(self)
+ rescue NoMethodError
+ if locator.respond_to?(:model_class)
+ raise
+ 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
+ end
if model <= GlobalID
raise ArgumentError, "GlobalID and SignedGlobalID cannot be used as model_class."
end
*** End Patch
*** End Patch
In lib/global_id/locator.rb, the default BaseLocator and BlockLocator classes now implement a basic model_class method that simply constantizes the GID's model name. The visibility of locator_for was also adjusted to be public, enabling the new call path.
@@
- 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
@@
- class BaseLocator
+ class BaseLocator
+ def model_class(gid)
+ gid.model_name.constantize
+ end
@@
- class BlockLocator < Locator
+ class BlockLocator < Locator
+ def model_class(gid)
+ gid.model_name.constantize
+ end
The README was updated to document the new capability, showing how to inherit from BaseLocator and override model_class. Sample code demonstrates redirecting remote model names like User to local classes such as RemoteUser.
#### Custom Model Class Derivation
```ruby
class RemoteLocator < GlobalID::Locator::BaseLocator
def model_class(gid)
case gid.model_name
when 'User' then RemoteUser
when 'Profile' then RemoteProfile
else
super
end
end
end
Test suite additions verify three scenarios: a locator that implements `model_class`, a locator that inherits the default implementation, and a legacy locator that triggers the deprecation warning. These tests ensure both the new happy path and the backward‑compatibility path behave as intended.
```ruby:test/cases/global_locator_test.rb
@@
test 'locator with custom model_class derivation' do
@@
test 'locator without model_class method shows deprecation warning' do
@@
設計判断
The team chose to extend the existing Locator abstraction rather than introducing a separate mapping API. By adding model_class to BaseLocator and BlockLocator, existing custom locators can gain the new capability simply by inheriting from the provided base class. This minimizes the surface area of change and keeps the public contract familiar to current users.
When a locator does not implement model_class, the library falls back to the historic constantize approach but emits a deprecation warning. This design preserves runtime compatibility for legacy code while nudging developers toward the newer, more explicit inheritance pattern. The warning message explicitly references the missing method and recommends inheriting from BaseLocator, providing clear migration guidance.
Recommending inheritance from GlobalID::Locator::BaseLocator (or UnscopedLocator for ActiveRecord models) balances flexibility with safety. It allows custom logic for name translation without sacrificing the default fast path, and it keeps the locator interface cohesive by centralising related responsibilities (locate, locate_many, model_class) within a single class hierarchy.
まとめ
By allowing custom locators to override model_class(gid), GlobalID now supports arbitrary name-to‑class mappings, facilitating modular service architectures and legacy integrations. The change is backward compatible, guarded by a deprecation warning, and encourages a clean inheritance‑based extension pattern for future locator implementations.