Skip to content

A next-generation state machine for Rails, 5x faster than AASM

License

Notifications You must be signed in to change notification settings

corp-gp/enum_machine

Repository files navigation

Enum Machine

Enum Machine is a library for defining enums and setting state machines for attributes in ActiveRecord models and plain Ruby classes.

You can visualize transitions map with enum_machine-contrib

Why is enum_machine better then state_machines / aasm?

  • faster 5x
  • code lines: enum_machine - 348, AASM - 2139
  • namespaced (via attr) by default: order.state.to_collected
  • aliases
  • guarantees of existing transitions
  • simple run transitions with callbacks order.update(state: "collected") or order.state.to_collected
  • aasm / state_machinesevent driven, enum_machinestate driven
# aasmevent:completedo# complete/collected - dichotomy between states and eventsbefore{puts"event complete"}transitionsfrom: :collecting,to: :collectedend# pay/archived difficult to remember the relationship between statuses and events# try to explain this to the logic of business stakeholdersevent:paydotransitionsfrom: [:created,:collected],to: :archivedendorder=Order.create(state: "collecting")order.update(state: "archived")# not check transitions, invalid logicorder.update(state: "collected")# not run callbacksorder.complete# need use event for transition, but your object in UI and DB have only states# enum_machinetransitions(# simple readable transitions map"collecting"=>"collected","collected"=>"archived",)before_transition("collecting"=>"collected"){puts"event complete"}order=Order.create(state: "collecting")order.update(state: "archived")# checked transitions, raise exceptionorder.update(state: "collected")# run callbacks

Installation

Add to your Gemfile:

gem"enum_machine"

Usage

Enums

# With ActiveRecordclassProduct < ActiveRecord::Baseenum_machine:color,%w[redgreen]end# Or with plain classclassProduct# attributes must be defined before including the EnumMachine moduleattr_accessor:colorincludeEnumMachine[color: {enum: %w[redgreen]}]# or reuse from modelProduct::COLOR.enum_decoratorendProduct::COLOR.values# => ["red", "green"]Product::COLOR::RED# => "red"Product::COLOR::RED.red?# => trueProduct::COLOR::RED.human_name# => "Красный"Product::COLOR::RED__GREEN# => ["red", "green"]Product::COLOR["red"].red?# => trueProduct::COLOR["red"].human_name# => "Красный"product=Product.newproduct.color# => nilproduct.color="red"product.color.red?# => trueproduct.color.human_name# => "Красный"

Aliases

classProduct < ActiveRecord::Baseenum_machine:state,%w[createdapprovedpublished]doaliases("forming"=>%w[createdapproved],)endendProduct::STATE.forming# => %w[created approved]product=Product.new(state: "created")product.state.forming?# => true

Value decorator

You can extend value object with decorator

# Value classes nested from base classmoduleColorDecoratordefhexcaseselfwhenProduct::COLOR::REDthen"#ff0000"whenProduct::COLOR::GREENthen"#00ff00"endendendclassProductattr_accessor:colorincludeEnumMachine[color: {enum: %w[redgreen],value_decorator: ColorDecorator}]endproduct=Product.newproduct.color="red"product.color.hex# => "#ff0000"

Transitions

classProduct < ActiveRecord::Baseenum_machine:color,%w[redgreenblue]enum_machine:state,%w[createdapprovedcancelledactivated]do# transitions(any => any) - allow all transitionstransitions(nil=>"created","created"=>[nil,"approved"],%w[cancelledapproved]=>"activated","activated"=>%w[createdcancelled],)# Will be executed in `before_save` callbackbefore_transition"created"=>"approved"do |product| product.color="green"ifproduct.color.red?end# Will be executed in `after_save` callbackafter_transition%w[created]=>%w[approved]do |product| product.color="red"endafter_transitionany=>"cancelled"do |product| product.cancelled_at=Time.zone.nowendendendproduct=Product.create(state: "created")product.state.possible_transitions# => [nil, "approved"]product.state.can_activated?# => falseproduct.state.to_activated!# => EnumMachine::Error: transition "created" => "activated" not defined in enum_machineproduct.state.to_approved!# => true; equal to `product.update!(state: "approve")`

Skip transitions

product=Product.new(state: "created")product.skip_state_transitions{product.save}

method generated as skip_#{enum_name}_transitions

Skip in factories

FactoryBot.definedofactory:productdoname{Faker::Commerce.product_name}to_create{ |product| product.skip_state_transitions{product.save!}}endend

I18n

ru.yml

ru: enums: product: color: red: Красныйgreen: Зеленый
# ActiveRecordclassProduct < ActiveRecord::Baseenum_machine:color,%w[redgreen]end# Plain classclassProduct# attributes must be defined before including the EnumMachine moduleattr_accessor:color# `i18n_scope` option must be explicitly set to use methods belowincludeEnumMachine[color: {enum: %w[redgreen],i18n_scope: "product"}]endProduct::COLOR.human_name_for("red")# => "Красный"Product::COLOR.values_for_form# => [["Красный", "red"], ["Зеленый", "green"]]product=Product.new(color: "red")product.color.human_name# => "Красный"

I18n scope can be changed with i18n_scope option:

# For AciveRecordclassProduct < ActiveRecord::Baseenum_machine:color,%w[redgreen],i18n_scope: "users.product"end# For plain classclassProductincludeEnumMachine[color: {enum: %w[redgreen],i18n_scope: "users.product"}]end

Benchmarks

test/performance.rb

GemMethod
enum_machineorder.state.forming?894921.3 i/s
state_machinesorder.forming?189901.8 i/s - 4.71x slower
aasmorder.forming?127073.7 i/s - 7.04x slower
enum_machineorder.state.can_closed?473150.4 i/s
aasmorder.may_to_closed?24459.1 i/s - 19.34x slower
state_machinesorder.can_to_closed?12136.8 i/s - 38.98x slower
enum_machineOrder::STATE.values6353820.4 i/s
aasmOrder.aasm(:state).states.map(&:name)131390.5 i/s - 48.36x slower
state_machinesOrder.state_machines[:state].states.map(&:value)108449.7 i/s - 58.59x slower
enum_machineorder.state = "forming" and order.valid?13873.4 i/s
state_machinesorder.state_event = "to_forming" and order.valid?6173.6 i/s - 2.25x slower
aasmorder.to_forming3095.9 i/s - 4.48x slower

License

The gem is available as open source under the terms of the MIT License.

About

A next-generation state machine for Rails, 5x faster than AASM

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 5