Class: Anchormodel

Inherits:
Object
  • Object
show all
Defined in:
lib/anchormodel.rb,
lib/anchormodel/version.rb,
lib/anchormodel/simple_form_inputs/helpers/anchormodel_inputs_common.rb

Overview

An Anchormodel is a registry of named constants that behave like first-class objects. It is a richer alternative to Rails Enums: each entry is a real Ruby instance that can carry behavior and per-key attributes while still being persistable to a String column in the database.

Place anchormodel subclasses under app/anchormodels/your_anchor_model.rb so Rails autoloading picks them up. Refer to the README for usage.

Examples:

Define an anchormodel

class Role < Anchormodel
  include Comparable
  attr_reader :privilege_level

  def <=>(other) = privilege_level <=> other.privilege_level

  new :guest,   privilege_level: 0
  new :manager, privilege_level: 1
  new :admin,   privilege_level: 2
end

Use it from an ActiveRecord model

class User < ApplicationRecord
  belongs_to_anchormodel :role
end

user = User.create!(role: :admin)
user.role                   # => #<Role<admin>:...>
user.role.privilege_level   # => 2
user.admin?                 # => true

Defined Under Namespace

Modules: ModelMixin, SimpleFormInputs, Util, Version Classes: ActiveModelTypeValueMulti, ActiveModelTypeValueSingle, Attribute, InvalidKey

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key, **attributes) ⇒ Anchormodel

Registers a new entry. Called in the body of an Anchormodel subclass. All keyword arguments become instance attributes accessible via attr_reader.

Examples:

class Role < Anchormodel
  attr_reader :privilege_level
  new :guest, privilege_level: 0
end

Parameters:

  • key (String, Symbol)

    The unique key under which the entry is registered.

  • attributes (Hash)

    Arbitrary attributes exposed as instance variables.

Raises:

  • (RuntimeError)

    if key is already registered for this subclass.



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/anchormodel.rb', line 115

def initialize(key, **attributes)
  self.class.setup! unless self.class.setup_completed

  @key = key.to_sym
  @index = entries_list.count

  # Save attributes as instance variables
  attributes.each do |k, v|
    instance_variable_set(:"@#{k}", v)
  end

  # Register self
  fail("Duplicate anchor model key #{key.inspect} for #{self.class}.") if entries_hash.key?(key)
  entries_list << self
  entries_hash[key] = self

  # Register valid keys
  valid_keys << key

  # Define boolean reader
  self.class.define_method(:"#{key}?") do
    @key == key
  end
end

Instance Attribute Details

#indexObject (readonly)

Returns the value of attribute index.



37
38
39
# File 'lib/anchormodel.rb', line 37

def index
  @index
end

#keyObject (readonly)

Returns the value of attribute key.



33
34
35
# File 'lib/anchormodel.rb', line 33

def key
  @key
end

Class Method Details

.allArray<Anchormodel>

Returns All registered entries of this subclass, in declaration order.

Examples:

Role.all # => [#<Role<guest>>, #<Role<manager>>, #<Role<admin>>]

Returns:

  • (Array<Anchormodel>)

    All registered entries of this subclass, in declaration order.



63
64
65
# File 'lib/anchormodel.rb', line 63

def self.all
  entries_list
end

.find(key) ⇒ Anchormodel?

Retrieves the entry registered under key.

Examples:

Role.find(:admin)   # => #<Role<admin>>
Role.find('admin')  # => #<Role<admin>>   (same singleton instance)
Role.find(nil)      # => nil
Role.find(:nope)    # raises Anchormodel::InvalidKey

Parameters:

  • key (String, Symbol, nil)

    The key of the value that should be retrieved.

Returns:

  • (Anchormodel, nil)

    The matching entry, or nil if key is nil.

Raises:



99
100
101
102
# File 'lib/anchormodel.rb', line 99

def self.find(key)
  return nil if key.nil?
  return entries_hash[key.to_sym] || raise(InvalidKey, "Retrieved undefined anchor model key #{key.inspect} for #{inspect}.")
end

.firstAnchormodel?

Shorthand for all.first. Provided so callers can avoid Rubocop's Style/FirstElementInCollection-style warnings on Foo.all.first.

Returns:

  • (Anchormodel, nil)

    The first registered entry, or nil if the registry is empty.



70
71
72
# File 'lib/anchormodel.rb', line 70

def self.first
  all.first
end

.form_collectionArray<Array(String,String)>

Builds a [label, key_string] tuple list suitable for Rails form select helpers.

Examples:

<%= form.select :role, Role.form_collection %>

Returns:

  • (Array<Array(String,String)>)


78
79
80
# File 'lib/anchormodel.rb', line 78

def self.form_collection
  entries_list.map { |el| [el.label, el.key.to_s] }
end

.setup!void

This method returns an undefined value.

Initializes the per-subclass registry on first use. Called automatically from the first new invocation. Each subclass gets its own duped copies of the registry class attributes to prevent cross-class pollution.

You normally do not need to call this directly.

Raises:

  • (RuntimeError)

    if called twice for the same subclass.



52
53
54
55
56
57
58
# File 'lib/anchormodel.rb', line 52

def self.setup!
  fail("`setup!` was called twice for Anchormodel subclass #{self}.") if setup_completed
  self.entries_list = entries_list.dup
  self.entries_hash = entries_hash.dup
  self.valid_keys = valid_keys.dup
  self.setup_completed = true
end

Instance Method Details

#==(other) ⇒ Boolean Also known as: eql?

Two anchormodels are equal iff they have the same concrete class and key. Different subclasses sharing a key are not equal.

Parameters:

  • other (Object)

Returns:

  • (Boolean)


144
145
146
# File 'lib/anchormodel.rb', line 144

def ==(other)
  self.class == other.class && key == other.key
end

#as_jsonString

JSON serialization returns the key as a String so anchormodels round-trip cleanly through JSON (e.g. for API payloads).

Examples:

Role.find(:admin).as_json # => "admin"

Returns:

  • (String)


184
185
186
# File 'lib/anchormodel.rb', line 184

def as_json
  key.to_s
end

#hashInteger

Hash matches == (class + key) so Set and Hash membership work correctly even for copies (dup, Marshal round-trip) of the singleton entries.

Returns:

  • (Integer)


152
153
154
# File 'lib/anchormodel.rb', line 152

def hash
  [self.class, key].hash
end

#inspectString

Returns Debug representation like "#<Role<admin>:HASH>".

Returns:

  • (String)

    Debug representation like "#<Role<admin>:HASH>".



167
168
169
# File 'lib/anchormodel.rb', line 167

def inspect
  "#<#{self.class.name}<#{key}>:#{hash}>"
end

#labelString

Returns a translatable label for this entry, compatible with the Rails FastGettext gem. The translation key is "<SubclassName>|<Humanized key>".

Examples:

Role.find(:admin).label # => "Role|Admin" (or its I18n translation)

Returns:

  • (String)


162
163
164
# File 'lib/anchormodel.rb', line 162

def label
  I18n.t("#{self.class.name.demodulize}|#{key.to_s.humanize}")
end

#to_sString

Same as #inspect. Anchormodel intentionally overrides to_s so string interpolation in templates is unambiguous; render #label or #key directly if you want a user-facing form.

Returns:

  • (String)


175
176
177
# File 'lib/anchormodel.rb', line 175

def to_s
  inspect
end