is-enum 0.8.4 → 0.8.8

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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +159 -668
  3. data/lib/is-enum/info.rb +17 -0
  4. data/lib/is-enum.rb +226 -70
  5. metadata +37 -25
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IS
4
+ class Enum; end
5
+ end
6
+
7
+ # @api private
8
+ module IS::Enum::Info
9
+
10
+ NAME = 'is-enum'
11
+ VERSION = '0.8.8'
12
+ SUMMARY = 'Enum types for Ruby'
13
+ AUTHOR = 'Ivan Shikhalev'
14
+ HOMEPAGE = 'https://github.com/inat-get/is-enum'
15
+ LICENSE = 'LGPL-3.0-only'
16
+
17
+ end
data/lib/is-enum.rb CHANGED
@@ -4,6 +4,32 @@ require 'set'
4
4
 
5
5
  module IS; end
6
6
 
7
+ # @note Thread safety
8
+ #
9
+ # Enum definition ({.define}) and finalization ({.finalize!}) are
10
+ # thread-safe. Lookup operations are thread-safe after definition.
11
+ #
12
+ # @note Class variables
13
+ #
14
+ # Uses class variables (`@@enums`, `@@mutex`) shared across inheritance
15
+ # hierarchy. All enum classes register in global `@@enums` for {.parse}.
16
+ #
17
+ # @note Custom attributes
18
+ #
19
+ # Additional attributes passed to {.define} are stored in `@attrs`.
20
+ # Subclasses may access this hash directly to implement custom properties.
21
+ #
22
+ # class Status < IS::Enum
23
+ # define :error, 1, http_code: 500, retryable: false
24
+ #
25
+ # def http_code
26
+ # @attrs[:http_code]
27
+ # end
28
+ #
29
+ # def retryable?
30
+ # @attrs[:retryable]
31
+ # end
32
+ # end
7
33
  class IS::Enum
8
34
 
9
35
  include Comparable
@@ -12,20 +38,18 @@ class IS::Enum
12
38
 
13
39
  include Enumerable
14
40
 
15
- def [] name_or_order
16
- case name_or_order
17
- when String, Symbol
18
- key = name_or_order.to_sym
19
- @values[key] || @aliases[key]
20
- when Integer
21
- @values.values.find { |v| v.order_no == name_or_order }
22
- else
23
- raise ArgumentError, "Invalid value for name or order_no: #{ name_or_order.inspect }", caller_locations
24
- end
25
- end
41
+ # @group Conversion
26
42
 
43
+ # In specific enum class: get enum value by name; in {IS::Enum} itself: parse string like "Class.name" to enum value.
44
+ #
45
+ # @param source [String] the string to parse
46
+ # @return [IS::Enum] the enum value
47
+ # @raise [ArgumentError] if source is not a String or value not found
48
+ # @note Security consideration
49
+ # Converts strings to Symbols internally. Do not use with untrusted
50
+ # user input to avoid memory exhaustion from symbol creation.
27
51
  def parse source
28
- raise ArgumentError, "Invalid source for parsing: #{ source.inspect }", caller_locations
52
+ raise ArgumentError, "Invalid source for parsing: #{ source.inspect }", caller_locations unless source.is_a?(String)
29
53
  if self == IS::Enum
30
54
  parts = source.split '.'
31
55
  raise ArgumentError, "Parsing error from #{ source.inspect }", caller_locations unless parts.is_a?(Array) && parts.size == 2
@@ -41,6 +65,12 @@ class IS::Enum
41
65
  end
42
66
  end
43
67
 
68
+ # Strict lookup by name. Raises if not found.
69
+ # See {.[]} for lenient lookup
70
+ #
71
+ # @param name [Symbol]
72
+ # @return [IS::Enum]
73
+ # @raise [ArgumentError] if name is not a Symbol or value not found
44
74
  def of name
45
75
  raise ArgumentError, "Invalid name of #{ self }: #{ name.inspect }" unless name.is_a?(Symbol)
46
76
  val = @values[name] || @aliases[name]
@@ -48,6 +78,14 @@ class IS::Enum
48
78
  return val
49
79
  end
50
80
 
81
+ # Converts various types to enum values.
82
+ #
83
+ # @param [IS::Enum, nil, Range, Set, Enumerable, Symbol, String, Integer] value
84
+ # @return [IS::Enum, nil, Range<IS::Enum>, Set<IS::Enum>, Array<IS::Enum>]
85
+ # @example Convert range
86
+ # MyEnum.from(:alpha..:gamma) # => range of enum values
87
+ # @example Convert array
88
+ # MyEnum.from([:alpha, :beta]) # => [MyEnum.alpha, MyEnum.beta]
51
89
  def from value
52
90
  case value
53
91
  when nil
@@ -61,107 +99,192 @@ class IS::Enum
61
99
  when Enumerable
62
100
  value.map { |v| from(v) }
63
101
  else
64
- raise ArgumentError, "Invalid value of #{ self }: #{ value.inspect }", caller_locations
102
+ self[value]
65
103
  end
66
104
  end
67
105
 
106
+ # @endgroup
107
+
108
+ # @group Collection
109
+
110
+ # Lookup by name or order number. Returns nil if not found.
111
+ # See {.of} for strict lookup that raises on missing value
112
+ #
113
+ # @param name_or_order [String, Symbol, Integer]
114
+ # @return [IS::Enum, nil]
115
+ def [](name_or_order)
116
+ case name_or_order
117
+ when String, Symbol
118
+ key = name_or_order.to_sym
119
+ @values[key] || @aliases[key]
120
+ when Integer
121
+ @values.values.find { |v| v.order_no == name_or_order }
122
+ else
123
+ raise ArgumentError, "Invalid value for name or order_no: #{name_or_order.inspect}", caller_locations
124
+ end
125
+ end
126
+
127
+ # @return [Enumerator, self]
68
128
  def each
69
129
  return to_enum(__method__) unless block_given?
70
130
  @values.values.sort_by { |v| v.order_no }.each { |v| yield v }
131
+ self
71
132
  end
72
133
 
134
+ # @return [Array<IS::Enum>]
73
135
  def values
74
- @values.values.sort_by { |v| v.order_no }
136
+ @sorted ||= @values.values.sort_by { |v| v.order_no }
75
137
  end
76
138
 
139
+ # @return [Hash<Symbol, IS::Enum>] hash of alias names to target values
77
140
  def aliases
78
141
  @aliases
79
142
  end
80
143
 
144
+ # @return [IS::Enum, nil] last value by order_no, or nil if empty
81
145
  def last
82
- values.to_a.last
146
+ values.last
83
147
  end
84
148
 
149
+ # @return [IS::Enum, nil] first value by order_no, or nil if empty
150
+ def first
151
+ values.first
152
+ end
153
+
154
+ # @return [Range<IS::Enum>] range from first to last value
85
155
  def to_range
86
156
  (first .. last)
87
157
  end
88
158
 
159
+ # @return [Hash<Symbol => IS::Enum>] hash of all names and aliases
160
+ # @note Both canonical names and aliases are included. To distinguish,
161
+ # check {.aliases} for alias keys.
89
162
  def to_h
90
163
  result = {}
91
164
  result.merge! @values
92
165
  result.merge! @aliases
93
166
  end
94
167
 
168
+ # @endgroup
169
+
95
170
  protected
96
171
 
172
+ # @group DSL
173
+
174
+ # Defines new enum value or alias.
175
+ #
176
+ # @param name [Symbol, String] name of the value
177
+ # @param order_no [Integer, nil] explicit order number (auto-generated if nil)
178
+ # @param attrs [Hash] additional attributes
179
+ # @option attrs [IS::Enum, Symbol, String, nil] :alias create alias to existing value
180
+ # @option attrs [String, nil] :description description of the value
181
+ # @return [IS::Enum] defined value (or aliased value for alias)
182
+ # @raise [ArgumentError] on duplicate name, invalid alias, or invalid order_no
183
+ #
184
+ # @example Define values
185
+ # class Status < IS::Enum
186
+ # define :pending, 1, description: "Waiting for processing"
187
+ # define :active, 2
188
+ # define :archived, alias: :active
189
+ # end
97
190
  def define name, order_no = nil, **attrs
98
- @values ||= {}
99
- @aliases ||= {}
100
- case name
101
- when String
102
- name = name.to_sym
103
- when Symbol
104
- # do nothing
105
- else
106
- raise ArgumentError, "Invalid name: #{ name.inspect }", caller_locations
107
- end
108
- raise ArgumentError, "Duplicate value name: #{ name.inspect }", caller_locations if @values.has_key?(name) || @aliases.has_key?(name)
109
- als = attrs.delete :alias
110
- case als
111
- when self
112
- @aliases[name] = als
113
- define_singleton_method name do
114
- als
191
+ @mutex ||= Thread::Mutex::new
192
+ @mutex.synchronize do
193
+ @sorted = nil
194
+ @values ||= {}
195
+ @aliases ||= {}
196
+ case name
197
+ when String
198
+ name = name.to_sym
199
+ when Symbol
200
+ # do nothing
201
+ else
202
+ raise ArgumentError, "Invalid name: #{ name.inspect }", caller_locations
115
203
  end
116
- return als
117
- when Symbol, String
118
- als = als.to_sym
119
- als_value = @values[als] || @aliases[als]
120
- raise ArgumentError, "Invalid alias #{ als.inspect }: value not found", caller_locations unless als_value
121
- @aliases[name] = als_value
122
- define_singleton_method name do
123
- als_value
204
+ raise ArgumentError, "Duplicate value name: #{ name.inspect }", caller_locations if @values.has_key?(name) || @aliases.has_key?(name)
205
+ als = attrs.delete :alias
206
+ case als
207
+ when self
208
+ @aliases[name] = als
209
+ define_singleton_method name do
210
+ als
211
+ end
212
+ return als
213
+ when Symbol, String
214
+ als = als.to_sym
215
+ als_value = @values[als] || @aliases[als]
216
+ raise ArgumentError, "Invalid alias #{ als.inspect }: value not found", caller_locations unless als_value
217
+ @aliases[name] = als_value
218
+ define_singleton_method name do
219
+ als_value
220
+ end
221
+ return als_value
222
+ when nil
223
+ # do nothing
224
+ else
225
+ raise ArgumentError, "Invalid alias value: #{ als.inspect }", caller_locations
226
+ end
227
+ case order_no
228
+ when Integer
229
+ # do nothing
230
+ when nil
231
+ order_no = (@values.values.map(&:order_no).max || 0) + 1
232
+ else
233
+ raise ArgumentError, "Invalid order_no value: #{ order_no.inspect }", caller_locations
234
+ end
235
+ description = attrs.delete :description
236
+ raise ArgumentError, "Invalid description value: #{ description.inspect }", caller_locations unless description.nil? || description.is_a?(String)
237
+ value = new(order_no, name, description, **attrs).freeze
238
+ @values[name] = value
239
+ define_singleton_method name do
240
+ value
124
241
  end
125
- return als_value
126
- when nil
127
- # do nothing
128
- else
129
- raise ArgumentError, "Invalid alias value: #{ als.inspect }", caller_locations
130
- end
131
- case order_no
132
- when Integer
133
- # do nothing
134
- when nil
135
- order_no = (@values.values.map(&:order_no).max || 0) + 1
136
- else
137
- raise ArgumentError, "Invalid order_no value: #{ order_no.inspect }", caller_locations
138
- end
139
- description = attrs.delete :description
140
- raise ArgumentError, "Invalid description value: #{ description.inspect }", caller_locations unless description.nil? || description.is_a?(String)
141
- value = new(order_no, name, description, **attrs).freeze
142
- @values[name] = value
143
- define_singleton_method name do
144
242
  value
145
243
  end
146
- value
147
244
  end
148
245
 
246
+ # Freezes internal structures, preventing further modifications.
247
+ # After calling, {.define} will raise +RuntimeError+.
248
+ #
249
+ # @return [void]
149
250
  def finalize!
150
- @values.freeze
151
- @aliases.freeze
251
+ @mutex ||= Thread::Mutex::new
252
+ @mutex.synchronize do
253
+ @values.freeze
254
+ @aliases.freeze
255
+ end
152
256
  end
153
257
 
258
+ # @endgroup
259
+
260
+ # @private
154
261
  def inherited subclass
155
- @@enums ||= {}
156
- @@enums[subclass.name] = subclass
262
+ @@mutex ||= Thread::Mutex::new
263
+ @@mutex.synchronize do
264
+ @@enums ||= {}
265
+ @@enums[subclass.name] = subclass
266
+ end
157
267
  end
158
268
 
159
269
  private :new
160
270
 
161
271
  end
162
272
 
163
- attr_reader :order_no, :name, :description
273
+ # Order No for sorting and comparison
274
+ # @note Non-unique order numbers
275
+ # Multiple values may be defined with the same `order_no`. This affects
276
+ # sorting order (undefined when equal) and comparison behavior.
277
+ # See {#<=>} for comparison semantics.
278
+ # @return [Integer]
279
+ attr_reader :order_no
280
+
281
+ # @return [Symbol] value name
282
+ attr_reader :name
164
283
 
284
+ # @return [String, nil] optional value description
285
+ attr_reader :description
286
+
287
+ # @private
165
288
  def initialize order_no, name, description, **attrs
166
289
  @order_no = order_no
167
290
  @name = name
@@ -169,12 +292,33 @@ class IS::Enum
169
292
  @attrs = attrs
170
293
  end
171
294
 
295
+ # @group Ordering
296
+
297
+ # Returns +1+ if +self > other+; +0+ if +self == other+; +-1+ if +self < other+. +nil+ if other is not same type.
298
+ #
299
+ # @see Comparable
300
+ # @return [Integer, nil]
301
+ # @note Comparison semantics
302
+ # `==` and `<=>` compare by `order_no`, while `eql?` compares object identity.
303
+ # Multiple values may share the same `order_no`; they compare as equal
304
+ # but are distinct objects.
305
+ #
306
+ # class Alpha < IS::Enum
307
+ # define :alpha, 10
308
+ # define :beta, 20
309
+ # define :bi, 20
310
+ # define :Gamma, 30
311
+ # define :g_letter, alias: :Gamma
312
+ # end
313
+ # Alpha.beta == Alpha.bi # => true (same order_no: 20)
314
+ # Alpha.beta.eql?(Alpha.bi) # => false (different objects)
315
+ # Alpha.Gamma.eql?(Alpha.g_letter) # => true (alias is same object)
172
316
  def <=> other
173
317
  case other
174
318
  when self.class
175
319
  self.order_no <=> other.order_no
176
320
  when Symbol, String
177
- self.order_no <=> self.class[other.to_sym].order_no
321
+ self.order_no <=> self.class[other.to_sym]&.order_no
178
322
  when Integer
179
323
  self.order_no <=> other
180
324
  else
@@ -182,18 +326,28 @@ class IS::Enum
182
326
  end
183
327
  end
184
328
 
185
- def to_sym
186
- name
187
- end
188
-
329
+ # Returns the next value by order_no, or nil if last.
330
+ #
331
+ # @return [IS::Enum, nil]
189
332
  def succ
190
333
  self.class.values.find { |v| v.order_no > self.order_no }
191
334
  end
192
335
 
336
+ # @endgroup
337
+
338
+ # @group Conversion
339
+
340
+ # @return [Symbol] name as symbol
341
+ def to_sym
342
+ name
343
+ end
344
+
345
+ # @return [String] name as string
193
346
  def to_s
194
347
  name.to_s
195
348
  end
196
349
 
350
+ # @return [String] detailed inspection string with class, name, order_no and attributes
197
351
  def inspect
198
352
  data = [ "#{ self.class }.#{ self.name }", "order_no=#{ @order_no }" ]
199
353
  data << "description=#{ @description.inspect }" if @description
@@ -203,4 +357,6 @@ class IS::Enum
203
357
  "[enum #{ data.join(' ') }]"
204
358
  end
205
359
 
360
+ # @endgroup
361
+
206
362
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: is-enum
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.4
4
+ version: 0.8.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Shikhalev
@@ -13,75 +13,86 @@ dependencies:
13
13
  name: rspec
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '3.13'
18
+ version: '0'
19
19
  type: :development
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - "~>"
23
+ - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '3.13'
25
+ version: '0'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: rake
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - "~>"
30
+ - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: '13.3'
32
+ version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '13.3'
39
+ version: '0'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: simplecov
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - "~>"
44
+ - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: 0.22.0
46
+ version: '0'
47
47
  type: :development
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - "~>"
51
+ - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: 0.22.0
53
+ version: '0'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: yard
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
- - - "~>"
58
+ - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: '0.9'
60
+ version: '0'
61
61
  type: :development
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
- - - "~>"
65
+ - - ">="
66
66
  - !ruby/object:Gem::Version
67
- version: '0.9'
67
+ version: '0'
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: redcarpet
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
- - - "~>"
72
+ - - ">="
73
73
  - !ruby/object:Gem::Version
74
- version: '3.6'
74
+ version: '0'
75
75
  type: :development
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
- - - "~>"
79
+ - - ">="
80
80
  - !ruby/object:Gem::Version
81
- version: '3.6'
82
- description: Enum types for Ruby.
83
- email:
84
- - shikhalev@gmail.com
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rdoc
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
85
96
  executables: []
86
97
  extensions: []
87
98
  extra_rdoc_files: []
@@ -89,9 +100,10 @@ files:
89
100
  - LICENSE
90
101
  - README.md
91
102
  - lib/is-enum.rb
103
+ - lib/is-enum/info.rb
92
104
  homepage: https://github.com/inat-get/is-enum
93
105
  licenses:
94
- - GPL-3.0-or-later
106
+ - LGPL-3.0-only
95
107
  metadata: {}
96
108
  rdoc_options: []
97
109
  require_paths: