peeky 0.0.19 → 0.0.33
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/.rubocop.yml +21 -0
- data/.rubocop_todo.yml +4 -0
- data/CODE_OF_CONDUCT.md +1 -1
- data/Gemfile +6 -3
- data/Guardfile +4 -4
- data/README.md +96 -9
- data/{README-stories.md → STORIES.md} +28 -4
- data/USAGE.md +438 -0
- data/lib/peeky.rb +4 -0
- data/lib/peeky/api.rb +74 -0
- data/lib/peeky/class_info.rb +139 -29
- data/lib/peeky/example/yard_sample.rb +124 -0
- data/lib/peeky/method_info.rb +5 -4
- data/lib/peeky/parameter_info.rb +96 -21
- data/lib/peeky/predicates/attr_reader_predicate.rb +16 -4
- data/lib/peeky/predicates/attr_writer_predicate.rb +8 -0
- data/lib/peeky/renderer/class_debug_render.rb +95 -0
- data/lib/peeky/renderer/class_interface_render.rb +9 -9
- data/lib/peeky/renderer/class_interface_yard_render.rb +141 -0
- data/lib/peeky/renderer/method_call_minimum_params_render.rb +4 -5
- data/lib/peeky/renderer/method_signature_render.rb +44 -13
- data/lib/peeky/renderer/method_signature_with_debug_render.rb +11 -11
- data/lib/peeky/version.rb +1 -1
- data/peeky.gemspec +15 -3
- metadata +35 -8
data/lib/peeky/parameter_info.rb
CHANGED
@@ -20,11 +20,8 @@ module Peeky
|
|
20
20
|
# type of the parameter
|
21
21
|
attr_accessor :type
|
22
22
|
|
23
|
-
#
|
24
|
-
attr_accessor :
|
25
|
-
|
26
|
-
# minimal required usage in a call to the method with this paramater
|
27
|
-
attr_accessor :minimal_call_format
|
23
|
+
# default value for positional or keyed parameters
|
24
|
+
attr_accessor :default_value
|
28
25
|
|
29
26
|
def initialize(param)
|
30
27
|
map(param)
|
@@ -58,47 +55,125 @@ module Peeky
|
|
58
55
|
puts "minimal_call_format : #{minimal_call_format}"
|
59
56
|
end
|
60
57
|
|
58
|
+
# ruby code formatted for use in a method signature
|
59
|
+
def signature_format
|
60
|
+
@_signature_format ||= begin
|
61
|
+
method_name = "signature_format_#{@type}".to_sym
|
62
|
+
|
63
|
+
m = method(method_name)
|
64
|
+
m.call
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# minimal required usage in a call to the method with this paramater
|
69
|
+
def minimal_call_format
|
70
|
+
@_minimal_call_format ||= begin
|
71
|
+
method_name = "minimal_call_format_#{@type}".to_sym
|
72
|
+
|
73
|
+
if respond_to?(method_name, true)
|
74
|
+
m = method(method_name)
|
75
|
+
m.call
|
76
|
+
else
|
77
|
+
minimal_call_format_ignore
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
61
82
|
private
|
62
83
|
|
63
84
|
# Convert the limited information provided by ruby method.parameters
|
64
85
|
# to a richer structure.
|
65
|
-
# rubocop:disable Metrics/
|
86
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
66
87
|
def map(param)
|
67
88
|
@name = param.length > 1 ? param[1].to_s : ''
|
68
89
|
|
90
|
+
@default_value = nil
|
91
|
+
|
69
92
|
case param[0]
|
70
93
|
when :req
|
71
94
|
@type = :param_required
|
72
|
-
@signature_format = name.to_s
|
73
|
-
@minimal_call_format = "'#{name}'"
|
74
95
|
when :opt
|
75
96
|
@type = :param_optional
|
76
|
-
@signature_format = "#{name} = nil"
|
77
|
-
@minimal_call_format = ''
|
78
97
|
when :rest
|
79
98
|
@type = :splat
|
80
|
-
@signature_format = "*#{name}"
|
81
|
-
@minimal_call_format = ''
|
82
99
|
when :keyreq
|
83
100
|
@type = :key_required
|
84
|
-
@signature_format = "#{name}:"
|
85
|
-
@minimal_call_format = "#{name}: '#{name}'"
|
86
101
|
when :key
|
87
102
|
@type = :key_optional
|
88
|
-
@signature_format = "#{name}: nil"
|
89
|
-
@minimal_call_format = ''
|
90
103
|
when :keyrest
|
91
104
|
@type = :double_splat
|
92
|
-
@signature_format = "**#{name}"
|
93
|
-
@minimal_call_format = ''
|
94
105
|
when :block
|
95
106
|
@type = :block
|
96
|
-
@signature_format = "&#{name}"
|
97
|
-
@minimal_call_format = ''
|
98
107
|
else
|
99
108
|
raise 'unknown type'
|
100
109
|
end
|
101
110
|
end
|
102
|
-
# rubocop:enable Metrics/
|
111
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
112
|
+
|
113
|
+
# Signature format *: Is used to format a parameter when it is used
|
114
|
+
# inside of a method signature, eg. def my_method(p1, p2 = 'xyz', p3: :name_value)
|
115
|
+
|
116
|
+
def signature_format_param_required
|
117
|
+
name.to_s
|
118
|
+
end
|
119
|
+
|
120
|
+
def signature_format_param_optional
|
121
|
+
"#{name} = nil" # signature format needs to be moved to a method
|
122
|
+
end
|
123
|
+
|
124
|
+
def signature_format_splat
|
125
|
+
"*#{name}"
|
126
|
+
end
|
127
|
+
|
128
|
+
def signature_format_key_required
|
129
|
+
"#{name}:"
|
130
|
+
end
|
131
|
+
|
132
|
+
def signature_format_key_optional
|
133
|
+
"#{name}: nil"
|
134
|
+
end
|
135
|
+
|
136
|
+
def signature_format_double_splat
|
137
|
+
"**#{name}"
|
138
|
+
end
|
139
|
+
|
140
|
+
def signature_format_block
|
141
|
+
"&#{name}"
|
142
|
+
end
|
143
|
+
|
144
|
+
# Minimal call format *: Is used to format a call to a method with the least
|
145
|
+
# number of parameters needed to make it work.
|
146
|
+
|
147
|
+
def minimal_call_format_ignore
|
148
|
+
''
|
149
|
+
end
|
150
|
+
|
151
|
+
def minimal_call_format_param_required
|
152
|
+
"'#{@name}'"
|
153
|
+
end
|
154
|
+
|
155
|
+
# def minimal_call_format_param_optional
|
156
|
+
# ''
|
157
|
+
# end
|
158
|
+
|
159
|
+
# def minimal_call_format_splat
|
160
|
+
# ''
|
161
|
+
# end
|
162
|
+
|
163
|
+
def minimal_call_format_key_required
|
164
|
+
"#{@name}: '#{@name}'"
|
165
|
+
end
|
166
|
+
|
167
|
+
# def minimal_call_format_key_optional
|
168
|
+
# ''
|
169
|
+
# end
|
170
|
+
|
171
|
+
# def minimal_call_format_double_splat
|
172
|
+
# ''
|
173
|
+
# end
|
174
|
+
|
175
|
+
# def minimal_call_format_block
|
176
|
+
# ''
|
177
|
+
# end
|
103
178
|
end
|
104
179
|
end
|
@@ -7,27 +7,39 @@ module Peeky
|
|
7
7
|
# Attr Reader Predicate will match true if the method info could be considered
|
8
8
|
# a valid attr_reader
|
9
9
|
class AttrReaderPredicate
|
10
|
+
# Match will return true if the method_info seems to be an :attr_reader
|
11
|
+
#
|
12
|
+
# @param instance [Object] instance the object that has this method (required)
|
13
|
+
# @param method_info [String] method info (required)
|
10
14
|
def match(instance, method_info)
|
11
15
|
return false unless prerequisites(instance, method_info)
|
12
16
|
|
13
|
-
variable_name = "@#{method_info.name}"
|
14
17
|
method_name = method_info.name
|
15
18
|
|
16
19
|
# Refactor: Fragile
|
17
20
|
# Really need to handle exceptions and types better
|
18
21
|
# old_value = instance.send(method_name)
|
22
|
+
|
23
|
+
# This code works by
|
24
|
+
# 1. Set @name_of_method variable to random value
|
25
|
+
# 2. Call method name and see if it returns that value
|
26
|
+
# 3. Return match<true> if the values are equal
|
19
27
|
new_value = SecureRandom.alphanumeric(20)
|
20
28
|
code = <<-RUBY
|
21
|
-
|
29
|
+
@#{method_name} = '#{new_value}' # eg. @variable = 'a3bj7a3bj7a3bj7a3bj7'
|
22
30
|
RUBY
|
23
|
-
|
24
|
-
|
31
|
+
|
32
|
+
cloned = instance.clone
|
33
|
+
|
34
|
+
cloned.instance_eval(code)
|
35
|
+
current_value = cloned.send(method_name)
|
25
36
|
current_value == new_value
|
26
37
|
end
|
27
38
|
|
28
39
|
private
|
29
40
|
|
30
41
|
def prerequisites(instance, method_info)
|
42
|
+
# look for obvious NON :attr_reader patterns
|
31
43
|
return false if %w[! ? =].include?(method_info.name.to_s[-1..-1])
|
32
44
|
return false unless method_info.parameters.length.zero?
|
33
45
|
return false unless instance.respond_to?(method_info.name)
|
@@ -7,16 +7,24 @@ module Peeky
|
|
7
7
|
# Attr Writer Predicate will match true if the method info could be considered
|
8
8
|
# a valid attr_writer
|
9
9
|
class AttrWriterPredicate
|
10
|
+
# Match will return true if the method_info seems to be an :attr_writer
|
11
|
+
#
|
12
|
+
# @param instance [Object] instance the object that has this method (required)
|
13
|
+
# @param method_info [String] method info (required)
|
10
14
|
def match(instance, method_info)
|
11
15
|
return false unless prerequisites(instance, method_info)
|
12
16
|
|
13
17
|
param = method_info.parameters.first
|
18
|
+
# Taking advantage of an odd reflection concept in ruby where by
|
19
|
+
# method.parameters returns this array value [:req] for :attr_writer
|
20
|
+
# while ordinary methods return [:req, some_param_name]
|
14
21
|
param.type == :param_required && param.name.empty?
|
15
22
|
end
|
16
23
|
|
17
24
|
private
|
18
25
|
|
19
26
|
def prerequisites(instance, method_info)
|
27
|
+
# look for obvious NON :attr_writer patterns
|
20
28
|
return false if %w[! ?].include?(method_info.name.to_s[-1..-1])
|
21
29
|
return false unless method_info.name.to_s.end_with?('=')
|
22
30
|
return false unless instance.respond_to?(method_info.name)
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Peeky
|
4
|
+
module Renderer
|
5
|
+
# Class Debug Render
|
6
|
+
class ClassDebugRender
|
7
|
+
attr_reader :class_info
|
8
|
+
|
9
|
+
def initialize(class_info)
|
10
|
+
@key_width = 30
|
11
|
+
@class_info = class_info
|
12
|
+
end
|
13
|
+
|
14
|
+
# Render the class interface
|
15
|
+
# rubocop:disable Metrics/AbcSize
|
16
|
+
def render
|
17
|
+
output = []
|
18
|
+
output.push class_details
|
19
|
+
attributes = render_accessors + render_readers + render_writers
|
20
|
+
|
21
|
+
if attributes.length.positive?
|
22
|
+
output.push("-- Attributes #{'-' * 56}")
|
23
|
+
output.push(*attributes)
|
24
|
+
output.push('')
|
25
|
+
end
|
26
|
+
|
27
|
+
methods = render_methods
|
28
|
+
|
29
|
+
if methods.length.positive?
|
30
|
+
output.push("-- Public Methods #{'-' * 52}")
|
31
|
+
output.push(*methods)
|
32
|
+
output.push('')
|
33
|
+
end
|
34
|
+
|
35
|
+
output.pop if output.last == ''
|
36
|
+
|
37
|
+
output.join("\n")
|
38
|
+
end
|
39
|
+
# rubocop:enable Metrics/AbcSize
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def lj(value, size = 20)
|
44
|
+
value.to_s.ljust(size)
|
45
|
+
end
|
46
|
+
|
47
|
+
def kv(key, value)
|
48
|
+
"#{key.to_s.ljust(@key_width)}: #{value}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def class_details
|
52
|
+
[
|
53
|
+
'-' * 70,
|
54
|
+
kv('class name', @class_info.class_name),
|
55
|
+
kv('module name', @class_info.module_name),
|
56
|
+
kv('class full name', @class_info.class_full_name),
|
57
|
+
''
|
58
|
+
]
|
59
|
+
end
|
60
|
+
|
61
|
+
def render_accessors
|
62
|
+
@class_info.accessors.map { |attr| kv('attr_accessor', attr.name) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def render_readers
|
66
|
+
@class_info.readers.map { |attr| kv('attr_reader', attr.name) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def render_writers
|
70
|
+
@class_info.writers.map { |attr| kv('attr_writer', attr.name) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def render_methods
|
74
|
+
@class_info.methods.flat_map do |method|
|
75
|
+
[
|
76
|
+
"#{method.name}::",
|
77
|
+
*render_paramaters(method.parameters),
|
78
|
+
''
|
79
|
+
]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def render_paramaters(parameters)
|
84
|
+
result = [
|
85
|
+
"#{lj('name')} #{lj('param format')} #{lj('type')}",
|
86
|
+
'-' * 70
|
87
|
+
]
|
88
|
+
|
89
|
+
result + parameters.map do |param|
|
90
|
+
"#{lj(param.name)} #{lj(param.signature_format)} #{lj(param.type)}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -30,12 +30,14 @@ module Peeky
|
|
30
30
|
# def z(aaa, bbb = nil, *ccc, ddd:, eee: nil, **fff, &ggg); end
|
31
31
|
# end
|
32
32
|
class ClassInterfaceRender
|
33
|
+
# ClassInfo with information about the class instance to be rendered.
|
33
34
|
attr_reader :class_info
|
34
35
|
|
35
36
|
def initialize(class_info)
|
36
37
|
@class_info = class_info
|
37
38
|
end
|
38
39
|
|
40
|
+
# Render the class interface
|
39
41
|
def render
|
40
42
|
@indent = ''
|
41
43
|
output = []
|
@@ -53,30 +55,32 @@ module Peeky
|
|
53
55
|
output.join("\n")
|
54
56
|
end
|
55
57
|
|
58
|
+
private
|
59
|
+
|
56
60
|
def render_start
|
57
|
-
"#{@indent}class #{class_info.class_name}"
|
61
|
+
"#{@indent}class #{@class_info.class_name}"
|
58
62
|
end
|
59
63
|
|
60
64
|
def render_accessors
|
61
|
-
result = class_info.accessors.map { |attr| "#{@indent}attr_accessor :#{attr.name}" }
|
65
|
+
result = @class_info.accessors.map { |attr| "#{@indent}attr_accessor :#{attr.name}" }
|
62
66
|
result.push '' unless result.length.zero?
|
63
67
|
result
|
64
68
|
end
|
65
69
|
|
66
70
|
def render_readers
|
67
|
-
result = class_info.readers.map { |attr| "#{@indent}attr_reader :#{attr.name}" }
|
71
|
+
result = @class_info.readers.map { |attr| "#{@indent}attr_reader :#{attr.name}" }
|
68
72
|
result.push '' unless result.length.zero?
|
69
73
|
result
|
70
74
|
end
|
71
75
|
|
72
76
|
def render_writers
|
73
|
-
result = class_info.writers.map { |attr| "#{@indent}attr_writer :#{attr.name}" }
|
77
|
+
result = @class_info.writers.map { |attr| "#{@indent}attr_writer :#{attr.name}" }
|
74
78
|
result.push '' unless result.length.zero?
|
75
79
|
result
|
76
80
|
end
|
77
81
|
|
78
82
|
def render_methods
|
79
|
-
result = class_info.methods.map do |method_signature|
|
83
|
+
result = @class_info.methods.map do |method_signature|
|
80
84
|
render_signature = Peeky::Renderer::MethodSignatureRender.new(method_signature)
|
81
85
|
"#{@indent}#{render_signature.render}"
|
82
86
|
end
|
@@ -87,10 +91,6 @@ module Peeky
|
|
87
91
|
def render_end
|
88
92
|
"#{@indent}end"
|
89
93
|
end
|
90
|
-
|
91
|
-
def debug
|
92
|
-
puts render
|
93
|
-
end
|
94
94
|
end
|
95
95
|
end
|
96
96
|
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/string'
|
4
|
+
|
5
|
+
module Peeky
|
6
|
+
module Renderer
|
7
|
+
# Render: Class Interface with YARD documentation
|
8
|
+
class ClassInterfaceYardRender
|
9
|
+
# Indentation prefix as a string, defaults to +''+
|
10
|
+
#
|
11
|
+
# If you were writing a class into a file with an existing
|
12
|
+
# module, you may set the indent to +' '+ if you wanted this
|
13
|
+
# render to indent by two spaces
|
14
|
+
attr_accessor :indent
|
15
|
+
|
16
|
+
# Default param type when documenting positional and named parameters.
|
17
|
+
# Defaults to <String>
|
18
|
+
attr_accessor :default_param_type
|
19
|
+
|
20
|
+
# Default param type when documenting splat *parameters.
|
21
|
+
# Defaults to <Object>
|
22
|
+
attr_accessor :default_splat_param_type
|
23
|
+
|
24
|
+
# ClassInfo with information about the class instance to be rendered.
|
25
|
+
attr_reader :class_info
|
26
|
+
|
27
|
+
def initialize(class_info)
|
28
|
+
@class_info = class_info
|
29
|
+
@indent = ''
|
30
|
+
@default_param_type = 'String'
|
31
|
+
@default_splat_param_type = 'Object'
|
32
|
+
end
|
33
|
+
|
34
|
+
# Render the class interface with YARD documentation
|
35
|
+
def render
|
36
|
+
output = []
|
37
|
+
output.push render_start
|
38
|
+
@indent += ' '
|
39
|
+
output += (render_accessors + render_readers + render_writers + render_methods)
|
40
|
+
output.pop if output.last == ''
|
41
|
+
|
42
|
+
@indent = @indent[0..-3]
|
43
|
+
|
44
|
+
output.push render_end
|
45
|
+
|
46
|
+
output.join("\n")
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def render_start
|
52
|
+
[
|
53
|
+
"#{@indent}# #{@class_info.class_name.titleize.humanize}",
|
54
|
+
"#{@indent}class #{@class_info.class_name}"
|
55
|
+
]
|
56
|
+
end
|
57
|
+
|
58
|
+
def render_accessors
|
59
|
+
result = []
|
60
|
+
@class_info.accessors.map.with_index do |attr, index|
|
61
|
+
result.push '' if index.positive?
|
62
|
+
result.push "#{@indent}# #{attr.name.to_s.humanize}"
|
63
|
+
result.push "#{@indent}attr_accessor :#{attr.name}"
|
64
|
+
end
|
65
|
+
result.push '' unless result.length.zero?
|
66
|
+
result
|
67
|
+
end
|
68
|
+
|
69
|
+
def render_readers
|
70
|
+
result = []
|
71
|
+
@class_info.readers.map.with_index do |attr, index|
|
72
|
+
result.push '' if index.positive?
|
73
|
+
result.push "#{@indent}# #{attr.name.to_s.humanize}"
|
74
|
+
result.push "#{@indent}attr_reader :#{attr.name}"
|
75
|
+
end
|
76
|
+
result.push '' unless result.length.zero?
|
77
|
+
result
|
78
|
+
end
|
79
|
+
|
80
|
+
def render_writers
|
81
|
+
result = []
|
82
|
+
class_info.writers.map.with_index do |attr, index|
|
83
|
+
result.push '' if index.positive?
|
84
|
+
result.push "#{@indent}# #{attr.name.to_s.humanize}"
|
85
|
+
result.push "#{@indent}attr_writer :#{attr.name}"
|
86
|
+
end
|
87
|
+
result.push '' unless result.length.zero?
|
88
|
+
result
|
89
|
+
end
|
90
|
+
|
91
|
+
# rubocop:disable Metrics/AbcSize, Metrics/BlockLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
92
|
+
def render_methods
|
93
|
+
result = []
|
94
|
+
class_info.methods.map.with_index do |method_signature, index|
|
95
|
+
result.push '' if index.positive?
|
96
|
+
result.push "#{@indent}# #{method_signature.name.to_s.humanize}"
|
97
|
+
|
98
|
+
method_signature.parameters.each_with_index do |parameter, param_index|
|
99
|
+
result.push "#{@indent}#" if param_index.zero?
|
100
|
+
|
101
|
+
case parameter.type
|
102
|
+
when :splat
|
103
|
+
result.push "#{@indent}# @param #{parameter.name} [Array<#{default_splat_param_type}>] *#{parameter.name} - list of #{parameter.name.to_s.humanize.downcase}"
|
104
|
+
when :double_splat
|
105
|
+
result.push "#{@indent}# @param #{parameter.name} [<key: value>...] **#{parameter.name} - list of key/values"
|
106
|
+
when :block
|
107
|
+
result.push "#{@indent}# @param #{parameter.name} [Block] &#{parameter.name}"
|
108
|
+
when :key_required
|
109
|
+
result.push "#{@indent}# @param #{parameter.name} [#{default_param_type}] #{parameter.name}: <value for #{parameter.name.to_s.humanize.downcase}> (required)"
|
110
|
+
when :key_optional
|
111
|
+
result.push "#{@indent}# @param #{parameter.name} [#{default_param_type}] #{parameter.name}: <value for #{parameter.name.to_s.humanize.downcase}> (optional)"
|
112
|
+
when :param_required
|
113
|
+
result.push "#{@indent}# @param #{parameter.name} [#{default_param_type}] #{parameter.name.to_s.humanize.downcase} (required)"
|
114
|
+
when :param_optional
|
115
|
+
result.push "#{@indent}# @param #{parameter.name} [#{default_param_type}] #{parameter.name.to_s.humanize.downcase} (optional)"
|
116
|
+
else
|
117
|
+
result.push "#{@indent}# @param #{parameter.name} [#{default_param_type}] #{parameter.name.to_s.humanize.downcase}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
if method_signature.name.to_s.end_with?('?')
|
122
|
+
result.push ''
|
123
|
+
result.push "#{@indent}# @return [Boolean] true when #{method_signature.name.to_s.humanize.downcase}"
|
124
|
+
end
|
125
|
+
|
126
|
+
render_signature = Peeky::Renderer::MethodSignatureRender.new(method_signature)
|
127
|
+
render_signature.indent = @indent
|
128
|
+
render_signature.style = :default
|
129
|
+
result.push render_signature.render
|
130
|
+
end
|
131
|
+
result.push '' unless result.length.zero?
|
132
|
+
result
|
133
|
+
end
|
134
|
+
# rubocop:enable Metrics/AbcSize, Metrics/BlockLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
135
|
+
|
136
|
+
def render_end
|
137
|
+
"#{@indent}end"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|