hanami-utils 0.0.0 → 0.7.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,299 @@
1
+ require 'hanami/utils/duplicable'
2
+
3
+ module Hanami
4
+ module Utils
5
+ # Hash on steroids
6
+ # @since 0.1.0
7
+ class Hash
8
+ # @since 0.6.0
9
+ # @api private
10
+ #
11
+ # @see Hanami::Utils::Hash#deep_dup
12
+ # @see Hanami::Utils::Duplicable
13
+ DUPLICATE_LOGIC = Proc.new do |value|
14
+ case value
15
+ when Hash
16
+ value.deep_dup
17
+ when ::Hash
18
+ Hash.new(value).deep_dup.to_h
19
+ end
20
+ end.freeze
21
+
22
+ # Initialize the hash
23
+ #
24
+ # @param hash [#to_h] the value we want to use to initialize this instance
25
+ # @param blk [Proc] define the default value
26
+ #
27
+ # @return [Hanami::Utils::Hash] self
28
+ #
29
+ # @since 0.1.0
30
+ #
31
+ # @see http://www.ruby-doc.org/core/Hash.html#method-c-5B-5D
32
+ #
33
+ # @example Passing a Hash
34
+ # require 'hanami/utils/hash'
35
+ #
36
+ # hash = Hanami::Utils::Hash.new('l' => 23)
37
+ # hash['l'] # => 23
38
+ #
39
+ # @example Passing a block for default
40
+ # require 'hanami/utils/hash'
41
+ #
42
+ # hash = Hanami::Utils::Hash.new {|h,k| h[k] = [] }
43
+ # hash['foo'].push 'bar'
44
+ #
45
+ # hash.to_h # => { 'foo' => ['bar'] }
46
+ def initialize(hash = {}, &blk)
47
+ @hash = hash.to_h
48
+ @hash.default_proc = blk
49
+ end
50
+
51
+ # Convert in-place all the keys to Symbol instances, nested hashes are converted too.
52
+ #
53
+ # @return [Hash] self
54
+ #
55
+ # @since 0.1.0
56
+ #
57
+ # @example
58
+ # require 'hanami/utils/hash'
59
+ #
60
+ # hash = Hanami::Utils::Hash.new 'a' => 23, 'b' => { 'c' => ['x','y','z'] }
61
+ # hash.symbolize!
62
+ #
63
+ # hash.keys # => [:a, :b]
64
+ # hash.inspect # => {:a=>23, :b=>{:c=>["x", "y", "z"]}}
65
+ def symbolize!
66
+ keys.each do |k|
67
+ v = delete(k)
68
+ v = Hash.new(v).symbolize! if v.is_a?(::Hash)
69
+
70
+ self[k.to_sym] = v
71
+ end
72
+
73
+ self
74
+ end
75
+
76
+ # Convert in-place all the keys to Symbol instances, nested hashes are converted too.
77
+ #
78
+ # @return [Hash] self
79
+ #
80
+ # @since 0.3.2
81
+ #
82
+ # @example
83
+ # require 'hanami/utils/hash'
84
+ #
85
+ # hash = Hanami::Utils::Hash.new a: 23, b: { c: ['x','y','z'] }
86
+ # hash.stringify!
87
+ #
88
+ # hash.keys # => [:a, :b]
89
+ # hash.inspect # => {"a"=>23, "b"=>{"c"=>["x", "y", "z"]}}
90
+ def stringify!
91
+ keys.each do |k|
92
+ v = delete(k)
93
+ v = Hash.new(v).stringify! if v.is_a?(::Hash)
94
+
95
+ self[k.to_s] = v
96
+ end
97
+
98
+ self
99
+ end
100
+
101
+ # Return a deep copy of the current Hanami::Utils::Hash
102
+ #
103
+ # @return [Hash] a deep duplicated self
104
+ #
105
+ # @since 0.3.1
106
+ #
107
+ # @example
108
+ # require 'hanami/utils/hash'
109
+ #
110
+ # hash = Hanami::Utils::Hash.new(
111
+ # 'nil' => nil,
112
+ # 'false' => false,
113
+ # 'true' => true,
114
+ # 'symbol' => :foo,
115
+ # 'fixnum' => 23,
116
+ # 'bignum' => 13289301283 ** 2,
117
+ # 'float' => 1.0,
118
+ # 'complex' => Complex(0.3),
119
+ # 'bigdecimal' => BigDecimal.new('12.0001'),
120
+ # 'rational' => Rational(0.3),
121
+ # 'string' => 'foo bar',
122
+ # 'hash' => { a: 1, b: 'two', c: :three },
123
+ # 'u_hash' => Hanami::Utils::Hash.new({ a: 1, b: 'two', c: :three })
124
+ # )
125
+ #
126
+ # duped = hash.deep_dup
127
+ #
128
+ # hash.class # => Hanami::Utils::Hash
129
+ # duped.class # => Hanami::Utils::Hash
130
+ #
131
+ # hash.object_id # => 70147385937100
132
+ # duped.object_id # => 70147385950620
133
+ #
134
+ # # unduplicated values
135
+ # duped['nil'] # => nil
136
+ # duped['false'] # => false
137
+ # duped['true'] # => true
138
+ # duped['symbol'] # => :foo
139
+ # duped['fixnum'] # => 23
140
+ # duped['bignum'] # => 176605528590345446089
141
+ # duped['float'] # => 1.0
142
+ # duped['complex'] # => (0.3+0i)
143
+ # duped['bigdecimal'] # => #<BigDecimal:7f9ffe6e2fd0,'0.120001E2',18(18)>
144
+ # duped['rational'] # => 5404319552844595/18014398509481984)
145
+ #
146
+ # # it duplicates values
147
+ # duped['string'].reverse!
148
+ # duped['string'] # => "rab oof"
149
+ # hash['string'] # => "foo bar"
150
+ #
151
+ # # it deeply duplicates Hash, by preserving the class
152
+ # duped['hash'].class # => Hash
153
+ # duped['hash'].delete(:a)
154
+ # hash['hash'][:a] # => 1
155
+ #
156
+ # duped['hash'][:b].upcase!
157
+ # duped['hash'][:b] # => "TWO"
158
+ # hash['hash'][:b] # => "two"
159
+ #
160
+ # # it deeply duplicates Hanami::Utils::Hash, by preserving the class
161
+ # duped['u_hash'].class # => Hanami::Utils::Hash
162
+ def deep_dup
163
+ Hash.new.tap do |result|
164
+ @hash.each {|k, v| result[k] = Duplicable.dup(v, &DUPLICATE_LOGIC) }
165
+ end
166
+ end
167
+
168
+ # Returns a new array populated with the keys from this hash
169
+ #
170
+ # @return [Array] the keys
171
+ #
172
+ # @since 0.3.0
173
+ #
174
+ # @see http://www.ruby-doc.org/core/Hash.html#method-i-keys
175
+ def keys
176
+ @hash.keys
177
+ end
178
+
179
+ # Deletes the key-value pair and returns the value from hsh whose key is
180
+ # equal to key.
181
+ #
182
+ # @param key [Object] the key to remove
183
+ #
184
+ # @return [Object,nil] the value hold by the given key, if present
185
+ #
186
+ # @since 0.3.0
187
+ #
188
+ # @see http://www.ruby-doc.org/core/Hash.html#method-i-keys
189
+ def delete(key)
190
+ @hash.delete(key)
191
+ end
192
+
193
+ # Retrieves the value object corresponding to the key object.
194
+ #
195
+ # @param key [Object] the key
196
+ #
197
+ # @return [Object,nil] the correspoding value, if present
198
+ #
199
+ # @since 0.3.0
200
+ #
201
+ # @see http://www.ruby-doc.org/core/Hash.html#method-i-5B-5D
202
+ def [](key)
203
+ @hash[key]
204
+ end
205
+
206
+ # Associates the value given by value with the key given by key.
207
+ #
208
+ # @param key [Object] the key to assign
209
+ # @param value [Object] the value to assign
210
+ #
211
+ # @since 0.3.0
212
+ #
213
+ # @see http://www.ruby-doc.org/core/Hash.html#method-i-5B-5D-3D
214
+ def []=(key, value)
215
+ @hash[key] = value
216
+ end
217
+
218
+ # Returns a Ruby Hash as duplicated version of self
219
+ #
220
+ # @return [::Hash] the hash
221
+ #
222
+ # @since 0.3.0
223
+ #
224
+ # @see http://www.ruby-doc.org/core/Hash.html#method-i-to_h
225
+ def to_h
226
+ @hash.each_with_object({}) do |(k, v), result|
227
+ v = v.to_h if v.is_a?(self.class)
228
+ result[k] = v
229
+ end
230
+ end
231
+
232
+ alias_method :to_hash, :to_h
233
+
234
+ # Converts into a nested array of [ key, value ] arrays.
235
+ #
236
+ # @return [::Array] the array
237
+ #
238
+ # @since 0.3.0
239
+ #
240
+ # @see http://www.ruby-doc.org/core/Hash.html#method-i-to_a
241
+ def to_a
242
+ @hash.to_a
243
+ end
244
+
245
+ # Equality
246
+ #
247
+ # @return [TrueClass,FalseClass]
248
+ #
249
+ # @since 0.3.0
250
+ def ==(other)
251
+ @hash == other.to_h
252
+ end
253
+
254
+ alias_method :eql?, :==
255
+
256
+ # Returns the hash of the internal @hash
257
+ #
258
+ # @return [Fixnum]
259
+ #
260
+ # @since 0.3.0
261
+ def hash
262
+ @hash.hash
263
+ end
264
+
265
+ # Returns a string describing the internal @hash
266
+ #
267
+ # @return [String]
268
+ #
269
+ # @since 0.3.0
270
+ def inspect
271
+ @hash.inspect
272
+ end
273
+
274
+ # Override Ruby's method_missing in order to provide ::Hash interface
275
+ #
276
+ # @api private
277
+ # @since 0.3.0
278
+ #
279
+ # @raise [NoMethodError] If doesn't respond to the given method
280
+ def method_missing(m, *args, &blk)
281
+ if respond_to?(m)
282
+ h = @hash.__send__(m, *args, &blk)
283
+ h = self.class.new(h) if h.is_a?(::Hash)
284
+ h
285
+ else
286
+ raise NoMethodError.new(%(undefined method `#{ m }' for #{ @hash }:#{ self.class }))
287
+ end
288
+ end
289
+
290
+ # Override Ruby's respond_to_missing? in order to support ::Hash interface
291
+ #
292
+ # @api private
293
+ # @since 0.3.0
294
+ def respond_to_missing?(m, include_private=false)
295
+ @hash.respond_to?(m, include_private)
296
+ end
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,439 @@
1
+ require 'hanami/utils/class_attribute'
2
+
3
+ module Hanami
4
+ module Utils
5
+ # String inflector
6
+ #
7
+ # @since 0.4.1
8
+ module Inflector
9
+ # Rules for irregular plurals
10
+ #
11
+ # @since 0.6.0
12
+ # @api private
13
+ class IrregularRules
14
+ # @since 0.6.0
15
+ # @api private
16
+ def initialize(rules)
17
+ @rules = rules
18
+ end
19
+
20
+ # @since 0.6.0
21
+ # @api private
22
+ def add(key, value)
23
+ @rules[key.downcase] = value.downcase
24
+ end
25
+
26
+ # @since 0.6.0
27
+ # @api private
28
+ def ===(other)
29
+ key = other.downcase
30
+ @rules.key?(key) || @rules.value?(key)
31
+ end
32
+
33
+ # @since 0.6.0
34
+ # @api private
35
+ def apply(string)
36
+ key = string.downcase
37
+ result = @rules[key] || @rules.rassoc(key).last
38
+
39
+ string[0] + result[1..-1]
40
+ end
41
+ end
42
+
43
+ # Matcher for blank strings
44
+ #
45
+ # @since 0.4.1
46
+ # @api private
47
+ BLANK_STRING_MATCHER = /\A[[:space:]]*\z/.freeze
48
+
49
+ # @since 0.4.1
50
+ # @api private
51
+ A = 'a'.freeze
52
+
53
+ # @since 0.4.1
54
+ # @api private
55
+ CH = 'ch'.freeze
56
+
57
+ # @since 0.4.1
58
+ # @api private
59
+ CHES = 'ches'.freeze
60
+
61
+ # @since 0.4.1
62
+ # @api private
63
+ EAUX = 'eaux'.freeze
64
+
65
+ # @since 0.6.0
66
+ # @api private
67
+ ES = 'es'.freeze
68
+
69
+ # @since 0.4.1
70
+ # @api private
71
+ F = 'f'.freeze
72
+
73
+ # @since 0.4.1
74
+ # @api private
75
+ I = 'i'.freeze
76
+
77
+ # @since 0.4.1
78
+ # @api private
79
+ ICE = 'ice'.freeze
80
+
81
+ # @since 0.4.1
82
+ # @api private
83
+ ICES = 'ices'.freeze
84
+
85
+ # @since 0.4.1
86
+ # @api private
87
+ IDES = 'ides'.freeze
88
+
89
+ # @since 0.4.1
90
+ # @api private
91
+ IES = 'ies'.freeze
92
+
93
+ # @since 0.4.1
94
+ # @api private
95
+ IFE = 'ife'.freeze
96
+
97
+ # @since 0.4.1
98
+ # @api private
99
+ INA = 'ina'.freeze
100
+
101
+ # @since 0.4.1
102
+ # @api private
103
+ IS = 'is'.freeze
104
+
105
+ # @since 0.4.1
106
+ # @api private
107
+ IVES = 'ives'.freeze
108
+
109
+ # @since 0.4.1
110
+ # @api private
111
+ MA = 'ma'.freeze
112
+
113
+ # @since 0.4.1
114
+ # @api private
115
+ MATA = 'mata'.freeze
116
+
117
+ # @since 0.4.1
118
+ # @api private
119
+ MEN = 'men'.freeze
120
+
121
+ # @since 0.4.1
122
+ # @api private
123
+ MINA = 'mina'.freeze
124
+
125
+ # @since 0.6.0
126
+ # @api private
127
+ NA = 'na'.freeze
128
+
129
+ # @since 0.6.0
130
+ # @api private
131
+ NON = 'non'.freeze
132
+
133
+ # @since 0.4.1
134
+ # @api private
135
+ O = 'o'.freeze
136
+
137
+ # @since 0.4.1
138
+ # @api private
139
+ OES = 'oes'.freeze
140
+
141
+ # @since 0.4.1
142
+ # @api private
143
+ OUSE = 'ouse'.freeze
144
+
145
+ # @since 0.4.1
146
+ # @api private
147
+ S = 's'.freeze
148
+
149
+ # @since 0.4.1
150
+ # @api private
151
+ SES = 'ses'.freeze
152
+
153
+ # @since 0.4.1
154
+ # @api private
155
+ SSES = 'sses'.freeze
156
+
157
+ # @since 0.6.0
158
+ # @api private
159
+ TA = 'ta'.freeze
160
+
161
+ # @since 0.4.1
162
+ # @api private
163
+ UM = 'um'.freeze
164
+
165
+ # @since 0.4.1
166
+ # @api private
167
+ US = 'us'.freeze
168
+
169
+ # @since 0.4.1
170
+ # @api private
171
+ USES = 'uses'.freeze
172
+
173
+ # @since 0.4.1
174
+ # @api private
175
+ VES = 'ves'.freeze
176
+
177
+ # @since 0.4.1
178
+ # @api private
179
+ X = 'x'.freeze
180
+
181
+ # @since 0.4.1
182
+ # @api private
183
+ XES = 'xes'.freeze
184
+
185
+ # @since 0.4.1
186
+ # @api private
187
+ Y = 'y'.freeze
188
+
189
+ include Utils::ClassAttribute
190
+
191
+ # Irregular rules for plurals
192
+ #
193
+ # @since 0.6.0
194
+ # @api private
195
+ class_attribute :plurals
196
+ self.plurals = IrregularRules.new({
197
+ # irregular
198
+ 'cactus' => 'cacti',
199
+ 'child' => 'children',
200
+ 'corpus' => 'corpora',
201
+ 'foot' => 'feet',
202
+ 'genus' => 'genera',
203
+ 'goose' => 'geese',
204
+ 'man' => 'men',
205
+ 'ox' => 'oxen',
206
+ 'person' => 'people',
207
+ 'quiz' => 'quizzes',
208
+ 'sex' => 'sexes',
209
+ 'testis' => 'testes',
210
+ 'tooth' => 'teeth',
211
+ 'woman' => 'women',
212
+ # uncountable
213
+ 'deer' => 'deer',
214
+ 'equipment' => 'equipment',
215
+ 'fish' => 'fish',
216
+ 'information' => 'information',
217
+ 'means' => 'means',
218
+ 'money' => 'money',
219
+ 'news' => 'news',
220
+ 'offspring' => 'offspring',
221
+ 'rice' => 'rice',
222
+ 'series' => 'series',
223
+ 'sheep' => 'sheep',
224
+ 'species' => 'species',
225
+ })
226
+
227
+ # Irregular rules for singulars
228
+ #
229
+ # @since 0.6.0
230
+ # @api private
231
+ class_attribute :singulars
232
+ self.singulars = IrregularRules.new({
233
+ # irregular
234
+ 'cacti' => 'cactus',
235
+ 'children'=> 'child',
236
+ 'corpora' => 'corpus',
237
+ 'feet' => 'foot',
238
+ 'genera' => 'genus',
239
+ 'geese' => 'goose',
240
+ 'men' => 'man',
241
+ 'oxen' => 'ox',
242
+ 'people' => 'person',
243
+ 'quizzes' => 'quiz',
244
+ 'sexes' => 'sex',
245
+ 'testes' => 'testis',
246
+ 'teeth' => 'tooth',
247
+ 'women' => 'woman',
248
+ # uncountable
249
+ 'deer' => 'deer',
250
+ 'equipment' => 'equipment',
251
+ 'fish' => 'fish',
252
+ 'information' => 'information',
253
+ 'means' => 'means',
254
+ 'money' => 'money',
255
+ 'news' => 'news',
256
+ 'offspring' => 'offspring',
257
+ 'rice' => 'rice',
258
+ 'series' => 'series',
259
+ 'sheep' => 'sheep',
260
+ 'species' => 'species',
261
+ 'police' => 'police',
262
+ # fallback
263
+ 'hives' => 'hive',
264
+ 'horses' => 'horse',
265
+ })
266
+
267
+ # Block for custom inflection rules.
268
+ #
269
+ # @param [Proc] blk custom inflections
270
+ #
271
+ # @since 0.6.0
272
+ #
273
+ # @see Hanami::Utils::Inflector.exception
274
+ # @see Hanami::Utils::Inflector.uncountable
275
+ #
276
+ # @example
277
+ # require 'hanami/utils/inflector'
278
+ #
279
+ # Hanami::Utils::Inflector.inflections do
280
+ # exception 'analysis', 'analyses'
281
+ # exception 'alga', 'algae'
282
+ # uncountable 'music', 'butter'
283
+ # end
284
+ def self.inflections(&blk)
285
+ class_eval(&blk)
286
+ end
287
+
288
+ # Add a custom inflection exception
289
+ #
290
+ # @param [String] singular form
291
+ # @param [String] plural form
292
+ #
293
+ # @since 0.6.0
294
+ #
295
+ # @see Hanami::Utils::Inflector.inflections
296
+ # @see Hanami::Utils::Inflector.uncountable
297
+ #
298
+ # @example
299
+ # require 'hanami/utils/inflector'
300
+ #
301
+ # Hanami::Utils::Inflector.inflections do
302
+ # exception 'alga', 'algae'
303
+ # end
304
+ def self.exception(singular, plural)
305
+ singulars.add(plural, singular)
306
+ plurals.add(singular, plural)
307
+ end
308
+
309
+ # Add an uncountable word
310
+ #
311
+ # @param [Array<String>] words
312
+ #
313
+ # @since 0.6.0
314
+ #
315
+ # @see Hanami::Utils::Inflector.inflections
316
+ # @see Hanami::Utils::Inflector.exception
317
+ #
318
+ # @example
319
+ # require 'hanami/utils/inflector'
320
+ #
321
+ # Hanami::Utils::Inflector.inflections do
322
+ # uncountable 'music', 'art'
323
+ # end
324
+ def self.uncountable(*words)
325
+ Array(words).each do |word|
326
+ exception(word, word)
327
+ end
328
+ end
329
+
330
+ # Pluralize the given string
331
+ #
332
+ # @param string [String] a string to pluralize
333
+ #
334
+ # @return [String,NilClass] the pluralized string, if present
335
+ #
336
+ # @api private
337
+ # @since 0.4.1
338
+ def self.pluralize(string)
339
+ return string if string.nil? || string.match(BLANK_STRING_MATCHER)
340
+
341
+ case string
342
+ when plurals
343
+ plurals.apply(string)
344
+ when /\A((.*)[^aeiou])ch\z/
345
+ $1 + CHES
346
+ when /\A((.*)[^aeiou])y\z/
347
+ $1 + IES
348
+ when /\A(.*)(ex|ix)\z/
349
+ $1 + ICES
350
+ when /\A(.*)(eau|#{ EAUX })\z/
351
+ $1 + EAUX
352
+ when /\A(.*)x\z/
353
+ $1 + XES
354
+ when /\A(.*)ma\z/
355
+ string + TA
356
+ when /\A(.*)(um|#{ A })\z/
357
+ $1 + A
358
+ when /\A(.*)(ouse|#{ ICE })\z/
359
+ $1 + ICE
360
+ when /\A(buffal|domin|ech|embarg|her|mosquit|potat|tomat)#{ O }\z/i
361
+ $1 + OES
362
+ when /\A(.*)(en|#{ INA })\z/
363
+ $1 + INA
364
+ when /\A(.*)(?:([^f]))f[e]*\z/
365
+ $1 + $2 + VES
366
+ when /\A(.*)us\z/
367
+ $1 + USES
368
+ when /\A(.*)non\z/
369
+ $1 + NA
370
+ when /\A((.*)[^aeiou])is\z/
371
+ $1 + ES
372
+ when /\A(.*)ss\z/
373
+ $1 + SSES
374
+ when /s\z/
375
+ string
376
+ else
377
+ string + S
378
+ end
379
+ end
380
+
381
+ # Singularize the given string
382
+ #
383
+ # @param string [String] a string to singularize
384
+ #
385
+ # @return [String,NilClass] the singularized string, if present
386
+ #
387
+ # @api private
388
+ # @since 0.4.1
389
+ def self.singularize(string)
390
+ return string if string.nil? || string.match(BLANK_STRING_MATCHER)
391
+
392
+ case string
393
+ when singulars
394
+ singulars.apply(string)
395
+ when /\A.*[^aeiou]#{CHES}\z/
396
+ string.sub(CHES, CH)
397
+ when /\A.*[^aeiou]#{IES}\z/
398
+ string.sub(IES, Y)
399
+ when /\A(.*)#{ICE}\z/
400
+ $1 + OUSE
401
+ when /\A.*#{EAUX}\z/
402
+ string.chop
403
+ when /\A(.*)#{IDES}\z/
404
+ $1 + IS
405
+ when /\A(.*)#{US}\z/
406
+ $1 + I
407
+ when /\A(.*)#{SES}\z/
408
+ $1 + S
409
+ when /\A(.*)#{OUSE}\z/
410
+ $1 + ICE
411
+ when /\A(.*)#{MATA}\z/
412
+ $1 + MA
413
+ when /\A(.*)#{OES}\z/
414
+ $1 + O
415
+ when /\A(.*)#{MINA}\z/
416
+ $1 + MEN
417
+ when /\A(.*)#{XES}\z/
418
+ $1 + X
419
+ when /\A(.*)#{IVES}\z/
420
+ $1 + IFE
421
+ when /\A(.*)#{VES}\z/
422
+ $1 + F
423
+ when /\A(.*)#{I}\z/
424
+ $1 + US
425
+ when /\A(.*)ae\z/
426
+ $1 + A
427
+ when /\A(.*)na\z/
428
+ $1 + NON
429
+ when /\A(.*)#{A}\z/
430
+ $1 + UM
431
+ when /[^s]\z/
432
+ string
433
+ else
434
+ string.chop
435
+ end
436
+ end
437
+ end
438
+ end
439
+ end