enum_machine 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/Gemfile.lock +7 -5
- data/README.md +150 -15
- data/docs/MIGRATING_FROM_RAILS_STRING_ENUM.ru.md +54 -0
- data/lib/enum_machine/attribute_persistence_methods.rb +33 -0
- data/lib/enum_machine/build_attribute.rb +27 -71
- data/lib/enum_machine/build_class.rb +47 -33
- data/lib/enum_machine/driver_active_record.rb +88 -28
- data/lib/enum_machine/driver_simple_class.rb +33 -28
- data/lib/enum_machine/machine.rb +47 -17
- data/lib/enum_machine/version.rb +1 -1
- data/lib/enum_machine.rb +14 -0
- metadata +20 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 14dc3dff76ded6b1aea680c93860bdd7aad7edb2c11b65e4e0a741ebd9d71e92
|
4
|
+
data.tar.gz: 5a30e39776ee6de5073e987863e55a768faf88f68cc8075e49dbf01fc335d908
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 516f79da30dc8ca2ac1aaa152a566fc30b681de1f0e4091001715a6598517d6630b274b6e215b4563a8afc411adf52731e76efa7aa948e7b34218631fcdb29e1
|
7
|
+
data.tar.gz: 23b0ca783312d70257e4654550810585c3ccf7ce69014f729e11d719147116d3db7bbd6496bc0120f8d0362544d3078f353f29c64cb06a14e6f98539f6167442
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -19,10 +19,10 @@ GIT
|
|
19
19
|
PATH
|
20
20
|
remote: .
|
21
21
|
specs:
|
22
|
-
enum_machine (
|
23
|
-
activemodel
|
24
|
-
activerecord
|
25
|
-
activesupport
|
22
|
+
enum_machine (1.0.0)
|
23
|
+
activemodel
|
24
|
+
activerecord
|
25
|
+
activesupport
|
26
26
|
|
27
27
|
GEM
|
28
28
|
remote: https://rubygems.org/
|
@@ -118,6 +118,8 @@ GEM
|
|
118
118
|
zeitwerk (2.5.4)
|
119
119
|
|
120
120
|
PLATFORMS
|
121
|
+
arm64-darwin-21
|
122
|
+
x86_64-darwin-21
|
121
123
|
x86_64-linux
|
122
124
|
|
123
125
|
DEPENDENCIES
|
@@ -131,4 +133,4 @@ DEPENDENCIES
|
|
131
133
|
sqlite3 (~> 1.4)
|
132
134
|
|
133
135
|
BUNDLED WITH
|
134
|
-
2.
|
136
|
+
2.3.15
|
data/README.md
CHANGED
@@ -1,38 +1,173 @@
|
|
1
|
-
#
|
1
|
+
# Enum machine
|
2
2
|
|
3
|
-
|
3
|
+
Enum machine is a library for defining enums and setting state machines for attributes in ActiveRecord models and plain Ruby classes.
|
4
4
|
|
5
|
-
|
5
|
+
You can visualize the state machine with [enum_machine-contrib](https://github.com/corp-gp/enum_machine-contrib)
|
6
|
+
|
7
|
+
## Why not state_machines/aasm?
|
8
|
+
|
9
|
+
The [aasm](https://github.com/aasm/aasm) and [state_machines](https://github.com/state-machines/state_machines) gems suggest calling special methods to change the `state`. In practice, this can lead to errors. In enum_machine, the `state` is changed by updating the corresponding field, and the validation of the ability to change from one state to another is done in the `after_validation` callback. This allows the `state` of a model to be changed consistently with the usual `save!`. In addition aasm/state_machines add many autogenerated methods to the model class and instances. This makes it much more difficult to search by the project. Pollutes the method's space. Adds a bottleneck in method naming because you have to remember these methods in your code.
|
10
|
+
|
11
|
+
Performance comparison (see [test/performance.rb](../master/test/performance.rb))
|
12
|
+
|
13
|
+
| Gem | Method | |
|
14
|
+
| :--- | ---: | :--- |
|
15
|
+
| enum_machine | order.state.forming? | 894921.3 i/s |
|
16
|
+
| state_machines | order.forming? | 189901.8 i/s - 4.71x slower |
|
17
|
+
| aasm | order.forming? | 127073.7 i/s - 7.04x slower |
|
18
|
+
| | | |
|
19
|
+
| enum_machine | order.state.can_closed? | 473150.4 i/s |
|
20
|
+
| aasm | order.may_to_closed? | 24459.1 i/s - 19.34x slower |
|
21
|
+
| state_machines | order.can_to_closed? | 12136.8 i/s - 38.98x slower |
|
22
|
+
| | | |
|
23
|
+
| enum_machine | Order::STATE.values | 6353820.4 i/s |
|
24
|
+
| aasm | Order.aasm(:state).states.map(&:name) | 131390.5 i/s - 48.36x slower |
|
25
|
+
| state_machines | Order.state_machines[:state].states.map(&:value) | 108449.7 i/s - 58.59x slower |
|
26
|
+
| | | |
|
27
|
+
| enum_machine | order.state = "forming" and order.valid? | 13873.4 i/s |
|
28
|
+
| state_machines | order.state_event = "to_forming" and order.valid? | 6173.6 i/s - 2.25x slower |
|
29
|
+
| aasm | order.to_forming | 3095.9 i/s - 4.48x slower |
|
6
30
|
|
7
31
|
## Installation
|
8
32
|
|
9
|
-
Add
|
33
|
+
Add to your Gemfile:
|
10
34
|
|
11
35
|
```ruby
|
12
36
|
gem 'enum_machine'
|
13
37
|
```
|
14
38
|
|
15
|
-
|
39
|
+
## Usage
|
16
40
|
|
17
|
-
|
41
|
+
### Enums
|
18
42
|
|
19
|
-
|
43
|
+
```ruby
|
44
|
+
# With ActiveRecord
|
45
|
+
class Product < ActiveRecord::Base
|
46
|
+
enum_machine :color, %w(red green)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Or with plain class
|
50
|
+
class Product
|
51
|
+
include EnumMachine[color: { enum: %w[red green] }]
|
52
|
+
end
|
53
|
+
|
54
|
+
Product::COLOR.values # => ["red", "green"]
|
55
|
+
Product::COLOR::RED # => "red"
|
56
|
+
Product::COLOR::RED__GREEN # => ["red", "green"]
|
57
|
+
|
58
|
+
product = Product.new
|
59
|
+
product.color # => nil
|
60
|
+
product.color = 'red'
|
61
|
+
product.color.red? # => true
|
62
|
+
```
|
20
63
|
|
21
|
-
|
64
|
+
### Aliases
|
22
65
|
|
23
|
-
|
66
|
+
```ruby
|
67
|
+
class Product < ActiveRecord::Base
|
68
|
+
enum_machine :state, %w[created approved published] do
|
69
|
+
aliases(
|
70
|
+
'forming' => %w[created approved],
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
Product::STATE.forming # => %w[created approved]
|
75
|
+
|
76
|
+
product = Product.new(state: 'created')
|
77
|
+
product.state.forming? # => true
|
78
|
+
```
|
24
79
|
|
25
|
-
|
80
|
+
### Transitions
|
26
81
|
|
27
|
-
|
82
|
+
```ruby
|
83
|
+
class Product < ActiveRecord::Base
|
84
|
+
enum_machine :color, %w[red green blue]
|
85
|
+
enum_machine :state, %w[created approved cancelled activated] do
|
86
|
+
transitions(
|
87
|
+
nil => 'red',
|
88
|
+
'created' => [nil, 'approved'],
|
89
|
+
%w[cancelled approved] => 'activated',
|
90
|
+
'activated' => %w[created cancelled],
|
91
|
+
)
|
92
|
+
|
93
|
+
# Will be executed in `before_save` callback
|
94
|
+
before_transition 'created' => 'approved' do |product|
|
95
|
+
product.color = 'green' if product.color.red?
|
96
|
+
end
|
97
|
+
|
98
|
+
# Will be executed in `after_save` callback
|
99
|
+
after_transition %w[created] => %w[approved] do |product|
|
100
|
+
product.color = 'red'
|
101
|
+
end
|
102
|
+
|
103
|
+
after_transition any => 'cancelled' do |product|
|
104
|
+
product.cancelled_at = Time.zone.now
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
product = Product.create(state: 'created')
|
110
|
+
product.state.possible_transitions # => [nil, "approved"]
|
111
|
+
product.state.can_activated? # => false
|
112
|
+
product.state.to_activated! # => EnumMachine::Error: transition "created" => "activated" not defined in enum_machine
|
113
|
+
product.state.to_approved! # => true; equal to `product.update!(state: 'approved')`
|
114
|
+
```
|
28
115
|
|
29
|
-
|
116
|
+
### I18n
|
30
117
|
|
31
|
-
|
118
|
+
**ru.yml**
|
119
|
+
```yml
|
120
|
+
ru:
|
121
|
+
enums:
|
122
|
+
product:
|
123
|
+
color:
|
124
|
+
red: Красный
|
125
|
+
green: Зеленый
|
126
|
+
```
|
32
127
|
|
33
|
-
|
128
|
+
```ruby
|
129
|
+
# ActiveRecord
|
130
|
+
class Product < ActiveRecord::Base
|
131
|
+
enum_machine :color, %w(red green)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Plain class
|
135
|
+
class Product
|
136
|
+
# `i18n_scope` option must be explicitly set to use methods below
|
137
|
+
include EnumMachine[color: { enum: %w[red green], i18n_scope: 'product' }]
|
138
|
+
end
|
139
|
+
|
140
|
+
Product::COLOR.human_name_for('red') # => 'Красный'
|
141
|
+
Product::COLOR.values_for_form # => [["Красный", "red"], ["Зеленый", "green"]]
|
142
|
+
|
143
|
+
product = Product.new(color: 'red')
|
144
|
+
product.color.human_name # => 'Красный'
|
145
|
+
```
|
34
146
|
|
35
|
-
|
147
|
+
I18n scope can be changed with `i18n_scope` option:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
# For AciveRecord
|
151
|
+
class Product < ActiveRecord::Base
|
152
|
+
enum_machine :color, %w(red green), i18n_scope: 'users.product'
|
153
|
+
end
|
154
|
+
|
155
|
+
# For plain class
|
156
|
+
class Product
|
157
|
+
include EnumMachine[color: { enum: %w[red green], i18n_scope: 'users.product' }]
|
158
|
+
end
|
159
|
+
```
|
160
|
+
|
161
|
+
**ru.yml**
|
162
|
+
```yml
|
163
|
+
ru:
|
164
|
+
enums:
|
165
|
+
users:
|
166
|
+
product:
|
167
|
+
color:
|
168
|
+
red: Красный
|
169
|
+
green: Зеленый
|
170
|
+
```
|
36
171
|
|
37
172
|
## License
|
38
173
|
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# Переход с `rails_string_enum` на `enum_machine`
|
2
|
+
|
3
|
+
### 1. Объявление в классе
|
4
|
+
```ruby
|
5
|
+
class Product
|
6
|
+
string_enum :color, %w[red green] # Было
|
7
|
+
enum_machine :color, %w[red green] # Стало
|
8
|
+
end
|
9
|
+
```
|
10
|
+
|
11
|
+
### 2. Константы
|
12
|
+
|
13
|
+
Все константы находятся в Product::COLOR
|
14
|
+
|
15
|
+
* `Product::RED` => `Product::COLOR::RED`
|
16
|
+
* `Product::RED__GREEN` => `Product::COLOR::RED__GREEN`
|
17
|
+
* `Product::COLORS` => `Product::COLOR.values`
|
18
|
+
|
19
|
+
### 3. Методы инстанса
|
20
|
+
|
21
|
+
* `@product.red?` => `@product.color.red?`
|
22
|
+
|
23
|
+
### 4. I18n хелперы
|
24
|
+
|
25
|
+
* `Product.colors_i18n` => `Product::COLOR.values_for_form`
|
26
|
+
* `Product.color_i18n_for('red')` => `Product::COLOR.human_name_for('red')`
|
27
|
+
* `@product.color_i18n` => `@product.color.human_name`
|
28
|
+
|
29
|
+
### 5. scopes
|
30
|
+
|
31
|
+
В `enum_machine` нет опции `scopes`, нужно задать необходимые вручную
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class Product
|
35
|
+
# Было
|
36
|
+
string_enum :color, %w[red green], scopes: true
|
37
|
+
|
38
|
+
# Стало
|
39
|
+
enum_machine :color, %w[red green]
|
40
|
+
scope :only_red, -> { where(color: COLOR::RED) }
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
### 6. Интеграция с `simple_form`
|
45
|
+
|
46
|
+
`enum_machine` не предоставляет интеграцию с `simple_form`, тип инпута и коллекцию нужно передавать самостоятельно
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
# Было
|
50
|
+
f.input :color
|
51
|
+
|
52
|
+
# Стало
|
53
|
+
f.input :color, as: :select, collection: Product::COLOR.values_for_form
|
54
|
+
```
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EnumMachine
|
4
|
+
module AttributePersistenceMethods
|
5
|
+
|
6
|
+
def self.[](attr, enum_values)
|
7
|
+
Module.new do
|
8
|
+
define_singleton_method(:extended) do |klass|
|
9
|
+
klass.attr_accessor :parent
|
10
|
+
|
11
|
+
klass.define_method(:inspect) do
|
12
|
+
"#<EnumMachine:BuildAttribute value=#{self} parent=#{parent.inspect}>"
|
13
|
+
end
|
14
|
+
|
15
|
+
enum_values.each do |enum_value|
|
16
|
+
enum_name = enum_value.underscore
|
17
|
+
|
18
|
+
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
19
|
+
# def to_created!
|
20
|
+
# parent.update!('state' => 'created')
|
21
|
+
# end
|
22
|
+
|
23
|
+
def to_#{enum_name}!
|
24
|
+
parent.update!('#{attr}' => '#{enum_value}')
|
25
|
+
end
|
26
|
+
RUBY
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -3,116 +3,72 @@
|
|
3
3
|
module EnumMachine
|
4
4
|
module BuildAttribute
|
5
5
|
|
6
|
-
def self.call(
|
7
|
-
|
8
|
-
|
9
|
-
Class.new do
|
10
|
-
def initialize(parent)
|
11
|
-
@parent = parent
|
12
|
-
end
|
6
|
+
def self.call(enum_values:, i18n_scope:, machine: nil)
|
7
|
+
aliases = machine&.instance_variable_get(:@aliases) || {}
|
13
8
|
|
9
|
+
Class.new(String) do
|
14
10
|
define_method(:machine) { machine } if machine
|
15
11
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
# end
|
20
|
-
#
|
21
|
-
# def inspect
|
22
|
-
# '<enum_machine :state>'
|
23
|
-
# end
|
24
|
-
#
|
25
|
-
# def ==(other)
|
26
|
-
# raise EnumMachine::Error, "use `state.\#{other}?` instead `state == '\#{other}'`"
|
27
|
-
# end
|
28
|
-
|
29
|
-
def to_s
|
30
|
-
#{parent_attr}
|
31
|
-
end
|
12
|
+
def inspect
|
13
|
+
"#<EnumMachine:BuildAttribute value=#{self}>"
|
14
|
+
end
|
32
15
|
|
33
|
-
|
34
|
-
|
16
|
+
if machine&.transitions?
|
17
|
+
def possible_transitions
|
18
|
+
machine.possible_transitions(self)
|
35
19
|
end
|
36
20
|
|
37
|
-
def
|
38
|
-
|
21
|
+
def can?(enum_value)
|
22
|
+
possible_transitions.include?(enum_value)
|
39
23
|
end
|
40
|
-
|
24
|
+
end
|
41
25
|
|
42
26
|
enum_values.each do |enum_value|
|
27
|
+
enum_name = enum_value.underscore
|
28
|
+
|
43
29
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
44
30
|
# def active?
|
45
|
-
#
|
46
|
-
# end
|
47
|
-
#
|
48
|
-
# def in?(values)
|
49
|
-
# values.include?(@parent.__state)
|
31
|
+
# self == 'active'
|
50
32
|
# end
|
51
33
|
|
52
|
-
def #{
|
53
|
-
|
54
|
-
end
|
55
|
-
|
56
|
-
def in?(values)
|
57
|
-
values.include?(#{parent_attr})
|
34
|
+
def #{enum_name}?
|
35
|
+
self == '#{enum_value}'
|
58
36
|
end
|
59
37
|
RUBY
|
60
38
|
|
61
39
|
if machine&.transitions?
|
62
40
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
63
41
|
# def can_active?
|
64
|
-
#
|
65
|
-
# end
|
66
|
-
#
|
67
|
-
# def to_canceled!
|
68
|
-
# @parent.update!('state' => 'canceled')
|
42
|
+
# possible_transitions.include?('canceled')
|
69
43
|
# end
|
70
44
|
|
71
|
-
def can_#{
|
72
|
-
|
73
|
-
end
|
74
|
-
|
75
|
-
def to_#{enum_value}!
|
76
|
-
@parent.update!('#{attr}' => '#{enum_value}')
|
45
|
+
def can_#{enum_name}?
|
46
|
+
possible_transitions.include?('#{enum_value}')
|
77
47
|
end
|
78
48
|
RUBY
|
79
49
|
end
|
80
50
|
end
|
81
51
|
|
82
|
-
|
83
|
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
84
|
-
# def possible_transitions
|
85
|
-
# machine.possible_transitions('active')
|
86
|
-
# end
|
87
|
-
|
88
|
-
def possible_transitions
|
89
|
-
machine.possible_transitions(#{parent_attr})
|
90
|
-
end
|
91
|
-
RUBY
|
92
|
-
end
|
93
|
-
|
94
|
-
aliases_keys.each do |key|
|
52
|
+
aliases.each_key do |key|
|
95
53
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
96
54
|
# def forming?
|
97
|
-
#
|
55
|
+
# machine.fetch_alias('forming').include?(self)
|
98
56
|
# end
|
99
57
|
|
100
58
|
def #{key}?
|
101
|
-
|
59
|
+
machine.fetch_alias('#{key}').include?(self)
|
102
60
|
end
|
103
61
|
RUBY
|
104
62
|
end
|
105
63
|
|
106
64
|
if i18n_scope
|
107
65
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
108
|
-
# def
|
109
|
-
#
|
110
|
-
# ::I18n.t(enum_value, scope: "enums.product.state", default: enum_value)
|
66
|
+
# def human_name
|
67
|
+
# ::I18n.t(self, scope: "enums.product.state", default: self)
|
111
68
|
# end
|
112
69
|
|
113
|
-
def
|
114
|
-
|
115
|
-
::I18n.t(enum_value, scope: "enums.#{i18n_scope}", default: enum_value)
|
70
|
+
def human_name
|
71
|
+
::I18n.t(self, scope: "enums.#{i18n_scope}", default: self)
|
116
72
|
end
|
117
73
|
RUBY
|
118
74
|
end
|
@@ -1,40 +1,54 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module EnumMachine
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
4
|
+
module BuildClass
|
5
|
+
|
6
|
+
def self.call(enum_values:, i18n_scope:, machine: nil)
|
7
|
+
aliases = machine&.instance_variable_get(:@aliases) || {}
|
8
|
+
|
9
|
+
Class.new do
|
10
|
+
define_singleton_method(:machine) { machine } if machine
|
11
|
+
define_singleton_method(:values) { enum_values }
|
12
|
+
|
13
|
+
if i18n_scope
|
14
|
+
def self.values_for_form(specific_values = nil) # rubocop:disable Gp/OptArgParameters
|
15
|
+
(specific_values || values).map { |v| [human_name_for(v), v] }
|
16
|
+
end
|
17
|
+
|
18
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
19
|
+
# def self.human_name_for(name)
|
20
|
+
# ::I18n.t(name, scope: "enums.test_model", default: name)
|
21
|
+
# end
|
22
|
+
|
23
|
+
def self.human_name_for(name)
|
24
|
+
::I18n.t(name, scope: "enums.#{i18n_scope}", default: name)
|
25
|
+
end
|
26
|
+
RUBY
|
27
|
+
end
|
28
|
+
|
29
|
+
enum_values.each do |enum_value|
|
30
|
+
const_set enum_value.underscore.upcase, enum_value.freeze
|
31
|
+
end
|
32
|
+
|
33
|
+
aliases.each_key do |key|
|
34
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
35
|
+
# def self.forming
|
36
|
+
# @alias_forming ||= machine.fetch_alias('forming').freeze
|
37
|
+
# end
|
38
|
+
|
39
|
+
def self.#{key}
|
40
|
+
@alias_#{key} ||= machine.fetch_alias('#{key}').freeze
|
41
|
+
end
|
42
|
+
RUBY
|
43
|
+
end
|
44
|
+
|
45
|
+
private_class_method def self.const_missing(name)
|
46
|
+
name_s = name.to_s
|
47
|
+
return super unless name_s.include?('__')
|
48
|
+
|
49
|
+
const_set name_s, name_s.split('__').map { |i| const_get(i) }.freeze
|
50
|
+
end
|
26
51
|
end
|
27
|
-
|
28
|
-
memo_attr(name_s, array_values)
|
29
|
-
end
|
30
|
-
|
31
|
-
def respond_to_missing?(name_s, include_all)
|
32
|
-
name_s.include?('__') || super
|
33
|
-
end
|
34
|
-
|
35
|
-
private def memo_attr(name, value)
|
36
|
-
self.class.attr_reader(name)
|
37
|
-
instance_variable_set("@#{name}", value)
|
38
52
|
end
|
39
53
|
|
40
54
|
end
|
@@ -6,53 +6,113 @@ module EnumMachine
|
|
6
6
|
def enum_machine(attr, enum_values, i18n_scope: nil, &block)
|
7
7
|
klass = self
|
8
8
|
|
9
|
-
attr_klass_name = attr.to_s.capitalize
|
10
|
-
read_method = "_read_attribute('#{attr}')"
|
11
9
|
i18n_scope ||= "#{klass.base_class.to_s.underscore}.#{attr}"
|
12
10
|
|
13
|
-
|
11
|
+
enum_const_name = attr.to_s.upcase
|
12
|
+
machine = Machine.new(enum_values, klass, enum_const_name)
|
14
13
|
machine.instance_eval(&block) if block
|
15
|
-
aliases = machine.instance_variable_get(:@aliases)
|
16
14
|
|
17
|
-
|
18
|
-
|
15
|
+
enum_klass = BuildClass.call(enum_values: enum_values, i18n_scope: i18n_scope, machine: machine)
|
16
|
+
|
17
|
+
enum_value_klass = BuildAttribute.call(enum_values: enum_values, i18n_scope: i18n_scope, machine: machine)
|
18
|
+
enum_value_klass.extend(AttributePersistenceMethods[attr, enum_values])
|
19
|
+
|
20
|
+
enum_klass.const_set :VALUE_KLASS, enum_value_klass
|
19
21
|
|
22
|
+
# Hash.new with default_proc for working with custom values not defined in enum list
|
23
|
+
value_attribute_mapping = Hash.new { |hash, enum_value| hash[enum_value] = enum_klass::VALUE_KLASS.new(enum_value).freeze }
|
24
|
+
enum_klass.define_singleton_method(:value_attribute_mapping) { value_attribute_mapping }
|
25
|
+
|
26
|
+
if machine.transitions?
|
20
27
|
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1 # rubocop:disable Style/DocumentDynamicEvalDefinition
|
21
|
-
|
22
|
-
|
23
|
-
|
28
|
+
before_save :__enum_machine_#{attr}_before_save
|
29
|
+
after_save :__enum_machine_#{attr}_after_save
|
30
|
+
|
31
|
+
def __enum_machine_#{attr}_before_save
|
32
|
+
if (attr_changes = changes['#{attr}']) && !@__enum_machine_#{attr}_skip_transitions
|
33
|
+
value_was, value_new = *attr_changes
|
34
|
+
self.class::#{enum_const_name}.machine.fetch_before_transitions(attr_changes).each do |block|
|
35
|
+
@__enum_machine_#{attr}_forced_value = value_was
|
36
|
+
instance_exec(self, value_was, value_new, &block)
|
37
|
+
ensure
|
38
|
+
@__enum_machine_#{attr}_forced_value = nil
|
39
|
+
end
|
24
40
|
end
|
25
41
|
end
|
26
|
-
|
27
|
-
|
28
|
-
|
42
|
+
|
43
|
+
def __enum_machine_#{attr}_after_save
|
44
|
+
if (attr_changes = previous_changes['#{attr}']) && !@__enum_machine_#{attr}_skip_transitions
|
45
|
+
self.class::#{enum_const_name}.machine.fetch_after_transitions(attr_changes).each { |block| instance_exec(self, *attr_changes, &block) }
|
29
46
|
end
|
30
47
|
end
|
31
48
|
RUBY
|
32
49
|
end
|
33
50
|
|
34
|
-
|
35
|
-
|
36
|
-
attribute_klass =
|
37
|
-
BuildAttribute.call(
|
38
|
-
attr: attr,
|
39
|
-
read_method: read_method,
|
40
|
-
enum_values: enum_values,
|
41
|
-
i18n_scope: i18n_scope,
|
42
|
-
machine: machine,
|
43
|
-
aliases_keys: aliases.keys,
|
44
|
-
)
|
45
|
-
klass.class_variable_set("@@#{attr}_attribute", attribute_klass)
|
46
|
-
|
47
|
-
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
51
|
+
define_methods = Module.new
|
52
|
+
define_methods.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
48
53
|
# def state
|
49
|
-
# @
|
54
|
+
# enum_value = @__enum_machine_state_forced_value || super()
|
55
|
+
# return unless enum_value
|
56
|
+
#
|
57
|
+
# unless @__enum_value_state == enum_value
|
58
|
+
# @__enum_value_state = self.class::STATE.value_attribute_mapping[enum_value].dup
|
59
|
+
# @__enum_value_state.parent = self
|
60
|
+
# @__enum_value_state.freeze
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# @__enum_value_state
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# def skip_state_transitions
|
67
|
+
# @__enum_machine_state_skip_transitions = true
|
68
|
+
# yield
|
69
|
+
# ensure
|
70
|
+
# @__enum_machine_state_skip_transitions = false
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# def initialize_dup(other)
|
74
|
+
# @__enum_value_state = nil
|
75
|
+
# super
|
50
76
|
# end
|
51
77
|
|
52
78
|
def #{attr}
|
53
|
-
|
79
|
+
enum_value = @__enum_machine_#{attr}_forced_value || super()
|
80
|
+
return unless enum_value
|
81
|
+
|
82
|
+
unless @__enum_value_#{attr} == enum_value
|
83
|
+
@__enum_value_#{attr} = self.class::#{enum_const_name}.value_attribute_mapping[enum_value].dup
|
84
|
+
@__enum_value_#{attr}.parent = self
|
85
|
+
@__enum_value_#{attr}.freeze
|
86
|
+
end
|
87
|
+
|
88
|
+
@__enum_value_#{attr}
|
89
|
+
end
|
90
|
+
|
91
|
+
def skip_#{attr}_transitions
|
92
|
+
@__enum_machine_#{attr}_skip_transitions = true
|
93
|
+
yield
|
94
|
+
ensure
|
95
|
+
@__enum_machine_#{attr}_skip_transitions = false
|
96
|
+
end
|
97
|
+
|
98
|
+
def initialize_dup(other)
|
99
|
+
@__enum_value_#{attr} = nil
|
100
|
+
super
|
54
101
|
end
|
55
102
|
RUBY
|
103
|
+
|
104
|
+
enum_decorator =
|
105
|
+
Module.new do
|
106
|
+
define_singleton_method(:included) do |decorating_klass|
|
107
|
+
decorating_klass.prepend define_methods
|
108
|
+
decorating_klass.const_set enum_const_name, enum_klass
|
109
|
+
end
|
110
|
+
end
|
111
|
+
enum_klass.define_singleton_method(:decorator_module) { enum_decorator }
|
112
|
+
|
113
|
+
klass.include(enum_decorator)
|
114
|
+
|
115
|
+
enum_decorator
|
56
116
|
end
|
57
117
|
|
58
118
|
end
|
@@ -14,35 +14,40 @@ module EnumMachine
|
|
14
14
|
args.each do |attr, params|
|
15
15
|
enum_values = params.fetch(:enum)
|
16
16
|
i18n_scope = params.fetch(:i18n_scope, nil)
|
17
|
-
enum_class = params.fetch(:enum_class, true)
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
)
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
18
|
+
if defined?(ActiveRecord) && klass <= ActiveRecord::Base
|
19
|
+
klass.enum_machine(attr, enum_values, i18n_scope: i18n_scope)
|
20
|
+
else
|
21
|
+
enum_const_name = attr.to_s.upcase
|
22
|
+
enum_klass = BuildClass.call(enum_values: enum_values, i18n_scope: i18n_scope)
|
23
|
+
|
24
|
+
enum_value_klass = BuildAttribute.call(enum_values: enum_values, i18n_scope: i18n_scope)
|
25
|
+
enum_klass.const_set :VALUE_KLASS, enum_value_klass
|
26
|
+
|
27
|
+
value_attribute_mapping = enum_values.to_h { |enum_value| [enum_value, enum_klass::VALUE_KLASS.new(enum_value).freeze] }
|
28
|
+
|
29
|
+
define_methods =
|
30
|
+
Module.new do
|
31
|
+
define_method(attr) do
|
32
|
+
enum_value = super()
|
33
|
+
return unless enum_value
|
34
|
+
|
35
|
+
value_attribute_mapping.fetch(enum_value)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
enum_decorator =
|
40
|
+
Module.new do
|
41
|
+
define_singleton_method(:included) do |decorating_klass|
|
42
|
+
decorating_klass.prepend define_methods
|
43
|
+
decorating_klass.const_set enum_const_name, enum_klass
|
44
|
+
end
|
45
|
+
end
|
46
|
+
enum_klass.define_singleton_method(:decorator_module) { enum_decorator }
|
47
|
+
|
48
|
+
klass.include(enum_decorator)
|
49
|
+
enum_decorator
|
50
|
+
end
|
46
51
|
end
|
47
52
|
end
|
48
53
|
end
|
data/lib/enum_machine/machine.rb
CHANGED
@@ -3,10 +3,12 @@
|
|
3
3
|
module EnumMachine
|
4
4
|
class Machine
|
5
5
|
|
6
|
-
attr_reader :enum_values
|
6
|
+
attr_reader :enum_values, :base_klass, :enum_const_name
|
7
7
|
|
8
|
-
def initialize(enum_values)
|
8
|
+
def initialize(enum_values, base_klass = nil, enum_const_name = nil) # rubocop:disable Gp/OptArgParameters
|
9
9
|
@enum_values = enum_values
|
10
|
+
@base_klass = base_klass
|
11
|
+
@enum_const_name = enum_const_name
|
10
12
|
@transitions = {}
|
11
13
|
@before_transition = {}
|
12
14
|
@after_transition = {}
|
@@ -18,8 +20,8 @@ module EnumMachine
|
|
18
20
|
def transitions(from__to_hash)
|
19
21
|
validate_state!(from__to_hash)
|
20
22
|
|
21
|
-
from__to_hash.each do |from_arr,
|
22
|
-
array_wrap(from_arr).each do |from|
|
23
|
+
from__to_hash.each do |from_arr, to_arr|
|
24
|
+
array_wrap(from_arr).product(array_wrap(to_arr)).each do |from, to|
|
23
25
|
@transitions[from] ||= []
|
24
26
|
@transitions[from] << to
|
25
27
|
end
|
@@ -32,9 +34,7 @@ module EnumMachine
|
|
32
34
|
def before_transition(from__to_hash, &block)
|
33
35
|
validate_state!(from__to_hash)
|
34
36
|
|
35
|
-
|
36
|
-
array_wrap(from).product(Array(to)).each do |from_pair_to|
|
37
|
-
valid_transition!(from_pair_to)
|
37
|
+
filter_transitions(from__to_hash).each do |from_pair_to|
|
38
38
|
@before_transition[from_pair_to] ||= []
|
39
39
|
@before_transition[from_pair_to] << block
|
40
40
|
end
|
@@ -46,30 +46,36 @@ module EnumMachine
|
|
46
46
|
def after_transition(from__to_hash, &block)
|
47
47
|
validate_state!(from__to_hash)
|
48
48
|
|
49
|
-
|
50
|
-
array_wrap(from).product(Array(to)).each do |from_pair_to|
|
51
|
-
valid_transition!(from_pair_to)
|
49
|
+
filter_transitions(from__to_hash).each do |from_pair_to|
|
52
50
|
@after_transition[from_pair_to] ||= []
|
53
51
|
@after_transition[from_pair_to] << block
|
54
52
|
end
|
55
53
|
end
|
56
54
|
|
57
55
|
# public api
|
58
|
-
def
|
59
|
-
enum_values
|
56
|
+
def any
|
57
|
+
@any ||= AnyEnumValues.new(enum_values + [nil])
|
60
58
|
end
|
61
59
|
|
62
60
|
def aliases(hash)
|
63
61
|
@aliases = hash
|
64
62
|
end
|
65
63
|
|
64
|
+
def fetch_aliases
|
65
|
+
@aliases
|
66
|
+
end
|
67
|
+
|
66
68
|
def transitions?
|
67
69
|
@transitions.present?
|
68
70
|
end
|
69
71
|
|
72
|
+
def fetch_transitions
|
73
|
+
@transitions
|
74
|
+
end
|
75
|
+
|
70
76
|
# internal api
|
71
77
|
def fetch_before_transitions(from__to)
|
72
|
-
|
78
|
+
validate_transition!(from__to)
|
73
79
|
@before_transition.fetch(from__to, [])
|
74
80
|
end
|
75
81
|
|
@@ -78,6 +84,11 @@ module EnumMachine
|
|
78
84
|
@after_transition.fetch(from__to, [])
|
79
85
|
end
|
80
86
|
|
87
|
+
# internal api
|
88
|
+
def fetch_alias(alias_key)
|
89
|
+
array_wrap(@aliases.fetch(alias_key))
|
90
|
+
end
|
91
|
+
|
81
92
|
# internal api
|
82
93
|
def possible_transitions(from)
|
83
94
|
@transitions.fetch(from, [])
|
@@ -89,10 +100,25 @@ module EnumMachine
|
|
89
100
|
end
|
90
101
|
end
|
91
102
|
|
92
|
-
private def
|
93
|
-
|
94
|
-
|
95
|
-
|
103
|
+
private def filter_transitions(from__to_hash)
|
104
|
+
from_arr, to_arr = from__to_hash.to_a.first
|
105
|
+
is_any_enum_values = from_arr.is_a?(AnyEnumValues) || to_arr.is_a?(AnyEnumValues)
|
106
|
+
|
107
|
+
array_wrap(from_arr).product(array_wrap(to_arr)).filter do |from__to|
|
108
|
+
if is_any_enum_values
|
109
|
+
from, to = from__to
|
110
|
+
possible_transitions(from).include?(to)
|
111
|
+
else
|
112
|
+
validate_transition!(from__to)
|
113
|
+
true
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
private def validate_transition!(from__to)
|
119
|
+
from, to = from__to
|
120
|
+
unless possible_transitions(from).include?(to)
|
121
|
+
raise EnumMachine::InvalidTransition.new(self, from, to)
|
96
122
|
end
|
97
123
|
end
|
98
124
|
|
@@ -104,5 +130,9 @@ module EnumMachine
|
|
104
130
|
end
|
105
131
|
end
|
106
132
|
|
133
|
+
class AnyEnumValues < Array
|
134
|
+
|
135
|
+
end
|
136
|
+
|
107
137
|
end
|
108
138
|
end
|
data/lib/enum_machine/version.rb
CHANGED
data/lib/enum_machine.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require_relative 'enum_machine/version'
|
4
4
|
require_relative 'enum_machine/driver_simple_class'
|
5
5
|
require_relative 'enum_machine/build_attribute'
|
6
|
+
require_relative 'enum_machine/attribute_persistence_methods'
|
6
7
|
require_relative 'enum_machine/build_class'
|
7
8
|
require_relative 'enum_machine/machine'
|
8
9
|
require 'active_support'
|
@@ -11,6 +12,19 @@ module EnumMachine
|
|
11
12
|
|
12
13
|
class Error < StandardError; end
|
13
14
|
|
15
|
+
class InvalidTransition < Error
|
16
|
+
|
17
|
+
attr_reader :from, :to, :enum_const
|
18
|
+
|
19
|
+
def initialize(machine, from, to)
|
20
|
+
@from = from
|
21
|
+
@to = to
|
22
|
+
@enum_const = machine.base_klass.const_get(machine.enum_const_name)
|
23
|
+
super "Transition #{from.inspect} => #{to.inspect} not defined in enum_machine #{enum_const.name}"
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
14
28
|
def self.[](args)
|
15
29
|
DriverSimpleClass.call(args)
|
16
30
|
end
|
metadata
CHANGED
@@ -1,58 +1,59 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: enum_machine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ermolaev Andrey
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-09-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: activerecord
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: activesupport
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '0'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
55
|
-
description:
|
54
|
+
version: '0'
|
55
|
+
description: Enum machine is a library for defining enums and setting state machines
|
56
|
+
for attributes in ActiveRecord models and plain Ruby classes.
|
56
57
|
email:
|
57
58
|
- andruhafirst@yandex.ru
|
58
59
|
executables: []
|
@@ -68,7 +69,9 @@ files:
|
|
68
69
|
- Rakefile
|
69
70
|
- bin/console
|
70
71
|
- bin/setup
|
72
|
+
- docs/MIGRATING_FROM_RAILS_STRING_ENUM.ru.md
|
71
73
|
- lib/enum_machine.rb
|
74
|
+
- lib/enum_machine/attribute_persistence_methods.rb
|
72
75
|
- lib/enum_machine/build_attribute.rb
|
73
76
|
- lib/enum_machine/build_class.rb
|
74
77
|
- lib/enum_machine/driver_active_record.rb
|
@@ -91,7 +94,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
91
94
|
requirements:
|
92
95
|
- - ">="
|
93
96
|
- !ruby/object:Gem::Version
|
94
|
-
version:
|
97
|
+
version: 3.0.0
|
95
98
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
99
|
requirements:
|
97
100
|
- - ">="
|
@@ -101,5 +104,5 @@ requirements: []
|
|
101
104
|
rubygems_version: 3.1.6
|
102
105
|
signing_key:
|
103
106
|
specification_version: 4
|
104
|
-
summary: state machine
|
107
|
+
summary: fast and siple usage state machine in your app
|
105
108
|
test_files: []
|