Class: Unmagic::Enum
- Inherits:
-
Object
- Object
- Unmagic::Enum
- 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
-
#key ⇒ Object
readonly
The key (identifier used in code - preserves original type).
-
#key_string ⇒ Object
readonly
The key as a string (used for lookups and comparisons).
-
#value ⇒ Object
readonly
The value (what gets stored in database).
Class Method Summary collapse
-
.[](lookup) ⇒ Object
Look up enum by key or value.
-
.all ⇒ Object
Get all enum values.
-
.attribute(*names, **options) ⇒ Object
Declare attributes for this enum class with options.
-
.attribute_metadata ⇒ Object
Get metadata for an attribute.
-
.attributes ⇒ Object
Get declared attributes (just the names).
-
.call(value) ⇒ Object
Alias for [] to support dry-initializer type coercion dry-initializer expects types to respond to .call with 1 argument.
-
.inherited(subclass) ⇒ Object
Ensure each subclass has its own metadata.
-
.instances ⇒ Object
Backward compatibility - instances is an alias for instances_by_key.
-
.instances_by_key ⇒ Object
Get enum instances dynamically from constants.
-
.instances_by_value ⇒ Object
Get enum instances by value (database value).
-
.keys ⇒ Object
Get all valid keys (identifiers used in code).
-
.reserved_methods ⇒ Object
Get reserved method names (from aliases).
-
.valid?(value) ⇒ Boolean
Check if a value is valid for this enum.
-
.values ⇒ Object
Get all valid database values (useful for validations).
Instance Method Summary collapse
-
#==(other) ⇒ Object
Override equality to work with strings and same-class enums.
-
#as_json ⇒ Object
Return the value when used in JSON.
-
#blank? ⇒ Boolean
Rails presence validation support - enums are never blank.
-
#eql?(other) ⇒ Boolean
Ensure different enum classes don’t match.
-
#hash ⇒ Object
Hash code based on string value and class.
-
#initialize(key, **attributes) ⇒ Enum
constructor
Initialize the enum with key and optional value.
-
#inspect ⇒ Object
Human-readable inspect showing how to reference this enum in code.
-
#method_missing(method_name, *args) ⇒ Object
Implement query methods like ‘user?` for checking enum keys.
-
#present? ⇒ Boolean
Rails presence validation support - enums are always present.
-
#respond_to_missing?(method_name, include_private = false) ⇒ Boolean
Properly handle respond_to? for query methods.
-
#to_s ⇒ Object
Return the database value (for serialization).
-
#to_str ⇒ Object
Allow enum to be used directly in database queries and assignments.
Methods included from ActiveRecordExtensions
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
#key ⇒ Object (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_string ⇒ Object (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 |
#value ⇒ Object (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 |
.all ⇒ Object
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, **) @attribute_metadata ||= {} names.each do |name| # Store metadata for this attribute @attribute_metadata[name] = # Create the reader method attr_reader name # Create alias if specified next unless [:alias] aliases = Array([: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_metadata ⇒ Object
Get metadata for an attribute
154 155 156 |
# File 'lib/unmagic/enum.rb', line 154 def @attribute_metadata || {} end |
.attributes ⇒ Object
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 |
.instances ⇒ Object
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_key ⇒ Object
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_value ⇒ Object
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 |
.keys ⇒ Object
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_methods ⇒ Object
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
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 |
.values ⇒ Object
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_json ⇒ Object
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
340 341 342 |
# File 'lib/unmagic/enum.rb', line 340 def blank? false end |
#eql?(other) ⇒ Boolean
Ensure different enum classes don’t match
236 237 238 |
# File 'lib/unmagic/enum.rb', line 236 def eql?(other) other.is_a?(self.class) && to_s == other.to_s end |
#hash ⇒ Object
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 |
#inspect ⇒ Object
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
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
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_s ⇒ Object
Return the database value (for serialization)
267 268 269 |
# File 'lib/unmagic/enum.rb', line 267 def to_s @value end |
#to_str ⇒ Object
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 |