bond 0.4.2-java

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 (46) hide show
  1. data/.gemspec +28 -0
  2. data/.travis.yml +8 -0
  3. data/CHANGELOG.rdoc +91 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.rdoc +242 -0
  6. data/Rakefile +47 -0
  7. data/lib/bond.rb +127 -0
  8. data/lib/bond/agent.rb +108 -0
  9. data/lib/bond/completion.rb +16 -0
  10. data/lib/bond/completions/activerecord.rb +12 -0
  11. data/lib/bond/completions/array.rb +1 -0
  12. data/lib/bond/completions/bond.rb +6 -0
  13. data/lib/bond/completions/hash.rb +3 -0
  14. data/lib/bond/completions/kernel.rb +15 -0
  15. data/lib/bond/completions/module.rb +10 -0
  16. data/lib/bond/completions/object.rb +21 -0
  17. data/lib/bond/completions/struct.rb +1 -0
  18. data/lib/bond/input.rb +28 -0
  19. data/lib/bond/m.rb +146 -0
  20. data/lib/bond/mission.rb +151 -0
  21. data/lib/bond/missions/anywhere_mission.rb +15 -0
  22. data/lib/bond/missions/default_mission.rb +21 -0
  23. data/lib/bond/missions/method_mission.rb +197 -0
  24. data/lib/bond/missions/object_mission.rb +44 -0
  25. data/lib/bond/missions/operator_method_mission.rb +27 -0
  26. data/lib/bond/rc.rb +48 -0
  27. data/lib/bond/readline.rb +38 -0
  28. data/lib/bond/readlines/jruby.rb +13 -0
  29. data/lib/bond/readlines/rawline.rb +15 -0
  30. data/lib/bond/readlines/ruby.rb +9 -0
  31. data/lib/bond/search.rb +74 -0
  32. data/lib/bond/version.rb +3 -0
  33. data/test/agent_test.rb +235 -0
  34. data/test/anywhere_mission_test.rb +34 -0
  35. data/test/bond_test.rb +141 -0
  36. data/test/completion_test.rb +148 -0
  37. data/test/completions_test.rb +98 -0
  38. data/test/deps.rip +4 -0
  39. data/test/m_test.rb +34 -0
  40. data/test/method_mission_test.rb +246 -0
  41. data/test/mission_test.rb +51 -0
  42. data/test/object_mission_test.rb +59 -0
  43. data/test/operator_method_mission_test.rb +66 -0
  44. data/test/search_test.rb +140 -0
  45. data/test/test_helper.rb +69 -0
  46. metadata +167 -0
@@ -0,0 +1,15 @@
1
+ # A mission which completes anywhere i.e. even after non word break characters
2
+ # such as '[' or '}'. With options :prefix and :anywhere, this mission matches
3
+ # on the following regexp condition /:prefix?(:anywhere)$/ and passes the first
4
+ # capture group to the mission action.
5
+ class Bond::AnywhereMission < Bond::Mission
6
+ def initialize(options={}) #@private
7
+ options[:on] = Regexp.new("#{options[:prefix]}(#{options[:anywhere]})$")
8
+ super
9
+ end
10
+
11
+ def after_match(input) #@private
12
+ @completion_prefix = input.to_s.sub(/#{Regexp.escape(@matched[1])}$/, '')
13
+ create_input @matched[1]
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ # This is the mission called when none of the others match.
2
+ class Bond::DefaultMission < Bond::Mission
3
+ ReservedWords = [
4
+ "BEGIN", "END", "alias", "and", "begin", "break", "case", "class", "def", "defined", "do", "else", "elsif", "end", "ensure",
5
+ "false", "for", "if", "in", "module", "next", "nil", "not", "or", "redo", "rescue", "retry", "return", "self", "super",
6
+ "then", "true", "undef", "unless", "until", "when", "while", "yield"
7
+ ]
8
+
9
+
10
+ # Default action which generates methods, private methods, reserved words, local variables and constants.
11
+ def self.completions(input=nil)
12
+ Bond::Mission.current_eval("methods | private_methods | local_variables | " +
13
+ "self.class.constants | instance_variables") | ReservedWords
14
+ end
15
+
16
+ def initialize(options={}) #@private
17
+ options[:action] ||= self.class.method(:completions)
18
+ super
19
+ end
20
+ def default_on; end #@private
21
+ end
@@ -0,0 +1,197 @@
1
+ module Bond
2
+ # A mission which completes arguments for any module/class method that isn't an operator method.
3
+ # To create this mission or OperatorMethodMission, :method or :methods must be passed to Bond.complete.
4
+ # A completion for a given module/class effects any object that has it as an ancestor. If an object
5
+ # has two ancestors that have completions for the same method, the ancestor closer to the object is
6
+ # picked. For example, if Array#collect and Enumerable#collect have completions, argument completion on
7
+ # '[].collect ' would use Array#collect.
8
+ #
9
+ # ==== Bond.complete Options:
10
+ # [:action] If a string, value is assumed to be a :method and that method's action is copied.
11
+ # Otherwise defaults to normal :action behavior.
12
+ # [:search] If :action is a :method string, defaults to copying its search.
13
+ # Otherwise defaults to normal :search behavior.
14
+ # [:name, :place] These options aren't supported by a MethodMission/OperatorMethodMission completion.
15
+ # ==== Examples:
16
+ # Bond.complete(:methods => %w{delete index rindex}, :class => "Array#") {|e| e.object }
17
+ # Bond.complete(:method => "Hash#index") {|e| e.object.values }
18
+ #
19
+ # ==== Argument Format
20
+ # All method arguments can autocomplete as symbols or strings and the first argument can be prefixed
21
+ # with '(':
22
+ # >> Bond.complete(:method => 'example') { %w{some example eh} }
23
+ # => true
24
+ # >> example '[TAB]
25
+ # eh example some
26
+ # >> example :[TAB]
27
+ # :eh :example :some
28
+ #
29
+ # >> example("[TAB]
30
+ # eh example some
31
+ #
32
+ # ==== Multiple Arguments
33
+ # Every time a comma appears after a method, Bond starts a new completion. This allows a method to
34
+ # complete multiple arguments as well as complete keys for a hash. *Each* argument can be have a unique
35
+ # set of completions since a completion action is aware of what argument it is currently completing:
36
+ # >> Bond.complete(:method => 'FileUtils.chown') {|e|
37
+ # e.argument > 3 ? %w{noop verbose} : %w{root admin me} }
38
+ # => true
39
+ # >> FileUtils.chown 'r[TAB]
40
+ # >> FileUtils.chown 'root'
41
+ # >> FileUtils.chown 'root', 'a[TAB]
42
+ # >> FileUtils.chown 'root', 'admin'
43
+ # >> FileUtils.chown 'root', 'admin', 'some_file', :v[TAB]
44
+ # >> FileUtils.chown 'root', 'admin', 'some_file', :verbose
45
+ # >> FileUtils.chown 'root', 'admin', 'some_file', :verbose => true
46
+ #
47
+ # ==== Developer Notes
48
+ # Unlike other missions, creating these missions with Bond.complete doesn't add more completion rules
49
+ # for an Agent to look through. Instead, all :method(s) completions are handled by one MethodMission
50
+ # object which looks them up with its own hashes. In the same way, all operator methods are
51
+ # handled by one OperatorMethodMission object.
52
+ class MethodMission < Bond::Mission
53
+ class<<self
54
+ # Hash of instance method completions which maps methods to hashes of modules to arrays ([action, search])
55
+ attr_accessor :actions
56
+ # Same as :actions but for class methods
57
+ attr_accessor :class_actions
58
+ # Stores last search result from MethodMission.find
59
+ attr_accessor :last_find
60
+ # Stores class from last search in MethodMission.find
61
+ attr_accessor :last_class
62
+
63
+ # Creates a method action given the same options as Bond.complete
64
+ def create(options)
65
+ if options[:action].is_a?(String)
66
+ klass, klass_meth = split_method(options[:action])
67
+ if (arr = (current_actions(options[:action])[klass_meth] || {})[klass])
68
+ options[:action], options[:search] = [arr[0], options[:search] || arr[1]]
69
+ else
70
+ raise InvalidMissionError, "string :action"
71
+ end
72
+ end
73
+
74
+ raise InvalidMissionError, "array :method" if options[:method].is_a?(Array)
75
+ meths = options[:methods] || Array(options[:method])
76
+ raise InvalidMissionError, "non-string :method(s)" unless meths.all? {|e| e.is_a?(String) }
77
+ if options[:class].is_a?(String)
78
+ options[:class] << '#' unless options[:class][/[#.]$/]
79
+ meths.map! {|e| options[:class] + e }
80
+ end
81
+
82
+ meths.each {|meth|
83
+ klass, klass_meth = split_method(meth)
84
+ (current_actions(meth)[klass_meth] ||= {})[klass] = [options[:action], options[:search]].compact
85
+ }
86
+ nil
87
+ end
88
+
89
+ # Resets all instance and class method actions.
90
+ def reset
91
+ @actions = {}
92
+ @class_actions = {}
93
+ end
94
+
95
+ # Lists method names
96
+ def action_methods
97
+ (actions.keys + class_actions.keys).uniq
98
+ end
99
+
100
+ # Lists full method names, prefixed with class/module
101
+ def all_methods
102
+ (class_actions.map {|m,h| h.map {|k,v| "#{k}.#{m}" } } +
103
+ actions.map {|m,h| h.map {|k,v| "#{k}##{m}" } }).flatten.sort
104
+ end
105
+
106
+ # Returns the first completion by looking up the object's ancestors and finding the closest
107
+ # one that has a completion definition for the given method. Completion is returned
108
+ # as an array containing action proc and optional search to go with it.
109
+ def find(obj, meth)
110
+ last_find = find_with(obj, meth, :<=, @class_actions) if obj.is_a?(Module)
111
+ last_find = find_with(obj, meth, :is_a?, @actions) unless last_find
112
+ @last_class = last_find.is_a?(Array) ? last_find[0] : nil
113
+ @last_find = last_find ? last_find[1] : last_find
114
+ end
115
+
116
+ # Returns a constant like Module#const_get no matter what namespace it's nested in.
117
+ # Returns nil if the constant is not found.
118
+ def any_const_get(name)
119
+ return name if name.is_a?(Module)
120
+ klass = Object
121
+ name.split('::').each {|e| klass = klass.const_get(e) }
122
+ klass
123
+ rescue
124
+ nil
125
+ end
126
+
127
+ protected
128
+ def current_actions(meth)
129
+ meth.include?('.') ? @class_actions : @actions
130
+ end
131
+
132
+ def split_method(meth)
133
+ meth = "Kernel##{meth}" if !meth.to_s[/[.#]/]
134
+ meth.split(/[.#]/,2)
135
+ end
136
+
137
+ def find_with(obj, meth, find_meth, actions)
138
+ (actions[meth] || {}).select {|k,v| get_class(k) }.
139
+ sort {|a,b| get_class(a[0]) <=> get_class(b[0]) || -1 }.
140
+ find {|k,v| obj.send(find_meth, get_class(k)) }
141
+ end
142
+
143
+ def get_class(klass)
144
+ (@klasses ||= {})[klass] ||= any_const_get(klass)
145
+ end
146
+ end
147
+
148
+ self.reset
149
+ OBJECTS = Mission::OBJECTS + %w{\S*?}
150
+ CONDITION = %q{(OBJECTS)\.?(METHODS)(?:\s+|\()(['":])?(.*)$}
151
+
152
+ def match_message #@private
153
+ "Matches completion for method '#{@meth}' in '#{MethodMission.last_class}'."
154
+ end
155
+
156
+ protected
157
+ def do_match(input)
158
+ (@on = default_on) && super && eval_object(@matched[1] ? @matched[1] : 'self') &&
159
+ MethodMission.find(@evaled_object, @meth = matched_method)
160
+ end
161
+
162
+ def default_on
163
+ Regexp.new condition_with_objects.sub('METHODS',Regexp.union(*current_methods).to_s)
164
+ end
165
+
166
+ def current_methods
167
+ self.class.action_methods - OPERATORS
168
+ end
169
+
170
+ def default_action
171
+ MethodMission.last_find[0]
172
+ end
173
+
174
+ def matched_method
175
+ @matched[2]
176
+ end
177
+
178
+ def set_action_and_search
179
+ @action = default_action
180
+ @search = MethodMission.last_find[1] || Search.default_search
181
+ end
182
+
183
+ def after_match(input)
184
+ set_action_and_search
185
+ @completion_prefix, typed = @matched[3], @matched[-1]
186
+ input_options = {:object => @evaled_object, :argument => 1+typed.count(','),
187
+ :arguments => (@completion_prefix.to_s+typed).split(/\s*,\s*/) }
188
+ if typed.to_s.include?(',') && (match = typed.match(/(.*?\s*)([^,]*)$/))
189
+ typed = match[2]
190
+ typed.sub!(/^(['":])/,'')
191
+ @completion_prefix = typed.empty? ? '' : "#{@matched[3]}#{match[1]}#{$1}"
192
+ end
193
+ create_input typed, input_options
194
+ end
195
+
196
+ end
197
+ end
@@ -0,0 +1,44 @@
1
+ # A mission which completes an object's methods. For this mission to match, the
2
+ # condition must match and the current object must have an ancestor that matches
3
+ # :object. Note: To access to the current object being completed on within an
4
+ # action, use the input's object attribute.
5
+ #
6
+ # ==== Bond.complete Options:
7
+ # [:action] If an action is not specified, the default action is to complete an
8
+ # object's non-operator methods.
9
+ #
10
+ # ===== Example:
11
+ # Bond.complete(:object => 'ActiveRecord::Base') {|input| input.object.class.instance_methods(false) }
12
+ class Bond::ObjectMission < Bond::Mission
13
+ OBJECTS = %w<\S+> + Bond::Mission::OBJECTS
14
+ CONDITION = '(OBJECTS)\.(\w*(?:\?|!)?)$'
15
+ def initialize(options={}) #@private
16
+ @object_condition = /^#{options[:object]}$/
17
+ options[:on] ||= Regexp.new condition_with_objects
18
+ super
19
+ end
20
+
21
+ def match_message #@private
22
+ "Matches completion for object with ancestor matching #{@object_condition.inspect}."
23
+ end
24
+
25
+ protected
26
+ def unique_id
27
+ "#{@object_condition.inspect}+#{@on.inspect}"
28
+ end
29
+
30
+ def do_match(input)
31
+ super && eval_object(@matched[1]) && @evaled_object.class.respond_to?(:ancestors) &&
32
+ @evaled_object.class.ancestors.any? {|e| e.to_s =~ @object_condition }
33
+ end
34
+
35
+ def after_match(input)
36
+ @completion_prefix = @matched[1] + "."
37
+ @action ||= lambda {|e| default_action(e.object) }
38
+ create_input @matched[2], :object => @evaled_object
39
+ end
40
+
41
+ def default_action(obj)
42
+ obj.methods.map {|e| e.to_s} - OPERATORS
43
+ end
44
+ end
@@ -0,0 +1,27 @@
1
+ module Bond
2
+ # A mission which completes arguments for any module/class method that is an
3
+ # operator i.e. '>' or '*'. Takes same Bond.complete options as
4
+ # MethodMission. The only operator method this mission doesn't complete is
5
+ # '[]='. The operator '[]' should cover the first argument completion of '[]='
6
+ # anyways.
7
+ class OperatorMethodMission < MethodMission
8
+ OPERATORS = Mission::OPERATORS - ["[]", "[]="]
9
+ OBJECTS = Mission::OBJECTS + %w{\S+}
10
+ CONDITION = %q{(OBJECTS)\s*(METHODS)\s*(['":])?(.*)$}
11
+
12
+ protected
13
+ def current_methods
14
+ (OPERATORS & MethodMission.action_methods) + ['[']
15
+ end
16
+
17
+ def matched_method
18
+ {'['=>'[]'}[@matched[2]] || @matched[2]
19
+ end
20
+
21
+ def after_match(input)
22
+ set_action_and_search
23
+ @completion_prefix, typed = input.to_s.sub(/#{Regexp.quote(@matched[-1])}$/, ''), @matched[-1]
24
+ create_input typed, :object => @evaled_object, :argument => 1
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,48 @@
1
+ module Bond
2
+ # Namespace in which completion files, ~/.bondrc and ~/.bond/completions/*.rb, are evaluated. Methods in this module
3
+ # and Search are the DSL in completion files and can be used within completion actions.
4
+ #
5
+ # === Example ~/.bondrc
6
+ # # complete arguments for any object's :respond_to?
7
+ # complete(:method => "Object#respond_to?") {|e| e.object.methods }
8
+ # # complete arguments for any module's :public
9
+ # complete(:method => "Module#public") {|e| e.object.instance_methods }
10
+ #
11
+ # # Share generate_tags action across completions
12
+ # complete(:method => "edit_tags", :action => :generate_tags)
13
+ # complete(:method => "delete_tags", :search => false) {|e| generate_tags(e).grep(/#{e}/i) }
14
+ #
15
+ # def generate_tags(input)
16
+ # ...
17
+ # end
18
+ module Rc
19
+ extend self, Search
20
+
21
+ # See {Bond#complete}
22
+ def complete(*args, &block); M.complete(*args, &block); end
23
+ # See {Bond#recomplete}
24
+ def recomplete(*args, &block); M.recomplete(*args, &block); end
25
+
26
+ # Action method with search which returns array of files that match current input.
27
+ def files(input)
28
+ (::Readline::FILENAME_COMPLETION_PROC.call(input) || []).map {|f|
29
+ f =~ /^~/ ? File.expand_path(f) : f
30
+ }
31
+ end
32
+
33
+ # Helper method which returns objects of a given class.
34
+ def objects_of(klass)
35
+ object = []
36
+ ObjectSpace.each_object(klass) {|e| object.push(e) }
37
+ object
38
+ end
39
+
40
+ # Calls eval with Mission.current_eval, rescuing any exceptions to return nil.
41
+ # If Bond.config[:debug] is true, exceptions are raised again.
42
+ def eval(str)
43
+ Mission.current_eval(str)
44
+ rescue Exception
45
+ raise if Bond.config[:debug]
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,38 @@
1
+ # This is the default readline plugin for Bond. A valid plugin must be an object
2
+ # that responds to methods setup and line_buffer as described below.
3
+ class Bond::Readline
4
+ DefaultBreakCharacters = " \t\n\"\\'`><=;|&{("
5
+
6
+ # Loads the readline-like library and sets the completion_proc to the given agent.
7
+ def self.setup(agent)
8
+ readline_setup
9
+
10
+ # Reinforcing irb defaults
11
+ Readline.completion_append_character = nil
12
+ if Readline.respond_to?("basic_word_break_characters=")
13
+ Readline.basic_word_break_characters = DefaultBreakCharacters
14
+ end
15
+
16
+ Readline.completion_proc = agent
17
+ end
18
+
19
+ def self.readline_setup
20
+ require 'readline'
21
+ load_extension unless Readline.respond_to?(:line_buffer)
22
+ if (Readline::VERSION rescue nil).to_s[/editline/i]
23
+ puts "Bond has detected EditLine and may not work with it." +
24
+ " See the README's Limitations section."
25
+ end
26
+ end
27
+
28
+ def self.load_extension
29
+ require 'readline_line_buffer'
30
+ rescue LoadError
31
+ $stderr.puts "Bond Error: Failed to load readline_line_buffer. Ensure that it exists and was built correctly."
32
+ end
33
+
34
+ # Returns full line of what the user has typed.
35
+ def self.line_buffer
36
+ Readline.line_buffer
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ # Readline for Jruby
2
+ class Bond::Jruby < Bond::Readline
3
+ def self.readline_setup
4
+ require 'readline'
5
+ require 'jruby'
6
+ class << Readline
7
+ ReadlineExt = org.jruby.ext.Readline
8
+ def line_buffer
9
+ ReadlineExt.s_get_line_buffer(JRuby.runtime.current_context, JRuby.reference(self))
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ # A pure ruby readline which requires {rawline}[http://github.com/h3rald/rawline].
2
+ class Bond::Rawline < Bond::Readline
3
+ def self.setup(agent)
4
+ require 'rawline'
5
+ Rawline.completion_append_character = nil
6
+ Rawline.basic_word_break_characters= " \t\n\"\\'`><;|&{("
7
+ Rawline.completion_proc = agent
8
+ rescue LoadError
9
+ abort "Bond Error: rawline gem is required for this readline plugin -> gem install rawline"
10
+ end
11
+
12
+ def self.line_buffer
13
+ Rawline.editor.line.text
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ # A pure ruby readline which requires {rb-readline}[https://github.com/luislavena/rb-readline].
2
+ class Bond::Ruby < Bond::Readline
3
+ def self.readline_setup
4
+ require 'rb-readline'
5
+ rescue LoadError
6
+ abort "Bond Error: rb-readline gem is required for this readline plugin" +
7
+ " -> gem install rb-readline"
8
+ end
9
+ end
@@ -0,0 +1,74 @@
1
+ module Bond
2
+ # Contains search methods used to filter possible completions given what the user has typed for that completion.
3
+ # For a search method to be used by Bond.complete it must end in '_search' and take two arguments: the Input
4
+ # string and an array of possible completions.
5
+ #
6
+ # ==== Creating a search method
7
+ # Say you want to create a custom search which ignores completions containing '-'.
8
+ # In a completion file under Rc namespace, define this method:
9
+ # def ignore_hyphen_search(input, list)
10
+ # normal_search(input, list.select {|e| e !~ /-/ })
11
+ # end
12
+ #
13
+ # Now you can pass this custom search to any complete() as :search => :ignore_hyphen
14
+ module Search
15
+ class<<self
16
+ # Default search used across missions, set by Bond.config[:default_search]
17
+ attr_accessor :default_search
18
+ end
19
+
20
+ # Searches completions from the beginning of the string.
21
+ def normal_search(input, list)
22
+ list.grep(/^#{Regexp.escape(input)}/)
23
+ end
24
+
25
+ # Searches completions anywhere in the string.
26
+ def anywhere_search(input, list)
27
+ list.grep(/#{Regexp.escape(input)}/)
28
+ end
29
+
30
+ # Searches completions from the beginning and ignores case.
31
+ def ignore_case_search(input, list)
32
+ list.grep(/^#{Regexp.escape(input)}/i)
33
+ end
34
+
35
+ # A normal_search which also provides aliasing of underscored words.
36
+ # For example 'some_dang_long_word' can be specified as 's_d_l_w'. Aliases can be any unique string
37
+ # at the beginning of an underscored word. For example, to choose the first completion between 'so_long'
38
+ # and 'so_larger', type 's_lo'.
39
+ def underscore_search(input, list)
40
+ if input[/_([^_]+)$/]
41
+ regex = input.split('_').map {|e| Regexp.escape(e) }.join("([^_]+)?_")
42
+ list.select {|e| e =~ /^#{regex}/ }
43
+ else
44
+ normal_search(input, list)
45
+ end
46
+ end
47
+
48
+ # Default search across missions to be invoked by a search that wrap another search i.e. files_search.
49
+ def default_search(input, list)
50
+ send("#{Search.default_search}_search", input, list)
51
+ end
52
+
53
+ # Does default_search on the given paths but only returns ones that match the input's current
54
+ # directory depth, determined by '/'. For example if a user has typed 'irb/c', this search returns
55
+ # matching paths that are one directory deep i.e. 'irb/cmd/ irb/completion.rb irb/context.rb'.
56
+ def files_search(input, list)
57
+ incremental_filter(input, list, '/')
58
+ end
59
+
60
+ # Does the same as files_search but for modules. A module depth is delimited by '::'.
61
+ def modules_search(input, list)
62
+ incremental_filter(input, list, '::')
63
+ end
64
+
65
+ # Used by files_search and modules_search.
66
+ def incremental_filter(input, list, delim)
67
+ i = 0; input.gsub(delim) {|e| i+= 1 }
68
+ delim_chars = delim.split('').uniq.join('')
69
+ current_matches, future_matches = default_search(input, list).partition {|e|
70
+ e[/^[^#{delim_chars}]*(#{delim}[^#{delim_chars}]+){0,#{i}}$/] }
71
+ (current_matches + future_matches.map {|e| e[/^(([^#{delim_chars}]*#{delim}){0,#{i+1}})/, 1] }).uniq
72
+ end
73
+ end
74
+ end