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.
@@ -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
@@ -0,0 +1,3 @@
1
+ class EnumX
2
+ VERSION = '1.0.0'
3
+ 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)