activesupport 1.3.1 → 1.4.0

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 (55) hide show
  1. data/CHANGELOG +232 -2
  2. data/README +43 -0
  3. data/lib/active_support.rb +4 -1
  4. data/lib/active_support/breakpoint.rb +5 -0
  5. data/lib/active_support/core_ext/array.rb +2 -16
  6. data/lib/active_support/core_ext/array/conversions.rb +30 -4
  7. data/lib/active_support/core_ext/array/grouping.rb +55 -0
  8. data/lib/active_support/core_ext/bigdecimal.rb +3 -0
  9. data/lib/active_support/core_ext/bigdecimal/formatting.rb +7 -0
  10. data/lib/active_support/core_ext/class/inheritable_attributes.rb +6 -1
  11. data/lib/active_support/core_ext/date/conversions.rb +13 -7
  12. data/lib/active_support/core_ext/enumerable.rb +41 -10
  13. data/lib/active_support/core_ext/exception.rb +2 -2
  14. data/lib/active_support/core_ext/hash/conversions.rb +123 -12
  15. data/lib/active_support/core_ext/hash/indifferent_access.rb +18 -9
  16. data/lib/active_support/core_ext/integer/inflections.rb +10 -4
  17. data/lib/active_support/core_ext/load_error.rb +3 -3
  18. data/lib/active_support/core_ext/module.rb +2 -0
  19. data/lib/active_support/core_ext/module/aliasing.rb +58 -0
  20. data/lib/active_support/core_ext/module/attr_internal.rb +31 -0
  21. data/lib/active_support/core_ext/module/delegation.rb +27 -2
  22. data/lib/active_support/core_ext/name_error.rb +20 -0
  23. data/lib/active_support/core_ext/string.rb +2 -0
  24. data/lib/active_support/core_ext/string/access.rb +5 -5
  25. data/lib/active_support/core_ext/string/inflections.rb +93 -4
  26. data/lib/active_support/core_ext/string/unicode.rb +42 -0
  27. data/lib/active_support/core_ext/symbol.rb +1 -1
  28. data/lib/active_support/core_ext/time/calculations.rb +7 -5
  29. data/lib/active_support/core_ext/time/conversions.rb +1 -2
  30. data/lib/active_support/dependencies.rb +417 -50
  31. data/lib/active_support/deprecation.rb +201 -0
  32. data/lib/active_support/inflections.rb +1 -2
  33. data/lib/active_support/inflector.rb +117 -19
  34. data/lib/active_support/json.rb +14 -3
  35. data/lib/active_support/json/encoders/core.rb +21 -18
  36. data/lib/active_support/multibyte.rb +7 -0
  37. data/lib/active_support/multibyte/chars.rb +129 -0
  38. data/lib/active_support/multibyte/generators/generate_tables.rb +149 -0
  39. data/lib/active_support/multibyte/handlers/passthru_handler.rb +9 -0
  40. data/lib/active_support/multibyte/handlers/utf8_handler.rb +453 -0
  41. data/lib/active_support/multibyte/handlers/utf8_handler_proc.rb +44 -0
  42. data/lib/active_support/option_merger.rb +3 -3
  43. data/lib/active_support/ordered_options.rb +24 -23
  44. data/lib/active_support/reloadable.rb +39 -5
  45. data/lib/active_support/values/time_zone.rb +1 -1
  46. data/lib/active_support/values/unicode_tables.dat +0 -0
  47. data/lib/active_support/vendor/builder/blankslate.rb +16 -6
  48. data/lib/active_support/vendor/builder/xchar.rb +112 -0
  49. data/lib/active_support/vendor/builder/xmlbase.rb +12 -10
  50. data/lib/active_support/vendor/builder/xmlmarkup.rb +26 -7
  51. data/lib/active_support/vendor/xml_simple.rb +1021 -0
  52. data/lib/active_support/version.rb +2 -2
  53. data/lib/active_support/whiny_nil.rb +1 -1
  54. metadata +26 -4
  55. data/lib/active_support/core_ext/hash/conversions.rb.rej +0 -28
@@ -0,0 +1,31 @@
1
+ class Module
2
+ # Declare an attribute reader backed by an internally-named instance variable.
3
+ def attr_internal_reader(*attrs)
4
+ attrs.each do |attr|
5
+ module_eval "def #{attr}() #{attr_internal_ivar_name(attr)} end"
6
+ end
7
+ end
8
+
9
+ # Declare an attribute writer backed by an internally-named instance variable.
10
+ def attr_internal_writer(*attrs)
11
+ attrs.each do |attr|
12
+ module_eval "def #{attr}=(v) #{attr_internal_ivar_name(attr)} = v end"
13
+ end
14
+ end
15
+
16
+ # Declare attributes backed by 'internal' instance variables names.
17
+ def attr_internal_accessor(*attrs)
18
+ attr_internal_reader(*attrs)
19
+ attr_internal_writer(*attrs)
20
+ end
21
+
22
+ alias_method :attr_internal, :attr_internal_accessor
23
+
24
+ private
25
+ mattr_accessor :attr_internal_naming_format
26
+ self.attr_internal_naming_format = '@_%s'
27
+
28
+ def attr_internal_ivar_name(attr)
29
+ attr_internal_naming_format % attr
30
+ end
31
+ end
@@ -1,8 +1,33 @@
1
1
  class Module
2
+ # Provides a delegate class method to easily expose contained objects' methods
3
+ # as your own. Pass one or more methods (specified as symbols or strings)
4
+ # and the name of the target object as the final :to option (also a symbol
5
+ # or string). At least one method and the :to option are required.
6
+ #
7
+ # Delegation is particularly useful with Active Record associations:
8
+ # class Greeter < ActiveRecord::Base
9
+ # def hello() "hello" end
10
+ # def goodbye() "goodbye" end
11
+ # end
12
+ #
13
+ # class Foo < ActiveRecord::Base
14
+ # belongs_to :greeter
15
+ # delegate :hello, :to => :greeter
16
+ # end
17
+ #
18
+ # Foo.new.hello # => "hello"
19
+ # Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>
20
+ #
21
+ # Multiple delegates to the same target are allowed:
22
+ # class Foo < ActiveRecord::Base
23
+ # delegate :hello, :goodbye, :to => :greeter
24
+ # end
25
+ #
26
+ # Foo.new.goodbye # => "goodbye"
2
27
  def delegate(*methods)
3
28
  options = methods.pop
4
29
  unless options.is_a?(Hash) && to = options[:to]
5
- raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key"
30
+ raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, :to => :greeter)."
6
31
  end
7
32
 
8
33
  methods.each do |method|
@@ -13,4 +38,4 @@ class Module
13
38
  EOS
14
39
  end
15
40
  end
16
- end
41
+ end
@@ -0,0 +1,20 @@
1
+
2
+ # Add a +missing_name+ method to NameError instances.
3
+ class NameError < StandardError
4
+
5
+ # Add a method to obtain the missing name from a NameError.
6
+ def missing_name
7
+ $1 if /((::)?([A-Z]\w*)(::[A-Z]\w*)*)$/ =~ message
8
+ end
9
+
10
+ # Was this exception raised because the given name was missing?
11
+ def missing_name?(name)
12
+ if name.is_a? Symbol
13
+ last_name = (missing_name || '').split('::').last
14
+ last_name == name.to_s
15
+ else
16
+ missing_name == name.to_s
17
+ end
18
+ end
19
+
20
+ end
@@ -3,6 +3,7 @@ require File.dirname(__FILE__) + '/string/conversions'
3
3
  require File.dirname(__FILE__) + '/string/access'
4
4
  require File.dirname(__FILE__) + '/string/starts_ends_with'
5
5
  require File.dirname(__FILE__) + '/string/iterators'
6
+ require File.dirname(__FILE__) + '/string/unicode'
6
7
 
7
8
  class String #:nodoc:
8
9
  include ActiveSupport::CoreExtensions::String::Access
@@ -10,4 +11,5 @@ class String #:nodoc:
10
11
  include ActiveSupport::CoreExtensions::String::Inflections
11
12
  include ActiveSupport::CoreExtensions::String::StartsEndsWith
12
13
  include ActiveSupport::CoreExtensions::String::Iterators
14
+ include ActiveSupport::CoreExtensions::String::Unicode
13
15
  end
@@ -10,7 +10,7 @@ module ActiveSupport #:nodoc:
10
10
  # "hello".at(4) # => "o"
11
11
  # "hello".at(10) # => nil
12
12
  def at(position)
13
- self[position, 1]
13
+ chars[position, 1].to_s
14
14
  end
15
15
 
16
16
  # Returns the remaining of the string from the +position+ treating the string as an array (where 0 is the first character).
@@ -20,7 +20,7 @@ module ActiveSupport #:nodoc:
20
20
  # "hello".from(2) # => "llo"
21
21
  # "hello".from(10) # => nil
22
22
  def from(position)
23
- self[position..-1]
23
+ chars[position..-1].to_s
24
24
  end
25
25
 
26
26
  # Returns the beginning of the string up to the +position+ treating the string as an array (where 0 is the first character).
@@ -30,7 +30,7 @@ module ActiveSupport #:nodoc:
30
30
  # "hello".to(2) # => "hel"
31
31
  # "hello".to(10) # => "hello"
32
32
  def to(position)
33
- self[0..position]
33
+ chars[0..position].to_s
34
34
  end
35
35
 
36
36
  # Returns the first character of the string or the first +limit+ characters.
@@ -40,7 +40,7 @@ module ActiveSupport #:nodoc:
40
40
  # "hello".first(2) # => "he"
41
41
  # "hello".first(10) # => "hello"
42
42
  def first(limit = 1)
43
- self[0..(limit - 1)]
43
+ chars[0..(limit - 1)].to_s
44
44
  end
45
45
 
46
46
  # Returns the last character of the string or the last +limit+ characters.
@@ -50,7 +50,7 @@ module ActiveSupport #:nodoc:
50
50
  # "hello".last(2) # => "lo"
51
51
  # "hello".last(10) # => "hello"
52
52
  def last(limit = 1)
53
- self[(-limit)..-1] || self
53
+ (chars[(-limit)..-1] || self).to_s
54
54
  end
55
55
  end
56
56
  end
@@ -1,17 +1,48 @@
1
- require File.dirname(__FILE__) + '/../../inflector' unless defined? Inflector
1
+ require 'active_support/inflector'
2
+
2
3
  module ActiveSupport #:nodoc:
3
4
  module CoreExtensions #:nodoc:
4
5
  module String #:nodoc:
5
- # Makes it possible to do "posts".singularize that returns "post" and "MegaCoolClass".underscore that returns "mega_cool_class".
6
+ # String inflections define new methods on the String class to transform names for different purposes.
7
+ # For instance, you can figure out the name of a database from the name of a class.
8
+ # "ScaleScore".tableize => "scale_scores"
6
9
  module Inflections
10
+ # Returns the plural form of the word in the string.
11
+ #
12
+ # Examples
13
+ # "post".pluralize #=> "posts"
14
+ # "octopus".pluralize #=> "octopi"
15
+ # "sheep".pluralize #=> "sheep"
16
+ # "words".pluralize #=> "words"
17
+ # "the blue mailman".pluralize #=> "the blue mailmen"
18
+ # "CamelOctopus".pluralize #=> "CamelOctopi"
7
19
  def pluralize
8
20
  Inflector.pluralize(self)
9
21
  end
10
22
 
23
+ # The reverse of pluralize, returns the singular form of a word in a string.
24
+ #
25
+ # Examples
26
+ # "posts".singularize #=> "post"
27
+ # "octopi".singularize #=> "octopus"
28
+ # "sheep".singluarize #=> "sheep"
29
+ # "word".singluarize #=> "word"
30
+ # "the blue mailmen".singularize #=> "the blue mailman"
31
+ # "CamelOctopi".singularize #=> "CamelOctopus"
11
32
  def singularize
12
33
  Inflector.singularize(self)
13
34
  end
14
35
 
36
+ # By default, camelize converts strings to UpperCamelCase. If the argument to camelize
37
+ # is set to ":lower" then camelize produces lowerCamelCase.
38
+ #
39
+ # camelize will also convert '/' to '::' which is useful for converting paths to namespaces
40
+ #
41
+ # Examples
42
+ # "active_record".camelize #=> "ActiveRecord"
43
+ # "active_record".camelize(:lower) #=> "activeRecord"
44
+ # "active_record/errors".camelize #=> "ActiveRecord::Errors"
45
+ # "active_record/errors".camelize(:lower) #=> "activeRecord::Errors"
15
46
  def camelize(first_letter = :upper)
16
47
  case first_letter
17
48
  when :upper then Inflector.camelize(self, true)
@@ -20,41 +51,99 @@ module ActiveSupport #:nodoc:
20
51
  end
21
52
  alias_method :camelcase, :camelize
22
53
 
54
+ # Capitalizes all the words and replaces some characters in the string to create
55
+ # a nicer looking title. Titleize is meant for creating pretty output. It is not
56
+ # used in the Rails internals.
57
+ #
58
+ # titleize is also aliased as as titlecase
59
+ #
60
+ # Examples
61
+ # "man from the boondocks".titleize #=> "Man From The Boondocks"
62
+ # "x-men: the last stand".titleize #=> "X Men: The Last Stand"
23
63
  def titleize
24
64
  Inflector.titleize(self)
25
65
  end
26
66
  alias_method :titlecase, :titleize
27
67
 
68
+ # The reverse of +camelize+. Makes an underscored form from the expression in the string.
69
+ #
70
+ # Changes '::' to '/' to convert namespaces to paths.
71
+ #
72
+ # Examples
73
+ # "ActiveRecord".underscore #=> "active_record"
74
+ # "ActiveRecord::Errors".underscore #=> active_record/errors
28
75
  def underscore
29
76
  Inflector.underscore(self)
30
77
  end
31
78
 
79
+ # Replaces underscores with dashes in the string.
80
+ #
81
+ # Example
82
+ # "puni_puni" #=> "puni-puni"
32
83
  def dasherize
33
84
  Inflector.dasherize(self)
34
85
  end
35
86
 
87
+ # Removes the module part from the expression in the string
88
+ #
89
+ # Examples
90
+ # "ActiveRecord::CoreExtensions::String::Inflections".demodulize #=> "Inflections"
91
+ # "Inflections".demodulize #=> "Inflections"
36
92
  def demodulize
37
93
  Inflector.demodulize(self)
38
94
  end
39
95
 
96
+ # Create the name of a table like Rails does for models to table names. This method
97
+ # uses the pluralize method on the last word in the string.
98
+ #
99
+ # Examples
100
+ # "RawScaledScorer".tableize #=> "raw_scaled_scorers"
101
+ # "egg_and_ham".tableize #=> "egg_and_hams"
102
+ # "fancyCategory".tableize #=> "fancy_categories"
40
103
  def tableize
41
104
  Inflector.tableize(self)
42
105
  end
43
106
 
107
+ # Create a class name from a table name like Rails does for table names to models.
108
+ # Note that this returns a string and not a Class. (To convert to an actual class
109
+ # follow classify with constantize.)
110
+ #
111
+ # Examples
112
+ # "egg_and_hams".classify #=> "EggAndHam"
113
+ # "post".classify #=> "Post"
44
114
  def classify
45
115
  Inflector.classify(self)
46
116
  end
47
117
 
48
- # Capitalizes the first word and turns underscores into spaces and strips _id, so "employee_salary" becomes "Employee salary"
49
- # and "author_id" becomes "Author".
118
+ # Capitalizes the first word and turns underscores into spaces and strips _id.
119
+ # Like titleize, this is meant for creating pretty output.
120
+ #
121
+ # Examples
122
+ # "employee_salary" #=> "Employee salary"
123
+ # "author_id" #=> "Author"
50
124
  def humanize
51
125
  Inflector.humanize(self)
52
126
  end
53
127
 
128
+ # Creates a foreign key name from a class name.
129
+ # +separate_class_name_and_id_with_underscore+ sets whether
130
+ # the method should put '_' between the name and 'id'.
131
+ #
132
+ # Examples
133
+ # "Message".foreign_key #=> "message_id"
134
+ # "Message".foreign_key(false) #=> "messageid"
135
+ # "Admin::Post".foreign_key #=> "post_id"
54
136
  def foreign_key(separate_class_name_and_id_with_underscore = true)
55
137
  Inflector.foreign_key(self, separate_class_name_and_id_with_underscore)
56
138
  end
57
139
 
140
+ # Constantize tries to find a declared constant with the name specified
141
+ # in the string. It raises a NameError when the name is not in CamelCase
142
+ # or is not initialized.
143
+ #
144
+ # Examples
145
+ # "Module".constantize #=> Module
146
+ # "Class".constantize #=> Class
58
147
  def constantize
59
148
  Inflector.constantize(self)
60
149
  end
@@ -0,0 +1,42 @@
1
+ module ActiveSupport #:nodoc:
2
+ module CoreExtensions #:nodoc:
3
+ module String #:nodoc:
4
+ # Define methods for handeling unicode data.
5
+ module Unicode
6
+ # +chars+ is a Unicode safe proxy for string methods. It creates and returns an instance of the
7
+ # ActiveSupport::Multibyte::Chars class which encapsulates the original string. A Unicode safe version of all
8
+ # the String methods are defined on this proxy class. Undefined methods are forwarded to String, so all of the
9
+ # string overrides can also be called through the +chars+ proxy.
10
+ #
11
+ # name = 'Claus Müller'
12
+ # name.reverse #=> "rell??M sualC"
13
+ # name.length #=> 13
14
+ #
15
+ # name.chars.reverse.to_s #=> "rellüM sualC"
16
+ # name.chars.length #=> 12
17
+ #
18
+ #
19
+ # All the methods on the chars proxy which normally return a string will return a Chars object. This allows
20
+ # method chaining on the result of any of these methods.
21
+ #
22
+ # name.chars.reverse.length #=> 12
23
+ #
24
+ # The Char object tries to be as interchangeable with String objects as possible: sorting and comparing between
25
+ # String and Char work like expected. The bang! methods change the internal string representation in the Chars
26
+ # object. Interoperability problems can be resolved easily with a +to_s+ call.
27
+ #
28
+ # For more information about the methods defined on the Chars proxy see ActiveSupport::Multibyte::Chars and
29
+ # ActiveSupport::Multibyte::Handlers::UTF8Handler
30
+ def chars
31
+ ActiveSupport::Multibyte::Chars.new(self)
32
+ end
33
+
34
+ # Returns true if the string has UTF-8 semantics (a String used for purely byte resources is unlikely to have
35
+ # them), returns false otherwise.
36
+ def is_utf8?
37
+ ActiveSupport::Multibyte::Handlers::UTF8Handler.consumes?(self)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -7,6 +7,6 @@ class Symbol
7
7
  # # The same as people.select { |p| p.manager? }.collect { |p| p.salary }
8
8
  # people.select(&:manager?).collect(&:salary)
9
9
  def to_proc
10
- Proc.new { |obj, *args| obj.send(self, *args) }
10
+ Proc.new { |*args| args.shift.__send__(self, *args) }
11
11
  end
12
12
  end
@@ -3,8 +3,7 @@ module ActiveSupport #:nodoc:
3
3
  module Time #:nodoc:
4
4
  # Enables the use of time calculations within Time itself
5
5
  module Calculations
6
- def self.append_features(base) #:nodoc:
7
- super
6
+ def self.included(base) #:nodoc:
8
7
  base.extend(ClassMethods)
9
8
  end
10
9
 
@@ -26,7 +25,7 @@ module ActiveSupport #:nodoc:
26
25
 
27
26
  # Seconds since midnight: Time.now.seconds_since_midnight
28
27
  def seconds_since_midnight
29
- self.hour.hours + self.min.minutes + self.sec + (self.usec/1.0e+6)
28
+ self.to_i - self.change(:hour => 0).to_i + (self.usec/1.0e+6)
30
29
  end
31
30
 
32
31
  # Returns a new Time where one or more of the elements have been changed according to the +options+ parameter. The time options
@@ -57,13 +56,16 @@ module ActiveSupport #:nodoc:
57
56
  # Returns a new Time representing the time a number of seconds ago, this is basically a wrapper around the Numeric extension
58
57
  # Do not use this method in combination with x.months, use months_ago instead!
59
58
  def ago(seconds)
60
- seconds.until(self)
59
+ self.since(-seconds)
61
60
  end
62
61
 
63
62
  # Returns a new Time representing the time a number of seconds since the instance time, this is basically a wrapper around
64
63
  #the Numeric extension. Do not use this method in combination with x.months, use months_since instead!
65
64
  def since(seconds)
66
- seconds.since(self)
65
+ initial_dst = self.dst? ? 1 : 0
66
+ f = seconds.since(self)
67
+ final_dst = f.dst? ? 1 : 0
68
+ (seconds.abs >= 86400 && initial_dst != final_dst) ? f + (initial_dst - final_dst).hours : f
67
69
  end
68
70
  alias :in :since
69
71
 
@@ -13,8 +13,7 @@ module ActiveSupport #:nodoc:
13
13
  :rfc822 => "%a, %d %b %Y %H:%M:%S %z"
14
14
  }
15
15
 
16
- def self.append_features(klass)
17
- super
16
+ def self.included(klass)
18
17
  klass.send(:alias_method, :to_default_s, :to_s)
19
18
  klass.send(:alias_method, :to_s, :to_formatted_s)
20
19
  end
@@ -21,13 +21,44 @@ module Dependencies #:nodoc:
21
21
  # Should we load files or require them?
22
22
  mattr_accessor :mechanism
23
23
  self.mechanism = :load
24
-
24
+
25
+ # The set of directories from which we may automatically load files. Files
26
+ # under these directories will be reloaded on each request in development mode,
27
+ # unless the directory also appears in load_once_paths.
28
+ mattr_accessor :load_paths
29
+ self.load_paths = []
30
+
31
+ # The set of directories from which automatically loaded constants are loaded
32
+ # only once. All directories in this set must also be present in +load_paths+.
33
+ mattr_accessor :load_once_paths
34
+ self.load_once_paths = []
35
+
36
+ # An array of qualified constant names that have been loaded. Adding a name to
37
+ # this array will cause it to be unloaded the next time Dependencies are cleared.
38
+ mattr_accessor :autoloaded_constants
39
+ self.autoloaded_constants = []
40
+
41
+ # An array of constant names that need to be unloaded on every request. Used
42
+ # to allow arbitrary constants to be marked for unloading.
43
+ mattr_accessor :explicitly_unloadable_constants
44
+ self.explicitly_unloadable_constants = []
45
+
46
+ # Set to true to enable logging of const_missing and file loads
47
+ mattr_accessor :log_activity
48
+ self.log_activity = false
49
+
50
+ # :nodoc:
51
+ # An internal stack used to record which constants are loaded by any block.
52
+ mattr_accessor :constant_watch_stack
53
+ self.constant_watch_stack = []
54
+
25
55
  def load?
26
56
  mechanism == :load
27
57
  end
28
58
 
29
59
  def depend_on(file_name, swallow_load_errors = false)
30
- require_or_load(file_name)
60
+ path = search_for_file(file_name)
61
+ require_or_load(path || file_name)
31
62
  rescue LoadError
32
63
  raise unless swallow_load_errors
33
64
  end
@@ -37,36 +68,318 @@ module Dependencies #:nodoc:
37
68
  end
38
69
 
39
70
  def clear
71
+ log_call
40
72
  loaded.clear
73
+ remove_unloadable_constants!
41
74
  end
42
75
 
43
- def require_or_load(file_name)
76
+ def require_or_load(file_name, const_path = nil)
77
+ log_call file_name, const_path
44
78
  file_name = $1 if file_name =~ /^(.*)\.rb$/
45
- return if loaded.include?(file_name)
79
+ expanded = File.expand_path(file_name)
80
+ return if loaded.include?(expanded)
46
81
 
47
82
  # Record that we've seen this file *before* loading it to avoid an
48
83
  # infinite loop with mutual dependencies.
49
- loaded << file_name
50
-
84
+ loaded << expanded
85
+
51
86
  if load?
87
+ log "loading #{file_name}"
52
88
  begin
53
89
  # Enable warnings iff this file has not been loaded before and
54
90
  # warnings_on_first_load is set.
55
- if !warnings_on_first_load or history.include?(file_name)
56
- load "#{file_name}.rb"
91
+ load_args = ["#{file_name}.rb"]
92
+ load_args << const_path unless const_path.nil?
93
+
94
+ if !warnings_on_first_load or history.include?(expanded)
95
+ result = load_file(*load_args)
57
96
  else
58
- enable_warnings { load "#{file_name}.rb" }
97
+ enable_warnings { result = load_file(*load_args) }
59
98
  end
60
- rescue
61
- loaded.delete file_name
99
+ rescue Exception
100
+ loaded.delete expanded
62
101
  raise
63
102
  end
64
103
  else
65
- require file_name
104
+ log "requiring #{file_name}"
105
+ result = require file_name
66
106
  end
67
107
 
68
108
  # Record history *after* loading so first load gets warnings.
69
- history << file_name
109
+ history << expanded
110
+ return result
111
+ end
112
+
113
+ # Is the provided constant path defined?
114
+ def qualified_const_defined?(path)
115
+ raise NameError, "#{path.inspect} is not a valid constant name!" unless
116
+ /^(::)?([A-Z]\w*)(::[A-Z]\w*)*$/ =~ path
117
+
118
+ names = path.split('::')
119
+ names.shift if names.first.empty?
120
+
121
+ # We can't use defined? because it will invoke const_missing for the parent
122
+ # of the name we are checking.
123
+ names.inject(Object) do |mod, name|
124
+ return false unless mod.const_defined? name
125
+ mod.const_get name
126
+ end
127
+ return true
128
+ end
129
+
130
+ # Given +path+, a filesystem path to a ruby file, return an array of constant
131
+ # paths which would cause Dependencies to attempt to load this file.
132
+ #
133
+ def loadable_constants_for_path(path, bases = load_paths)
134
+ path = $1 if path =~ /\A(.*)\.rb\Z/
135
+ expanded_path = File.expand_path(path)
136
+
137
+ bases.collect do |root|
138
+ expanded_root = File.expand_path(root)
139
+ next unless %r{\A#{Regexp.escape(expanded_root)}(/|\\)} =~ expanded_path
140
+
141
+ nesting = expanded_path[(expanded_root.size)..-1]
142
+ nesting = nesting[1..-1] if nesting && nesting[0] == ?/
143
+ next if nesting.blank?
144
+
145
+ [
146
+ nesting.camelize,
147
+ # Special case: application.rb might define ApplicationControlller.
148
+ ('ApplicationController' if nesting == 'application')
149
+ ]
150
+ end.flatten.compact.uniq
151
+ end
152
+
153
+ # Search for a file in load_paths matching the provided suffix.
154
+ def search_for_file(path_suffix)
155
+ path_suffix = path_suffix + '.rb' unless path_suffix.ends_with? '.rb'
156
+ load_paths.each do |root|
157
+ path = File.join(root, path_suffix)
158
+ return path if File.file? path
159
+ end
160
+ nil # Gee, I sure wish we had first_match ;-)
161
+ end
162
+
163
+ # Does the provided path_suffix correspond to an autoloadable module?
164
+ # Instead of returning a boolean, the autoload base for this module is returned.
165
+ def autoloadable_module?(path_suffix)
166
+ load_paths.each do |load_path|
167
+ return load_path if File.directory? File.join(load_path, path_suffix)
168
+ end
169
+ nil
170
+ end
171
+
172
+ def load_once_path?(path)
173
+ load_once_paths.any? { |base| path.starts_with? base }
174
+ end
175
+
176
+ # Attempt to autoload the provided module name by searching for a directory
177
+ # matching the expect path suffix. If found, the module is created and assigned
178
+ # to +into+'s constants with the name +const_name+. Provided that the directory
179
+ # was loaded from a reloadable base path, it is added to the set of constants
180
+ # that are to be unloaded.
181
+ def autoload_module!(into, const_name, qualified_name, path_suffix)
182
+ return nil unless base_path = autoloadable_module?(path_suffix)
183
+ mod = Module.new
184
+ into.const_set const_name, mod
185
+ autoloaded_constants << qualified_name unless load_once_paths.include?(base_path)
186
+ return mod
187
+ end
188
+
189
+ # Load the file at the provided path. +const_paths+ is a set of qualified
190
+ # constant names. When loading the file, Dependencies will watch for the
191
+ # addition of these constants. Each that is defined will be marked as
192
+ # autoloaded, and will be removed when Dependencies.clear is next called.
193
+ #
194
+ # If the second parameter is left off, then Dependencies will construct a set
195
+ # of names that the file at +path+ may define. See
196
+ # +loadable_constants_for_path+ for more details.
197
+ def load_file(path, const_paths = loadable_constants_for_path(path))
198
+ log_call path, const_paths
199
+ const_paths = [const_paths].compact unless const_paths.is_a? Array
200
+ parent_paths = const_paths.collect { |const_path| /(.*)::[^:]+\Z/ =~ const_path ? $1 : :Object }
201
+
202
+ result = nil
203
+ newly_defined_paths = new_constants_in(*parent_paths) do
204
+ result = load_without_new_constant_marking path
205
+ end
206
+
207
+ autoloaded_constants.concat newly_defined_paths unless load_once_path?(path)
208
+ autoloaded_constants.uniq!
209
+ log "loading #{path} defined #{newly_defined_paths * ', '}" unless newly_defined_paths.empty?
210
+ return result
211
+ end
212
+
213
+ # Return the constant path for the provided parent and constant name.
214
+ def qualified_name_for(mod, name)
215
+ mod_name = to_constant_name mod
216
+ (%w(Object Kernel).include? mod_name) ? name.to_s : "#{mod_name}::#{name}"
217
+ end
218
+
219
+ # Load the constant named +const_name+ which is missing from +from_mod+. If
220
+ # it is not possible to laod the constant into from_mod, try its parent module
221
+ # using const_missing.
222
+ def load_missing_constant(from_mod, const_name)
223
+ log_call from_mod, const_name
224
+ if from_mod == Kernel
225
+ if ::Object.const_defined?(const_name)
226
+ log "Returning Object::#{const_name} for Kernel::#{const_name}"
227
+ return ::Object.const_get(const_name)
228
+ else
229
+ log "Substituting Object for Kernel"
230
+ from_mod = Object
231
+ end
232
+ end
233
+
234
+ # If we have an anonymous module, all we can do is attempt to load from Object.
235
+ from_mod = Object if from_mod.name.empty?
236
+
237
+ unless qualified_const_defined?(from_mod.name) && from_mod.name.constantize.object_id == from_mod.object_id
238
+ raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!"
239
+ end
240
+
241
+ raise ArgumentError, "#{from_mod} is not missing constant #{const_name}!" if from_mod.const_defined?(const_name)
242
+
243
+ qualified_name = qualified_name_for from_mod, const_name
244
+ path_suffix = qualified_name.underscore
245
+ name_error = NameError.new("uninitialized constant #{qualified_name}")
246
+
247
+ file_path = search_for_file(path_suffix)
248
+ if file_path && ! loaded.include?(File.expand_path(file_path)) # We found a matching file to load
249
+ require_or_load file_path
250
+ raise LoadError, "Expected #{file_path} to define #{qualified_name}" unless from_mod.const_defined?(const_name)
251
+ return from_mod.const_get(const_name)
252
+ elsif mod = autoload_module!(from_mod, const_name, qualified_name, path_suffix)
253
+ return mod
254
+ elsif (parent = from_mod.parent) && parent != from_mod &&
255
+ ! from_mod.parents.any? { |p| p.const_defined?(const_name) }
256
+ # If our parents do not have a constant named +const_name+ then we are free
257
+ # to attempt to load upwards. If they do have such a constant, then this
258
+ # const_missing must be due to from_mod::const_name, which should not
259
+ # return constants from from_mod's parents.
260
+ begin
261
+ return parent.const_missing(const_name)
262
+ rescue NameError => e
263
+ raise unless e.missing_name? qualified_name_for(parent, const_name)
264
+ raise name_error
265
+ end
266
+ else
267
+ raise name_error
268
+ end
269
+ end
270
+
271
+ # Remove the constants that have been autoloaded, and those that have been
272
+ # marked for unloading.
273
+ def remove_unloadable_constants!
274
+ autoloaded_constants.each { |const| remove_constant const }
275
+ autoloaded_constants.clear
276
+ explicitly_unloadable_constants.each { |const| remove_constant const }
277
+ end
278
+
279
+ # Determine if the given constant has been automatically loaded.
280
+ def autoloaded?(desc)
281
+ # No name => anonymous module.
282
+ return false if desc.is_a?(Module) && desc.name.blank?
283
+ name = to_constant_name desc
284
+ return false unless qualified_const_defined? name
285
+ return autoloaded_constants.include?(name)
286
+ end
287
+
288
+ # Will the provided constant descriptor be unloaded?
289
+ def will_unload?(const_desc)
290
+ autoloaded?(desc) ||
291
+ explicitly_unloadable_constants.include?(to_constant_name(const_desc))
292
+ end
293
+
294
+ # Mark the provided constant name for unloading. This constant will be
295
+ # unloaded on each request, not just the next one.
296
+ def mark_for_unload(const_desc)
297
+ name = to_constant_name const_desc
298
+ if explicitly_unloadable_constants.include? name
299
+ return false
300
+ else
301
+ explicitly_unloadable_constants << name
302
+ return true
303
+ end
304
+ end
305
+
306
+ # Run the provided block and detect the new constants that were loaded during
307
+ # its execution. Constants may only be regarded as 'new' once -- so if the
308
+ # block calls +new_constants_in+ again, then the constants defined within the
309
+ # inner call will not be reported in this one.
310
+ #
311
+ # If the provided block does not run to completion, and instead raises an
312
+ # exception, any new constants are regarded as being only partially defined
313
+ # and will be removed immediately.
314
+ def new_constants_in(*descs)
315
+ log_call(*descs)
316
+
317
+ # Build the watch frames. Each frame is a tuple of
318
+ # [module_name_as_string, constants_defined_elsewhere]
319
+ watch_frames = descs.collect do |desc|
320
+ if desc.is_a? Module
321
+ mod_name = desc.name
322
+ initial_constants = desc.constants
323
+ elsif desc.is_a?(String) || desc.is_a?(Symbol)
324
+ mod_name = desc.to_s
325
+
326
+ # Handle the case where the module has yet to be defined.
327
+ initial_constants = if qualified_const_defined?(mod_name)
328
+ mod_name.constantize.constants
329
+ else
330
+ []
331
+ end
332
+ else
333
+ raise Argument, "#{desc.inspect} does not describe a module!"
334
+ end
335
+
336
+ [mod_name, initial_constants]
337
+ end
338
+
339
+ constant_watch_stack.concat watch_frames
340
+
341
+ aborting = true
342
+ begin
343
+ yield # Now yield to the code that is to define new constants.
344
+ aborting = false
345
+ ensure
346
+ # Find the new constants.
347
+ new_constants = watch_frames.collect do |mod_name, prior_constants|
348
+ # Module still doesn't exist? Treat it as if it has no constants.
349
+ next [] unless qualified_const_defined?(mod_name)
350
+
351
+ mod = mod_name.constantize
352
+ next [] unless mod.is_a? Module
353
+ new_constants = mod.constants - prior_constants
354
+
355
+ # Make sure no other frames takes credit for these constants.
356
+ constant_watch_stack.each do |frame_name, constants|
357
+ constants.concat new_constants if frame_name == mod_name
358
+ end
359
+
360
+ new_constants.collect do |suffix|
361
+ mod_name == "Object" ? suffix : "#{mod_name}::#{suffix}"
362
+ end
363
+ end.flatten
364
+
365
+ log "New constants: #{new_constants * ', '}"
366
+
367
+ if aborting
368
+ log "Error during loading, removing partially loaded constants "
369
+ new_constants.each { |name| remove_constant name }
370
+ new_constants.clear
371
+ end
372
+ end
373
+
374
+ return new_constants
375
+ ensure
376
+ # Remove the stack frames that we added.
377
+ if defined?(watch_frames) && ! watch_frames.empty?
378
+ frame_ids = watch_frames.collect(&:object_id)
379
+ constant_watch_stack.delete_if do |watch_frame|
380
+ frame_ids.include? watch_frame.object_id
381
+ end
382
+ end
70
383
  end
71
384
 
72
385
  class LoadingModule
@@ -79,6 +392,51 @@ module Dependencies #:nodoc:
79
392
  end
80
393
  end
81
394
  end
395
+
396
+ protected
397
+
398
+ # Convert the provided const desc to a qualified constant name (as a string).
399
+ # A module, class, symbol, or string may be provided.
400
+ def to_constant_name(desc)
401
+ name = case desc
402
+ when String then desc.starts_with?('::') ? desc[2..-1] : desc
403
+ when Symbol then desc.to_s
404
+ when Module
405
+ raise ArgumentError, "Anonymous modules have no name to be referenced by" if desc.name.blank?
406
+ desc.name
407
+ else raise TypeError, "Not a valid constant descriptor: #{desc.inspect}"
408
+ end
409
+ end
410
+
411
+ def remove_constant(const)
412
+ return false unless qualified_const_defined? const
413
+
414
+ const = $1 if /\A::(.*)\Z/ =~ const.to_s
415
+ names = const.split('::')
416
+ if names.size == 1 # It's under Object
417
+ parent = Object
418
+ else
419
+ parent = (names[0..-2] * '::').constantize
420
+ end
421
+
422
+ log "removing constant #{const}"
423
+ parent.send :remove_const, names.last
424
+ return true
425
+ end
426
+
427
+ def log_call(*args)
428
+ arg_str = args.collect(&:inspect) * ', '
429
+ /in `([a-z_\?\!]+)'/ =~ caller(1).first
430
+ selector = $1 || '<unknown>'
431
+ log "called #{selector}(#{arg_str})"
432
+ end
433
+
434
+ def log(msg)
435
+ if defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER && log_activity
436
+ RAILS_DEFAULT_LOGGER.debug "Dependencies: #{msg}"
437
+ end
438
+ end
439
+
82
440
  end
83
441
 
84
442
  Object.send(:define_method, :require_or_load) { |file_name| Dependencies.require_or_load(file_name) } unless Object.respond_to?(:require_or_load)
@@ -92,37 +450,13 @@ class Module #:nodoc:
92
450
  # Use const_missing to autoload associations so we don't have to
93
451
  # require_association when using single-table inheritance.
94
452
  def const_missing(class_id)
95
- file_name = class_id.to_s.demodulize.underscore
96
- file_path = as_load_path.empty? ? file_name : "#{as_load_path}/#{file_name}"
97
- begin
98
- require_dependency(file_path)
99
- brief_name = self == Object ? '' : "#{name}::"
100
- raise NameError.new("uninitialized constant #{brief_name}#{class_id}") unless const_defined?(class_id)
101
- return const_get(class_id)
102
- rescue MissingSourceFile => e
103
- # Re-raise the error if it does not concern the file we were trying to load.
104
- raise unless e.is_missing? file_path
105
-
106
- # Look for a directory in the load path that we ought to load.
107
- if $LOAD_PATH.any? { |base| File.directory? "#{base}/#{file_path}" }
108
- mod = Module.new
109
- const_set class_id, mod # Create the new module
110
- return mod
111
- end
112
-
113
- # Attempt to access the name from the parent, unless we don't have a valid
114
- # parent, or the constant is already defined in the parent. If the latter
115
- # is the case, then we are being queried via self::class_id, and we should
116
- # avoid returning the constant from the parent if possible.
117
- if parent && parent != self && ! parents.any? { |p| p.const_defined?(class_id) }
118
- suppress(NameError) do
119
- return parent.send(:const_missing, class_id)
120
- end
121
- end
122
-
123
- raise NameError.new("uninitialized constant #{class_id}").copy_blame!(e)
124
- end
453
+ Dependencies.load_missing_constant self, class_id
125
454
  end
455
+
456
+ def unloadable(const_desc = self)
457
+ super(const_desc)
458
+ end
459
+
126
460
  end
127
461
 
128
462
  class Class
@@ -130,25 +464,58 @@ class Class
130
464
  if [Object, Kernel].include?(self) || parent == self
131
465
  super
132
466
  else
133
- parent.send :const_missing, class_id
467
+ begin
468
+ begin
469
+ Dependencies.load_missing_constant self, class_id
470
+ rescue NameError
471
+ parent.send :const_missing, class_id
472
+ end
473
+ rescue NameError => e
474
+ # Make sure that the name we are missing is the one that caused the error
475
+ parent_qualified_name = Dependencies.qualified_name_for parent, class_id
476
+ raise unless e.missing_name? parent_qualified_name
477
+ qualified_name = Dependencies.qualified_name_for self, class_id
478
+ raise NameError.new("uninitialized constant #{qualified_name}").copy_blame!(e)
479
+ end
134
480
  end
135
481
  end
136
482
  end
137
483
 
138
484
  class Object #:nodoc:
485
+
486
+ alias_method :load_without_new_constant_marking, :load
487
+
139
488
  def load(file, *extras)
140
- super(file, *extras)
141
- rescue Object => exception
489
+ Dependencies.new_constants_in(Object) { super(file, *extras) }
490
+ rescue Exception => exception # errors from loading file
142
491
  exception.blame_file! file
143
492
  raise
144
493
  end
145
494
 
146
495
  def require(file, *extras)
147
- super(file, *extras)
148
- rescue Object => exception
496
+ Dependencies.new_constants_in(Object) { super(file, *extras) }
497
+ rescue Exception => exception # errors from required file
149
498
  exception.blame_file! file
150
499
  raise
151
500
  end
501
+
502
+ # Mark the given constant as unloadable. Unloadable constants are removed each
503
+ # time dependencies are cleared.
504
+ #
505
+ # Note that marking a constant for unloading need only be done once. Setup
506
+ # or init scripts may list each unloadable constant that may need unloading;
507
+ # each constant will be removed for every subsequent clear, as opposed to for
508
+ # the first clear.
509
+ #
510
+ # The provided constant descriptor may be a (non-anonymous) module or class,
511
+ # or a qualified constant name as a string or symbol.
512
+ #
513
+ # Returns true if the constant was not previously marked for unloading, false
514
+ # otherwise.
515
+ def unloadable(const_desc)
516
+ Dependencies.mark_for_unload const_desc
517
+ end
518
+
152
519
  end
153
520
 
154
521
  # Add file-blaming to exceptions
@@ -163,11 +530,11 @@ class Exception #:nodoc:
163
530
 
164
531
  def describe_blame
165
532
  return nil if blamed_files.empty?
166
- "This error occured while loading the following files:\n #{blamed_files.join "\n "}"
533
+ "This error occurred while loading the following files:\n #{blamed_files.join "\n "}"
167
534
  end
168
535
 
169
536
  def copy_blame!(exc)
170
537
  @blamed_files = exc.blamed_files.clone
171
538
  self
172
539
  end
173
- end
540
+ end