enum-x 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|