Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 additions & 14 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ GIT

GIT
remote: https://github.com/corp-gp/rubocop-gp.git
revision: 61d6ca243e1a5f7d209e9619417c8fc1d303d181
revision: 867f7e1351c3730897cacdc20ce2d727604de245
specs:
rubocop-gp (0.0.4)
rubocop
Expand Down Expand Up @@ -73,20 +73,20 @@ GEM
zeitwerk (~> 2.6)
i18n (1.14.6)
concurrent-ruby (~> 1.0)
json (2.7.2)
json (2.8.2)
language_server-protocol (3.17.0.3)
logger (1.6.1)
method_source (1.1.0)
minitest (5.25.1)
minitest (5.25.2)
parallel (1.26.3)
parser (3.3.5.0)
parser (3.3.6.0)
ast (~> 2.4.1)
racc
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
racc (1.8.1)
rack (3.1.7)
rack (3.1.8)
rainbow (3.1.1)
rake (13.2.1)
regexp_parser (2.9.2)
Expand All @@ -103,49 +103,52 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.1)
rubocop (1.66.1)
rubocop (1.69.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.4, < 3.0)
rubocop-ast (>= 1.32.2, < 2.0)
rubocop-ast (>= 1.36.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.32.3)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.36.1)
parser (>= 3.3.1.0)
rubocop-capybara (2.21.0)
rubocop (~> 1.41)
rubocop-factory_bot (2.26.1)
rubocop (~> 1.61)
rubocop-performance (1.22.1)
rubocop-performance (1.23.0)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.26.2)
rubocop-rails (2.27.0)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.0.5)
rubocop-rspec (3.2.0)
rubocop (~> 1.61)
rubocop-rspec_rails (2.30.0)
rubocop (~> 1.61)
rubocop-rspec (~> 3, >= 3.0.1)
ruby-progressbar (1.13.0)
rumoji (0.5.0)
securerandom (0.3.1)
securerandom (0.3.2)
sqlite3 (1.7.3-arm64-darwin)
sqlite3 (1.7.3-x86_64-darwin)
sqlite3 (1.7.3-x86_64-linux)
timeout (0.4.1)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.6.0)
unicode-display_width (3.1.2)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
zeitwerk (2.6.18)

PLATFORMS
arm64-darwin-21
arm64-darwin-23
x86_64-darwin-21
x86_64-linux

Expand Down
40 changes: 36 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Enum machine
# Enum Machine

Enum machine is a library for defining enums and setting state machines for attributes in ActiveRecord models and plain Ruby classes.
`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](https://github.com/corp-gp/enum_machine-contrib)

Expand Down Expand Up @@ -29,7 +29,7 @@ end

order = Order.create(state: "collecting")
order.update(state: "archived") # not check transitions, invalid logic
order.update(state: "collected") # not run callbacks
order.update(state: "collected") # not run callbacks
order.complete # need use event for transition, but your object in UI and DB have only states

# enum_machine
Expand Down Expand Up @@ -66,7 +66,7 @@ end
class Product
# attributes must be defined before including the EnumMachine module
attr_accessor :color

include EnumMachine[color: { enum: %w[red green] }]
# or reuse from model
Product::COLOR.decorator_module
Expand All @@ -76,6 +76,9 @@ Product::COLOR.values # => ["red", "green"]
Product::COLOR::RED # => "red"
Product::COLOR::RED__GREEN # => ["red", "green"]

Product::COLOR["red"].red? # => true
Product::COLOR["red"].human_name # => "Красный"

product = Product.new
product.color # => nil
product.color = "red"
Expand All @@ -100,6 +103,35 @@ product = Product.new(state: "created")
product.state.forming? # => true
```

### Value decorator

You can extend value object with decorator

```ruby
# Value classes nested from base class
module ColorDecorator
def hex
case self
when Product::COLOR::RED then "#ff0000"
when Product::COLOR::GREEN then "#00ff00"
end
end
end

class Product
attr_accessor :color

include EnumMachine[color: {
enum: %w[red green],
decorator: ColorDecorator
}]
end

product = Product.new
product.color = "red"
product.color.hex # => "#ff0000"
```

### Transitions

```ruby
Expand Down
4 changes: 0 additions & 4 deletions lib/enum_machine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@
require "active_support"

module EnumMachine

class Error < StandardError; end

class InvalidTransition < Error

attr_reader :from, :to, :enum_const

def initialize(machine, from, to)
Expand All @@ -26,13 +24,11 @@ def initialize(machine, from, to)
end
super("Transition #{from.inspect} => #{to.inspect} not defined in enum_machine :#{machine.attr_name}")
end

end

def self.[](args)
DriverSimpleClass.call(args)
end

end

ActiveSupport.on_load(:active_record) do
Expand Down
6 changes: 0 additions & 6 deletions lib/enum_machine/attribute_persistence_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,11 @@

module EnumMachine
module AttributePersistenceMethods

def self.[](attr, enum_values)
Module.new do
define_singleton_method(:extended) do |klass|
klass.attr_accessor :parent

klass.define_method(:inspect) do
"#<EnumMachine:BuildAttribute value=#{self} parent=#{parent.inspect}>"
end

enum_values.each do |enum_value|
enum_name = enum_value.underscore

Expand All @@ -28,6 +23,5 @@ def to_#{enum_name}!
end
end
end

end
end
8 changes: 4 additions & 4 deletions lib/enum_machine/build_attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@

module EnumMachine
module BuildAttribute

def self.call(enum_values:, i18n_scope:, machine: nil)
def self.call(enum_values:, i18n_scope:, decorator:, machine: nil)
aliases = machine&.instance_variable_get(:@aliases) || {}

Class.new(String) do
include(decorator) if decorator

define_method(:machine) { machine } if machine

def inspect
"#<EnumMachine:BuildAttribute value=#{self}>"
"#<EnumMachine \"#{self}\">"
end

if machine&.transitions?
Expand Down Expand Up @@ -74,6 +75,5 @@ def human_name
end
end
end

end
end
16 changes: 11 additions & 5 deletions lib/enum_machine/build_class.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@

module EnumMachine
module BuildClass

def self.call(enum_values:, i18n_scope:, machine: nil)
def self.call(enum_values:, i18n_scope:, value_class:, machine: nil)
aliases = machine&.instance_variable_get(:@aliases) || {}

Class.new do
define_singleton_method(:machine) { machine } if machine
define_singleton_method(:values) { enum_values }
define_singleton_method(:values) { enum_values.map { value_class.new(_1).freeze } }

value_attribute_mapping = values.to_h { [_1.to_s, _1] }
define_singleton_method(:value_attribute_mapping) { value_attribute_mapping }
define_singleton_method(:[]) do |enum_value|
key = enum_value.to_s
# Check for key existence because `[]` will call `default_proc`, and we don’t want that
value_attribute_mapping[key] if value_attribute_mapping.key?(key)
end

if i18n_scope
def self.values_for_form(specific_values = nil) # rubocop:disable Gp/OptArgParameters
Expand All @@ -27,7 +34,7 @@ def self.human_name_for(name)
end

enum_values.each do |enum_value|
const_set enum_value.underscore.upcase, enum_value.freeze
const_set enum_value.underscore.upcase, enum_value.to_s.freeze
end

aliases.each_key do |key|
Expand All @@ -50,6 +57,5 @@ def self.#{key}
end
end
end

end
end
20 changes: 9 additions & 11 deletions lib/enum_machine/driver_active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

module EnumMachine
module DriverActiveRecord

def enum_machine(attr, enum_values, i18n_scope: nil, &block)
def enum_machine(attr, enum_values, i18n_scope: nil, decorator: nil, &block)
klass = self

i18n_scope ||= "#{klass.base_class.to_s.underscore}.#{attr}"
Expand All @@ -12,16 +11,16 @@ def enum_machine(attr, enum_values, i18n_scope: nil, &block)
machine = Machine.new(enum_values, klass, enum_const_name, attr)
machine.instance_eval(&block) if block

enum_klass = BuildClass.call(enum_values: enum_values, i18n_scope: i18n_scope, machine: machine)

enum_value_klass = BuildAttribute.call(enum_values: enum_values, i18n_scope: i18n_scope, machine: machine)
enum_value_klass.extend(AttributePersistenceMethods[attr, enum_values])
value_class = BuildAttribute.call(enum_values: enum_values, i18n_scope: i18n_scope, machine: machine, decorator: decorator)
enum_klass = BuildClass.call(enum_values: enum_values, i18n_scope: i18n_scope, machine: machine, value_class: value_class)

enum_klass.const_set :VALUE_KLASS, enum_value_klass
value_class.extend(AttributePersistenceMethods[attr, enum_values])

# Hash.new with default_proc for working with custom values not defined in enum list
value_attribute_mapping = Hash.new { |hash, enum_value| hash[enum_value] = enum_klass::VALUE_KLASS.new(enum_value).freeze }
enum_klass.define_singleton_method(:value_attribute_mapping) { value_attribute_mapping }
# default_proc for working with custom values not defined in enum list but may exists in db
enum_klass.value_attribute_mapping.default_proc =
proc do |hash, enum_value|
hash[enum_value] = value_class.new(enum_value).freeze
end

if machine.transitions?
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1 # rubocop:disable Style/DocumentDynamicEvalDefinition
Expand Down Expand Up @@ -114,6 +113,5 @@ def initialize_dup(other)

enum_decorator
end

end
end
13 changes: 4 additions & 9 deletions lib/enum_machine/driver_simple_class.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

module EnumMachine
module DriverSimpleClass

# include EnumMachine[
# state: { enum: %w[choice in_delivery], i18n_scope: 'line_item.state' },
# color: { enum: %w[red green yellow] },
Expand All @@ -14,25 +13,22 @@ def self.call(args)
args.each do |attr, params|
enum_values = params.fetch(:enum)
i18n_scope = params.fetch(:i18n_scope, nil)
decorator = params.fetch(:decorator, nil)

if defined?(ActiveRecord) && klass <= ActiveRecord::Base
klass.enum_machine(attr, enum_values, i18n_scope: i18n_scope)
else
enum_const_name = attr.to_s.upcase
enum_klass = BuildClass.call(enum_values: enum_values, i18n_scope: i18n_scope)

enum_value_klass = BuildAttribute.call(enum_values: enum_values, i18n_scope: i18n_scope)
enum_klass.const_set :VALUE_KLASS, enum_value_klass

value_attribute_mapping = enum_values.to_h { |enum_value| [enum_value, enum_klass::VALUE_KLASS.new(enum_value).freeze] }
value_class = BuildAttribute.call(enum_values: enum_values, i18n_scope: i18n_scope, decorator: decorator)
enum_klass = BuildClass.call(enum_values: enum_values, i18n_scope: i18n_scope, value_class: value_class)

define_methods =
Module.new do
define_method(attr) do
enum_value = super()
return unless enum_value

value_attribute_mapping.fetch(enum_value)
enum_klass.value_attribute_mapping.fetch(enum_value)
end
end

Expand All @@ -52,6 +48,5 @@ def self.call(args)
end
end
end

end
end
3 changes: 0 additions & 3 deletions lib/enum_machine/machine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

module EnumMachine
class Machine

attr_reader :enum_values, :base_klass, :enum_const_name, :attr_name

def initialize(enum_values, base_klass = nil, enum_const_name = nil, attr_name = nil) # rubocop:disable Gp/OptArgParameters
Expand Down Expand Up @@ -132,8 +131,6 @@ def possible_transitions(from)
end

class AnyEnumValues < Array

end

end
end
2 changes: 0 additions & 2 deletions lib/enum_machine/version.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# frozen_string_literal: true

module EnumMachine

VERSION = "1.0.0"

end
Loading