nd-enum 0.1.2 → 0.2.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: e8c39fe2ee839c606ce8bba0ed32181fff0ab45b39d091f6d5f4070d560eff9e
4
- data.tar.gz: 190d2bd1a22ed5b7551b91a1ffe086e8f80d2010228cc568c86d1dc8b7d99fab
3
+ metadata.gz: 1319016da965b32314532d641038f67316c07087b03f8a0a195786d783307c08
4
+ data.tar.gz: e88397852614473313ba633c1d3029de0cad1466b9ddfa0787b54e5cc7133eb8
5
5
  SHA512:
6
- metadata.gz: f4adb87f1df4b296625c32c3eedaef521bc5fccf8de94ef8bf77644be19324e5395cbed035cb96bd023a8555c30ca7843a802c2289f553ef310f5fb2f9e788a3
7
- data.tar.gz: 8df576525c8d46cdb705e7ade16a4158e0d0387baaf7429239fc9e74e7ddb6dd6878787d77dde8fa0f8a8d3c61b55d58e64aea82cd6994d447c30568336e5884
6
+ metadata.gz: 65596f68728490a34c575fac8191438d85b4490268a5938c725f5ed7c0fbe3e0616679b332971dd8991f0989c34bd41c51b89bdaf1153f822f6446a0714613fc
7
+ data.tar.gz: c583f3f1e8a655bcf9ba604ef3851cbb27dba1acc7743ea5c158a763c33792e3cd2dc826659b7e53c1ae3380ac045aef60e53e6fd1bff3b9d8e83b315bd37b5f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2022-08-10
4
+
5
+ - Add initializer
6
+ - Validate translations
7
+
3
8
  ## [0.1.2] - 2022-08-08
4
9
 
5
10
  - Add specs
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nd-enum (0.1.1)
4
+ nd-enum (0.2.0)
5
5
  activerecord (>= 6.0.0)
6
6
  activesupport (>= 6.0.0)
7
7
 
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # ND::Enum
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/nd/enum`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ This gem allows you to create and use enums easily and quickly in your Rails project.
6
4
 
7
5
  ## Installation
8
6
 
@@ -16,17 +14,222 @@ If bundler is not being used to manage dependencies, install the gem by executin
16
14
 
17
15
  ## Usage
18
16
 
19
- TODO: Write usage instructions here
17
+ - [Basic usage](#basic-usage)
18
+ - [Configuration](#configuration)
19
+ - [I18n](#i18n)
20
+ - [Validate translations presence](#validate-translations-presence)
21
+ - [Enforce translations presence](#enforce-translations-presence)
22
+ - [ActiveRecord Enum](#activerecord-enum)
23
+
24
+ ### Basic usage
25
+
26
+ Define your enum in an ActiveRecord model:
27
+
28
+ ```ruby
29
+ class User < ApplicationRecord
30
+ nd_enum(role: %i(user admin))
31
+ end
32
+ ```
33
+
34
+ It creates a module for your enum that contains one constant per enum value. Say goodbye to [magic strings](https://en.wikipedia.org/wiki/Magic_string)!
35
+
36
+ In our example, the module is `User::Role`, and the constants are `User::Role::USER` and `User::Role::ADMIN`.
37
+
38
+ ```ruby
39
+ irb(main)> User::Role::USER
40
+ => "user"
41
+
42
+ irb(main)> User::Role::ADMIN
43
+ => "admin"
44
+
45
+ irb(main)> User::Role.all
46
+ => ["user", "admin"]
47
+
48
+ irb(main)> User::Role.length
49
+ => 2
50
+
51
+ irb(main)> User::Role[1]
52
+ => "admin"
53
+
54
+ irb(main)> User::Role[:user]
55
+ => "user"
56
+
57
+ irb(main)> User::Role.include?('foobar')
58
+ => false
59
+
60
+ irb(main)> User::Role.include?('user')
61
+ => true
62
+ ```
63
+
64
+ ND::Enum inheritates from [`Enumerable`](https://ruby-doc.org/core-3.1.2/Enumerable.html), so it is possible to use all `Enumerable` methods on the enum module: `each`, `map`, `find`...
65
+
66
+ ### Configuration
67
+
68
+ Fine-tune `ND::Enum` behaviour by creating an initializer (for example: `config/initializers/nd_enum.rb`).
69
+ The values shown in the example below are the default values.
70
+
71
+ ```ruby
72
+ ND::Enum.configure do |c|
73
+ c.default_i18n_scope = :base
74
+ c.default_i18n_validation_mode = :ignore # Allowed values: ignore, log, enforce
75
+ end
76
+ ```
77
+
78
+ ### I18n
79
+
80
+ Allows to translate your enum values.
81
+ Add to your locale files:
82
+
83
+ ```yaml
84
+ en:
85
+ users: # Model.table_name
86
+ role: # attribute
87
+ base: # default scope
88
+ user: User
89
+ admin: Admin
90
+ foobar: # custom scope
91
+ user: The user
92
+ admin: The admin
93
+ ```
94
+
95
+ Then call `t` (or `translate`) method:
96
+
97
+ ```ruby
98
+ irb(main)> User::Role.t(:user) # Or `translate` method (alias)
99
+ => "translation missing: en.users.role.base.user"
100
+ ```
101
+
102
+ Use a different scope to have several translations for a single value, depending on context:
103
+
104
+ ```ruby
105
+ irb(main)> User::Role.t(:user, :foobar)
106
+ => "translation missing: en.users.role.foobar.user"
107
+ ```
108
+
109
+ Please note that the default scope (`base`) can be configured using the `default_i18n_scope` initializer option.
110
+
111
+ #### Validate translations presence
112
+
113
+ Validate that your enum are translated. By default, this feature is disabled.
114
+
115
+ ```ruby
116
+ class User < ApplicationRecord
117
+ nd_enum(role: %i(user admin), i18n: { mode: :log })
118
+ end
119
+ ```
120
+
121
+ It will log the missing translations for each scope & locale. For example:
122
+
123
+ ```
124
+ I, [2022-08-10T21:17:53.931669 #67401] INFO -- : ND::Enum: User#role scopes=[:base, :short]
125
+ I, [2022-08-10T21:17:53.931669 #67401] INFO -- : ND::Enum: User#role locale=en missing_keys=[]
126
+ I, [2022-08-10T21:17:53.931669 #67401] INFO -- : ND::Enum: User#role locale=nl missing_keys=["users.role.base.user", "users.role.short.user"]
127
+ ```
128
+
129
+ This mode can be used as default with the `c.default_i18n_validation_mode = :log` initializer option.
130
+
131
+ #### Enforce translations presence
132
+
133
+ Raise an exception when some translations are missing.
134
+
135
+ ```ruby
136
+ class User < ApplicationRecord
137
+ nd_enum(role: %i(user admin), i18n: { mode: :enforce })
138
+ end
139
+ ```
140
+
141
+ ```
142
+ (irb):1:in `<main>': One or several translations are missing (ND::Enum::MissingTranslationError)
143
+ from bin/console:15:in `<main>'
144
+ ```
145
+
146
+ This mode can be used as default with the `c.default_i18n_validation_mode = :enforce` initializer option.
147
+
148
+ ### `ActiveRecord` Enum
149
+
150
+ Add a wrapper to [`ActiveRecord` Enum](https://api.rubyonrails.org/classes/ActiveRecord/Enum.html) by specifying the `db: true` option.
151
+
152
+ ```ruby
153
+ class User < ApplicationRecord
154
+ nd_enum(role: %i(user admin), db: true)
155
+ end
156
+
157
+ # It does exactly the same thing than below, but shorter:
158
+
159
+ class User < ApplicationRecord
160
+ nd_enum(role: %i(user admin))
161
+ enum(role: User::Role.to_h) # Or `enum(role: { user: 'user', admin: 'admin '})`
162
+ end
163
+ ```
164
+
165
+ It allows to use these methods:
166
+
167
+ ```ruby
168
+ user.admin!
169
+ user.admin? # => true
170
+ user.role # => "admin"
171
+ ```
172
+
173
+ And these scopes:
174
+
175
+ ```ruby
176
+ User.admin
177
+ User.not_admin
178
+
179
+ User.user
180
+ User.not_user
181
+
182
+ # ...
183
+ ```
184
+
185
+ Disable scope definition by setting `scopes: false` to your enum:
186
+
187
+ ```ruby
188
+ class User < ApplicationRecord
189
+ nd_enum(role: %i(user admin), db: { scopes: false })
190
+ end
191
+ ```
192
+
193
+ Set the default enum:
194
+
195
+ ```ruby
196
+ class User < ApplicationRecord
197
+ nd_enum(role: %i(user admin), db: { default: :admin })
198
+ end
199
+ ```
200
+
201
+ Add a `prefix` or `suffix` option when you need to define multiple enums with same values. If the passed value is true, the methods are prefixed/suffixed with the name of the enum. It is also possible to supply a custom value:
202
+
203
+ ```ruby
204
+ class User < ApplicationRecord
205
+ nd_enum(role: %i(user admin), db: { prefix: true })
206
+
207
+ # Scopes: `User.role_admin`, `User.role_user` ...
208
+ # Methods: `User.role_admin!`, `User.role_user!` ...
209
+
210
+ nd_enum(role: %i(user admin), db: { suffix: true })
211
+
212
+ # Scopes: `User.admin_role`, `User.user_role` ...
213
+ # Methods: `User.admin_role!`, `User.user_role!` ...
214
+
215
+ nd_enum(role: %i(user admin), db: { prefix: 'foobar' })
216
+
217
+ # Scopes: `User.foobar_admin`, `User.foobar_user` ...
218
+ # Methods: `User.foobar_admin!`, `User.foobar_user!` ...
219
+ end
220
+ ```
20
221
 
21
222
  ## Development
22
223
 
23
224
  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.
24
225
 
25
- 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).
226
+ Guard is also installed: `bundle exec guard`.
227
+
228
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, run `gem bump` (or manually 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).
26
229
 
27
230
  ## Contributing
28
231
 
29
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/nd-enum.
232
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rclavel/nd-enum.
30
233
 
31
234
  ## License
32
235
 
data/lib/nd/enum/base.rb CHANGED
@@ -3,34 +3,38 @@
3
3
  require 'forwardable'
4
4
  require 'active_support'
5
5
 
6
- module ND
7
- module Enum
8
- module Base
9
- extend ActiveSupport::Concern
10
-
11
- included do
12
- class << self
13
- include Enumerable
14
- extend Forwardable
15
-
16
- def_delegators :all, :size, :length, :[], :empty?, :last, :index
17
-
18
- def each(&block)
19
- all.each(&block)
20
- end
21
-
22
- def to_h
23
- all.map { |value| [value.to_sym, value] }.to_h
24
- end
25
-
26
- def [](value)
27
- value.is_a?(Integer) ? all[value] : to_h[value.to_sym]
28
- end
29
-
30
- def t(value, scope = :base)
31
- I18n.t(value, scope: "#{model.table_name}.#{attribute}.#{scope}")
32
- end
33
- end
6
+ module ND::Enum::Base
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ class << self
11
+ include Enumerable
12
+ extend Forwardable
13
+
14
+ def_delegators :all, :size, :length, :[], :empty?, :last, :index
15
+
16
+ def each(&block)
17
+ all.each(&block)
18
+ end
19
+
20
+ def to_h
21
+ all.map { |value| [value.to_sym, value] }.to_h
22
+ end
23
+
24
+ def [](value)
25
+ value.is_a?(Integer) ? all[value] : to_h[value.to_sym]
26
+ end
27
+
28
+ def t(value, scope = nil)
29
+ scope ||= configuration.default_i18n_scope
30
+ ::I18n.t(value, scope: "#{model.table_name}.#{attribute}.#{scope}")
31
+ end
32
+ alias_method :translate, :t
33
+
34
+ private
35
+
36
+ def configuration
37
+ ND::Enum.configuration
34
38
  end
35
39
  end
36
40
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ND::Enum::Configuration
4
+ Configuration = Struct.new(:default_i18n_validation_mode, :default_i18n_scope)
5
+ DEFAULT_CONFIGURATION = {
6
+ default_i18n_validation_mode: :ignore,
7
+ default_i18n_scope: :base,
8
+ }
9
+
10
+ def configuration
11
+ @_configuration ||= Configuration.new(*DEFAULT_CONFIGURATION.values_at(*Configuration.members))
12
+ end
13
+
14
+ def configure
15
+ yield(configuration)
16
+ end
17
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ND::Enum::I18n
4
+ class << self
5
+ # TODO: Transform into a nd_enum
6
+ module Mode
7
+ LOG = 'log'
8
+ ENFORCE = 'enforce'
9
+ IGNORE = 'ignore'
10
+
11
+ def self.all
12
+ [LOG, ENFORCE, IGNORE]
13
+ end
14
+ end
15
+
16
+ def validate!(options)
17
+ mode = get_mode_from_options(options)
18
+ return if mode == Mode::IGNORE
19
+
20
+ i18n_scope = build_i18n_scope(options)
21
+ scopes = get_scopes_from_translations(i18n_scope)
22
+ missing_keys_by_locale = get_missing_keys_by_locale(options, i18n_scope, scopes)
23
+
24
+ log_missing_keys(options, scopes, missing_keys_by_locale)
25
+
26
+ if mode == Mode::ENFORCE && missing_keys_by_locale.values.any?(&:present?)
27
+ raise ND::Enum::MissingTranslationError
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def get_mode_from_options(options)
34
+ case options.dig(:i18n, :validate)
35
+ when 'log', :log then Mode::LOG
36
+ when 'enforce', :enforce then Mode::ENFORCE
37
+ else
38
+ default_mode = configuration.default_i18n_validation_mode&.to_s
39
+ Mode.all.include?(default_mode) ? default_mode : Mode::IGNORE
40
+ end
41
+ end
42
+
43
+ def build_i18n_scope(options)
44
+ "#{options[:model].table_name}.#{options[:attribute]}"
45
+ end
46
+
47
+ def get_scopes_from_translations(i18n_scope)
48
+ scopes = %i(base)
49
+
50
+ I18n.available_locales.each do |locale|
51
+ configuration = I18n.t(i18n_scope, locale: locale, default: {})
52
+ configuration.each_key do |scope|
53
+ scopes << scope.to_sym unless scopes.include?(scope.to_sym)
54
+ end
55
+ end
56
+
57
+ scopes
58
+ end
59
+
60
+ def get_missing_keys_by_locale(options, i18n_scope, scopes)
61
+ I18n.available_locales.each_with_object({}) do |locale, missing_keys_by_locale|
62
+ missing_keys = []
63
+
64
+ scopes.each do |scope|
65
+ options[:values].each do |value|
66
+ value_i18n_scope = "#{i18n_scope}.#{scope}.#{value}"
67
+ next if I18n.exists?(value_i18n_scope, locale: locale)
68
+
69
+ missing_keys << value_i18n_scope
70
+ end
71
+ end
72
+
73
+ missing_keys_by_locale[locale] = missing_keys
74
+ end
75
+ end
76
+
77
+ def log_missing_keys(options, scopes, missing_keys_by_locale)
78
+ prefix = "ND::Enum: #{options[:model_name]}##{options[:attribute]}"
79
+ logger.info("#{prefix} scopes=#{scopes}")
80
+
81
+ missing_keys_by_locale.each do |locale, missing_keys|
82
+ logger.info("#{prefix} locale=#{locale} missing_keys=#{missing_keys}")
83
+ end
84
+ end
85
+
86
+ def logger
87
+ @_logger ||= Logger.new(STDOUT)
88
+ end
89
+
90
+ def configuration
91
+ ND::Enum.configuration
92
+ end
93
+ end
94
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ND
4
4
  module Enum
5
- VERSION = '0.1.2'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
data/lib/nd/enum.rb CHANGED
@@ -8,64 +8,79 @@ require 'active_support/core_ext/string/inflections.rb'
8
8
 
9
9
  require_relative 'enum/version'
10
10
  require_relative 'enum/base'
11
+ require_relative 'enum/configuration'
12
+ require_relative 'enum/i18n'
11
13
 
12
- module ND
13
- module Enum
14
- extend ActiveSupport::Concern
14
+ module ND::Enum
15
+ extend ActiveSupport::Concern
15
16
 
16
- included do
17
- def self.nd_enum(db: false, i18n: {}, **configuration)
18
- options = ND::Enum.set_options(binding, self)
19
- enum_module = ND::Enum.define_module(options)
17
+ included do
18
+ def self.nd_enum(db: false, i18n: {}, model: self, model_name: nil, **configuration)
19
+ options = ND::Enum.set_options(binding, model, model_name)
20
+ enum_module = ND::Enum.define_module(options)
20
21
 
21
- ND::Enum.define_db_enum(options, enum_module) if options[:db]
22
+ ND::Enum.define_db_enum(options, enum_module) if options[:db]
23
+ ND::Enum::I18n.validate!(options)
22
24
 
23
- const_set(options[:attribute].to_s.camelize, enum_module)
24
- end
25
+ const_set(options[:attribute].to_s.camelize, enum_module)
25
26
  end
27
+ end
26
28
 
27
- class << self
28
- def set_options(caller_binding, caller_class)
29
- options = caller_class.method(:nd_enum).parameters.each_with_object({}) do |(_, name), options|
30
- options[name.to_sym] = caller_binding.local_variable_get(name)
31
- end
32
- options[:attribute], options[:values] = options.delete(:configuration).to_a.first
33
- options[:model] = caller_class
29
+ class Error < StandardError; end
30
+ class MissingTranslationError < Error
31
+ def message
32
+ 'One or several translations are missing'
33
+ end
34
+ end
35
+
36
+ class << self
37
+ include ND::Enum::Configuration
34
38
 
35
- options
39
+ def set_options(caller_binding, caller_class, caller_class_name)
40
+ options = caller_class.method(:nd_enum).parameters.each_with_object({}) do |(_, name), options|
41
+ options[name.to_sym] = caller_binding.local_variable_get(name)
36
42
  end
43
+ options[:attribute], options[:values] = options.delete(:configuration).to_a.first
44
+ options[:model] = caller_class
45
+ options[:model_name] = caller_class_name
46
+
47
+ options
48
+ end
37
49
 
38
- def define_module(options)
39
- Module.new do
40
- include ND::Enum::Base
50
+ def define_module(options)
51
+ Module.new do
52
+ include ND::Enum::Base
41
53
 
42
- # Public methods
54
+ # Public methods
43
55
 
44
- define_singleton_method(:all) { options[:values] }
56
+ define_singleton_method(:all) { options[:values] }
45
57
 
46
- # Private methods
58
+ # Private methods
47
59
 
48
- define_singleton_method(:options) { options }
49
- options.each_key do |name|
50
- define_singleton_method(name) { options[name] }
51
- end
60
+ define_singleton_method(:options) { options }
61
+ options.each_key do |name|
62
+ define_singleton_method(name) { options[name] }
63
+ end
52
64
 
53
- [:options, *options.keys].each do |method_name|
54
- singleton_class.class_eval { private method_name.to_sym }
55
- end
65
+ [:options, *options.keys].each do |method_name|
66
+ singleton_class.class_eval { private method_name.to_sym }
67
+ end
56
68
 
57
- # Constants
69
+ # Constants
58
70
 
59
- options[:values].map do |value|
60
- const_set(value.to_s.upcase, value.to_s)
61
- end
71
+ options[:values].map do |value|
72
+ const_set(value.to_s.upcase, value.to_s)
62
73
  end
63
74
  end
75
+ end
64
76
 
65
- def define_db_enum(options, enum_module)
66
- enum_options = options[:db].is_a?(Hash) ? options[:db] : {}
67
- options[:model].enum(options[:attribute] => enum_module.to_h, **enum_options)
68
- end
77
+ def define_db_enum(options, enum_module)
78
+ enum_options = options[:db].is_a?(Hash) ? options[:db] : {}
79
+
80
+ enum_options[:_prefix] = enum_options.delete(:prefix) if enum_options.key?(:prefix)
81
+ enum_options[:_suffix] = enum_options.delete(:suffix) if enum_options.key?(:suffix)
82
+
83
+ options[:model].enum(options[:attribute] => enum_module.to_h, **enum_options)
69
84
  end
70
85
  end
71
86
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nd-enum
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Romain Clavel
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-08 00:00:00.000000000 Z
11
+ date: 2022-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -55,6 +55,8 @@ files:
55
55
  - Rakefile
56
56
  - lib/nd/enum.rb
57
57
  - lib/nd/enum/base.rb
58
+ - lib/nd/enum/configuration.rb
59
+ - lib/nd/enum/i18n.rb
58
60
  - lib/nd/enum/version.rb
59
61
  - nd-enum.gemspec
60
62
  - sig/nd/enum.rbs