nd-enum 0.1.2 → 0.2.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: 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