enu 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +134 -77
- data/enu.gemspec +2 -3
- data/lib/enu.rb +41 -46
- data/lib/enu/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2b1f886bc7124afb3308e97ca245f525de0c314ffd01a64978fcfdd039e161ca
|
4
|
+
data.tar.gz: eba9554f9f8e1c4431b9b5caee7f97259b58c428d50ed0413ce96a3729ca0bc1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 55af3d8c3e08f270259a66a42193187441eed608013d0e9e5a340aac1ebde36ebcb56371ca40e7b9fee31c00a2f4e3a1019fe057efa0f0f54013093bc5d41e1f
|
7
|
+
data.tar.gz: d6c15d8edac06b856c57d4487dce5f6206c084ec3e32f4deb326ff52fe5dda3139f65b4d5527fba860bc6aeba7113cc8f1a0112ed19e514bbe7c9a2395f4b6cc
|
data/Gemfile.lock
CHANGED
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
|
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
|
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
|
-
|
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
|
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
|
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
|
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
|
-
|
93
|
+
### Scoped constants
|
80
94
|
|
81
|
-
|
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
|
-
|
86
|
-
post.draft? # true
|
87
|
-
post.published! # true
|
88
|
-
post.status # "published"
|
97
|
+
Each `option` definition generates matching value method:
|
89
98
|
|
90
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 [
|
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
|
-
|
138
|
-
|
147
|
+
# ...
|
148
|
+
end
|
149
|
+
end
|
150
|
+
```
|
139
151
|
|
140
|
-
|
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
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
154
|
+
```ruby
|
155
|
+
class Post < ApplicationRecord
|
156
|
+
include AASM
|
157
|
+
enum status: PostStatus
|
149
158
|
|
150
|
-
|
151
|
-
|
152
|
-
|
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 `
|
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!
|
164
|
-
post.status
|
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
|
-
|
167
|
-
post.status # => "published"
|
203
|
+
### Inheriting enumerations
|
168
204
|
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
235
|
+
### Namespaces
|
179
236
|
|
180
|
-
|
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`
|
248
|
+
Default Rails autoload configuration will successfully resolve `Enums::PostStatus` as well.
|
192
249
|
|
193
|
-
|
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
|
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.
|
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
37
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
34
|
+
singleton_class.class_eval do
|
35
|
+
define_method(key) { key }
|
36
|
+
define_method("#{key}_value") { explicit_value }
|
37
|
+
end
|
44
38
|
|
45
|
-
|
46
|
-
|
47
|
-
end
|
39
|
+
nil
|
40
|
+
end
|
48
41
|
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
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 :
|
53
|
+
private_class_method :new, :option, :options=
|
59
54
|
end
|
data/lib/enu/version.rb
CHANGED
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
|
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-
|
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:
|
226
|
+
summary: Missing enum type for Ruby and Rails
|
227
227
|
test_files: []
|