autoloaded 1.2.0 → 1.3.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.
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