Class: Unmagic::Enum

Inherits:
Object
  • Object
show all
Includes:
ActiveRecordExtensions
Defined in:
lib/unmagic/enum.rb,
lib/unmagic/enum/version.rb,
lib/unmagic/enum/active_record_extensions.rb

Overview

Base class for creating type-safe enums with string values

Basic usage:

class Status < Unmagic::Enum
  ACTIVE = new("active")
  PENDING = new("pending")
  ARCHIVED = new("archived")
end

With attributes:

class Priority < Unmagic::Enum
  attribute :label
  attribute :color

  HIGH = new("high", label: "High Priority", color: "red")
  MEDIUM = new("medium", label: "Medium Priority", color: "yellow")
  LOW = new("low", label: "Low Priority", color: "green")
end

Key/Value separation (useful for database migrations):

class MessageType < Unmagic::Enum
  # The key is what you use in code, value is what's stored in DB
  USER = new("user")                           # key and value both "user"
  ENTITY = new("entity", value: "bot")         # key: "entity", value: "bot" (legacy DB)
  SYSTEM = new("system", value: "s")           # key: "system", value: "s" (short code)
end

Different key types (symbols, integers, classes):

class MixedEnum < Unmagic::Enum
  # Keys preserve their original type
  SYMBOL = new(:active)                        # key is :active (Symbol)
  INTEGER = new(1, value: "one")               # key is 1 (Integer)
  CLASS = new(User)                            # key is User (Class)
end

STI (Single Table Inheritance) integration:

class User < ApplicationRecord
  class Type < Unmagic::Enum
    # Pass the actual class - no constantize needed!
    CUSTOMER = new(Customer)                   # key: Customer class, value: "Customer"
    ADMIN = new(Admin, value: "a")            # key: Admin class, value: "a"
    MODERATOR = new(Moderator)                # key: Moderator class, value: "Moderator"
  end

  attribute :type, Type.column_type

  # Clean STI integration - enum.key returns the actual class
  def self.find_sti_class(type_name)
    if enum_value = Type[type_name]
      enum_value.key  # Returns the class directly, no constantize!
    else
      super
    end
  end

  def self.sti_name
    Type.all.find { |e| e.key == self }&.value || name
  end
end

Usage patterns:

status = Status::ACTIVE
status.active?                # => true (query method)
status == "active"            # => true (string equality)
status == :active             # => false (symbols don't match strings)
status.to_s                   # => "active" (for database)

# Lookups work with any type
Status["active"]              # => Status::ACTIVE
MixedEnum[:active]            # => MixedEnum::SYMBOL
MixedEnum[1]                  # => MixedEnum::INTEGER
MixedEnum[User]               # => MixedEnum::CLASS

Defined Under Namespace

Modules: ActiveRecordExtensions Classes: InvalidValueError, ReservedValueError

Constant Summary collapse

VERSION =

Current version of the unmagic-enum gem

"0.1.1"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ActiveRecordExtensions

included

Constructor Details

#initialize(key, **attributes) ⇒ Enum

Initialize the enum with key and optional value



277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/unmagic/enum.rb', line 277

def initialize(key, **attributes)
  @key = key # Keep original type (class, symbol, integer, string, etc.)
  @key_string = key.to_s # String version for lookups and comparisons

  # Extract the special 'value' option, default to string version of key
  @value = attributes.delete(:value)&.to_s || @key_string

  # Check for duplicate keys
  if self.class.instances_by_key[@key_string]
    raise InvalidValueError.new("Enum key '#{@key_string}' has already been defined")
  end

  # Check for duplicate values
  existing = self.class.instances_by_value[@value]
  if existing
    raise InvalidValueError.new("Enum value '#{@value}' has already been defined for key '#{existing.key_string}'")
  end

  # Check for conflicts with reserved methods (using string key for query methods)
  key_method = "#{@key_string}?"
  if self.class.reserved_methods.include?(key_method)
    raise ReservedValueError.new("Cannot create enum key '#{@key_string}' because it would conflict with alias method '#{key_method}'")
  end

  # Set declared attributes with defaults
  self.class..each do |attr, |
    value = if attributes.key?(attr)
              attributes[attr]
            elsif .key?(:default)
              [:default]
            else
              nil
            end
    instance_variable_set("@#{attr}", value)
  end

  # Warn about undeclared attributes in development
  if defined?(Rails) && Rails.env.development?
    extra_attrs = attributes.keys - self.class.attributes
    if extra_attrs.any?
      warn "[Unmagic::Enum] Undeclared attributes passed to #{self.class.name}: #{extra_attrs.join(', ')}"
    end
  end

  freeze
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args) ⇒ Object

Implement query methods like ‘user?` for checking enum keys



325
326
327
328
329
330
331
332
# File 'lib/unmagic/enum.rb', line 325

def method_missing(method_name, *args)
  if method_name.to_s.end_with?('?')
    key_to_check = method_name.to_s[0..-2] # Remove the '?'
    @key_string == key_to_check # Compare string versions
  else
    super
  end
end

Instance Attribute Details

#keyObject (readonly)

The key (identifier used in code - preserves original type)



213
214
215
# File 'lib/unmagic/enum.rb', line 213

def key
  @key
end

#key_stringObject (readonly)

The key as a string (used for lookups and comparisons)



216
217
218
# File 'lib/unmagic/enum.rb', line 216

def key_string
  @key_string
end

#valueObject (readonly)

The value (what gets stored in database)



219
220
221
# File 'lib/unmagic/enum.rb', line 219

def value
  @value
end

Class Method Details

.[](lookup) ⇒ Object

Look up enum by key or value



169
170
171
172
173
# File 'lib/unmagic/enum.rb', line 169

def [](lookup)
  lookup_str = lookup.to_s
  # Try key first, then value
  instances_by_key[lookup_str] || instances_by_value[lookup_str]
end

.allObject

Get all enum values



164
165
166
# File 'lib/unmagic/enum.rb', line 164

def all
  instances.values
end

.attribute(*names, **options) ⇒ Object

Declare attributes for this enum class with options



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/unmagic/enum.rb', line 124

def attribute(*names, **options)
  @attribute_metadata ||= {}

  names.each do |name|
    # Store metadata for this attribute
    @attribute_metadata[name] = options

    # Create the reader method
    attr_reader name

    # Create alias if specified
    next unless options[:alias]

    aliases = Array(options[:alias])
    aliases.each do |alias_name|
      alias_method alias_name, name

      # Track reserved method names to prevent conflicts
      @reserved_methods ||= Set.new
      @reserved_methods.add(alias_name.to_s)
    end
  end
end

.attribute_metadataObject

Get metadata for an attribute



154
155
156
# File 'lib/unmagic/enum.rb', line 154

def 
  @attribute_metadata || {}
end

.attributesObject

Get declared attributes (just the names)



149
150
151
# File 'lib/unmagic/enum.rb', line 149

def attributes
  @attribute_metadata&.keys || []
end

.call(value) ⇒ Object

Alias for [] to support dry-initializer type coercion dry-initializer expects types to respond to .call with 1 argument



177
178
179
# File 'lib/unmagic/enum.rb', line 177

def call(value)
  self[value]
end

.inherited(subclass) ⇒ Object

Ensure each subclass has its own metadata



204
205
206
207
208
209
# File 'lib/unmagic/enum.rb', line 204

def inherited(subclass)
  super
  # Initialize metadata for attributes and reserved methods
  subclass.instance_variable_set(:@attribute_metadata, {})
  subclass.instance_variable_set(:@reserved_methods, Set.new)
end

.instancesObject

Backward compatibility - instances is an alias for instances_by_key



119
120
121
# File 'lib/unmagic/enum.rb', line 119

def instances
  instances_by_key
end

.instances_by_keyObject

Get enum instances dynamically from constants



97
98
99
100
101
102
103
104
105
# File 'lib/unmagic/enum.rb', line 97

def instances_by_key
  # Build hash from constants each time (stateless)
  constants.each_with_object({}) do |const_name, hash|
    const = const_get(const_name)
    next unless const.is_a?(Unmagic::Enum)

    hash[const.key_string] = const
  end
end

.instances_by_valueObject

Get enum instances by value (database value)



108
109
110
111
112
113
114
115
116
# File 'lib/unmagic/enum.rb', line 108

def instances_by_value
  # Build hash from constants each time (stateless)
  constants.each_with_object({}) do |const_name, hash|
    const = const_get(const_name)
    next unless const.is_a?(Unmagic::Enum)

    hash[const.value] = const
  end
end

.keysObject

Get all valid keys (identifiers used in code)



187
188
189
# File 'lib/unmagic/enum.rb', line 187

def keys
  instances_by_key.keys
end

.reserved_methodsObject

Get reserved method names (from aliases)



159
160
161
# File 'lib/unmagic/enum.rb', line 159

def reserved_methods
  @reserved_methods || Set.new
end

.valid?(value) ⇒ Boolean

Check if a value is valid for this enum

Returns:

  • (Boolean)


192
193
194
195
196
197
198
199
200
201
# File 'lib/unmagic/enum.rb', line 192

def valid?(value)
  case value
  when self
    true
  when String
    instances.key?(value)
  else
    false
  end
end

.valuesObject

Get all valid database values (useful for validations)



182
183
184
# File 'lib/unmagic/enum.rb', line 182

def values
  instances_by_value.keys
end

Instance Method Details

#==(other) ⇒ Object

Override equality to work with strings and same-class enums



222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/unmagic/enum.rb', line 222

def ==(other)
  if other.is_a?(Unmagic::Enum)
    # Only equal if same class and same value
    other.class == self.class && @value == other.value
  elsif other.is_a?(String)
    # Check both key_string and value for flexibility
    [@key_string, @value].include?(other)
  else
    # Check if it matches the original key (for symbols, classes, etc.)
    @key == other
  end
end

#as_jsonObject

Return the value when used in JSON



272
273
274
# File 'lib/unmagic/enum.rb', line 272

def as_json
  to_s
end

#blank?Boolean

Rails presence validation support - enums are never blank

Returns:

  • (Boolean)


340
341
342
# File 'lib/unmagic/enum.rb', line 340

def blank?
  false
end

#eql?(other) ⇒ Boolean

Ensure different enum classes don’t match

Returns:

  • (Boolean)


236
237
238
# File 'lib/unmagic/enum.rb', line 236

def eql?(other)
  other.is_a?(self.class) && to_s == other.to_s
end

#hashObject

Hash code based on string value and class



241
242
243
# File 'lib/unmagic/enum.rb', line 241

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

#inspectObject

Human-readable inspect showing how to reference this enum in code



246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/unmagic/enum.rb', line 246

def inspect
  # Find the constant name for this enum instance
  constant_name = self.class.constants.find do |const|
    self.class.const_get(const) == self
  end

  if constant_name
    "#{self.class.name}::#{constant_name}"
  else
    # Fallback showing how to access via bracket notation
    # Show the original key type for clarity
    "#{self.class.name}[#{@key.inspect}]"
  end
end

#present?Boolean

Rails presence validation support - enums are always present

Returns:

  • (Boolean)


345
346
347
# File 'lib/unmagic/enum.rb', line 345

def present?
  true
end

#respond_to_missing?(method_name, include_private = false) ⇒ Boolean

Properly handle respond_to? for query methods

Returns:

  • (Boolean)


335
336
337
# File 'lib/unmagic/enum.rb', line 335

def respond_to_missing?(method_name, include_private = false)
  method_name.to_s.end_with?('?') || super
end

#to_sObject

Return the database value (for serialization)



267
268
269
# File 'lib/unmagic/enum.rb', line 267

def to_s
  @value
end

#to_strObject

Allow enum to be used directly in database queries and assignments



262
263
264
# File 'lib/unmagic/enum.rb', line 262

def to_str
  to_s
end