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.
- 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
|