enu 0.0.2 → 0.1.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: dc1b4752d455a471be538cbdfef720a49b96d56d2f56ae5dedfa7a34ccfa25f6
4
- data.tar.gz: 35fa945c3a4859a18460fb5cbe71babd6eae2a9dc12e7ef2ad509685592bc28a
3
+ metadata.gz: 2b1f886bc7124afb3308e97ca245f525de0c314ffd01a64978fcfdd039e161ca
4
+ data.tar.gz: eba9554f9f8e1c4431b9b5caee7f97259b58c428d50ed0413ce96a3729ca0bc1
5
5
  SHA512:
6
- metadata.gz: 52dc8a9a2703e01f62b9fc1d2e09d3f9cc80c96b6cedfcfe4ff6c65c4949b9bb92b3bd96f6c7026e7f30625386223aaa24e021479d8b00e1c8b00b14d7ce7cce
7
- data.tar.gz: 610af50ee26962f456e885b42475fc2831d396d1b5cd738df1b30ea43dbb40c62517b474e5eb819d0cee572dd1be21b2209cc9cec99c4907b84e80950ee9ef00
6
+ metadata.gz: 55af3d8c3e08f270259a66a42193187441eed608013d0e9e5a340aac1ebde36ebcb56371ca40e7b9fee31c00a2f4e3a1019fe057efa0f0f54013093bc5d41e1f
7
+ data.tar.gz: d6c15d8edac06b856c57d4487dce5f6206c084ec3e32f4deb326ff52fe5dda3139f65b4d5527fba860bc6aeba7113cc8f1a0112ed19e514bbe7c9a2395f4b6cc
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- enu (0.0.2)
4
+ enu (0.1.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,15 +1,12 @@
1
1
  # Enu
2
2
 
3
- ⚠️ WARNING! This implementation is experimental. Please do not use in production before stable release is announced.
4
-
5
3
  This gem introduces missing [enumerated type](https://en.wikipedia.org/wiki/Enumerated_type) for Ruby and Rails.
6
4
 
7
5
  Purpose and features:
8
6
 
9
7
  - Unify enum types definition for Rails model attributes, compatible with ActiveRecord's [enum declarations](https://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html).
10
- - Use structured constants instead of magic strings or numbers to define enum values.
11
- - Keep track on enum references to simplify refactoring.
12
- - Support explicit and implicit enum options definition.
8
+ - Use structured constants instead of magic strings or numbers to address enum values.
9
+ - Keep track on enum references to simplify refactoring and codebase navigation.
13
10
  - Provide a standardized way to export enum definitions to client-side JavaScript modules, managed by either Webpack or Rails Assets Pipeline.
14
11
 
15
12
  ## Installation
@@ -48,136 +45,196 @@ class PostStatus < Enu
48
45
  end
49
46
  ```
50
47
 
51
- This class defines an enum type for a blog post status which four optional states: `draft`, `published`, `moderated` and `deleted`. Each state automatically receives integer representation: 0 for `draft`, 1 for `published`, and so on. The first option will be treated as `default`.
48
+ This class defines an enum type for a blog post status with a set of possible states: `draft`, `published`, `moderated` and `deleted`. Each state automatically receives integer representation: 0 for `draft`, 1 for `published`, and so on. The top option will be treated as default.
49
+
50
+ It is also possible to specify explicit integer values:
51
+
52
+ ```ruby
53
+ class PostStatus < Enu
54
+ option :draft, 10
55
+ option :published, 20
56
+ end
57
+ ```
58
+
59
+ Or mix implicit and explicit approach:
60
+
61
+ ```ruby
62
+ class PostStatus < Enu
63
+ option :draft, 10
64
+ option :published
65
+ end
66
+ ```
67
+
68
+ Enu will ensure there are no collisions in the option names and values.
52
69
 
53
70
  ### Using enums
54
71
 
55
- After enum type is defined, it is possible to use it in a Rails model:
72
+ Enu classes are compatible with ActiveRecord's [enum](https://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html) declaration:
56
73
 
57
74
  ```ruby
58
- # app/models/post.rb
59
- #
60
- # Table name: posts
61
- #
62
- # id :integer not null, primary key
63
- # user_id :integer not null
64
- # status :integer default(0), not null
65
- # ...
66
- #
67
75
  class Post < ApplicationRecord
68
- enum status: PostStatus.options
69
- # ...
76
+ enum status: PostStatus
70
77
  end
78
+
79
+ # Use enum helpers as usual:
80
+ post = Post.create! # => #<Post:...>
81
+ Post.draft? # => true
82
+ post.published? # => false
83
+ Post.published.to_sql # => "SELECT "posts".* FROM "posts" WHERE "posts"."status" = 1"
71
84
  ```
72
85
 
73
- `options` class method will return enum representation in the form of a `Hash`, compatible with [ActiveRecord::Enum](https://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html) declaration:
86
+ Each `Enu` descendant inherits `options` class method, returning the options hash. In addition the enumeration class delegates some `Hash` methods, so ActiveRecord can treat it as an actual hash. In the last example `enum status: PostStatus` call is equivalent to `enum status: PostStatus.options`:
74
87
 
75
88
  ```ruby
76
- PostStatus.options # {"draft"=>0, "published"=>1, "moderated" => 2, "deleted"=>3}
89
+ PostStatus.options # => {:draft=>0, :published=>1, :moderated=>2, :deleted=>3}
90
+ PostStatus.each.to_h # => {:draft=>0, :published=>1, :moderated=>2, :deleted=>3}
77
91
  ```
78
92
 
79
- Use [enum helpers](https://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html) as usual:
93
+ ### Scoped constants
80
94
 
81
- ```ruby
82
- Post.new.draft? # true
83
- Post.new.published? # false
95
+ Sometimes ApplicationRecord helpers are not enough, and you need to address enum values directly. If this is the case, use scoped constants instead of magic strings or symbol values.
84
96
 
85
- post = Post.create! # #<Post:...>
86
- post.draft? # true
87
- post.published! # true
88
- post.status # "published"
97
+ Each `option` definition generates matching value method:
89
98
 
90
- Post.published.to_sql # "SELECT "posts".* FROM "posts" WHERE "posts"."status" = 1"
99
+ ```ruby
100
+ PostStatus.draft # => :draft
101
+ PostStatus.published # => :published
102
+ PostStatus.moderated # => :moderated
103
+ PostStatus.deleted # => :deleted
104
+
105
+ # Top option definition is the default
106
+ PostStatus.default # => :draft
91
107
  ```
92
108
 
93
- ### Scoped constants
109
+ Integer representation is available as well:
110
+
111
+ ```ruby
112
+ PostStatus.draft_value # => 0
113
+ PostStatus.published_value # => 1
114
+ PostStatus.moderated_value # => 2
115
+ PostStatus.deleted_value # => 3
116
+ ```
94
117
 
95
- Sometimes native helper methods are not enough, and you need to address enum values directly. If this is the case, use scoped constants instead of magic strings values. Say, you need to update multiple attributes for a set of DB records with a single query:
118
+ Say, you need to update multiple attributes for a set of DB records with a single query:
96
119
 
97
120
  ```ruby
98
- # app/models/user.rb
99
- #
100
- # Table name: posts
101
- #
102
- # id :integer not null, primary key
103
- # user_id :integer not null
104
- # ...
105
- #
106
121
  class User < ApplicationRecord
107
122
  has_many :posts
108
-
109
- def nasty_spammer?
110
- # ...
111
- end
123
+ # ...
112
124
  end
113
125
 
114
- use = User.first
126
+ user = User.first
115
127
 
116
128
  if user.nasty_spammer?
117
129
  user.posts.update_all(
118
130
  status: PostStatus.moderated,
119
131
  moderated_by: current_user,
120
- moderated_at: Time.new.utc,
121
132
  moderation_reason: 'being a nasty spammer'
122
133
  )
123
134
  end
124
135
  ```
125
136
 
126
- Another example is a state machine definition with [aasm](https://github.com/aasm/aasm) gem:
137
+ Another example is a state machine definition with [AASM](https://github.com/aasm/aasm) gem. Here is the Post model, complemented with state transitions logic:
127
138
 
128
139
  ```ruby
129
140
  class Post < ApplicationRecord
130
141
  include AASM
131
-
132
- enum status: PostStatus.options
142
+ enum status: PostStatus
133
143
 
134
144
  aasm column: :status, enum: true do
135
145
  state PostStatus.draft, initial: true
136
146
  state PostStatus.published
137
- state PostStatus.moderated
138
- state PostStatus.deleted
147
+ # ...
148
+ end
149
+ end
150
+ ```
139
151
 
140
- # Draft posts can be published
141
- event :publish do
142
- transitions from: PostStatus.draft, to: PostStatus.published
143
- end
152
+ But let's make `aasm` block more compact by using `Object#tap`:
144
153
 
145
- # Published posts can be moderated
146
- event :moderate do
147
- transitions from: PostStatus.published, to: PostStatus.moderated
148
- end
154
+ ```ruby
155
+ class Post < ApplicationRecord
156
+ include AASM
157
+ enum status: PostStatus
149
158
 
150
- # Any post can be deleted (but only once)
151
- event :dump do
152
- transitions from: PostStatus.keys.without(:deleted), to: PostStatus.deleted
159
+ aasm column: :status, enum: true do
160
+ PostStatus.tap do |ps|
161
+ state ps.draft, initial: true
162
+ state ps.published
163
+ state ps.moderated
164
+ state ps.deleted
165
+
166
+ event :publish do
167
+ transitions from: ps.draft, to: ps.published
168
+ end
169
+
170
+ event :unpublish do
171
+ transitions from: ps.published, to: ps.draft
172
+ end
173
+
174
+ event :moderate do
175
+ transitions from: ps.published, to: ps.moderated
176
+ end
177
+
178
+ event :soft_delete do
179
+ transitions from: ps.keys.without(:deleted), to: ps.deleted
180
+ end
153
181
  end
154
182
  end
155
183
  end
156
184
  ```
157
185
 
158
- Notice that `dump` event is using `PostStatus.keys` shortcut, instead of declaring a separate transition for each available post status. `Enu` provides `keys` and `values` class methods for each enum type.
186
+ Notice that `soft_delete` event uses `PostStatus.keys` shortcut, instead of declaring a separate transition for each post status.
159
187
 
160
- Now the `Post#state` field has transition rules:
188
+ Now the `Post#state` field has a set of transition rules:
161
189
 
162
190
  ```ruby
163
- post = Post.create! # new record status will be initialized with "draft" value
164
- post.status # => "draft"
191
+ post = Post.create!
192
+ post.status # => "draft"
193
+
194
+ post.publish! # perform sample transition and persist the new status
195
+ post.status # => "published"
196
+
197
+ post.soft_delete!
198
+ post.status # => "deleted"
199
+ post.moderate! # will raise an AASM::InvalidTransition, because deleted
200
+ # posts are not supposed to be moderated
201
+ ```
165
202
 
166
- post.publish! # performs sample transition and persist the new status
167
- post.status # => "published"
203
+ ### Inheriting enumerations
168
204
 
169
- post.dump!
170
- post.moderate! # will raise an AASM::InvalidTransition, because deleted
171
- # posts are not supposed to be moderated
205
+ Enu descendants are immutable. In other words, after a class is declared, there is no way to change it at runtime. Use inheritance to add more options:
206
+
207
+ ```ruby
208
+ class AdvancedPostStatus < PostStatus
209
+ option :pinned
210
+ option :featured
211
+ end
172
212
  ```
173
213
 
174
- ### Code base navigation
214
+ Complemented enum hash will look like this:
215
+
216
+ ```ruby
217
+ {
218
+ :draft => 0,
219
+ :published => 1,
220
+ :moderated => 2,
221
+ :deleted => 3,
222
+ :pinned => 4,
223
+ :featured => 5
224
+ }
225
+ ```
226
+
227
+ ### Tracking enums
228
+
229
+ Scoped constants help to look up enum type references in the code. Searching an enum class name or a specific value (i.e. `PostStatus.draft`) is a more efficient approach to navigation through the code, comparing to a situation with plain string literals or symbols, like in the [Rails Guides examples](https://guides.rubyonrails.org/active_record_querying.html#enums). Chances are that search results for "draft" will be much noisier in a larger codebase.
230
+
231
+ ### Structuring type definitions
175
232
 
176
- Scoped constants will help to track enum type references in your codebase. Looking up an enum class name or a specific value, like `PostStatus.draft`, is a more efficient way to navigate your codebase, comparing to a situation when you use plain string literals or symbols, like in the [Rails Guides examples](https://guides.rubyonrails.org/active_record_querying.html#enums). Chances are search results for `draft` will be much noisier in a large codebase.
233
+ Notice that `post_status.rb` is located under `app/enums/` subdirectory. Keeping enum classes in a separate location is not mandatory. Though, it will help to keep the project structure organized. Rails [autoload mechanism](https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#autoload-paths-and-eager-load-paths) will resolve all constants in `app` subdirectories, so there is no need to worry about requiring anything manually.
177
234
 
178
- Notice the source file location. Keeping enum type definitions in a separate location is not mandatory, though, it will help to keep the source code organized. In a typical Rails application project [autoload](https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#autoload-paths-and-eager-load-paths) mechanism will resolve all constants in `app` subdirectories, so there is no need to worry about requiring anything manually.
235
+ ### Namespaces
179
236
 
180
- This readme uses `PostStatus` for the sake of brevity. In a larger real-life code base, though, it is worth considering to organize sibling classes with a module, instead of polluting root namespace:
237
+ `PostStatus` example class is defined in the global namespace for the sake of brevity. In a larger real-life project it is worth considering to organize sibling classes with a module, instead of polluting root namespace:
181
238
 
182
239
  ```ruby
183
240
  # app/enums/post_status.rb
@@ -188,11 +245,11 @@ module Enums
188
245
  end
189
246
  ```
190
247
 
191
- Default Rails autoload configuration will successfully resolve `Enums::PostStatus` constant.
248
+ Default Rails autoload configuration will successfully resolve `Enums::PostStatus` as well.
192
249
 
193
- #### Spring gotcha
250
+ ### Spring gotcha
194
251
 
195
- There is a known issue with using custom directories in a Rails application. If you running your app with [Spring preloader](https://github.com/rails/spring) (which is true for default configuration), make sure to restart the preloader. Otherwise, Rails autoload will not resolve any constants under `app/enums/` or any other custom paths, and will keep raising `NameError`. This command will help:
252
+ There is a known issue with using custom directories in a Rails application. If you running your app with [Spring preloader](https://github.com/rails/spring) (which is true for default configuration), make sure to restart the preloader. Otherwise, Rails autoload will not resolve new constants under `app/enums/` or any other custom paths, and will keep raising `NameError`. This command will help:
196
253
 
197
254
  ```bash
198
255
  > bin/spring stop
data/enu.gemspec CHANGED
@@ -7,9 +7,8 @@ Gem::Specification.new do |spec|
7
7
  spec.version = Enu::VERSION
8
8
  spec.authors = ["Alex Musayev"]
9
9
  spec.email = ["alex.musayev@gmail.com"]
10
-
11
- spec.summary = "[Experimental] Missing enum type for Ruby and Rails"
12
- spec.description = ""
10
+ spec.summary = "Missing enum type for Ruby and Rails"
11
+ spec.description = "See the readme file for feature details and usage examples."
13
12
  spec.homepage = "https://github.com/dreikanter/enu"
14
13
  spec.license = "MIT"
15
14
 
data/lib/enu.rb CHANGED
@@ -1,59 +1,54 @@
1
+ require "forwardable"
1
2
  require "enu/version"
2
3
 
3
4
  class Enu
4
- def self.option(key, value = nil)
5
- value ||= next_value
6
-
7
- raise KeyError, "'#{key}' option already exists" if include?(key)
8
- raise TypeError, "enum values must be integer" unless value.is_a?(Integer)
9
- raise KeyError, "'#{key}' is a reserved key" if respond_to?(key)
10
-
11
- str_key = key.to_s
12
- @options[str_key] = value
13
-
14
- singleton_class.class_eval do
15
- define_method(key) { str_key }
16
- define_method("#{str_key}_value") { value }
5
+ class << self
6
+ extend Forwardable
7
+
8
+ attr_writer :options
9
+
10
+ def_delegators(
11
+ :options,
12
+ :each,
13
+ :each_pair,
14
+ :each_with_index,
15
+ :key?,
16
+ :keys,
17
+ :values
18
+ )
19
+
20
+ def options
21
+ @options ||= {}.freeze
17
22
  end
18
- end
19
-
20
- def self.include?(key)
21
- options.key?(key.to_s)
22
- end
23
-
24
- def self.options
25
- @options ||= {}
26
- end
27
-
28
- def self.keys
29
- options.keys
30
- end
31
23
 
32
- def self.values
33
- options.values
34
- end
24
+ def option(enum_key, value = nil)
25
+ key = enum_key.to_sym
26
+ raise KeyError, "'#{key}' option already exists" if key?(key)
27
+ raise ArgumentError, "'#{key}' key is reserved" if respond_to?(key)
28
+ raise TypeError, "non-integer value" if value && !value.is_a?(Integer)
29
+ raise ArgumentError, "repeating value" if values.include?(value)
35
30
 
36
- def self.default
37
- raise if options.empty?
38
- options.keys.first.to_s
39
- end
31
+ explicit_value = value || (options.none? ? 0 : values.max + 1)
32
+ self.options = options.merge(key => explicit_value).freeze
40
33
 
41
- def self.each
42
- options.each { |key, value| yield key, value }
43
- end
34
+ singleton_class.class_eval do
35
+ define_method(key) { key }
36
+ define_method("#{key}_value") { explicit_value }
37
+ end
44
38
 
45
- def self.each_pair(&block)
46
- each(&block)
47
- end
39
+ nil
40
+ end
48
41
 
49
- def self.each_with_index(&block)
50
- each(&block)
51
- end
42
+ def inherited(descendant)
43
+ inherited_frozen_options = options&.clone || {}.freeze
44
+ descendant.class_eval { self.options = inherited_frozen_options }
45
+ end
52
46
 
53
- def self.next_value
54
- return 0 if values.empty?
55
- values.max + 1
47
+ def default
48
+ raise "empty enum, sad enum" unless options&.any?
49
+ keys.first
50
+ end
56
51
  end
57
52
 
58
- private_class_method :next_value
53
+ private_class_method :new, :option, :options=
59
54
  end
data/lib/enu/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Enu
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: enu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Musayev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-31 00:00:00.000000000 Z
11
+ date: 2019-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -176,7 +176,7 @@ dependencies:
176
176
  - - "~>"
177
177
  - !ruby/object:Gem::Version
178
178
  version: 0.1.0
179
- description: ''
179
+ description: See the readme file for feature details and usage examples.
180
180
  email:
181
181
  - alex.musayev@gmail.com
182
182
  executables: []
@@ -223,5 +223,5 @@ requirements: []
223
223
  rubygems_version: 3.0.3
224
224
  signing_key:
225
225
  specification_version: 4
226
- summary: "[Experimental] Missing enum type for Ruby and Rails"
226
+ summary: Missing enum type for Ruby and Rails
227
227
  test_files: []