activesupport 3.1.12 → 3.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activesupport might be problematic. Click here for more details.

Files changed (78) hide show
  1. data/CHANGELOG.md +1539 -30
  2. data/README.rdoc +2 -2
  3. data/lib/active_support.rb +1 -1
  4. data/lib/active_support/benchmarkable.rb +13 -18
  5. data/lib/active_support/buffered_logger.rb +43 -55
  6. data/lib/active_support/cache.rb +109 -115
  7. data/lib/active_support/cache/file_store.rb +6 -17
  8. data/lib/active_support/cache/mem_cache_store.rb +10 -2
  9. data/lib/active_support/cache/null_store.rb +44 -0
  10. data/lib/active_support/cache/strategy/local_cache.rb +2 -2
  11. data/lib/active_support/callbacks.rb +38 -35
  12. data/lib/active_support/concern.rb +5 -10
  13. data/lib/active_support/core_ext/array.rb +1 -0
  14. data/lib/active_support/core_ext/array/access.rb +1 -1
  15. data/lib/active_support/core_ext/array/prepend_and_append.rb +7 -0
  16. data/lib/active_support/core_ext/array/wrap.rb +1 -1
  17. data/lib/active_support/core_ext/class.rb +0 -1
  18. data/lib/active_support/core_ext/class/attribute_accessors.rb +3 -2
  19. data/lib/active_support/core_ext/date/calculations.rb +37 -14
  20. data/lib/active_support/core_ext/date/freeze.rb +6 -4
  21. data/lib/active_support/core_ext/date_time/calculations.rb +1 -1
  22. data/lib/active_support/core_ext/enumerable.rb +25 -8
  23. data/lib/active_support/core_ext/file/atomic.rb +1 -2
  24. data/lib/active_support/core_ext/hash/conversions.rb +7 -25
  25. data/lib/active_support/core_ext/hash/indifferent_access.rb +1 -1
  26. data/lib/active_support/core_ext/io.rb +15 -0
  27. data/lib/active_support/core_ext/kernel.rb +0 -1
  28. data/lib/active_support/core_ext/kernel/agnostics.rb +2 -2
  29. data/lib/active_support/core_ext/kernel/debugger.rb +1 -7
  30. data/lib/active_support/core_ext/module.rb +2 -2
  31. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -2
  32. data/lib/active_support/core_ext/module/delegation.rb +32 -21
  33. data/lib/active_support/core_ext/module/qualified_const.rb +64 -0
  34. data/lib/active_support/core_ext/module/reachable.rb +1 -3
  35. data/lib/active_support/core_ext/module/synchronization.rb +2 -0
  36. data/lib/active_support/core_ext/object/blank.rb +14 -2
  37. data/lib/active_support/core_ext/object/inclusion.rb +17 -7
  38. data/lib/active_support/core_ext/object/to_json.rb +2 -2
  39. data/lib/active_support/core_ext/range/conversions.rb +1 -1
  40. data/lib/active_support/core_ext/string/conversions.rb +2 -2
  41. data/lib/active_support/core_ext/string/inflections.rb +44 -6
  42. data/lib/active_support/core_ext/string/multibyte.rb +1 -1
  43. data/lib/active_support/core_ext/string/output_safety.rb +22 -25
  44. data/lib/active_support/core_ext/time/calculations.rb +66 -12
  45. data/lib/active_support/dependencies.rb +51 -52
  46. data/lib/active_support/file_update_checker.rb +100 -15
  47. data/lib/active_support/hash_with_indifferent_access.rb +5 -1
  48. data/lib/active_support/i18n.rb +1 -1
  49. data/lib/active_support/i18n_railtie.rb +9 -4
  50. data/lib/active_support/inflections.rb +3 -3
  51. data/lib/active_support/inflector/inflections.rb +53 -92
  52. data/lib/active_support/inflector/methods.rb +173 -9
  53. data/lib/active_support/json/decoding.rb +3 -17
  54. data/lib/active_support/json/encoding.rb +11 -14
  55. data/lib/active_support/memoizable.rb +12 -1
  56. data/lib/active_support/message_encryptor.rb +52 -20
  57. data/lib/active_support/message_verifier.rb +15 -4
  58. data/lib/active_support/notifications.rb +87 -14
  59. data/lib/active_support/notifications/instrumenter.rb +1 -2
  60. data/lib/active_support/ordered_hash.rb +7 -3
  61. data/lib/active_support/tagged_logging.rb +63 -0
  62. data/lib/active_support/testing/assertions.rb +1 -1
  63. data/lib/active_support/testing/mochaing.rb +2 -2
  64. data/lib/active_support/testing/performance/ruby.rb +1 -1
  65. data/lib/active_support/testing/setup_and_teardown.rb +4 -12
  66. data/lib/active_support/time_with_zone.rb +6 -3
  67. data/lib/active_support/values/time_zone.rb +3 -7
  68. data/lib/active_support/version.rb +3 -3
  69. data/lib/active_support/xml_mini.rb +3 -3
  70. data/lib/active_support/xml_mini/jdom.rb +4 -10
  71. metadata +28 -21
  72. checksums.yaml +0 -7
  73. data/lib/active_support/cache/compressed_mem_cache_store.rb +0 -13
  74. data/lib/active_support/cache/synchronized_memory_store.rb +0 -11
  75. data/lib/active_support/core_ext/class/inheritable_attributes.rb +0 -178
  76. data/lib/active_support/core_ext/kernel/requires.rb +0 -28
  77. data/lib/active_support/core_ext/module/attr_accessor_with_default.rb +0 -31
  78. data/lib/active_support/secure_random.rb +0 -6
@@ -1,8 +1,28 @@
1
+ require "active_support/core_ext/array/wrap"
2
+ require "active_support/core_ext/array/extract_options"
3
+
1
4
  module ActiveSupport
2
- # This class is responsible to track files and invoke the given block
3
- # whenever one of these files are changed. For example, this class
4
- # is used by Rails to reload the I18n framework whenever they are
5
- # changed upon a new request.
5
+ # \FileUpdateChecker specifies the API used by Rails to watch files
6
+ # and control reloading. The API depends on four methods:
7
+ #
8
+ # * +initialize+ which expects two parameters and one block as
9
+ # described below;
10
+ #
11
+ # * +updated?+ which returns a boolean if there were updates in
12
+ # the filesystem or not;
13
+ #
14
+ # * +execute+ which executes the given block on initialization
15
+ # and updates the counter to the latest timestamp;
16
+ #
17
+ # * +execute_if_updated+ which just executes the block if it was updated;
18
+ #
19
+ # After initialization, a call to +execute_if_updated+ must execute
20
+ # the block only if there was really a change in the filesystem.
21
+ #
22
+ # == Examples
23
+ #
24
+ # This class is used by Rails to reload the I18n framework whenever
25
+ # they are changed upon a new request.
6
26
  #
7
27
  # i18n_reloader = ActiveSupport::FileUpdateChecker.new(paths) do
8
28
  # I18n.reload!
@@ -13,24 +33,89 @@ module ActiveSupport
13
33
  # end
14
34
  #
15
35
  class FileUpdateChecker
16
- attr_reader :paths, :last_update_at
17
-
18
- def initialize(paths, calculate=false, &block)
19
- @paths = paths
36
+ # It accepts two parameters on initialization. The first is an array
37
+ # of files and the second is an optional hash of directories. The hash must
38
+ # have directories as keys and the value is an array of extensions to be
39
+ # watched under that directory.
40
+ #
41
+ # This method must also receive a block that will be called once a path changes.
42
+ #
43
+ # == Implementation details
44
+ #
45
+ # This particular implementation checks for added and updated files,
46
+ # but not removed files. Directories lookup are compiled to a glob for
47
+ # performance. Therefore, while someone can add new files to the +files+
48
+ # array after initialization (and parts of Rails do depend on this feature),
49
+ # adding new directories after initialization is not allowed.
50
+ #
51
+ # Notice that other objects that implements FileUpdateChecker API may
52
+ # not even allow new files to be added after initialization. If this
53
+ # is the case, we recommend freezing the +files+ after initialization to
54
+ # avoid changes that won't make effect.
55
+ def initialize(files, dirs={}, &block)
56
+ @files = files
57
+ @glob = compile_glob(dirs)
20
58
  @block = block
21
- @last_update_at = calculate ? updated_at : nil
59
+ @updated_at = nil
60
+ @last_update_at = updated_at
61
+ end
62
+
63
+ # Check if any of the entries were updated. If so, the updated_at
64
+ # value is cached until the block is executed via +execute+ or +execute_if_updated+
65
+ def updated?
66
+ current_updated_at = updated_at
67
+ if @last_update_at < current_updated_at
68
+ @updated_at = updated_at
69
+ true
70
+ else
71
+ false
72
+ end
22
73
  end
23
74
 
24
- def updated_at
25
- paths.map { |path| File.stat(path).mtime }.max
75
+ # Executes the given block and updates the counter to latest timestamp.
76
+ def execute
77
+ @last_update_at = updated_at
78
+ @block.call
79
+ ensure
80
+ @updated_at = nil
26
81
  end
27
82
 
83
+ # Execute the block given if updated.
28
84
  def execute_if_updated
29
- current_update_at = self.updated_at
30
- if @last_update_at != current_update_at
31
- @last_update_at = current_update_at
32
- @block.call
85
+ if updated?
86
+ execute
87
+ true
88
+ else
89
+ false
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def updated_at #:nodoc:
96
+ @updated_at || begin
97
+ all = []
98
+ all.concat @files.select { |f| File.exists?(f) }
99
+ all.concat Dir[@glob] if @glob
100
+ all.map { |path| File.mtime(path) }.max || Time.at(0)
33
101
  end
34
102
  end
103
+
104
+ def compile_glob(hash) #:nodoc:
105
+ hash.freeze # Freeze so changes aren't accidently pushed
106
+ return if hash.empty?
107
+
108
+ globs = []
109
+ hash.each do |key, value|
110
+ globs << "#{key}/**/*#{compile_ext(value)}"
111
+ end
112
+ "{#{globs.join(",")}}"
113
+ end
114
+
115
+ def compile_ext(array) #:nodoc:
116
+ array = Array.wrap(array)
117
+ return if array.empty?
118
+ ".{#{array.join(",")}}"
119
+ end
35
120
  end
36
121
  end
@@ -16,6 +16,10 @@ module ActiveSupport
16
16
  dup
17
17
  end
18
18
 
19
+ def nested_under_indifferent_access
20
+ self
21
+ end
22
+
19
23
  def initialize(constructor = {})
20
24
  if constructor.is_a?(Hash)
21
25
  super()
@@ -112,7 +116,7 @@ module ActiveSupport
112
116
  end
113
117
  end
114
118
 
115
- # Merges the instantized and the specified hashes together, giving precedence to the values from the second hash
119
+ # Merges the instantized and the specified hashes together, giving precedence to the values from the second hash.
116
120
  # Does not overwrite the existing hash.
117
121
  def merge(hash)
118
122
  self.dup.update(hash)
@@ -2,7 +2,7 @@ begin
2
2
  require 'i18n'
3
3
  require 'active_support/lazy_load_hooks'
4
4
  rescue LoadError => e
5
- $stderr.puts "You don't have i18n installed in your application. Please add it to your Gemfile and run bundle install"
5
+ $stderr.puts "The i18n gem is not available. Please add it to your Gemfile and run bundle install"
6
6
  raise e
7
7
  end
8
8
 
@@ -10,14 +10,19 @@ module I18n
10
10
  config.i18n.fallbacks = ActiveSupport::OrderedOptions.new
11
11
 
12
12
  def self.reloader
13
- @reloader ||= ActiveSupport::FileUpdateChecker.new([]){ I18n.reload! }
13
+ @reloader ||= ActiveSupport::FileUpdateChecker.new(reloader_paths){ I18n.reload! }
14
+ end
15
+
16
+ def self.reloader_paths
17
+ @reloader_paths ||= []
14
18
  end
15
19
 
16
20
  # Add <tt>I18n::Railtie.reloader</tt> to ActionDispatch callbacks. Since, at this
17
21
  # point, no path was added to the reloader, I18n.reload! is not triggered
18
22
  # on to_prepare callbacks. This will only happen on the config.after_initialize
19
23
  # callback below.
20
- initializer "i18n.callbacks" do
24
+ initializer "i18n.callbacks" do |app|
25
+ app.reloaders << I18n::Railtie.reloader
21
26
  ActionDispatch::Reloader.to_prepare do
22
27
  I18n::Railtie.reloader.execute_if_updated
23
28
  end
@@ -58,8 +63,8 @@ module I18n
58
63
 
59
64
  init_fallbacks(fallbacks) if fallbacks && validate_fallbacks(fallbacks)
60
65
 
61
- reloader.paths.concat I18n.load_path
62
- reloader.execute_if_updated
66
+ reloader_paths.concat I18n.load_path
67
+ reloader.execute
63
68
 
64
69
  @i18n_inited = true
65
70
  end
@@ -16,8 +16,8 @@ module ActiveSupport
16
16
  inflect.plural(/([^aeiouy]|qu)y$/i, '\1ies')
17
17
  inflect.plural(/(x|ch|ss|sh)$/i, '\1es')
18
18
  inflect.plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices')
19
- inflect.plural(/([m|l])ouse$/i, '\1ice')
20
- inflect.plural(/([m|l])ice$/i, '\1ice')
19
+ inflect.plural(/(m|l)ouse$/i, '\1ice')
20
+ inflect.plural(/(m|l)ice$/i, '\1ice')
21
21
  inflect.plural(/^(ox)$/i, '\1en')
22
22
  inflect.plural(/^(oxen)$/i, '\1')
23
23
  inflect.plural(/(quiz)$/i, '\1zes')
@@ -35,7 +35,7 @@ module ActiveSupport
35
35
  inflect.singular(/(s)eries$/i, '\1eries')
36
36
  inflect.singular(/(m)ovies$/i, '\1ovie')
37
37
  inflect.singular(/(x|ch|ss|sh)es$/i, '\1')
38
- inflect.singular(/([m|l])ice$/i, '\1ouse')
38
+ inflect.singular(/(m|l)ice$/i, '\1ouse')
39
39
  inflect.singular(/(bus)es$/i, '\1')
40
40
  inflect.singular(/(o)es$/i, '\1')
41
41
  inflect.singular(/(shoe)s$/i, '\1')
@@ -20,10 +20,61 @@ module ActiveSupport
20
20
  @__instance__ ||= new
21
21
  end
22
22
 
23
- attr_reader :plurals, :singulars, :uncountables, :humans
23
+ attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex
24
24
 
25
25
  def initialize
26
- @plurals, @singulars, @uncountables, @humans = [], [], [], []
26
+ @plurals, @singulars, @uncountables, @humans, @acronyms, @acronym_regex = [], [], [], [], {}, /(?=a)b/
27
+ end
28
+
29
+ # Specifies a new acronym. An acronym must be specified as it will appear in a camelized string. An underscore
30
+ # string that contains the acronym will retain the acronym when passed to `camelize`, `humanize`, or `titleize`.
31
+ # A camelized string that contains the acronym will maintain the acronym when titleized or humanized, and will
32
+ # convert the acronym into a non-delimited single lowercase word when passed to +underscore+.
33
+ #
34
+ # Examples:
35
+ # acronym 'HTML'
36
+ # titleize 'html' #=> 'HTML'
37
+ # camelize 'html' #=> 'HTML'
38
+ # underscore 'MyHTML' #=> 'my_html'
39
+ #
40
+ # The acronym, however, must occur as a delimited unit and not be part of another word for conversions to recognize it:
41
+ #
42
+ # acronym 'HTTP'
43
+ # camelize 'my_http_delimited' #=> 'MyHTTPDelimited'
44
+ # camelize 'https' #=> 'Https', not 'HTTPs'
45
+ # underscore 'HTTPS' #=> 'http_s', not 'https'
46
+ #
47
+ # acronym 'HTTPS'
48
+ # camelize 'https' #=> 'HTTPS'
49
+ # underscore 'HTTPS' #=> 'https'
50
+ #
51
+ # Note: Acronyms that are passed to `pluralize` will no longer be recognized, since the acronym will not occur as
52
+ # a delimited unit in the pluralized result. To work around this, you must specify the pluralized form as an
53
+ # acronym as well:
54
+ #
55
+ # acronym 'API'
56
+ # camelize(pluralize('api')) #=> 'Apis'
57
+ #
58
+ # acronym 'APIs'
59
+ # camelize(pluralize('api')) #=> 'APIs'
60
+ #
61
+ # `acronym` may be used to specify any word that contains an acronym or otherwise needs to maintain a non-standard
62
+ # capitalization. The only restriction is that the word must begin with a capital letter.
63
+ #
64
+ # Examples:
65
+ # acronym 'RESTful'
66
+ # underscore 'RESTful' #=> 'restful'
67
+ # underscore 'RESTfulController' #=> 'restful_controller'
68
+ # titleize 'RESTfulController' #=> 'RESTful Controller'
69
+ # camelize 'restful' #=> 'RESTful'
70
+ # camelize 'restful_controller' #=> 'RESTfulController'
71
+ #
72
+ # acronym 'McDonald'
73
+ # underscore 'McDonald' #=> 'mcdonald'
74
+ # camelize 'mcdonald' #=> 'McDonald'
75
+ def acronym(word)
76
+ @acronyms[word.downcase] = word
77
+ @acronym_regex = /#{@acronyms.values.join("|")}/
27
78
  end
28
79
 
29
80
  # Specifies a new pluralization rule and its replacement. The rule can either be a string or a regular expression.
@@ -117,95 +168,5 @@ module ActiveSupport
117
168
  Inflections.instance
118
169
  end
119
170
  end
120
-
121
- # Returns the plural form of the word in the string.
122
- #
123
- # Examples:
124
- # "post".pluralize # => "posts"
125
- # "octopus".pluralize # => "octopi"
126
- # "sheep".pluralize # => "sheep"
127
- # "words".pluralize # => "words"
128
- # "CamelOctopus".pluralize # => "CamelOctopi"
129
- def pluralize(word)
130
- result = word.to_s.dup
131
-
132
- if word.empty? || inflections.uncountables.include?(result.downcase)
133
- result
134
- else
135
- inflections.plurals.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
136
- result
137
- end
138
- end
139
-
140
- # The reverse of +pluralize+, returns the singular form of a word in a string.
141
- #
142
- # Examples:
143
- # "posts".singularize # => "post"
144
- # "octopi".singularize # => "octopus"
145
- # "sheep".singularize # => "sheep"
146
- # "word".singularize # => "word"
147
- # "CamelOctopi".singularize # => "CamelOctopus"
148
- def singularize(word)
149
- result = word.to_s.dup
150
-
151
- if inflections.uncountables.any? { |inflection| result =~ /\b(#{inflection})\Z/i }
152
- result
153
- else
154
- inflections.singulars.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
155
- result
156
- end
157
- end
158
-
159
- # Capitalizes the first word and turns underscores into spaces and strips a
160
- # trailing "_id", if any. Like +titleize+, this is meant for creating pretty output.
161
- #
162
- # Examples:
163
- # "employee_salary" # => "Employee salary"
164
- # "author_id" # => "Author"
165
- def humanize(lower_case_and_underscored_word)
166
- result = lower_case_and_underscored_word.to_s.dup
167
-
168
- inflections.humans.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
169
- result.gsub(/_id$/, "").gsub(/_/, " ").capitalize
170
- end
171
-
172
- # Capitalizes all the words and replaces some characters in the string to create
173
- # a nicer looking title. +titleize+ is meant for creating pretty output. It is not
174
- # used in the Rails internals.
175
- #
176
- # +titleize+ is also aliased as as +titlecase+.
177
- #
178
- # Examples:
179
- # "man from the boondocks".titleize # => "Man From The Boondocks"
180
- # "x-men: the last stand".titleize # => "X Men: The Last Stand"
181
- def titleize(word)
182
- humanize(underscore(word)).gsub(/\b('?[a-z])/) { $1.capitalize }
183
- end
184
-
185
- # Create the name of a table like Rails does for models to table names. This method
186
- # uses the +pluralize+ method on the last word in the string.
187
- #
188
- # Examples
189
- # "RawScaledScorer".tableize # => "raw_scaled_scorers"
190
- # "egg_and_ham".tableize # => "egg_and_hams"
191
- # "fancyCategory".tableize # => "fancy_categories"
192
- def tableize(class_name)
193
- pluralize(underscore(class_name))
194
- end
195
-
196
- # Create a class name from a plural table name like Rails does for table names to models.
197
- # Note that this returns a string and not a Class. (To convert to an actual class
198
- # follow +classify+ with +constantize+.)
199
- #
200
- # Examples:
201
- # "egg_and_hams".classify # => "EggAndHam"
202
- # "posts".classify # => "Post"
203
- #
204
- # Singular names are not handled correctly:
205
- # "business".classify # => "Busines"
206
- def classify(table_name)
207
- # strip out any leading schema name
208
- camelize(singularize(table_name.to_s.sub(/.*\./, '')))
209
- end
210
171
  end
211
172
  end
@@ -1,3 +1,5 @@
1
+ require 'active_support/inflector/inflections'
2
+
1
3
  module ActiveSupport
2
4
  # The Inflector transforms words from singular to plural, class names to table names, modularized class names to ones without,
3
5
  # and class names to foreign keys. The default inflections for pluralization, singularization, and uncountable words are kept
@@ -10,6 +12,30 @@ module ActiveSupport
10
12
  module Inflector
11
13
  extend self
12
14
 
15
+ # Returns the plural form of the word in the string.
16
+ #
17
+ # Examples:
18
+ # "post".pluralize # => "posts"
19
+ # "octopus".pluralize # => "octopi"
20
+ # "sheep".pluralize # => "sheep"
21
+ # "words".pluralize # => "words"
22
+ # "CamelOctopus".pluralize # => "CamelOctopi"
23
+ def pluralize(word)
24
+ apply_inflections(word, inflections.plurals)
25
+ end
26
+
27
+ # The reverse of +pluralize+, returns the singular form of a word in a string.
28
+ #
29
+ # Examples:
30
+ # "posts".singularize # => "post"
31
+ # "octopi".singularize # => "octopus"
32
+ # "sheep".singularize # => "sheep"
33
+ # "word".singularize # => "word"
34
+ # "CamelOctopi".singularize # => "CamelOctopus"
35
+ def singularize(word)
36
+ apply_inflections(word, inflections.singulars)
37
+ end
38
+
13
39
  # By default, +camelize+ converts strings to UpperCamelCase. If the argument to +camelize+
14
40
  # is set to <tt>:lower</tt> then +camelize+ produces lowerCamelCase.
15
41
  #
@@ -25,12 +51,14 @@ module ActiveSupport
25
51
  # though there are cases where that does not hold:
26
52
  #
27
53
  # "SSLError".underscore.camelize # => "SslError"
28
- def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
29
- if first_letter_in_uppercase
30
- lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
54
+ def camelize(term, uppercase_first_letter = true)
55
+ string = term.to_s
56
+ if uppercase_first_letter
57
+ string = string.sub(/^[a-z\d]*/) { inflections.acronyms[$&] || $&.capitalize }
31
58
  else
32
- lower_case_and_underscored_word.to_s[0].chr.downcase + camelize(lower_case_and_underscored_word)[1..-1]
59
+ string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase }
33
60
  end
61
+ string.gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }.gsub('/', '::')
34
62
  end
35
63
 
36
64
  # Makes an underscored, lowercase form from the expression in the string.
@@ -48,13 +76,68 @@ module ActiveSupport
48
76
  def underscore(camel_cased_word)
49
77
  word = camel_cased_word.to_s.dup
50
78
  word.gsub!(/::/, '/')
51
- word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
79
+ word.gsub!(/(?:([A-Za-z\d])|^)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" }
80
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
52
81
  word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
53
82
  word.tr!("-", "_")
54
83
  word.downcase!
55
84
  word
56
85
  end
57
86
 
87
+ # Capitalizes the first word and turns underscores into spaces and strips a
88
+ # trailing "_id", if any. Like +titleize+, this is meant for creating pretty output.
89
+ #
90
+ # Examples:
91
+ # "employee_salary" # => "Employee salary"
92
+ # "author_id" # => "Author"
93
+ def humanize(lower_case_and_underscored_word)
94
+ result = lower_case_and_underscored_word.to_s.dup
95
+ inflections.humans.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
96
+ result.gsub!(/_id$/, "")
97
+ result.gsub(/(_)?([a-z\d]*)/i) { "#{$1 && ' '}#{inflections.acronyms[$2] || $2.downcase}" }.gsub(/^\w/) { $&.upcase }
98
+ end
99
+
100
+ # Capitalizes all the words and replaces some characters in the string to create
101
+ # a nicer looking title. +titleize+ is meant for creating pretty output. It is not
102
+ # used in the Rails internals.
103
+ #
104
+ # +titleize+ is also aliased as as +titlecase+.
105
+ #
106
+ # Examples:
107
+ # "man from the boondocks".titleize # => "Man From The Boondocks"
108
+ # "x-men: the last stand".titleize # => "X Men: The Last Stand"
109
+ # "TheManWithoutAPast".titleize # => "The Man Without A Past"
110
+ # "raiders_of_the_lost_ark".titleize # => "Raiders Of The Lost Ark"
111
+ def titleize(word)
112
+ humanize(underscore(word)).gsub(/\b('?[a-z])/) { $1.capitalize }
113
+ end
114
+
115
+ # Create the name of a table like Rails does for models to table names. This method
116
+ # uses the +pluralize+ method on the last word in the string.
117
+ #
118
+ # Examples
119
+ # "RawScaledScorer".tableize # => "raw_scaled_scorers"
120
+ # "egg_and_ham".tableize # => "egg_and_hams"
121
+ # "fancyCategory".tableize # => "fancy_categories"
122
+ def tableize(class_name)
123
+ pluralize(underscore(class_name))
124
+ end
125
+
126
+ # Create a class name from a plural table name like Rails does for table names to models.
127
+ # Note that this returns a string and not a Class. (To convert to an actual class
128
+ # follow +classify+ with +constantize+.)
129
+ #
130
+ # Examples:
131
+ # "egg_and_hams".classify # => "EggAndHam"
132
+ # "posts".classify # => "Post"
133
+ #
134
+ # Singular names are not handled correctly:
135
+ # "business".classify # => "Busines"
136
+ def classify(table_name)
137
+ # strip out any leading schema name
138
+ camelize(singularize(table_name.to_s.sub(/.*\./, '')))
139
+ end
140
+
58
141
  # Replaces underscores with dashes in the string.
59
142
  #
60
143
  # Example:
@@ -63,13 +146,32 @@ module ActiveSupport
63
146
  underscored_word.gsub(/_/, '-')
64
147
  end
65
148
 
66
- # Removes the module part from the expression in the string.
149
+ # Removes the module part from the expression in the string:
67
150
  #
68
- # Examples:
69
151
  # "ActiveRecord::CoreExtensions::String::Inflections".demodulize # => "Inflections"
70
152
  # "Inflections".demodulize # => "Inflections"
71
- def demodulize(class_name_in_module)
72
- class_name_in_module.to_s.gsub(/^.*::/, '')
153
+ #
154
+ # See also +deconstantize+.
155
+ def demodulize(path)
156
+ path = path.to_s
157
+ if i = path.rindex('::')
158
+ path[(i+2)..-1]
159
+ else
160
+ path
161
+ end
162
+ end
163
+
164
+ # Removes the rightmost segment from the constant expression in the string:
165
+ #
166
+ # "Net::HTTP".deconstantize # => "Net"
167
+ # "::Net::HTTP".deconstantize # => "::Net"
168
+ # "String".deconstantize # => ""
169
+ # "::String".deconstantize # => ""
170
+ # "".deconstantize # => ""
171
+ #
172
+ # See also +demodulize+.
173
+ def deconstantize(path)
174
+ path.to_s[0...(path.rindex('::') || 0)] # implementation based on the one in facets' Module#spacename
73
175
  end
74
176
 
75
177
  # Creates a foreign key name from a class name.
@@ -127,6 +229,39 @@ module ActiveSupport
127
229
  end
128
230
  end
129
231
 
232
+ # Tries to find a constant with the name specified in the argument string:
233
+ #
234
+ # "Module".safe_constantize # => Module
235
+ # "Test::Unit".safe_constantize # => Test::Unit
236
+ #
237
+ # The name is assumed to be the one of a top-level constant, no matter whether
238
+ # it starts with "::" or not. No lexical context is taken into account:
239
+ #
240
+ # C = 'outside'
241
+ # module M
242
+ # C = 'inside'
243
+ # C # => 'inside'
244
+ # "C".safe_constantize # => 'outside', same as ::C
245
+ # end
246
+ #
247
+ # nil is returned when the name is not in CamelCase or the constant (or part of it) is
248
+ # unknown.
249
+ #
250
+ # "blargle".safe_constantize # => nil
251
+ # "UnknownModule".safe_constantize # => nil
252
+ # "UnknownModule::Foo::Bar".safe_constantize # => nil
253
+ #
254
+ def safe_constantize(camel_cased_word)
255
+ begin
256
+ constantize(camel_cased_word)
257
+ rescue NameError => e
258
+ raise unless e.message =~ /uninitialized constant #{const_regexp(camel_cased_word)}$/ ||
259
+ e.name.to_s == camel_cased_word.to_s
260
+ rescue ArgumentError => e
261
+ raise unless e.message =~ /not missing constant #{const_regexp(camel_cased_word)}\!$/
262
+ end
263
+ end
264
+
130
265
  # Turns a number into an ordinal string used to denote the position in an
131
266
  # ordered sequence such as 1st, 2nd, 3rd, 4th.
132
267
  #
@@ -149,5 +284,34 @@ module ActiveSupport
149
284
  end
150
285
  end
151
286
  end
287
+
288
+ private
289
+
290
+ # Mount a regular expression that will match part by part of the constant.
291
+ # For instance, Foo::Bar::Baz will generate Foo(::Bar(::Baz)?)?
292
+ def const_regexp(camel_cased_word) #:nodoc:
293
+ parts = camel_cased_word.split("::")
294
+ last = parts.pop
295
+
296
+ parts.reverse.inject(last) do |acc, part|
297
+ part.empty? ? acc : "#{part}(::#{acc})?"
298
+ end
299
+ end
300
+
301
+ # Applies inflection rules for +singularize+ and +pluralize+.
302
+ #
303
+ # Examples:
304
+ # apply_inflections("post", inflections.plurals) # => "posts"
305
+ # apply_inflections("posts", inflections.singulars) # => "post"
306
+ def apply_inflections(word, rules)
307
+ result = word.to_s.dup
308
+
309
+ if word.empty? || inflections.uncountables.any? { |inflection| result =~ /\b#{inflection}\Z/i }
310
+ result
311
+ else
312
+ rules.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
313
+ result
314
+ end
315
+ end
152
316
  end
153
317
  end