dcparker-shopify 0.1.9 → 0.2.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,366 @@
1
+ module Extlib # :nodoc:all
2
+ #
3
+ # TODO: Write more documentation!
4
+ #
5
+ # Overview
6
+ # ========
7
+ #
8
+ # The Hook module is a very simple set of AOP helpers. Basically, it
9
+ # allows the developer to specify a method or block that should run
10
+ # before or after another method.
11
+ #
12
+ # Usage
13
+ # =====
14
+ #
15
+ # Halting The Hook Stack
16
+ #
17
+ # Inheritance
18
+ #
19
+ # Other Goodies
20
+ #
21
+ # Please bring up any issues regarding Hooks with carllerche on IRC
22
+ #
23
+ module Hook
24
+
25
+ def self.included(base)
26
+ base.extend(ClassMethods)
27
+ base.const_set("CLASS_HOOKS", {}) unless base.const_defined?("CLASS_HOOKS")
28
+ base.const_set("INSTANCE_HOOKS", {}) unless base.const_defined?("INSTANCE_HOOKS")
29
+ base.class_eval do
30
+ class << self
31
+ def method_added(name)
32
+ process_method_added(name, :instance)
33
+ end
34
+
35
+ def singleton_method_added(name)
36
+ process_method_added(name, :class)
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ module ClassMethods
43
+ include Extlib::Assertions
44
+ # Inject code that executes before the target class method.
45
+ #
46
+ # @param target_method<Symbol> the name of the class method to inject before
47
+ # @param method_sym<Symbol> the name of the method to run before the
48
+ # target_method
49
+ # @param block<Block> the code to run before the target_method
50
+ #
51
+ # @note
52
+ # Either method_sym or block is required.
53
+ # -
54
+ # @api public
55
+ def before_class_method(target_method, method_sym = nil, &block)
56
+ install_hook :before, target_method, method_sym, :class, &block
57
+ end
58
+
59
+ #
60
+ # Inject code that executes after the target class method.
61
+ #
62
+ # @param target_method<Symbol> the name of the class method to inject after
63
+ # @param method_sym<Symbol> the name of the method to run after the target_method
64
+ # @param block<Block> the code to run after the target_method
65
+ #
66
+ # @note
67
+ # Either method_sym or block is required.
68
+ # -
69
+ # @api public
70
+ def after_class_method(target_method, method_sym = nil, &block)
71
+ install_hook :after, target_method, method_sym, :class, &block
72
+ end
73
+
74
+ #
75
+ # Inject code that executes before the target instance method.
76
+ #
77
+ # @param target_method<Symbol> the name of the instance method to inject before
78
+ # @param method_sym<Symbol> the name of the method to run before the
79
+ # target_method
80
+ # @param block<Block> the code to run before the target_method
81
+ #
82
+ # @note
83
+ # Either method_sym or block is required.
84
+ # -
85
+ # @api public
86
+ def before(target_method, method_sym = nil, &block)
87
+ install_hook :before, target_method, method_sym, :instance, &block
88
+ end
89
+
90
+ #
91
+ # Inject code that executes after the target instance method.
92
+ #
93
+ # @param target_method<Symbol> the name of the instance method to inject after
94
+ # @param method_sym<Symbol> the name of the method to run after the
95
+ # target_method
96
+ # @param block<Block> the code to run after the target_method
97
+ #
98
+ # @note
99
+ # Either method_sym or block is required.
100
+ # -
101
+ # @api public
102
+ def after(target_method, method_sym = nil, &block)
103
+ install_hook :after, target_method, method_sym, :instance, &block
104
+ end
105
+
106
+ # Register a class method as hookable. Registering a method means that
107
+ # before hooks will be run immediately before the method is invoked and
108
+ # after hooks will be called immediately after the method is invoked.
109
+ #
110
+ # @param hookable_method<Symbol> The name of the class method that should
111
+ # be hookable
112
+ # -
113
+ # @api public
114
+ def register_class_hooks(*hooks)
115
+ hooks.each { |hook| register_hook(hook, :class) }
116
+ end
117
+
118
+ # Register aninstance method as hookable. Registering a method means that
119
+ # before hooks will be run immediately before the method is invoked and
120
+ # after hooks will be called immediately after the method is invoked.
121
+ #
122
+ # @param hookable_method<Symbol> The name of the instance method that should
123
+ # be hookable
124
+ # -
125
+ # @api public
126
+ def register_instance_hooks(*hooks)
127
+ hooks.each { |hook| register_hook(hook, :instance) }
128
+ end
129
+
130
+ # Not yet implemented
131
+ def reset_hook!(target_method, scope)
132
+ raise NotImplementedError
133
+ end
134
+
135
+ # --- Alright kids... the rest is internal stuff ---
136
+
137
+ # Returns the correct HOOKS Hash depending on whether we are
138
+ # working with class methods or instance methods
139
+ def hooks_with_scope(scope)
140
+ case scope
141
+ when :class then class_hooks
142
+ when :instance then instance_hooks
143
+ else raise ArgumentError, 'You need to pass :class or :instance as scope'
144
+ end
145
+ end
146
+
147
+ def class_hooks
148
+ self.const_get("CLASS_HOOKS")
149
+ end
150
+
151
+ def instance_hooks
152
+ self.const_get("INSTANCE_HOOKS")
153
+ end
154
+
155
+ # Registers a method as hookable. Registering hooks involves the following
156
+ # process
157
+ #
158
+ # * Create a blank entry in the HOOK Hash for the method.
159
+ # * Define the methods that execute the before and after hook stack.
160
+ # These methods will be no-ops at first, but everytime a new hook is
161
+ # defined, the methods will be redefined to incorporate the new hook.
162
+ # * Redefine the method that is to be hookable so that the hook stacks
163
+ # are invoked approprietly.
164
+ def register_hook(target_method, scope)
165
+ if scope == :instance && !method_defined?(target_method)
166
+ raise ArgumentError, "#{target_method} instance method does not exist"
167
+ elsif scope == :class && !respond_to?(target_method)
168
+ raise ArgumentError, "#{target_method} class method does not exist"
169
+ end
170
+
171
+ hooks = hooks_with_scope(scope)
172
+
173
+ if hooks[target_method].nil?
174
+ hooks[target_method] = {
175
+ # We need to keep track of which class in the Inheritance chain the
176
+ # method was declared hookable in. Every time a child declares a new
177
+ # hook for the method, the hook stack invocations need to be redefined
178
+ # in the original Class. See #define_hook_stack_execution_methods
179
+ :before => [], :after => [], :in => self
180
+ }
181
+
182
+ define_hook_stack_execution_methods(target_method, scope)
183
+ define_advised_method(target_method, scope)
184
+ end
185
+ end
186
+
187
+ # Is the method registered as a hookable in the given scope.
188
+ def registered_as_hook?(target_method, scope)
189
+ ! hooks_with_scope(scope)[target_method].nil?
190
+ end
191
+
192
+ # Generates names for the various utility methods. We need to do this because
193
+ # the various utility methods should not end in = so, while we're at it, we
194
+ # might as well get rid of all punctuation.
195
+ def hook_method_name(target_method, prefix, suffix)
196
+ target_method = target_method.to_s
197
+
198
+ case target_method[-1,1]
199
+ when '?' then "#{prefix}_#{target_method[0..-2]}_ques_#{suffix}"
200
+ when '!' then "#{prefix}_#{target_method[0..-2]}_bang_#{suffix}"
201
+ when '=' then "#{prefix}_#{target_method[0..-2]}_eq_#{suffix}"
202
+ # I add a _nan_ suffix here so that we don't ever encounter
203
+ # any naming conflicts.
204
+ else "#{prefix}_#{target_method[0..-1]}_nan_#{suffix}"
205
+ end
206
+ end
207
+
208
+ # This will need to be refactored
209
+ def process_method_added(method_name, scope)
210
+ hooks_with_scope(scope).each do |target_method, hooks|
211
+ if hooks[:before].any? { |hook| hook[:name] == method_name }
212
+ define_hook_stack_execution_methods(target_method, scope)
213
+ end
214
+
215
+ if hooks[:after].any? { |hook| hook[:name] == method_name }
216
+ define_hook_stack_execution_methods(target_method, scope)
217
+ end
218
+ end
219
+ end
220
+
221
+ # Defines two methods. One method executes the before hook stack. The other executes
222
+ # the after hook stack. This method will be called many times during the Class definition
223
+ # process. It should be called for each hook that is defined. It will also be called
224
+ # when a hook is redefined (to make sure that the arity hasn't changed).
225
+ def define_hook_stack_execution_methods(target_method, scope)
226
+ unless registered_as_hook?(target_method, scope)
227
+ raise ArgumentError, "#{target_method} has not be registered as a hookable #{scope} method"
228
+ end
229
+
230
+ hooks = hooks_with_scope(scope)
231
+
232
+ before_hooks = hooks[target_method][:before]
233
+ before_hooks = before_hooks.map{ |info| inline_call(info, scope) }.join("\n")
234
+
235
+ after_hooks = hooks[target_method][:after]
236
+ after_hooks = after_hooks.map{ |info| inline_call(info, scope) }.join("\n")
237
+
238
+ source = %{
239
+ private
240
+
241
+ def #{hook_method_name(target_method, 'execute_before', 'hook_stack')}(*args)
242
+ #{before_hooks}
243
+ end
244
+
245
+ def #{hook_method_name(target_method, 'execute_after', 'hook_stack')}(*args)
246
+ #{after_hooks}
247
+ end
248
+ }
249
+
250
+ source = %{class << self\n#{source}\nend} if scope == :class
251
+
252
+ hooks[target_method][:in].class_eval(source, __FILE__, __LINE__)
253
+ end
254
+
255
+ # Returns ruby code that will invoke the hook. It checks the arity of the hook method
256
+ # and passes arguments accordingly.
257
+ def inline_call(method_info, scope)
258
+ name = method_info[:name]
259
+
260
+ if scope == :instance
261
+ args = method_defined?(name) && instance_method(name).arity != 0 ? '*args' : ''
262
+ %(#{name}(#{args}) if self.class <= ObjectSpace._id2ref(#{method_info[:from].object_id}))
263
+ else
264
+ args = respond_to?(name) && method(name).arity != 0 ? '*args' : ''
265
+ %(#{name}(#{args}) if self <= ObjectSpace._id2ref(#{method_info[:from].object_id}))
266
+ end
267
+ end
268
+
269
+ def define_advised_method(target_method, scope)
270
+ args = args_for(method_with_scope(target_method, scope))
271
+
272
+ renamed_target = hook_method_name(target_method, 'hookable_', 'before_advised')
273
+
274
+ source = <<-EOD
275
+ def #{target_method}(#{args})
276
+ retval = nil
277
+ catch(:halt) do
278
+ #{hook_method_name(target_method, 'execute_before', 'hook_stack')}(#{args})
279
+ retval = #{renamed_target}(#{args})
280
+ #{hook_method_name(target_method, 'execute_after', 'hook_stack')}(retval, #{args})
281
+ retval
282
+ end
283
+ end
284
+ EOD
285
+
286
+ if scope == :instance && !instance_methods(false).include?(target_method.to_s)
287
+ send(:alias_method, renamed_target, target_method)
288
+
289
+ proxy_module = Module.new
290
+ proxy_module.class_eval(source, __FILE__, __LINE__)
291
+ self.send(:include, proxy_module)
292
+ else
293
+ source = %{alias_method :#{renamed_target}, :#{target_method}\n#{source}}
294
+ source = %{class << self\n#{source}\nend} if scope == :class
295
+ class_eval(source, __FILE__, __LINE__)
296
+ end
297
+ end
298
+
299
+ # --- Add a hook ---
300
+
301
+ def install_hook(type, target_method, method_sym, scope, &block)
302
+ assert_kind_of 'target_method', target_method, Symbol
303
+ assert_kind_of 'method_sym', method_sym, Symbol unless method_sym.nil?
304
+ assert_kind_of 'scope', scope, Symbol
305
+
306
+ if !block_given? and method_sym.nil?
307
+ raise ArgumentError, "You need to pass 2 arguments to \"#{type}\"."
308
+ end
309
+
310
+ if method_sym.to_s[-1,1] == '='
311
+ raise ArgumentError, "Methods ending in = cannot be hooks"
312
+ end
313
+
314
+ unless [ :class, :instance ].include?(scope)
315
+ raise ArgumentError, 'You need to pass :class or :instance as scope'
316
+ end
317
+
318
+ register_hook(target_method, scope) unless registered_as_hook?(target_method, scope)
319
+
320
+ hooks = hooks_with_scope(scope)
321
+
322
+ if block
323
+ method_sym = "__hooks_#{type}_#{quote_method(target_method)}_#{hooks[target_method][type].length}".to_sym
324
+ if scope == :class
325
+ (class << self; self; end;).instance_eval do
326
+ define_method(method_sym, &block)
327
+ end
328
+ else
329
+ define_method(method_sym, &block)
330
+ end
331
+ end
332
+
333
+ # Adds method to the stack an redefines the hook invocation method
334
+ hooks[target_method][type] << { :name => method_sym, :from => self }
335
+ define_hook_stack_execution_methods(target_method, scope)
336
+ end
337
+
338
+ # --- Helpers ---
339
+
340
+ def args_for(method)
341
+ if method.arity == 0
342
+ "&block"
343
+ elsif method.arity > 0
344
+ "_" << (1 .. method.arity).to_a.join(", _") << ", &block"
345
+ elsif (method.arity + 1) < 0
346
+ "_" << (1 .. (method.arity).abs - 1).to_a.join(", _") << ", *args, &block"
347
+ else
348
+ "*args, &block"
349
+ end
350
+ end
351
+
352
+ def method_with_scope(name, scope)
353
+ case scope
354
+ when :class then method(name)
355
+ when :instance then instance_method(name)
356
+ else raise ArgumentError, 'You need to pass :class or :instance as scope'
357
+ end
358
+ end
359
+
360
+ def quote_method(name)
361
+ name.to_s.gsub(/\?$/, '_q_').gsub(/!$/, '_b_').gsub(/=$/, '_eq_')
362
+ end
363
+ end
364
+
365
+ end
366
+ end
@@ -0,0 +1,436 @@
1
+ module Extlib # :nodoc:all
2
+
3
+ # = English Nouns Number Inflection.
4
+ #
5
+ # This module provides english singular <-> plural noun inflections.
6
+ module Inflection
7
+
8
+ class << self
9
+ # Take an underscored name and make it into a camelized name
10
+ #
11
+ # @example
12
+ # "egg_and_hams".classify #=> "EggAndHam"
13
+ # "post".classify #=> "Post"
14
+ #
15
+ def classify(name)
16
+ camelize(singularize(name.to_s.sub(/.*\./, '')))
17
+ end
18
+
19
+ # By default, camelize converts strings to UpperCamelCase.
20
+ #
21
+ # camelize will also convert '/' to '::' which is useful for converting paths to namespaces
22
+ #
23
+ # @example
24
+ # "active_record".camelize #=> "ActiveRecord"
25
+ # "active_record/errors".camelize #=> "ActiveRecord::Errors"
26
+ #
27
+ def camelize(lower_case_and_underscored_word, *args)
28
+ lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
29
+ end
30
+
31
+
32
+ # The reverse of +camelize+. Makes an underscored form from the expression in the string.
33
+ #
34
+ # Changes '::' to '/' to convert namespaces to paths.
35
+ #
36
+ # @example
37
+ # "ActiveRecord".underscore #=> "active_record"
38
+ # "ActiveRecord::Errors".underscore #=> active_record/errors
39
+ #
40
+ def underscore(camel_cased_word)
41
+ camel_cased_word.to_const_path
42
+ end
43
+
44
+ # Capitalizes the first word and turns underscores into spaces and strips _id.
45
+ # Like titleize, this is meant for creating pretty output.
46
+ #
47
+ # @example
48
+ # "employee_salary" #=> "Employee salary"
49
+ # "author_id" #=> "Author"
50
+ def humanize(lower_case_and_underscored_word)
51
+ lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
52
+ end
53
+
54
+ # Removes the module part from the expression in the string
55
+ #
56
+ # @example
57
+ # "ActiveRecord::CoreExtensions::String::Inflections".demodulize #=> "Inflections"
58
+ # "Inflections".demodulize #=> "Inflections"
59
+ def demodulize(class_name_in_module)
60
+ class_name_in_module.to_s.gsub(/^.*::/, '')
61
+ end
62
+
63
+ # Create the name of a table like Rails does for models to table names. This method
64
+ # uses the pluralize method on the last word in the string.
65
+ #
66
+ # @example
67
+ # "RawScaledScorer".tableize #=> "raw_scaled_scorers"
68
+ # "egg_and_ham".tableize #=> "egg_and_hams"
69
+ # "fancyCategory".tableize #=> "fancy_categories"
70
+ def tableize(class_name)
71
+ pluralize(class_name.to_const_path.gsub(/\//, '_'))
72
+ end
73
+
74
+ # Creates a foreign key name from a class name.
75
+ #
76
+ # @example
77
+ # "Message".foreign_key #=> "message_id"
78
+ # "Admin::Post".foreign_key #=> "post_id"
79
+ def foreign_key(class_name, key = "id")
80
+ underscore(demodulize(class_name.to_s)) << "_" << key.to_s
81
+ end
82
+
83
+ # Constantize tries to find a declared constant with the name specified
84
+ # in the string. It raises a NameError when the name is not in CamelCase
85
+ # or is not initialized.
86
+ #
87
+ # @example
88
+ # "Module".constantize #=> Module
89
+ # "Class".constantize #=> Class
90
+ def constantize(camel_cased_word)
91
+ unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ camel_cased_word
92
+ raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!"
93
+ end
94
+
95
+ Object.module_eval("::#{$1}", __FILE__, __LINE__)
96
+ end
97
+ end
98
+
99
+ @singular_of = {}
100
+ @plural_of = {}
101
+
102
+ @singular_rules = []
103
+ @plural_rules = []
104
+
105
+ class << self
106
+ # Defines a general inflection exception case.
107
+ #
108
+ # ==== Parameters
109
+ # singular<String>::
110
+ # singular form of the word
111
+ # plural<String>::
112
+ # plural form of the word
113
+ #
114
+ # ==== Examples
115
+ #
116
+ # Here we define erratum/errata exception case:
117
+ #
118
+ # English::Inflect.word "erratum", "errata"
119
+ #
120
+ # In case singular and plural forms are the same omit
121
+ # second argument on call:
122
+ #
123
+ # English::Inflect.word 'information'
124
+ def word(singular, plural=nil)
125
+ plural = singular unless plural
126
+ singular_word(singular, plural)
127
+ plural_word(singular, plural)
128
+ end
129
+
130
+ def clear(type = :all)
131
+ if type == :singular || type == :all
132
+ @singular_of = {}
133
+ @singular_rules = []
134
+ @singularization_rules, @singularization_regex = nil, nil
135
+ end
136
+ if type == :plural || type == :all
137
+ @singular_of = {}
138
+ @singular_rules = []
139
+ @singularization_rules, @singularization_regex = nil, nil
140
+ end
141
+ end
142
+
143
+
144
+ # Define a singularization exception.
145
+ #
146
+ # ==== Parameters
147
+ # singular<String>::
148
+ # singular form of the word
149
+ # plural<String>::
150
+ # plural form of the word
151
+ def singular_word(singular, plural)
152
+ @singular_of[plural] = singular
153
+ @singular_of[plural.capitalize] = singular.capitalize
154
+ end
155
+
156
+ # Define a pluralization exception.
157
+ #
158
+ # ==== Parameters
159
+ # singular<String>::
160
+ # singular form of the word
161
+ # plural<String>::
162
+ # plural form of the word
163
+ def plural_word(singular, plural)
164
+ @plural_of[singular] = plural
165
+ @plural_of[singular.capitalize] = plural.capitalize
166
+ end
167
+
168
+ # Define a general rule.
169
+ #
170
+ # ==== Parameters
171
+ # singular<String>::
172
+ # ending of the word in singular form
173
+ # plural<String>::
174
+ # ending of the word in plural form
175
+ # whole_word<Boolean>::
176
+ # for capitalization, since words can be
177
+ # capitalized (Man => Men) #
178
+ # ==== Examples
179
+ # Once the following rule is defined:
180
+ # English::Inflect.rule 'y', 'ies'
181
+ #
182
+ # You can see the following results:
183
+ # irb> "fly".plural
184
+ # => flies
185
+ # irb> "cry".plural
186
+ # => cries
187
+ # Define a general rule.
188
+
189
+ def rule(singular, plural, whole_word = false)
190
+ singular_rule(singular, plural)
191
+ plural_rule(singular, plural)
192
+ word(singular, plural) if whole_word
193
+ end
194
+
195
+ # Define a singularization rule.
196
+ #
197
+ # ==== Parameters
198
+ # singular<String>::
199
+ # ending of the word in singular form
200
+ # plural<String>::
201
+ # ending of the word in plural form
202
+ #
203
+ # ==== Examples
204
+ # Once the following rule is defined:
205
+ # English::Inflect.singular_rule 'o', 'oes'
206
+ #
207
+ # You can see the following results:
208
+ # irb> "heroes".singular
209
+ # => hero
210
+ def singular_rule(singular, plural)
211
+ @singular_rules << [singular, plural]
212
+ end
213
+
214
+ # Define a plurualization rule.
215
+ #
216
+ # ==== Parameters
217
+ # singular<String>::
218
+ # ending of the word in singular form
219
+ # plural<String>::
220
+ # ending of the word in plural form
221
+ #
222
+ # ==== Examples
223
+ # Once the following rule is defined:
224
+ # English::Inflect.singular_rule 'fe', 'ves'
225
+ #
226
+ # You can see the following results:
227
+ # irb> "wife".plural
228
+ # => wives
229
+ def plural_rule(singular, plural)
230
+ @plural_rules << [singular, plural]
231
+ end
232
+
233
+ # Read prepared singularization rules.
234
+ def singularization_rules
235
+ if defined?(@singularization_regex) && @singularization_regex
236
+ return [@singularization_regex, @singularization_hash]
237
+ end
238
+ # No sorting needed: Regexen match on longest string
239
+ @singularization_regex = Regexp.new("(" + @singular_rules.map {|s,p| p}.join("|") + ")$", "i")
240
+ @singularization_hash = Hash[*@singular_rules.flatten].invert
241
+ [@singularization_regex, @singularization_hash]
242
+ end
243
+
244
+ # Read prepared pluralization rules.
245
+ def pluralization_rules
246
+ if defined?(@pluralization_regex) && @pluralization_regex
247
+ return [@pluralization_regex, @pluralization_hash]
248
+ end
249
+ @pluralization_regex = Regexp.new("(" + @plural_rules.map {|s,p| s}.join("|") + ")$", "i")
250
+ @pluralization_hash = Hash[*@plural_rules.flatten]
251
+ [@pluralization_regex, @pluralization_hash]
252
+ end
253
+
254
+ attr_reader :singular_of, :plural_of
255
+
256
+ # Convert an English word from plurel to singular.
257
+ #
258
+ # "boys".singular #=> boy
259
+ # "tomatoes".singular #=> tomato
260
+ #
261
+ # ==== Parameters
262
+ # word<String>:: word to singularize
263
+ #
264
+ # ==== Returns
265
+ # <String>:: singularized form of word
266
+ #
267
+ # ==== Notes
268
+ # Aliased as singularize (a Railism)
269
+ def singular(word)
270
+ if result = singular_of[word]
271
+ return result.dup
272
+ end
273
+ result = word.dup
274
+ regex, hash = singularization_rules
275
+ result.sub!(regex) {|m| hash[m]}
276
+ singular_of[word] = result
277
+ return result
278
+ end
279
+
280
+ # Alias for #singular (a Railism).
281
+ #
282
+ alias_method(:singularize, :singular)
283
+
284
+ # Convert an English word from singular to plurel.
285
+ #
286
+ # "boy".plural #=> boys
287
+ # "tomato".plural #=> tomatoes
288
+ #
289
+ # ==== Parameters
290
+ # word<String>:: word to pluralize
291
+ #
292
+ # ==== Returns
293
+ # <String>:: pluralized form of word
294
+ #
295
+ # ==== Notes
296
+ # Aliased as pluralize (a Railism)
297
+ def plural(word)
298
+ # special exceptions
299
+ return "" if word == ""
300
+ if result = plural_of[word]
301
+ return result.dup
302
+ end
303
+ result = word.dup
304
+ regex, hash = pluralization_rules
305
+ result.sub!(regex) {|m| hash[m]}
306
+ plural_of[word] = result
307
+ return result
308
+ end
309
+
310
+ # Alias for #plural (a Railism).
311
+ alias_method(:pluralize, :plural)
312
+ end
313
+
314
+ # One argument means singular and plural are the same.
315
+
316
+ word 'equipment'
317
+ word 'information'
318
+ word 'money'
319
+ word 'species'
320
+ word 'series'
321
+ word 'fish'
322
+ word 'sheep'
323
+ word 'moose'
324
+ word 'hovercraft'
325
+ word 'grass'
326
+ word 'rain'
327
+ word 'milk'
328
+ word 'rice'
329
+ word 'plurals'
330
+ word 'postgres'
331
+ word 'status'
332
+
333
+ # Two arguments defines a singular and plural exception.
334
+ word 'status' , 'status'
335
+ word 'Swiss' , 'Swiss'
336
+ word 'life' , 'lives'
337
+ word 'wife' , 'wives'
338
+ word 'goose' , 'geese'
339
+ word 'criterion' , 'criteria'
340
+ word 'alias' , 'aliases'
341
+ word 'status' , 'statuses'
342
+ word 'axis' , 'axes'
343
+ word 'crisis' , 'crises'
344
+ word 'testis' , 'testes'
345
+ word 'potato' , 'potatoes'
346
+ word 'tomato' , 'tomatoes'
347
+ word 'buffalo' , 'buffaloes'
348
+ word 'torpedo' , 'torpedoes'
349
+ word 'quiz' , 'quizzes'
350
+ word 'matrix' , 'matrices'
351
+ word 'vertex' , 'vertices'
352
+ word 'index' , 'indices'
353
+ word 'ox' , 'oxen'
354
+ word 'mouse' , 'mice'
355
+ word 'louse' , 'lice'
356
+ word 'thesis' , 'theses'
357
+ word 'thief' , 'thieves'
358
+ word 'analysis' , 'analyses'
359
+ word 'erratum' , 'errata'
360
+ word 'phenomenon', 'phenomena'
361
+ word 'octopus' , 'octopi'
362
+ word 'thesaurus' , 'thesauri'
363
+ word 'movie' , 'movies'
364
+ word 'cactus' , 'cacti'
365
+ word 'plus' , 'plusses'
366
+ word 'cross' , 'crosses'
367
+ word 'medium' , 'media'
368
+ word 'datum' , 'data'
369
+ word 'basis' , 'bases'
370
+ word 'diagnosis' , 'diagnoses'
371
+
372
+ # One-way singularization exception (convert plural to singular).
373
+
374
+ # General rules.
375
+ rule 'person' , 'people', true
376
+ rule 'shoe' , 'shoes', true
377
+ rule 'hive' , 'hives', true
378
+ rule 'man' , 'men', true
379
+ rule 'child' , 'children', true
380
+ rule 'news' , 'news', true
381
+ rule 'rf' , 'rves'
382
+ rule 'af' , 'aves'
383
+ rule 'ero' , 'eroes'
384
+ rule 'man' , 'men'
385
+ rule 'ch' , 'ches'
386
+ rule 'sh' , 'shes'
387
+ rule 'ss' , 'sses'
388
+ rule 'ta' , 'tum'
389
+ rule 'ia' , 'ium'
390
+ rule 'ra' , 'rum'
391
+ rule 'ay' , 'ays'
392
+ rule 'ey' , 'eys'
393
+ rule 'oy' , 'oys'
394
+ rule 'uy' , 'uys'
395
+ rule 'y' , 'ies'
396
+ rule 'x' , 'xes'
397
+ rule 'lf' , 'lves'
398
+ rule 'ffe' , 'ffes'
399
+ rule 'afe' , 'aves'
400
+ rule 'ouse' , 'ouses'
401
+ # more cases of words ending in -oses not being singularized properly
402
+ # than cases of words ending in -osis
403
+ # rule 'osis' , 'oses'
404
+ rule 'ox' , 'oxes'
405
+ rule 'us' , 'uses'
406
+ rule '' , 's'
407
+
408
+ # One-way singular rules.
409
+
410
+ singular_rule 'of' , 'ofs' # proof
411
+ singular_rule 'o' , 'oes' # hero, heroes
412
+ singular_rule 'f' , 'ves'
413
+
414
+ # One-way plural rules.
415
+
416
+ #plural_rule 'fe' , 'ves' # safe, wife
417
+ plural_rule 's' , 'ses'
418
+ plural_rule 'ive' , 'ives' # don't want to snag wife
419
+ plural_rule 'fe' , 'ves' # don't want to snag perspectives
420
+
421
+
422
+ end
423
+ end
424
+
425
+ class Object # :nodoc:all
426
+ def singular
427
+ raise MethodNotFound, caller(1) unless self.respond_to?(:to_s)
428
+ Extlib::Inflection.singular(to_s)
429
+ end
430
+ alias_method(:singularize, :singular)
431
+ def plural
432
+ raise MethodNotFound, caller(1) unless self.respond_to?(:to_s)
433
+ Extlib::Inflection.plural(to_s)
434
+ end
435
+ alias_method(:pluralize, :plural)
436
+ end