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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +183 -0
- data/LICENSE.md +22 -0
- data/README.md +106 -7
- data/hanami-utils.gemspec +15 -13
- data/lib/hanami-utils.rb +1 -0
- data/lib/hanami/interactor.rb +497 -0
- data/lib/hanami/logger.rb +141 -0
- data/lib/hanami/utils.rb +31 -2
- data/lib/hanami/utils/attributes.rb +132 -0
- data/lib/hanami/utils/basic_object.rb +53 -0
- data/lib/hanami/utils/callbacks.rb +286 -0
- data/lib/hanami/utils/class.rb +94 -0
- data/lib/hanami/utils/class_attribute.rb +95 -0
- data/lib/hanami/utils/deprecation.rb +70 -0
- data/lib/hanami/utils/duplicable.rb +80 -0
- data/lib/hanami/utils/escape.rb +577 -0
- data/lib/hanami/utils/hash.rb +299 -0
- data/lib/hanami/utils/inflector.rb +439 -0
- data/lib/hanami/utils/io.rb +37 -0
- data/lib/hanami/utils/kernel.rb +1031 -0
- data/lib/hanami/utils/load_paths.rb +167 -0
- data/lib/hanami/utils/path_prefix.rb +146 -0
- data/lib/hanami/utils/string.rb +419 -0
- data/lib/hanami/utils/version.rb +4 -1
- metadata +51 -16
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- data/bin/setup +0 -8
@@ -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
|