Enhanced enum-like fields for ActiveRecord models with metadata support
- Ruby >= 3.1
- Rails >= 6.0 (ActiveRecord and ActiveSupport)
Add this line to your application's Gemfile:
gem"enum_fields"And then execute:
bundle installConfigure default behavior for all enum_field declarations:
# config/initializers/enum_fields.rbEnumFields.configuredo |config|
config.scopeable=true# default: trueconfig.validatable=true# default: trueconfig.nullable=true# default: trueconfig.inquirable=true# default: trueend| Option | Default | Description |
|---|---|---|
scopeable | true | Generate query scopes for each enum value |
validatable | true | Add inclusion validation for enum values |
nullable | true | Allow nil values in validation (polymorphic columns derive this from the association's optional flag instead) |
inquirable | true | Generate ? inquiry methods for each enum value |
Individual enum_field options override global configuration:
EnumFields.configuredo |config|
config.scopeable=falseendclassCampaign < ApplicationRecord# Uses global scopeable: falseenum_field:stage,definitions# Overrides global — scopes are generated for this fieldenum_field:priority,definitions,scopeable: trueendInclude the EnumFields module in your ApplicationRecord:
classApplicationRecord < ActiveRecord::BaseincludeEnumFieldsself.abstract_class=trueendNow all models inheriting from ApplicationRecord can use enum_field.
classCampaign < ApplicationRecordenum_field:stage,{pending: {value: "pending",label: "Pending",icon: "clock",color: "yellow",tooltip: "Campaign is awaiting processing",},processing: {value: "processing",label: "Processing",icon: "cog",color: "blue",tooltip: "Campaign is being processed",},shipped: {value: "shipped",label: "Shipped",icon: "truck",color: "green",tooltip: "Campaign has been shipped",},delivered: {value: "delivered",label: "Delivered",icon: "check",color: "green",tooltip: "Campaign has been delivered",},}endclassTask < ApplicationRecordenum_field:priority,["low","medium","high"]endThis automatically generates:
{low: {value: "low",label: "low",},medium: {value: "medium",label: "medium",},high: {value: "high",label: "high",},}For an enum field defined as:
classCampaign < ApplicationRecordenum_field:stage,{draft: {value: "draft",label: "Draft",icon: "file",color: "gray",},scheduled: {value: "scheduled",label: "Scheduled",icon: "calendar",color: "blue",},completed: {value: "completed",label: "Completed",icon: "check",color: "green",},}end# Returns the definitions as an HashWithIndifferentAccessCampaign.stages# Returns the count of definitionsCampaign.stages_count# 3# Returns the values of the definitionsCampaign.stage_values# ["draft", "scheduled", "completed"]# Returns the options for form helpersCampaign.stage_options# [["Draft", "draft"], ["Scheduled", "scheduled"], ["Completed", "completed"]]# Returns the value for a specific keyCampaign.draft_stage_value# "draft"Campaign.scheduled_stage_value# "scheduled"Campaign.completed_stage_value# "completed"If the accessor name differs from the column name, getter and setter methods are defined for the accessor.
campaign.stage# "draft"campaign.stage="scheduled"campaign.stage# "scheduled"campaign.stage- Get the current stage valuecampaign.stage = "scheduled"- Set the stage value
The gem automatically creates accessor methods for all properties defined in your enum definitions.
value(required) - The actual value stored in the databaselabel(auto-generated if not provided) - A human-readable label
Any additional properties you define (like icon, color, tooltip, etc.) will also get dedicated accessor methods automatically.
# Returns the full metadata hash for current valuecampaign.stage_metadata# => { value: "draft", label: "Draft", icon: "file", color: "gray" }# Access individual propertiescampaign.stage_value# "draft"campaign.stage_label# "Draft"campaign.stage_icon# "file"campaign.stage_color# "gray"# Returns true if the current value is "draft"campaign.draft_stage?# Returns true if the current value is "scheduled"campaign.scheduled_stage?# Returns true if the current value is "completed"campaign.completed_stage?# Returns all campaigns with draft stageCampaign.draft_stage# Returns all campaigns with scheduled stageCampaign.scheduled_stage# Returns all campaigns with completed stageCampaign.completed_stageAutomatically validates that the column value is included in the defined values. By default, nil values are allowed (see nullable option).
Map the accessor to a different database column name:
enum_field:role,definitions,column: :user_roleControls whether query scopes are generated. Defaults to true. Set to false to skip scope generation:
enum_field:speed,definitions,scopeable: falseControls whether inclusion validation is added. Defaults to true. Set to false to skip validation:
enum_field:speed,definitions,validatable: falseControls whether nil values pass validation. Defaults to true. Set to false to require a value:
enum_field:speed,definitions,nullable: falseFor polymorphic columns, nullability is derived from the association's optional flag rather than the global default. A belongs_to with optional: true allows nil; without it, nil is rejected. An explicit nullable option on the field still takes precedence:
classComment < ApplicationRecordbelongs_to:commentable,polymorphic: true,optional: true# nil allowed — derived from optional: trueenum_field:commentable_type,definitionsendclassAttachment < ApplicationRecordbelongs_to:attachable,polymorphic: true# nil rejected — association is required by defaultenum_field:attachable_type,definitions# Override: allow nil despite required associationenum_field:attachable_type,definitions,nullable: trueendControls whether ? inquiry methods are generated. Defaults to true. Set to false to skip:
enum_field:speed,definitions,inquirable: falseenum_field works with computed/virtual attributes that aren't backed by a database column. Define a method on the model and use scopeable: false and validatable: false since those features require a real column:
classSegment < ApplicationRecordenum_field:size_category,{small: {value: "small",label: "Small (< 100)",},medium: {value: "medium",label: "Medium (< 1K)",},large: {value: "large",label: "Large (< 10K)",},},scopeable: false,validatable: falsedefsize_categorycaseprofiles_countwhen ...100"small"when100...1_000"medium"else"large"endendendAll instance methods work as expected:
segment.size_category# "small"segment.size_category_label# "Small (< 100)"segment.size_category_metadata# { value: "small", label: "Small (< 100)" }segment.small_size_category?# trueClass methods (options, values, counts) also work normally:
Segment.size_category_options# [["Small (< 100)", "small"], ["Medium (< 1K)", "medium"], ...]Segment.size_category_values# ["small", "medium", "large"]You can add any custom properties to your definitions, and the gem will automatically create accessor methods for them:
classTicket < ApplicationRecordenum_field:priority,{low: {value: "low",label: "Low Priority",sla_hours: 72,notify_manager: false,},high: {value: "high",label: "High Priority",sla_hours: 4,notify_manager: true,},}end# Access custom properties directly via generated methodsticket.priority_sla_hours# 72ticket.priority_notify_manager# false# Or access via metadata hashticket.priority_metadata[:sla_hours]# 72ticket.priority_metadata[:notify_manager]# falseWhen enum_field is used in a model, definitions are automatically registered under a namespace derived from the model class name (e.g., Campaign becomes campaign).
You can also register definitions directly, outside of models, using the namespace DSL:
# config/initializers/enum_fields.rbEnumFields.namespace(:basic)doenum_field:priority,{low: {value: "low",label: "Low",},medium: {value: "medium",label: "Medium",},high: {value: "high",label: "High",},}enum_field:status,{active: {value: "active",label: "Active",},inactive: {value: "inactive",label: "Inactive",},}endAccess the raw registry:
EnumFields.registry# => { "basic" => { "priority" => { ... }, "status" => { ... } }, "campaign" => { ... } }EnumFields.catalog returns all registered definitions with namespaces sorted alphabetically, each field's entries as an array of metadata hashes (keys stripped):
EnumFields.catalog# => {# "basic" => {# "priority" => [# {# "value" => "low",# "label" => "Low",# },# {# "value" => "medium",# "label" => "Medium",# },# {# "value" => "high",# "label" => "High",# },# ],# "status" => [# {# "value" => "active",# "label" => "Active",# },# {# "value" => "inactive",# "label" => "Inactive",# },# ],# },# "campaign" => {# "stage" => [# {# "value" => "pending",# "label" => "Pending",# "icon" => "clock",# "color" => "yellow",# },# {# "value" => "processing",# "label" => "Processing",# "icon" => "cog",# "color" => "blue",# },# {# "value" => "shipped",# "label" => "Shipped",# "icon" => "truck",# "color" => "green",# },# {# "value" => "delivered",# "label" => "Delivered",# "icon" => "check",# "color" => "green",# },# ],# },# }After checking out the repo, run:
bundle installRun the test suite:
bundle exec rspecBug reports and pull requests are welcome on GitHub at https://github.com/kinnell/enum_fields.
The gem is available as open source under the terms of the MIT License.