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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3e8eb3957fd71860a322260031d255c8034c53a0d263bd43d68911bcf97507f
4
- data.tar.gz: 05306e2ad610425b729a1716ee854614e697acc4dbd0cf6a9d85a476dd1f5209
3
+ metadata.gz: 14dc3dff76ded6b1aea680c93860bdd7aad7edb2c11b65e4e0a741ebd9d71e92
4
+ data.tar.gz: 5a30e39776ee6de5073e987863e55a768faf88f68cc8075e49dbf01fc335d908
5
5
  SHA512:
6
- metadata.gz: ea4de98d78c4cb4d0a67655f6c6bcce58f3168696e43b311a5e23873caf49fa4624745de266c852ab34f5edc892c5a7d4459803e772669522d6cf04fa63dff28
7
- data.tar.gz: 9233aa651cd9efb8ed0c2cbb16172da70d99fb1b919ba0d4a9146b6c8c312ec85d0d12fc183af82bb3b90f0cdfb4af78c55305f3f53b78a73cd546bb41ac0b7a
6
+ metadata.gz: 516f79da30dc8ca2ac1aaa152a566fc30b681de1f0e4091001715a6598517d6630b274b6e215b4563a8afc411adf52731e76efa7aa948e7b34218631fcdb29e1
7
+ data.tar.gz: 23b0ca783312d70257e4654550810585c3ccf7ce69014f729e11d719147116d3db7bbd6496bc0120f8d0362544d3078f353f29c64cb06a14e6f98539f6167442
data/.rubocop.yml CHANGED
@@ -3,7 +3,7 @@ inherit_gem:
3
3
  - ./config/default.yml
4
4
 
5
5
  AllCops:
6
- TargetRubyVersion: 2.6
6
+ TargetRubyVersion: 3.0
7
7
 
8
8
  Rails/ApplicationRecord:
9
9
  Enabled: false
data/Gemfile.lock CHANGED
@@ -19,10 +19,10 @@ GIT
19
19
  PATH
20
20
  remote: .
21
21
  specs:
22
- enum_machine (0.1.0)
23
- activemodel (~> 6.0)
24
- activerecord (~> 6.0)
25
- activesupport (~> 6.0)
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.2.29
136
+ 2.3.15
data/README.md CHANGED
@@ -1,38 +1,173 @@
1
- # EnumMachine
1
+ # Enum machine
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/enum_machine`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Delete this and the text above, and describe your gem
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 this line to your application's Gemfile:
33
+ Add to your Gemfile:
10
34
 
11
35
  ```ruby
12
36
  gem 'enum_machine'
13
37
  ```
14
38
 
15
- And then execute:
39
+ ## Usage
16
40
 
17
- $ bundle install
41
+ ### Enums
18
42
 
19
- Or install it yourself as:
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
- $ gem install enum_machine
64
+ ### Aliases
22
65
 
23
- ## Usage
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
- TODO: Write usage instructions here
80
+ ### Transitions
26
81
 
27
- ## Development
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
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
116
+ ### I18n
30
117
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
118
+ **ru.yml**
119
+ ```yml
120
+ ru:
121
+ enums:
122
+ product:
123
+ color:
124
+ red: Красный
125
+ green: Зеленый
126
+ ```
32
127
 
33
- ## Contributing
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
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/enum_machine.
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(attr:, read_method:, enum_values:, i18n_scope:, machine: nil, aliases_keys: {})
7
- parent_attr = "@parent.#{read_method}"
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
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
17
- # def to_s
18
- # @parent.__state
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
- def inspect
34
- '<enum_machine :#{attr}>'
16
+ if machine&.transitions?
17
+ def possible_transitions
18
+ machine.possible_transitions(self)
35
19
  end
36
20
 
37
- def ==(other)
38
- raise EnumMachine::Error, "use `#{attr}.\#{other}?` instead `#{attr} == '\#{other}'`"
21
+ def can?(enum_value)
22
+ possible_transitions.include?(enum_value)
39
23
  end
40
- RUBY
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
- # @parent.__state == 'active'
46
- # end
47
- #
48
- # def in?(values)
49
- # values.include?(@parent.__state)
31
+ # self == 'active'
50
32
  # end
51
33
 
52
- def #{enum_value}?
53
- #{parent_attr} == '#{enum_value}'
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
- # machine.possible_transitions(@parent.__state).include?('canceled')
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_#{enum_value}?
72
- machine.possible_transitions(#{parent_attr}).include?('#{enum_value}')
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
- if machine&.transitions?
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
- # @parent.class::State.forming.include?('active')
55
+ # machine.fetch_alias('forming').include?(self)
98
56
  # end
99
57
 
100
58
  def #{key}?
101
- @parent.class::State.#{key}.include?(#{parent_attr})
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 i18n
109
- # enum_value = @parent.__state
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 i18n
114
- enum_value = #{parent_attr}
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
- class BuildClass
5
-
6
- attr_reader :values
7
-
8
- def initialize(values, aliases = {})
9
- @values = values
10
- @values.each { |v| memo_attr(v, v) }
11
- aliases.each { |k, v| memo_attr(k, v) }
12
- end
13
-
14
- def i18n_for(name)
15
- ::I18n.t(name, scope: "enums.#{i18n_scope}", default: name)
16
- end
17
-
18
- def method_missing(name)
19
- name_s = name.to_s
20
- return super unless name_s.include?('__')
21
-
22
- array_values = name_s.split('__').freeze
23
-
24
- unless (unexists_values = array_values - values).empty?
25
- raise EnumMachine::Error, "enums #{unexists_values} not exists"
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
- machine = Machine.new(enum_values)
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
- if machine.transitions?
18
- klass.class_variable_set("@@#{attr}_machine", machine)
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
- after_validation do
22
- unless (attr_changes = changes['#{attr}']).blank?
23
- @@#{attr}_machine.fetch_before_transitions(attr_changes).each { |i| i.call(self) }
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
- after_save do
27
- unless (attr_changes = previous_changes['#{attr}']).blank?
28
- @@#{attr}_machine.fetch_after_transitions(attr_changes).each { |i| i.call(self) }
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
- klass.const_set attr_klass_name, BuildClass.new(enum_values, aliases)
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
- # @state_enum ||= @@state_attribute.new(self)
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
- @#{attr}_enum ||= @@#{attr}_attribute.new(self)
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
- attr_klass_name = attr.to_s.capitalize
20
- read_method = "__#{attr}"
21
-
22
- attribute_klass =
23
- BuildAttribute.call(
24
- attr: attr,
25
- read_method: read_method,
26
- enum_values: enum_values,
27
- i18n_scope: i18n_scope,
28
- )
29
-
30
- klass.class_variable_set("@@#{attr}_attribute", attribute_klass)
31
-
32
- klass.alias_method read_method, attr
33
- klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
34
- # def state
35
- # @state_enum ||= @@state_attribute.new(self)
36
- # end
37
-
38
- def #{attr}
39
- @#{attr}_enum ||= @@#{attr}_attribute.new(self)
40
- end
41
- RUBY
42
-
43
- next unless enum_class
44
-
45
- klass.const_set attr_klass_name, BuildClass.new(enum_values)
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
@@ -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, to|
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
- from, to = from__to_hash.to_a.first
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
- from, to = from__to_hash.to_a.first
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 all
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
- valid_transition!(from__to)
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 valid_transition!(from_pair_to)
93
- from, to = from_pair_to
94
- unless @transitions[from]&.include?(to)
95
- raise EnumMachine::Error, "transition #{from} => #{to} not defined in enum_machine"
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module EnumMachine
4
4
 
5
- VERSION = '0.1.0'
5
+ VERSION = '1.0.0'
6
6
 
7
7
  end
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: 0.1.0
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: 2022-02-19 00:00:00.000000000 Z
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: '6.0'
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: '6.0'
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: '6.0'
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: '6.0'
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: '6.0'
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: '6.0'
55
- description: state machine for enums
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: 2.6.0
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 for enums
107
+ summary: fast and siple usage state machine in your app
105
108
  test_files: []