autoloaded 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|