hanami-utils 0.0.0 → 0.7.0

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