enum_machine 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|