Class: Familia::Features::ExternalIdentifier::ExternalIdentifierFieldType

Inherits:
Familia::FieldType
  • Object
show all
Defined in:
lib/familia/features/external_identifier.rb

Overview

ExternalIdentifierFieldType - Fields that derive deterministic external identifiers

External identifier fields derive shorter, public-facing identifiers that are deterministically derived from object identifiers. These IDs are safe for use in URLs, APIs, and other external contexts where shorter IDs are preferred.

Key characteristics:

  • Deterministic generation from objid ensures consistency
  • Shorter than objid (128-bit vs 256-bit) for external use
  • Base-36 encoding for URL-safe identifiers
  • Customizable format template (default: 'ext_' prefix)
  • Lazy generation preserves values from initialization

Examples:

Using external identifier fields with default format

class User < Familia::Horreum
  feature :object_identifier
  feature :external_identifier
  field :email
end
user = User.new(email: 'user@example.com')
user.objid  # => "01234567-89ab-7def-8000-123456789abc"
user.extid  # => "ext_abc123def456ghi789" (deterministic from objid)
# Same objid always produces same extid
user2 = User.new(objid: user.objid, email: 'user@example.com')
user2.extid  # => "ext_abc123def456ghi789" (identical to user.extid)

Using custom format template with hyphen separator

class APIKey < Familia::Horreum
  feature :object_identifier
  feature :external_identifier, format: 'api-%{id}'
end
key = APIKey.new
key.extid  # => "api-abc123def456ghi789"

Using custom format template with custom prefix

class Customer < Familia::Horreum
  feature :object_identifier
  feature :external_identifier, format: 'cust_%{id}'
end
customer = Customer.new
customer.extid  # => "cust_abc123def456ghi789"

Using format template without prefix

class Resource < Familia::Horreum
  feature :object_identifier
  feature :external_identifier, format: 'v2/%{id}'
end
resource = Resource.new
resource.extid  # => "v2/abc123def456ghi789"

Keying derivation with an application secret (recommended)

# By default the extid is a deterministic SHA-256 of the objid. Supply
# a `secret:` to derive it via a keyed HMAC instead, so external IDs
# cannot be forged or inverted from a known objid. The secret must be
# stable for a deployment (changing it changes every extid) and is
# typically sourced from the environment, never committed.
#
# Prefer a callable so the secret resolves lazily at first use: the model
# file still loads when the env var is absent, and ENV.fetch then raises
# loudly the first time an extid is derived rather than at class-load.
class ApiToken < Familia::Horreum
  feature :object_identifier
  feature :external_identifier, secret: -> { ENV.fetch('EXTID_HMAC_SECRET') }
end
# A plain string also works when the value is already in hand:
#   feature :external_identifier, secret: ENV['EXTID_HMAC_SECRET']

Instance Method Summary collapse

Constructor Details

This class inherits a constructor from Familia::FieldType

Instance Method Details

#categorySymbol

Category for external identifier fields

Returns:

  • (Symbol)

    :external_identifier



166
167
168
# File 'lib/familia/features/external_identifier.rb', line 166

def category
  :external_identifier
end

#define_getter(klass) ⇒ Object

Override getter to provide lazy generation from objid

Derives the external identifier deterministically from the object's objid. This ensures consistency - the same objid will always produce the same extid. Only derives when objid is available.

Parameters:

  • klass (Class)

    The class to define the method on



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/familia/features/external_identifier.rb', line 109

def define_getter(klass)
  field_name = @name
  method_name = @method_name

  handle_method_conflict(klass, method_name) do
    klass.define_method method_name do
      # Check if we already have a value (from initialization or previous generation)
      existing_value = instance_variable_get(:"@#{field_name}")
      return existing_value unless existing_value.nil?

      # Derive external identifier from objid if available
      derived_extid = derive_external_identifier
      return unless derived_extid

      instance_variable_set(:"@#{field_name}", derived_extid)

      derived_extid
    end
  end
end

#define_setter(klass) ⇒ Object

Override setter to preserve values during initialization

This ensures that values passed during object initialization (e.g., when loading from Valkey/Redis) are preserved and not overwritten by the lazy generation logic.

Parameters:

  • klass (Class)

    The class to define the method on



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/familia/features/external_identifier.rb', line 138

def define_setter(klass)
  field_name = @name
  method_name = @method_name

  handle_method_conflict(klass, :"#{method_name}=") do
    klass.define_method :"#{method_name}=" do |value|
      # Remove old mapping if extid is changing
      old_value = instance_variable_get(:"@#{field_name}")
      self.class.extid_lookup.remove_field(old_value) if old_value && old_value != value

      # Set the new value
      instance_variable_set(:"@#{field_name}", value)
    end
  end
end

#persistent?Boolean

External identifier fields are persisted to database

Returns:

  • (Boolean)

    true - external identifiers are always persisted



158
159
160
# File 'lib/familia/features/external_identifier.rb', line 158

def persistent?
  true
end