enum-x 1.0.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 +7 -0
- data/.gitignore +29 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +42 -0
- data/LICENSE.txt +22 -0
- data/README.md +74 -0
- data/Rakefile +1 -0
- data/enum-x.gemspec +26 -0
- data/lib/enum-x.rb +2 -0
- data/lib/enum_x/dsl.rb +389 -0
- data/lib/enum_x/monkey.rb +51 -0
- data/lib/enum_x/railtie.rb +14 -0
- data/lib/enum_x/value.rb +164 -0
- data/lib/enum_x/value_list.rb +69 -0
- data/lib/enum_x/version.rb +3 -0
- data/lib/enum_x.rb +336 -0
- data/spec/enum_x/dsl_spec.rb +345 -0
- data/spec/enum_x_spec.rb +365 -0
- data/spec/spec_helper.rb +19 -0
- metadata +136 -0
data/lib/enum_x/value.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
class EnumX
|
2
|
+
|
3
|
+
# One enum value. Each value has a name and may contain any format-specific values.
|
4
|
+
class Value
|
5
|
+
|
6
|
+
######
|
7
|
+
# Initialization
|
8
|
+
|
9
|
+
# Initializes a new enum value.
|
10
|
+
#
|
11
|
+
# @param [EnumX] enum The owning enum.
|
12
|
+
# @param [Hash|#to_s] value
|
13
|
+
# The actual value. If a Hash is specified, it must contain a key 'value' or :value, and may
|
14
|
+
# contain values for any other format. A format named :format is not allowed.
|
15
|
+
#
|
16
|
+
# == Examples
|
17
|
+
# EnumX::Value.new(enum, 'new')
|
18
|
+
# EnumX::Value.new(enum, {:value => 'new', :xml => '<new>'})
|
19
|
+
def initialize(enum, value)
|
20
|
+
raise ArgumentError, "enum required" unless enum
|
21
|
+
@enum = enum
|
22
|
+
|
23
|
+
@formats = {}
|
24
|
+
case value
|
25
|
+
when Hash
|
26
|
+
process_hash(value)
|
27
|
+
else
|
28
|
+
@value = value.to_s
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Processes a value hash.
|
33
|
+
def process_hash(hash)
|
34
|
+
hash = hash.dup
|
35
|
+
|
36
|
+
value = hash.delete(:value) || hash.delete('value')
|
37
|
+
raise ArgumentError, "key :value is required when a hash value is specified" unless value
|
38
|
+
|
39
|
+
@value = value.to_s
|
40
|
+
|
41
|
+
# Process all other options as formats.
|
42
|
+
hash.each do |key, value|
|
43
|
+
raise ArgumentError, "key :format is not allowed" if key.to_s == 'format'
|
44
|
+
@formats[key.to_s] = value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
######
|
49
|
+
# Attributes
|
50
|
+
|
51
|
+
# @!attribute [r] enum
|
52
|
+
# @return [EnumX] The EnumX defining this value.
|
53
|
+
attr_reader :enum
|
54
|
+
|
55
|
+
# @!attribute [r] value
|
56
|
+
# @return [String] The actual string value.
|
57
|
+
attr_reader :value
|
58
|
+
|
59
|
+
# @!attribute [r] formats
|
60
|
+
# @return [Hash] Any other formats supported by this value.
|
61
|
+
attr_reader :formats
|
62
|
+
|
63
|
+
# @!attribute [r] symbol
|
64
|
+
# @return [Symbol] The value symbol.
|
65
|
+
def symbol
|
66
|
+
value.to_sym
|
67
|
+
end
|
68
|
+
|
69
|
+
######
|
70
|
+
# Duplication
|
71
|
+
|
72
|
+
# Creates a duplicate of this enum value.
|
73
|
+
# @param [EnumX] enum A new owner enum of the value.
|
74
|
+
# @return [EnumX::Value]
|
75
|
+
def dup(enum = self.enum)
|
76
|
+
Value.new(enum, @formats.merge(:value => value))
|
77
|
+
end
|
78
|
+
|
79
|
+
######
|
80
|
+
# Value retrievers
|
81
|
+
|
82
|
+
alias :to_str :value
|
83
|
+
alias :to_s :value
|
84
|
+
alias :to_sym :symbol
|
85
|
+
|
86
|
+
# Pass numeric conversion to the string value.
|
87
|
+
def to_i; value.to_i end
|
88
|
+
def to_f; value.to_f end
|
89
|
+
|
90
|
+
def respond_to?(method)
|
91
|
+
if method =~ /^to_/ && !%w[ to_int to_a to_ary ].include?(method.to_s)
|
92
|
+
true
|
93
|
+
elsif method =~ /\?$/ && enum.values.include?($`)
|
94
|
+
true
|
95
|
+
else
|
96
|
+
super
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def method_missing(method, *args, &block)
|
101
|
+
if method =~ /^to_/ && !%w[ to_int to_a to_ary ].include?(method.to_s)
|
102
|
+
@formats[$'] || value
|
103
|
+
elsif method =~ /\?$/
|
104
|
+
value = $`
|
105
|
+
|
106
|
+
if enum.values.include?(value)
|
107
|
+
# If the owning enum defines the requested value, we treat this as an mnmenonic. Test if this
|
108
|
+
# is the current value.
|
109
|
+
self == value
|
110
|
+
else
|
111
|
+
# If the owning enum does not define the requested value, we treat this as a missing method.
|
112
|
+
super
|
113
|
+
end
|
114
|
+
else
|
115
|
+
super
|
116
|
+
end
|
117
|
+
end
|
118
|
+
private :method_missing
|
119
|
+
|
120
|
+
######
|
121
|
+
# I18n
|
122
|
+
|
123
|
+
def translate(options = {})
|
124
|
+
default_value = if defined?(ActiveSupport)
|
125
|
+
ActiveSupport::Inflector.humanize(to_s).downcase
|
126
|
+
else
|
127
|
+
to_s
|
128
|
+
end
|
129
|
+
I18n.translate value, options.merge(:scope => @enum.i18n_scope, :default => default_value)
|
130
|
+
end
|
131
|
+
def translate!(options = {})
|
132
|
+
I18n.translate value, options.merge(:scope => @enum.i18n_scope, :raise => true)
|
133
|
+
end
|
134
|
+
|
135
|
+
######
|
136
|
+
# Common value object handling
|
137
|
+
|
138
|
+
def ==(other)
|
139
|
+
return false if other.nil?
|
140
|
+
value == other.to_s
|
141
|
+
end
|
142
|
+
|
143
|
+
def eql?(other)
|
144
|
+
return false if other.nil?
|
145
|
+
other.is_a?(EnumX::Value) && other.enum == enum && other.value == value
|
146
|
+
end
|
147
|
+
|
148
|
+
def hash
|
149
|
+
value.hash
|
150
|
+
end
|
151
|
+
|
152
|
+
def as_json(*args)
|
153
|
+
value
|
154
|
+
end
|
155
|
+
|
156
|
+
# EnumX values are simply stored as their string values.
|
157
|
+
def encode_with(coder)
|
158
|
+
coder.tag = nil
|
159
|
+
coder.scalar = value
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
class EnumX
|
2
|
+
|
3
|
+
# A list of multiple enum values.
|
4
|
+
class ValueList
|
5
|
+
|
6
|
+
######
|
7
|
+
# Initialization
|
8
|
+
|
9
|
+
def initialize(enum, values)
|
10
|
+
@enum = enum
|
11
|
+
values = [ values ] unless values.is_a?(Enumerable)
|
12
|
+
@values = values.map { |value| @enum[value] || value }
|
13
|
+
end
|
14
|
+
|
15
|
+
######
|
16
|
+
# Attributes
|
17
|
+
|
18
|
+
attr_reader :enum
|
19
|
+
|
20
|
+
attr_reader :values
|
21
|
+
|
22
|
+
######
|
23
|
+
# Method delegation
|
24
|
+
|
25
|
+
# Delegate everything to Array, except querying methods
|
26
|
+
|
27
|
+
# Obtains an array version of the enum.
|
28
|
+
alias :to_ary :values
|
29
|
+
|
30
|
+
# Converts the enum to an array of {EnumX::Value}s.
|
31
|
+
alias :to_a :values
|
32
|
+
|
33
|
+
# Creates a string representation of the values.
|
34
|
+
def to_s
|
35
|
+
values.map(&:to_s).join(', ')
|
36
|
+
end
|
37
|
+
|
38
|
+
include Enumerable
|
39
|
+
|
40
|
+
# Create delegate methods for all of Enumerable's own methods.
|
41
|
+
(Enumerable.instance_methods + [ :empty?, :present?, :blank? ]).each do |method|
|
42
|
+
class_eval <<-RUBY, __FILE__, __LINE__+1
|
43
|
+
def #{method}(*args, &block)
|
44
|
+
values.__send__ :#{method}, *args, &block
|
45
|
+
end
|
46
|
+
RUBY
|
47
|
+
end
|
48
|
+
|
49
|
+
def [](value)
|
50
|
+
values.find { |val| val.to_s == value.to_s }
|
51
|
+
end
|
52
|
+
|
53
|
+
def include?(value)
|
54
|
+
values.any? { |val| val.to_s == value.to_s }
|
55
|
+
end
|
56
|
+
|
57
|
+
def ==(other)
|
58
|
+
case other
|
59
|
+
when Array then values == other
|
60
|
+
when EnumX::ValueList then values == other.values
|
61
|
+
when EnumX::Value then values == [ other ]
|
62
|
+
else false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
data/lib/enum_x.rb
ADDED
@@ -0,0 +1,336 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'json'
|
3
|
+
require 'i18n'
|
4
|
+
|
5
|
+
# Utility class representing an enumeration of options to choose from. This can be used
|
6
|
+
# in models to make a field have a certain number of allowed options.
|
7
|
+
#
|
8
|
+
# Enums are defined in configuration files which are loaded from load paths (see {.load_paths}).
|
9
|
+
#
|
10
|
+
# == EnumX file format
|
11
|
+
#
|
12
|
+
# EnumX files are YAML files defining enumerations in the following format:
|
13
|
+
#
|
14
|
+
# <name>: [ <value>, <value, ... ]
|
15
|
+
#
|
16
|
+
# E.g.
|
17
|
+
#
|
18
|
+
# statuses: [ draft, sent, returned ]
|
19
|
+
#
|
20
|
+
# == Extra formats
|
21
|
+
#
|
22
|
+
# EnumX values are designed to be converted to various formats. By default, they simply support a name,
|
23
|
+
# which is the value you specify in the YAML files. They also respond to any method starting with
|
24
|
+
# 'to_', e.g. +to_string+, +to_json+, +to_legacy+. These return the name of the value, unless otherwise
|
25
|
+
# specified explicitly when being defined.
|
26
|
+
#
|
27
|
+
# If you want to provide extra formats in the YAML, you may indicate them as follows:
|
28
|
+
#
|
29
|
+
# statuses: [ { value: 'draft', legacy: 'new' }, sent, { value: 'returned', legacy: 'back' } ]
|
30
|
+
#
|
31
|
+
# Now, the following will all be true:
|
32
|
+
#
|
33
|
+
# EnumX.statuses[:draft].value == 'draft'
|
34
|
+
# EnumX.statuses[:draft].symbol == :draft
|
35
|
+
# # => #symbol always returns the value converted to a Symbol
|
36
|
+
# EnumX.statuses[:draft].to_legacy == 'new'
|
37
|
+
# EnumX.statuses[:sent].value == 'sent'
|
38
|
+
# EnumX.statuses[:sent].symbol == :sent
|
39
|
+
# EnumX.statuses[:sent].to_legacy == 'sent'
|
40
|
+
# # => because no explicit value for the 'legacy' format was specified
|
41
|
+
class EnumX
|
42
|
+
|
43
|
+
autoload :DSL, 'enum_x/dsl'
|
44
|
+
autoload :Value, 'enum_x/value'
|
45
|
+
autoload :ValueList, 'enum_x/value_list'
|
46
|
+
|
47
|
+
######
|
48
|
+
# Registry class
|
49
|
+
|
50
|
+
# The enum registry. Provides indifferent access.
|
51
|
+
class Registry < Hash
|
52
|
+
def [](key)
|
53
|
+
super key.to_s
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
######
|
58
|
+
# ValueHash class
|
59
|
+
|
60
|
+
# A hash for enum values. This is a regular Hash, except that it
|
61
|
+
# has completely indifferent access also integers are possible as keys.
|
62
|
+
class ValueHash < Hash
|
63
|
+
def [](key)
|
64
|
+
super key.to_s
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
######
|
69
|
+
# EnumX retrieval
|
70
|
+
|
71
|
+
class << self
|
72
|
+
|
73
|
+
# An array of EnumX load paths.
|
74
|
+
#
|
75
|
+
# @example Add some enum load paths
|
76
|
+
# EnumX.load_paths += Dir[ Rails.root + 'config/enums/**/*.yml' ]
|
77
|
+
def load_paths=(paths)
|
78
|
+
@load_paths = Array.wrap(paths)
|
79
|
+
end
|
80
|
+
def load_paths
|
81
|
+
@load_paths ||= []
|
82
|
+
end
|
83
|
+
|
84
|
+
# Defines a new enum with the given values.
|
85
|
+
# @see EnumX#initialize
|
86
|
+
def define(name, values)
|
87
|
+
registry[name.to_s] = new(name, values)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Undefines an enum.
|
91
|
+
def undefine(name)
|
92
|
+
registry.delete name.to_s
|
93
|
+
end
|
94
|
+
|
95
|
+
# Retrieves an enum by name.
|
96
|
+
def [](name)
|
97
|
+
load_enums unless @registry
|
98
|
+
registry[name]
|
99
|
+
end
|
100
|
+
|
101
|
+
# Allows overriding of the load handler.
|
102
|
+
#
|
103
|
+
# Specify an object that responds to +load_enums_from(file, file_type)+ where +file+
|
104
|
+
# is a file name, and +file_type+ is either +:yaml+ or +:ruby+. Alternatively, a block
|
105
|
+
# may be specified that will receive these parameters.
|
106
|
+
#
|
107
|
+
# The method does not have to return anything, but should use calls to {define} to
|
108
|
+
# actually define the enums.
|
109
|
+
#
|
110
|
+
# By default, the loader parses the YAML file as a flat Hash, where the keys are enum
|
111
|
+
# names, and the values are arrays containing the values.
|
112
|
+
#
|
113
|
+
# == Usage
|
114
|
+
#
|
115
|
+
# EnumX.loader = proc do |file, file_type|
|
116
|
+
# if file_type == :ruby
|
117
|
+
# load file
|
118
|
+
# else
|
119
|
+
# ... # Process the YAML file here.
|
120
|
+
# end
|
121
|
+
# end
|
122
|
+
attr_accessor :loader
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
# @!attribute [r] registry
|
127
|
+
# @return [Hash] All defined enums.
|
128
|
+
def registry
|
129
|
+
@registry ||= EnumX::Registry.new
|
130
|
+
end
|
131
|
+
|
132
|
+
# Tries to look for an enum by the given name.
|
133
|
+
#
|
134
|
+
# EnumX.statuses == EnumX[:statuses]
|
135
|
+
def method_missing(method, *args, &block)
|
136
|
+
if enum = self[method]
|
137
|
+
enum
|
138
|
+
elsif method =~ /^to_/
|
139
|
+
super
|
140
|
+
else
|
141
|
+
raise NameError, "enum #{method} not found"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Loads enums from the defined load paths.
|
146
|
+
def load_enums
|
147
|
+
load_paths.each do |path|
|
148
|
+
file_type = case path
|
149
|
+
when /\.rb$/ then :ruby
|
150
|
+
when /\.ya?ml$/ then :yaml
|
151
|
+
else :other
|
152
|
+
end
|
153
|
+
|
154
|
+
case loader
|
155
|
+
when Proc
|
156
|
+
loader.call(path, file_type)
|
157
|
+
when ->(l){ l.respond_to?(:load_enums_from) }
|
158
|
+
loader.load_enums_from(path, file_type)
|
159
|
+
else
|
160
|
+
case file_type
|
161
|
+
when :ruby
|
162
|
+
load path
|
163
|
+
when :yaml
|
164
|
+
load_enums_from_yaml(path)
|
165
|
+
else
|
166
|
+
# ignore the file
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Load enums from a YAML file.
|
173
|
+
def load_enums_from_yaml(file)
|
174
|
+
yaml = YAML.load_file(file)
|
175
|
+
yaml.each do |name, values|
|
176
|
+
define name, values
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
|
182
|
+
######
|
183
|
+
# Initialization
|
184
|
+
|
185
|
+
# Initializes a new EnumX instance.
|
186
|
+
#
|
187
|
+
# @param [#to_s] name
|
188
|
+
# The name of the enum. This is used to load translations. See {EnumX::Value#to_s}.
|
189
|
+
# @param [Enumerable] values
|
190
|
+
# The values for the enumeration. Each item is passed to {EnumX::Value.new}.
|
191
|
+
def initialize(name, values)
|
192
|
+
@name = name.to_s
|
193
|
+
|
194
|
+
@values = ValueHash.new
|
195
|
+
values.each do |value|
|
196
|
+
add_value!(value)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
######
|
201
|
+
# Attributes
|
202
|
+
|
203
|
+
# @!attribute [r] name
|
204
|
+
# @return [String] The name of the enum.
|
205
|
+
attr_reader :name
|
206
|
+
|
207
|
+
# @!attribute [r] values
|
208
|
+
# @return [Array<EnumX::Value>] All allowed enum values.
|
209
|
+
def values
|
210
|
+
@values.values
|
211
|
+
end
|
212
|
+
|
213
|
+
# Make the enum enumerable. That's the least it should do!
|
214
|
+
include Enumerable
|
215
|
+
|
216
|
+
# Create delegate methods for all of Enumerable's own methods.
|
217
|
+
[ :each, :empty?, :present?, :blank? ].each do |method|
|
218
|
+
class_eval <<-RUBY, __FILE__, __LINE__+1
|
219
|
+
def #{method}(*args, &block)
|
220
|
+
values.__send__ :#{method}, *args, &block
|
221
|
+
end
|
222
|
+
RUBY
|
223
|
+
end
|
224
|
+
|
225
|
+
# Obtains a value by its key.
|
226
|
+
def [](key)
|
227
|
+
@values[key]
|
228
|
+
end
|
229
|
+
|
230
|
+
# Obtains an array version of the enum.
|
231
|
+
alias :to_ary :values
|
232
|
+
|
233
|
+
# Converts the enum to an array of {EnumX::Value}s.
|
234
|
+
alias :to_a :values
|
235
|
+
|
236
|
+
######
|
237
|
+
# Operations
|
238
|
+
|
239
|
+
# Creates a duplicate of this enum.
|
240
|
+
def dup
|
241
|
+
EnumX.new(name, values)
|
242
|
+
end
|
243
|
+
|
244
|
+
# Creates a clone of this enumeration but without the given values.
|
245
|
+
def without(*values)
|
246
|
+
EnumX.new(name, self.values.reject{|v| values.include?(v)})
|
247
|
+
end
|
248
|
+
|
249
|
+
# Creates a duplicate of this enumeration but with only the given values.
|
250
|
+
def only(*values)
|
251
|
+
EnumX.new(name, self.values.select{|v| values.include?(v)})
|
252
|
+
end
|
253
|
+
|
254
|
+
# Adds the given values to the enum
|
255
|
+
def extend!(*values)
|
256
|
+
values.each { |value| add_value!(value) }
|
257
|
+
end
|
258
|
+
|
259
|
+
######
|
260
|
+
# Translation
|
261
|
+
|
262
|
+
# The I18n scope for this enum. Override this to provide customization.
|
263
|
+
# Default: +enums.<name>+
|
264
|
+
def i18n_scope
|
265
|
+
[ :enums, name ]
|
266
|
+
end
|
267
|
+
|
268
|
+
######
|
269
|
+
# Find method
|
270
|
+
|
271
|
+
# Finds an enum with the given name on a class.
|
272
|
+
# TODO: Spec
|
273
|
+
def self.find(klass, name)
|
274
|
+
return nil unless klass
|
275
|
+
|
276
|
+
enum = klass.send(name) if klass.respond_to?(name) rescue nil
|
277
|
+
enum if enum.is_a?(EnumX)
|
278
|
+
end
|
279
|
+
|
280
|
+
######
|
281
|
+
# Value by <format>
|
282
|
+
|
283
|
+
# Finds an enum value by the given format.
|
284
|
+
#
|
285
|
+
# == Example
|
286
|
+
#
|
287
|
+
# Given the following enum:
|
288
|
+
#
|
289
|
+
# @enum = EnumX.define(:my_enum,
|
290
|
+
# { :value => 'one', :number => '1' },
|
291
|
+
# { :value => 'two', :number => '2' }
|
292
|
+
# )
|
293
|
+
#
|
294
|
+
# You can now access the values like such:
|
295
|
+
#
|
296
|
+
# @enum.value_with_format(:number, '1') # => @enum[:one]
|
297
|
+
# @enum.value_with_format(:number, '2') # => @enum[:two]
|
298
|
+
#
|
299
|
+
# Note that any undefined format for a value is defaulted to its value. Therefore, the following
|
300
|
+
# is also valid:
|
301
|
+
#
|
302
|
+
# @enum.value_with_format(:unknown, 'one') # => @enum[:one]
|
303
|
+
#
|
304
|
+
# You can also access values by a specific format using the shortcut 'value_with_<format>':
|
305
|
+
#
|
306
|
+
# @enum.value_with_number('1') # => @enum[:one]
|
307
|
+
# @enum.value_with_unknown('one') # => @enum[:one]
|
308
|
+
def value_with_format(format, search)
|
309
|
+
values.find { |val| val.send(:"to_#{format}") == search }
|
310
|
+
end
|
311
|
+
|
312
|
+
private
|
313
|
+
|
314
|
+
def method_missing(method, *args, &block)
|
315
|
+
if method =~ /^value_with_(.*)$/
|
316
|
+
raise ArgumentError, "`#{method}' accepts one argument, #{args.length} given" unless args.length == 1
|
317
|
+
value_with_format $1, args.first
|
318
|
+
else
|
319
|
+
super
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Add a new value to the enum values list
|
324
|
+
def add_value!(value)
|
325
|
+
value = case value
|
326
|
+
when Value then value.dup(self)
|
327
|
+
else Value.new(self, value)
|
328
|
+
end
|
329
|
+
@values[value.value] = value
|
330
|
+
end
|
331
|
+
|
332
|
+
end
|
333
|
+
|
334
|
+
# Extend Symbol & String with enum awareness.
|
335
|
+
require 'enum_x/monkey'
|
336
|
+
require 'enum_x/railtie' if defined?(Rails)
|