enum-x 1.0.0

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