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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/History.md +4 -0
- data/README.md +411 -60
- data/autoloaded.gemspec +19 -14
- data/lib/autoloaded.rb +104 -91
- data/lib/autoloaded/autoloader.rb +260 -0
- data/lib/autoloaded/compatibility/refine_and_using.rb +2 -0
- data/lib/autoloaded/constant.rb +5 -2
- data/lib/autoloaded/deprecation.rb +50 -0
- data/lib/autoloaded/inflection.rb +71 -0
- data/lib/autoloaded/load_pathed_directory.rb +112 -0
- data/lib/autoloaded/refine.rb +7 -1
- data/lib/autoloaded/refine/string.rb +7 -0
- data/lib/autoloaded/refine/string/to_source_filename.rb +12 -0
- data/lib/autoloaded/specification.rb +97 -0
- data/lib/autoloaded/specifications.rb +66 -0
- data/lib/autoloaded/version.rb +3 -1
- data/lib/autoloaded/warning.rb +125 -0
- data/spec/autoloaded/autoloader_spec.rb +469 -0
- data/spec/autoloaded/constant_spec.rb +0 -2
- data/spec/autoloaded/deprecation_spec.rb +23 -0
- data/spec/autoloaded/inflection_spec.rb +30 -0
- data/spec/autoloaded/load_pathed_directory_spec.rb +120 -0
- data/spec/autoloaded/refine/string/to_source_filename_spec.rb +0 -2
- data/spec/autoloaded/specification_spec.rb +98 -0
- data/spec/autoloaded/specifications_spec.rb +191 -0
- data/spec/autoloaded/version_spec.rb +0 -2
- data/spec/autoloaded/warning_spec.rb +115 -0
- data/spec/autoloaded_macro_sharedspec.rb +24 -0
- data/spec/autoloaded_spec.rb +277 -95
- data/spec/fixtures/autoloaded_with_conventional_filename.rb +3 -1
- data/spec/fixtures/autoloaded_with_conventional_filename/nested.rb +12 -1
- data/spec/fixtures/autoloaded_with_conventional_filename/nested/doubly_nested.rb +9 -0
- data/spec/fixtures/autoloaded_with_unconventional_filename.rb +12 -0
- data/spec/fixtures/autoloaded_with_unconventional_filename/N-est-ed.rb +7 -0
- data/spec/fixtures/autoloaded_with_unconventional_filename/nest_ed.rb +1 -0
- data/spec/fixtures/autoloaded_with_unconventional_filename/old_school_autoload.rb +5 -0
- data/spec/fixtures/not_autoloaded/nested.rb +1 -0
- data/spec/fixtures/old_api/autoloaded_with_conventional_filename.rb +10 -0
- data/spec/fixtures/old_api/autoloaded_with_conventional_filename/N-est-ed.rb +1 -0
- data/spec/fixtures/old_api/autoloaded_with_conventional_filename/nest_ed.rb +1 -0
- data/spec/fixtures/old_api/autoloaded_with_conventional_filename/nested.rb +5 -0
- data/spec/fixtures/old_api/autoloaded_with_conventional_filename/old_school_autoload.rb +5 -0
- data/spec/fixtures/{autoloaded_with_conventional_filename_only.rb → old_api/autoloaded_with_conventional_filename_only.rb} +1 -1
- data/spec/fixtures/{autoloaded_with_conventional_filename_only → old_api/autoloaded_with_conventional_filename_only}/nested.rb +0 -0
- data/spec/fixtures/{autoloaded_with_conventional_filename_only → old_api/autoloaded_with_conventional_filename_only}/old_school_autoload.rb +0 -0
- data/spec/fixtures/{autoloaded_with_unconventional_filenames.rb → old_api/autoloaded_with_unconventional_filenames.rb} +1 -1
- data/spec/fixtures/{autoloaded_with_unconventional_filenames → old_api/autoloaded_with_unconventional_filenames}/N-est-ed.rb +0 -0
- data/spec/fixtures/{autoloaded_with_unconventional_filenames → old_api/autoloaded_with_unconventional_filenames}/nest_ed.rb +0 -0
- data/spec/fixtures/{autoloaded_with_unconventional_filenames → old_api/autoloaded_with_unconventional_filenames}/old_school_autoload.rb +0 -0
- data/spec/fixtures/old_api/not_autoloaded.rb +6 -0
- data/spec/fixtures/old_api/not_autoloaded/nested.rb +1 -0
- data/spec/fixtures/old_api/not_autoloaded/old_school_autoload.rb +5 -0
- data/spec/matchers.rb +4 -33
- data/spec/spec_helper.rb +2 -0
- 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
|
data/lib/autoloaded/constant.rb
CHANGED
@@ -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
|
-
#
|
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
|
data/lib/autoloaded/refine.rb
CHANGED
@@ -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
|