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 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: []