autoloaded 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/History.md +4 -0
  4. data/README.md +411 -60
  5. data/autoloaded.gemspec +19 -14
  6. data/lib/autoloaded.rb +104 -91
  7. data/lib/autoloaded/autoloader.rb +260 -0
  8. data/lib/autoloaded/compatibility/refine_and_using.rb +2 -0
  9. data/lib/autoloaded/constant.rb +5 -2
  10. data/lib/autoloaded/deprecation.rb +50 -0
  11. data/lib/autoloaded/inflection.rb +71 -0
  12. data/lib/autoloaded/load_pathed_directory.rb +112 -0
  13. data/lib/autoloaded/refine.rb +7 -1
  14. data/lib/autoloaded/refine/string.rb +7 -0
  15. data/lib/autoloaded/refine/string/to_source_filename.rb +12 -0
  16. data/lib/autoloaded/specification.rb +97 -0
  17. data/lib/autoloaded/specifications.rb +66 -0
  18. data/lib/autoloaded/version.rb +3 -1
  19. data/lib/autoloaded/warning.rb +125 -0
  20. data/spec/autoloaded/autoloader_spec.rb +469 -0
  21. data/spec/autoloaded/constant_spec.rb +0 -2
  22. data/spec/autoloaded/deprecation_spec.rb +23 -0
  23. data/spec/autoloaded/inflection_spec.rb +30 -0
  24. data/spec/autoloaded/load_pathed_directory_spec.rb +120 -0
  25. data/spec/autoloaded/refine/string/to_source_filename_spec.rb +0 -2
  26. data/spec/autoloaded/specification_spec.rb +98 -0
  27. data/spec/autoloaded/specifications_spec.rb +191 -0
  28. data/spec/autoloaded/version_spec.rb +0 -2
  29. data/spec/autoloaded/warning_spec.rb +115 -0
  30. data/spec/autoloaded_macro_sharedspec.rb +24 -0
  31. data/spec/autoloaded_spec.rb +277 -95
  32. data/spec/fixtures/autoloaded_with_conventional_filename.rb +3 -1
  33. data/spec/fixtures/autoloaded_with_conventional_filename/nested.rb +12 -1
  34. data/spec/fixtures/autoloaded_with_conventional_filename/nested/doubly_nested.rb +9 -0
  35. data/spec/fixtures/autoloaded_with_unconventional_filename.rb +12 -0
  36. data/spec/fixtures/autoloaded_with_unconventional_filename/N-est-ed.rb +7 -0
  37. data/spec/fixtures/autoloaded_with_unconventional_filename/nest_ed.rb +1 -0
  38. data/spec/fixtures/autoloaded_with_unconventional_filename/old_school_autoload.rb +5 -0
  39. data/spec/fixtures/not_autoloaded/nested.rb +1 -0
  40. data/spec/fixtures/old_api/autoloaded_with_conventional_filename.rb +10 -0
  41. data/spec/fixtures/old_api/autoloaded_with_conventional_filename/N-est-ed.rb +1 -0
  42. data/spec/fixtures/old_api/autoloaded_with_conventional_filename/nest_ed.rb +1 -0
  43. data/spec/fixtures/old_api/autoloaded_with_conventional_filename/nested.rb +5 -0
  44. data/spec/fixtures/old_api/autoloaded_with_conventional_filename/old_school_autoload.rb +5 -0
  45. data/spec/fixtures/{autoloaded_with_conventional_filename_only.rb → old_api/autoloaded_with_conventional_filename_only.rb} +1 -1
  46. data/spec/fixtures/{autoloaded_with_conventional_filename_only → old_api/autoloaded_with_conventional_filename_only}/nested.rb +0 -0
  47. data/spec/fixtures/{autoloaded_with_conventional_filename_only → old_api/autoloaded_with_conventional_filename_only}/old_school_autoload.rb +0 -0
  48. data/spec/fixtures/{autoloaded_with_unconventional_filenames.rb → old_api/autoloaded_with_unconventional_filenames.rb} +1 -1
  49. data/spec/fixtures/{autoloaded_with_unconventional_filenames → old_api/autoloaded_with_unconventional_filenames}/N-est-ed.rb +0 -0
  50. data/spec/fixtures/{autoloaded_with_unconventional_filenames → old_api/autoloaded_with_unconventional_filenames}/nest_ed.rb +0 -0
  51. data/spec/fixtures/{autoloaded_with_unconventional_filenames → old_api/autoloaded_with_unconventional_filenames}/old_school_autoload.rb +0 -0
  52. data/spec/fixtures/old_api/not_autoloaded.rb +6 -0
  53. data/spec/fixtures/old_api/not_autoloaded/nested.rb +1 -0
  54. data/spec/fixtures/old_api/not_autoloaded/old_school_autoload.rb +5 -0
  55. data/spec/matchers.rb +4 -33
  56. data/spec/spec_helper.rb +2 -0
  57. metadata +95 -41
@@ -1,6 +1,7 @@
1
1
  # Fall back to monkeypatching if refinements are not supported.
2
2
 
3
3
  unless ::Module.private_instance_methods.include?(:refine)
4
+ # @api private
4
5
  class ::Module
5
6
 
6
7
  private
@@ -14,4 +15,5 @@ end
14
15
 
15
16
  unless private_methods.include?(:using)
16
17
  def using(*arguments); end
18
+ private :using
17
19
  end
@@ -1,11 +1,14 @@
1
- require 'autoloaded/refine/string/to_source_filename'
2
1
  require 'set'
3
2
 
4
3
  using ::Autoloaded::Refine::String::ToSourceFilename
5
4
 
6
5
  module Autoloaded; end
7
6
 
8
- # @private
7
+ # Represents a Ruby constant.
8
+ #
9
+ # @since 0.0.1
10
+ #
11
+ # @api private
9
12
  class Autoloaded::Constant
10
13
 
11
14
  attr_reader :name
@@ -0,0 +1,50 @@
1
+ module Autoloaded; end
2
+
3
+ # Prints deprecation messages to _stderr_.
4
+ #
5
+ # @since 1.3
6
+ #
7
+ # @api private
8
+ module Autoloaded::Deprecation
9
+
10
+ class << self
11
+
12
+ # Sets the deprecation stream.
13
+ #
14
+ # @param [IO] value a new value for #io
15
+ attr_writer :io
16
+
17
+ # The deprecation stream. Defaults to +$stderr+.
18
+ #
19
+ # @return [IO] the deprecation stream
20
+ def io
21
+ @io || $stderr
22
+ end
23
+
24
+ # Prints a deprecation message to _#io_ regarding the specified
25
+ # _deprecated_usage_.
26
+ #
27
+ # @param [String] deprecated_usage API usage that is soon to be discontinued
28
+ # @param [String] sanctioned_usage API usage that will succeed
29
+ # _deprecated_usage_
30
+ # @param [String] source_filename the file path of the source invoking the
31
+ # deprecated API
32
+ #
33
+ # @return [Module] _Deprecation_
34
+ def deprecate(deprecated_usage: raise(::ArgumentError,
35
+ 'missing keyword: deprecated_usage'),
36
+ sanctioned_usage: raise(::ArgumentError,
37
+ 'missing keyword: sanctioned_usage'),
38
+ source_filename: raise(::ArgumentError,
39
+ 'missing keyword: source_filename'))
40
+ deprecation = "\e[33m*** \e[7m DEPRECATED \e[0m " +
41
+ "\e[4m#{deprecated_usage}\e[0m -- use " +
42
+ "\e[4m#{sanctioned_usage}\e[0m instead in #{source_filename}"
43
+ io.puts deprecation
44
+
45
+ self
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,71 @@
1
+ module Autoloaded; end
2
+
3
+ # Translates source filenames into constants.
4
+ #
5
+ # @since 1.3
6
+ #
7
+ # @api private
8
+ module Autoloaded::Inflection
9
+
10
+ # Translates a _String_ representing a source filename into a _Symbol_
11
+ # representing a constant.
12
+ #
13
+ # @param [String] source_filename the name of a source code file
14
+ #
15
+ # @return [Symbol] the name of a constant corresponding to _source_filename_
16
+ #
17
+ # @raise [ArgumentError] _source_filename_ is +nil+ or empty
18
+ #
19
+ # @note Directories are ignored rather than translated into namespaces.
20
+ def self.to_constant_name(source_filename)
21
+ source_filename = source_filename.to_s
22
+ raise(::ArgumentError, "can't be blank") if source_filename.empty?
23
+
24
+ translate(source_filename, *%i(file_basename
25
+ camelize_if_lowercase
26
+ nonalphanumeric_to_underscore
27
+ delete_leading_nonalphabetic
28
+ capitalize_first)).to_sym
29
+ end
30
+
31
+ private
32
+
33
+ def self.camelize(string)
34
+ string.gsub(/(.)(?:_|-)+(.)/) do |match|
35
+ "#{match[0..0].downcase}#{match[-1..-1].upcase}"
36
+ end
37
+ end
38
+
39
+ def self.camelize_if_lowercase(string)
40
+ return string unless lowercase?(string)
41
+
42
+ camelize string
43
+ end
44
+
45
+ def self.capitalize_first(string)
46
+ "#{string[0..0].upcase}#{string[1..-1]}"
47
+ end
48
+
49
+ def self.delete_leading_nonalphabetic(string)
50
+ string.gsub(/^[^a-z]+/i, '')
51
+ end
52
+
53
+ def self.file_basename(string)
54
+ ::File.basename string, ::File.extname(string)
55
+ end
56
+
57
+ def self.lowercase?(string)
58
+ string == string.downcase
59
+ end
60
+
61
+ def self.nonalphanumeric_to_underscore(string)
62
+ string.gsub(/[^a-z0-9]+/i, '_')
63
+ end
64
+
65
+ def self.translate(string, *translations)
66
+ translations.inject string do |result, translation|
67
+ send translation, result
68
+ end
69
+ end
70
+
71
+ end
@@ -0,0 +1,112 @@
1
+ require 'pathname'
2
+
3
+ module Autoloaded; end
4
+
5
+ # Enumerates the source files in a directory, relativizing their paths using the
6
+ # Ruby load path.
7
+ #
8
+ # @since 1.3
9
+ #
10
+ # @api private
11
+ class Autoloaded::LoadPathedDirectory
12
+
13
+ # The file extension of source files.
14
+ SOURCE_FILE_EXTENSION = '.rb'
15
+
16
+ private
17
+
18
+ def self.join(path1, path2)
19
+ paths = [path1, path2].reject do |path|
20
+ path.to_s.empty?
21
+ end
22
+ (paths.length < 2) ? paths.first : ::File.join(*paths)
23
+ end
24
+
25
+ def self.ruby_load_paths
26
+ $:
27
+ end
28
+
29
+ def self.source_basename(source_filename)
30
+ ::File.basename source_filename, SOURCE_FILE_EXTENSION
31
+ end
32
+
33
+ public
34
+
35
+ # The full path of a source directory.
36
+ #
37
+ # @return [String] a directory path
38
+ attr_reader :path
39
+
40
+ # Constructs a new _LoadPathedDirectory_ with the specified _path_.
41
+ #
42
+ # @param [String] path the value of _#path_
43
+ #
44
+ # @raise [ArgumentError] _path_ is +nil+ or a relative path
45
+ def initialize(path)
46
+ raise ::ArgumentError, "can't be nil" if path.nil?
47
+
48
+ @path = path.dup.freeze
49
+ if ::Pathname.new(@path).relative?
50
+ raise ::ArgumentError, "can't be relative"
51
+ end
52
+ end
53
+
54
+ # Enumerates the source files in _#path_, relativizing their paths if possible
55
+ # using the longest applicable entry in the Ruby load path (that is,
56
+ # <tt>$:</tt>). File names are rendered without _SOURCE_FILE_EXTENSION_.
57
+ # Yielded paths are guaranteed usable in +require+ statements unless elements
58
+ # of the Ruby load path are removed or changed.
59
+ #
60
+ # @yield [String] each source file in _#path_, formatted for use in +require+
61
+ # statements
62
+ #
63
+ # @return [LoadPathedDirectory] the _LoadPathedDirectory_
64
+ #
65
+ # @see #path
66
+ # @see http://ruby-doc.org/core/Kernel.html#method-i-require Kernel#require
67
+ def each_source_filename
68
+ if (ruby_load_path = closest_ruby_load_path)
69
+ ::Dir.chdir ruby_load_path do
70
+ glob = self.class.join(path_from(ruby_load_path),
71
+ "*#{SOURCE_FILE_EXTENSION}")
72
+ ::Dir.glob glob do |file|
73
+ yield without_source_file_extension(file)
74
+ end
75
+ end
76
+ else
77
+ glob = self.class.join(path, "*#{SOURCE_FILE_EXTENSION}")
78
+ ::Dir.glob glob do |file|
79
+ yield without_source_file_extension(file)
80
+ end
81
+ end
82
+
83
+ self
84
+ end
85
+
86
+ private
87
+
88
+ def closest_ruby_load_path
89
+ closest = self.class.ruby_load_paths.inject(score: 0) do |close, load_path|
90
+ score = path.length - path_from(load_path).length
91
+ (close[:score] < score) ? {score: score, load_path: load_path} : close
92
+ end
93
+ closest[:load_path]
94
+ end
95
+
96
+ def path_from(other_path)
97
+ # Don't use Pathname#relative_path_from because we want to avoid introducing
98
+ # double dots. The intent is to render the path as relative, if and only if
99
+ # it is a subdirectory of 'other_path'.
100
+ pattern = /^#{::Regexp.escape other_path.chomp(::File::SEPARATOR)}#{::Regexp.escape ::File::SEPARATOR}?/
101
+ path.gsub pattern, ''
102
+ end
103
+
104
+ def without_source_file_extension(path)
105
+ if (dirname = ::File.dirname(path)) == '.'
106
+ dirname = nil
107
+ end
108
+ basename = ::File.basename(path, SOURCE_FILE_EXTENSION)
109
+ self.class.join dirname, basename
110
+ end
111
+
112
+ end
@@ -1,6 +1,12 @@
1
1
  module Autoloaded
2
2
 
3
- # @private
3
+ # Contains Ruby Core Library type refinements.
4
+ #
5
+ # @see http://ruby-doc.org/core Ruby Core Library
6
+ #
7
+ # @since 0.0.1
8
+ #
9
+ # @api private
4
10
  module Refine
5
11
 
6
12
  autoload :String, 'autoloaded/refine/string'
@@ -2,6 +2,13 @@ module Autoloaded
2
2
 
3
3
  module Refine
4
4
 
5
+ # Contains _String_ refinements.
6
+ #
7
+ # @see http://ruby-doc.org/core/String.html String
8
+ #
9
+ # @since 0.0.1
10
+ #
11
+ # @api private
5
12
  module String
6
13
 
7
14
  autoload :ToSourceFilename, 'autoloaded/refine/string/to_source_filename'
@@ -10,8 +10,20 @@ module Autoloaded
10
10
 
11
11
  end
12
12
 
13
+ # Refines _String_ to translate a constant name into a source filename.
14
+ #
15
+ # @since 0.0.1
16
+ #
17
+ # @api private
13
18
  module Autoloaded::Refine::String::ToSourceFilename
14
19
 
20
+ # @!method to_source_filename
21
+ # Translates the name of a constant into the name of a source file.
22
+ #
23
+ # @return [String] the name of a source file corresponding to the name of a
24
+ # constant
25
+ #
26
+ # @note Namespaces are ignored rather than translated into directories.
15
27
  refine ::String do
16
28
  def replace_nonalphanumeric_sequence_with_separator
17
29
  gsub(/[^a-z0-9]+/i, separator.to_s)
@@ -0,0 +1,97 @@
1
+ module Autoloaded
2
+
3
+ # Describes regulations for autoloading.
4
+ #
5
+ # @since 1.3
6
+ #
7
+ # @api private
8
+ class Specification
9
+
10
+ # The elements of the specification.
11
+ #
12
+ # @return [Array]
13
+ attr_reader :elements
14
+
15
+ # Constructs a new _Specification_ with the specified _elements_.
16
+ #
17
+ # @param elements the value of _#elements_
18
+ #
19
+ # Valid arguments include:
20
+ #
21
+ # * _Symbol_ values
22
+ # * _Array_ values comprising _Symbol_ values
23
+ # * _Hash_ values comprising _Symbol_, _String_, and/or _Array_ values
24
+ # described above, which will autoload specified constants from their
25
+ # associated source files
26
+ # * Any combination of the options described above
27
+ #
28
+ # @see #elements
29
+ def initialize(*elements)
30
+ @elements = elements
31
+ end
32
+
33
+ # Compares the _Specification_ with the specified _other_object_.
34
+ #
35
+ # @param other_object another object
36
+ #
37
+ # @return [true] if _other_object_ is equal to the _Specification_
38
+ # @return [false] if _other_object_ is not equal to the _Specification_
39
+ def ==(other_object)
40
+ return false unless other_object.kind_of?(self.class)
41
+
42
+ other_object.elements == elements
43
+ end
44
+
45
+ # Provides a matching constant from _#elements_ for the specified
46
+ # _source_filename_.
47
+ #
48
+ # @param [String] source_filename the name of a source file
49
+ #
50
+ # @return [Symbol] a matching constant
51
+ # @return [Array of Symbol] matching constants
52
+ # @return [nil] if there is no matching constant
53
+ #
54
+ # @see #elements
55
+ def match(source_filename)
56
+ matched = elements.detect do |element|
57
+ if element.kind_of?(::Hash)
58
+ element.each do |key, value|
59
+ return value if source_filename_match?(source_filename, key)
60
+
61
+ return key if source_filename_match?(source_filename, value)
62
+ end
63
+ false
64
+ else
65
+ source_filename_match? source_filename, element
66
+ end
67
+ end
68
+
69
+ matched.kind_of?(::String) ? Inflection.to_constant_name(matched) : matched
70
+ end
71
+
72
+ private
73
+
74
+ def source_filename_match?(file, array_or_file_or_constant)
75
+ case array_or_file_or_constant
76
+ when ::Array
77
+ array_or_file_or_constant.detect do |o|
78
+ source_filename_match? file, o
79
+ end
80
+ when ::Symbol
81
+ source_filename_match_constant_name? file, array_or_file_or_constant
82
+ else
83
+ source_filename_match_filename? file, array_or_file_or_constant
84
+ end
85
+ end
86
+
87
+ def source_filename_match_constant_name?(file, constant)
88
+ Inflection.to_constant_name(file).to_s.casecmp(constant.to_s).zero?
89
+ end
90
+
91
+ def source_filename_match_filename?(file1, file2)
92
+ file1.to_s.casecmp(file2.to_s).zero?
93
+ end
94
+
95
+ end
96
+
97
+ end
@@ -0,0 +1,66 @@
1
+ module Autoloaded; end
2
+
3
+ # Holds regulations for autoloading.
4
+ #
5
+ # @since 1.3
6
+ #
7
+ # @api private
8
+ class Autoloaded::Specifications
9
+
10
+ # @!method except
11
+ # Specifications for excluding source files from being autoloaded.
12
+ #
13
+ # @return [Array of Specification] a list of specifications
14
+ #
15
+ # @api private
16
+ #
17
+ # @!method only
18
+ # Specifications for narrowing the set of source files being autoloaded as
19
+ # well as optionally renaming and/or reorganizing their corresponding
20
+ # constants.
21
+ #
22
+ # @return [Array of Specification] a list of specifications
23
+ #
24
+ # @api private
25
+ #
26
+ # @!method with
27
+ # Specifications for renaming and/or reorganizing the constants corresponding
28
+ # to source files being autoloaded.
29
+ #
30
+ # @return [Array of Specification] a list of specifications
31
+ #
32
+ # @api private
33
+ %i(except only with).each do |attribute_name|
34
+ define_method attribute_name do
35
+ variable_name = "@#{attribute_name}"
36
+ (instance_variable_get(variable_name) || []).tap do |value|
37
+ instance_variable_set variable_name, value
38
+ end
39
+ end
40
+ end
41
+
42
+ # Evaluates the specifications for conflicts, in reference to the specified
43
+ # _attribute_.
44
+ #
45
+ # @param [Symbol] attribute the attribute (+:except+, +:only+, or +:with+)
46
+ # being modified
47
+ #
48
+ # @return [Specifications] the _Specifications_
49
+ #
50
+ # @raise [RuntimeError] _attribute_ is +:except+ and _#only_ is not empty
51
+ # @raise [RuntimeError] _attribute_ is +:only+ and _#except_ is not empty
52
+ #
53
+ # @see #except
54
+ # @see #only
55
+ def validate!(attribute)
56
+ other_attribute = {except: :only, only: :except}[attribute]
57
+ if other_attribute
58
+ unless send(attribute).empty? || send(other_attribute).empty?
59
+ raise "can't specify `#{attribute}' when `#{other_attribute}' is " +
60
+ 'already specified'
61
+ end
62
+ end
63
+
64
+ self
65
+ end
66
+ end