ki-repo 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,258 @@
1
+ # encoding: UTF-8
2
+
3
+ # Copyright 2012 Mikko Apo
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ # Attaches configurable behaviour to accessor methods
18
+ #
19
+ # class Foo
20
+ # attr_chain :name, :require
21
+ # attr_chain :email, -> {""}
22
+ # attr_chain :birth_day, :immutable, :valid => lambda { |i| (1870..Time.now.year+1).include?(i) }, :require => true
23
+ # attr_chain :children, :convert => lambda {|s| s.to_i}
24
+ # end
25
+ #
26
+ # Sets up public methods variable_name and variable_name= which both can be used to access the fields. Giving any parameters
27
+ # for the method makes it a "set" operation and giving no parameters makes it a "get" operation. "Set" stores the value
28
+ # and returns self, so set calls can be chained. "Get" returns the stored value.
29
+ #
30
+ # foo.email("test@email.com").name("test")
31
+ # foo.email => "test@email.com"
32
+ # foo.name => "test"
33
+ #
34
+ # Parameters can be given in short and long format. Short format works by identifying parameter types,
35
+ # long format works by given the name and value as hash parameters:
36
+ # * <tt>:require=>"You need to define xxx first"</tt>, <tt>:require=>true</tt>, short: <tt>:require</tt> - an exception is thrown if target field is not defined
37
+ # * <tt>:default=> -> {true}</tt>, short: <tt>-> {Array.new}</tt> - if target field has not been defined, executes proc and stores value. proc is executed using object.instance_exec: object's fields & methds are available
38
+ # * <tt>:immutable=>true</tt>, short: <tt>:immutable</tt> - an exception is thrown if target field is defined a second time
39
+ # * <tt>:valid=>[1,2,3,"a", lambda {|s| s.include?("b")}]</tt>, <tt>:valid => lambda {|s| s.include?("b")}</tt>, short: <tt>[1,2,3,"a"]</tt> - List of valid values. If any matches, sets value. If none matches, raises exception. Long form wraps single arguments to a list.
40
+ # * <tt>:convert=> ->(s) { s+1 }</tt> - Converts input value using the defined proc
41
+ # * <tt>:accessor=>InstanceVariableAccessor.new</tt> - Makes it possible to set values in other source, for example a hash. By default uses InstanceVariableAccessor
42
+ #
43
+ # Advantages for using attr_chain
44
+ # * attr_chain has a compact syntax for many important programming concepts -> less manually written boilerplate code is needed
45
+ # * :default makes it easy to isolate functionality to a default value while still making it easy to override the default behaviour
46
+ # * :default adds easy lazy evalution and memoization to the attribute, default value is evaluated only if needed
47
+ # * Testing becomes easier when objects have more exposed fields
48
+ # * :require converts tricky nil exceptions in to useful errors. Instead of the "undefined method `bar' for nil:NilClass" you get a good error message that states which field was not defined
49
+ # foo.name.bar # if name has not been defined, raises "'name' has not been set" exception
50
+ # * :immutable, :valid and :convert make complex validations and converts easy
51
+ #
52
+ # Warnings about attr_chain
53
+ # * Performance has not been measured and attr_chain is probably not efficient. If there are tight inner loops, it's better to cache the value and store it afterwards
54
+ # * There has not been tests for memory leaks. It's plain ruby so GC should take care of everything
55
+ # * Excessive attr_chain usage makes classes a mess. Try to keep your classes short and attr_chain count below 10.
56
+ # @see InstanceVariableAccessor
57
+ # @see Object.attr_chain
58
+ # @see Module#attr_chain
59
+ class AttrChain
60
+ # Parses parameters with parse_short_syntax and set_parameters and configures class methods
61
+ # * each attr_chain definition uses one instance of AttrChain which holds the configuration for the definition
62
+ # * Object::define_method is used to add two methods to target class and when called both of these methods call attr_chain with their parameters
63
+ # @see Object.attr_chain
64
+ # @see Module#attr_chain
65
+ def initialize(clazz, variable_name, attr_configs)
66
+ @variable_name = variable_name
67
+ @accessor = InstanceVariableAccess
68
+ set_parameters(variable_name, parse_short_syntax(variable_name, attr_configs))
69
+ me = self
70
+ attr_call = lambda { |*args| me.attr_chain(self, args) }
71
+ [variable_name, "#{variable_name}="].each do |method_name|
72
+ if clazz.method_defined?(method_name)
73
+ clazz.send(:undef_method, method_name)
74
+ end
75
+ clazz.send(:define_method, method_name, attr_call)
76
+ end
77
+ end
78
+
79
+ # Converts short syntax entries in attr_configs to long syntax
80
+ # * warns about not supported values and already defined values
81
+ def parse_short_syntax(variable_name, attr_configs)
82
+ params = {}
83
+ attr_configs.each do |attr_config|
84
+ key_values = if [:require, :immutable].include?(attr_config)
85
+ [[attr_config, true]]
86
+ elsif attr_config.kind_of?(Proc)
87
+ [[:default, attr_config]]
88
+ elsif attr_config.kind_of?(Array)
89
+ [[:valid, attr_config]]
90
+ elsif attr_config.kind_of?(Hash)
91
+ all = []
92
+ attr_config.each_pair do |pair|
93
+ all << pair
94
+ end
95
+ all
96
+ else
97
+ raise "attr_chain :#{variable_name} unsupported parameter: '#{attr_config.inspect}'"
98
+ end
99
+ key_values.each do |key, value|
100
+ if params.include?(key)
101
+ raise "attr_chain :#{variable_name}, :#{key} was already defined to '#{params[key]}' (new value: '#{value}')"
102
+ end
103
+ params[key]=value
104
+ end
105
+ end
106
+ params
107
+ end
108
+
109
+ # Parses long syntax values and sets configuration for this field
110
+ def set_parameters(variable_name, params)
111
+ params.each_pair do |key, value|
112
+ case key
113
+ when :require
114
+ @require = value
115
+ when :default
116
+ if !value.kind_of?(Proc)
117
+ raise "attr_chain :#{variable_name}, :default needs to be a Proc, not '#{value.inspect}'"
118
+ end
119
+ @default = value
120
+ when :immutable
121
+ @immutable = value
122
+ when :valid
123
+ if !value.kind_of?(Array)
124
+ value = [value]
125
+ end
126
+ value.each do |valid|
127
+ if valid.kind_of?(Proc)
128
+ @valid_procs ||= []
129
+ @valid_procs << valid
130
+ else
131
+ @valid_items ||= {}
132
+ @valid_items[valid]=valid
133
+ end
134
+ end
135
+ when :convert
136
+ if !value.kind_of?(Proc)
137
+ raise "attr_chain :#{variable_name}, :convert needs to be a Proc, not '#{value.inspect}'"
138
+ end
139
+ @convert = value
140
+ when :accessor
141
+ @accessor = value
142
+ else
143
+ raise "attr_chain :#{variable_name} unsupported parameter: '#{key.inspect}'"
144
+ end
145
+ end
146
+ end
147
+
148
+ # Handles incoming methods for "get" and "set"
149
+ # * called by methods defined to class
150
+ # * configuration is stored as instance variables, the class knows which variable is being handled
151
+ # * method call parameters come as list of parameters
152
+ def attr_chain(object, args)
153
+ if args.empty?
154
+ if !@accessor.defined?(object, @variable_name)
155
+ if defined? @default
156
+ @accessor.set(object, @variable_name, object.instance_exec(&@default))
157
+ elsif defined? @require
158
+ if @require.kind_of?(String)
159
+ raise "'#{@variable_name}' has not been set: #{@require}"
160
+ else
161
+ raise "'#{@variable_name}' has not been set"
162
+ end
163
+ end
164
+ end
165
+ @accessor.get(object, @variable_name)
166
+ else
167
+ if defined?(@immutable) && @accessor.defined?(object, @variable_name)
168
+ raise "'#{@variable_name}' has been set once already"
169
+ end
170
+ value_to_set = if args.size == 1
171
+ args.first
172
+ else
173
+ args
174
+ end
175
+ if defined? @convert
176
+ value_to_set = object.instance_exec(value_to_set, &@convert)
177
+ end
178
+ if defined?(@valid_items) || defined?(@valid_procs)
179
+ is_valid = false
180
+ if defined?(@valid_items) && @valid_items.include?(value_to_set)
181
+ is_valid = true
182
+ end
183
+ if is_valid == false && defined?(@valid_procs)
184
+ @valid_procs.each do |valid_proc|
185
+ if is_valid=object.instance_exec(value_to_set, &valid_proc)
186
+ break
187
+ end
188
+ end
189
+ end
190
+ if is_valid == false
191
+ raise "invalid value for '#{@variable_name}'"
192
+ end
193
+ end
194
+ @accessor.set(object, @variable_name, value_to_set)
195
+ object
196
+ end
197
+ end
198
+
199
+ # Wrapper for Object::instance_variable_get, Object::instance_variable_set and Object::instance_variable_defined?
200
+ class InstanceVariableAccessor
201
+ def edit_name(variable_name)
202
+ "@#{variable_name}".to_sym
203
+ end
204
+
205
+ def get(object, name)
206
+ n = edit_name(name)
207
+ if object.instance_variable_defined?(n)
208
+ object.instance_variable_get(n)
209
+ else
210
+ nil
211
+ end
212
+ end
213
+
214
+ def set(object, name, value)
215
+ object.instance_variable_set(edit_name(name), value)
216
+ end
217
+
218
+ def defined?(object, name)
219
+ object.instance_variable_defined?(edit_name(name))
220
+ end
221
+ end
222
+
223
+ InstanceVariableAccess = InstanceVariableAccessor.new
224
+
225
+ class HashAccessor
226
+ def get(object, name)
227
+ object[name.to_s]
228
+ end
229
+
230
+ def set(object, name, value)
231
+ object[name.to_s] = value
232
+ end
233
+
234
+ def defined?(object, name)
235
+ object.include?(name.to_s)
236
+ end
237
+ end
238
+
239
+ HashAccess = HashAccessor.new
240
+ end
241
+
242
+ class Object
243
+ # Configurable accessor methods
244
+ # @see AttrChain
245
+ # @return [void]
246
+ def self.attr_chain(variable_name, *attr_configs)
247
+ AttrChain.new(self, variable_name, attr_configs)
248
+ end
249
+ end
250
+
251
+ class Module
252
+ # When a module defines an attr_chain, the attr_chain methods are available to all classes that are extended with the module
253
+ # @see AttrChain
254
+ # @return [void]
255
+ def attr_chain(variable_name, *attr_configs)
256
+ AttrChain.new(self, variable_name, attr_configs)
257
+ end
258
+ end
@@ -0,0 +1,118 @@
1
+ # encoding: UTF-8
2
+
3
+ # Copyright 2012 Mikko Apo
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'thread'
18
+
19
+ # ExceptionCatcher makes it easy to execute multiple operations even though any might fail with exception
20
+ # * executed tasks can be named to make it easier to identify which tasks failed
21
+ # * collects result and exceptions for each executed task
22
+ # * raises a {MultipleExceptions} exception if there were more than one exception
23
+ #
24
+ # catcher = ExceptionCatcher.new
25
+ # catcher.catch(1){raise "1"}
26
+ # catcher.catch(2){raise "2"}
27
+ # catcher.catch(3){"ok!"}
28
+ # catcher.exception(1) # -> exception object raised by the task 1
29
+ # catcher.exception(2) # -> exception object raise by the task 2
30
+ # catcher.result(3) # -> "ok!"
31
+ # catcher.check # raises a MultipleExceptions exception
32
+ #
33
+ class ExceptionCatcher
34
+ attr_reader :tasks, :results, :exceptions, :mutex
35
+
36
+ def initialize
37
+ @mutex = Mutex.new
38
+ @results = {}
39
+ @exceptions = {}
40
+ @tasks = []
41
+ end
42
+
43
+ # Catches exceptions thrown by block
44
+ # @param [Object, nil] task if task is defined, results can be checked per task
45
+ # @param [Proc] block, block which is executed
46
+ # @return [Object, nil] returns block's result or nil if block raised an exception
47
+ def catch(task=nil, &block)
48
+ if task.nil?
49
+ task = block
50
+ end
51
+ @mutex.synchronize do
52
+ @tasks << task
53
+ end
54
+ begin
55
+ result = block.call
56
+ @mutex.synchronize do
57
+ @results[task]=result
58
+ end
59
+ result
60
+ rescue Exception => e
61
+ @mutex.synchronize do
62
+ @exceptions[task]=e
63
+ end
64
+ nil
65
+ end
66
+ end
67
+
68
+ # @param [Object] task identifies the executed task
69
+ # @return [Object,nil] result for the named block or nil if the block ended in exception
70
+ def result(task)
71
+ @mutex.synchronize do
72
+ @results[task]
73
+ end
74
+ end
75
+
76
+ # @param [Object] task identifies the executed task
77
+ # @return [Object,nil] block's exception or nil if the block did not raise an exception
78
+ def exception(task)
79
+ @mutex.synchronize do
80
+ @exceptions[task]
81
+ end
82
+ end
83
+
84
+ # @return [bool] exceptions? returns true if there has been exceptions
85
+ def exceptions?
86
+ @exceptions.size > 0
87
+ end
88
+
89
+ # Checks if there has been exceptions and raises the original exception if there has been one exception and {MultipleExceptions} if there has been many exceptions
90
+ def check
91
+ @mutex.synchronize do
92
+ if @exceptions.size == 1
93
+ e = @exceptions.values.first
94
+ raise e.class, e.message, e.backtrace
95
+ elsif @exceptions.size > 1
96
+ raise MultipleExceptions.new("Caught #{@exceptions.size} exceptions!").catcher(self)
97
+ end
98
+ end
99
+ end
100
+
101
+ # {ExceptionCatcher} raises MultipleExceptions if it has caught multiple exceptions.
102
+ # MultipleExceptions makes the {ExceptionCatcher} and its results available
103
+ class MultipleExceptions < RuntimeError
104
+ include Enumerable
105
+ # Reference to original {#ExceptionCatcher}
106
+ attr_chain :catcher, :require
107
+ # Map of exceptions
108
+ attr_chain :exceptions, -> { catcher.exceptions }
109
+ # Map of tasks that have been scheduled
110
+ attr_chain :tasks, -> { catcher.tasks }
111
+
112
+ def each(&block)
113
+ exceptions.values.each(&block)
114
+ end
115
+ end
116
+ end
117
+
118
+
data/lib/util/hash.rb ADDED
@@ -0,0 +1,46 @@
1
+ # encoding: UTF-8
2
+
3
+ # Copyright 2012 Mikko Apo
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'digest/sha1'
18
+ require 'digest/sha2'
19
+ require 'digest/md5'
20
+
21
+ module Ki
22
+
23
+ # SHA1, uses standard Ruby library
24
+ class SHA1
25
+ # SHA1, uses standard Ruby library
26
+ def SHA1.digest
27
+ Digest::SHA1.new
28
+ end
29
+ end
30
+
31
+ # SHA2, uses standard Ruby library
32
+ class SHA2
33
+ # SHA2, uses standard Ruby library
34
+ def SHA2.digest
35
+ Digest::SHA2.new
36
+ end
37
+ end
38
+
39
+ # MD5, uses standard Ruby library
40
+ class MD5
41
+ # MD5, uses standard Ruby library
42
+ def MD5.digest
43
+ Digest::MD5.new
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: UTF-8
2
+
3
+ # Copyright 2012 Mikko Apo
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module Ki
18
+ # Caching Hash, resolves values at request time
19
+ class HashCache < Hash
20
+ # If key has not been defined, uses block to resolve the value. Value is stored and returned
21
+ # @param key Key
22
+ # @param [Proc] block Block which is evaluated if the key does not have value yet. Block's value is stored to hash
23
+ # @return Existing value or one resolved with the block
24
+ def cache(key, &block)
25
+ if !include?(key)
26
+ store(key, block.call)
27
+ end
28
+ self[key]
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,137 @@
1
+ # encoding: UTF-8
2
+
3
+ # Copyright 2012 Mikko Apo
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module Ki
18
+ module KiEnumerable
19
+ def size!(*args)
20
+ args.each do |valid_size|
21
+ if valid_size.kind_of?(Range)
22
+ if valid_size.include?(size)
23
+ return self
24
+ end
25
+ elsif valid_size.respond_to?(:to_i)
26
+ if Integer(valid_size) == size
27
+ return self
28
+ end
29
+ else
30
+ raise "'#{valid_size.inspect}' not supported, needs to be either Range or have .to_i method"
31
+ end
32
+ end
33
+ raise "size #{size} does not match '#{args.map { |s| s.to_s }.join("', '")}'"
34
+ end
35
+
36
+ def any_matches?(value)
37
+ each do |pattern|
38
+ if value.match(pattern)
39
+ return pattern
40
+ end
41
+ end
42
+ false
43
+ end
44
+
45
+ def find_first(count=1, &block)
46
+ ret = []
47
+ each do |item|
48
+ if block.nil? || block.call(item)
49
+ ret << item
50
+ if ret.size == count
51
+ break
52
+ end
53
+ end
54
+ end
55
+ if count==1
56
+ ret.at(0)
57
+ else
58
+ ret
59
+ end
60
+ end
61
+
62
+ def to_h(separator=nil, &block)
63
+ ret = {}
64
+ each do |item|
65
+ if separator
66
+ key, *values = item.split(separator)
67
+ if values.size > 0 || item.include?(separator)
68
+ ret[key]=values.join(separator)
69
+ else
70
+ ret[key]=true
71
+ end
72
+ elsif block
73
+ key, value = block.call(item)
74
+ ret[key]=value
75
+ end
76
+ end
77
+ ret
78
+ end
79
+
80
+ end
81
+ end
82
+
83
+ class Array
84
+ include Ki::KiEnumerable
85
+
86
+ def Array.wrap(maybe_arr)
87
+ if maybe_arr.kind_of?(Array)
88
+ maybe_arr
89
+ else
90
+ [maybe_arr]
91
+ end
92
+ end
93
+ end
94
+
95
+ module Enumerable
96
+ include Ki::KiEnumerable
97
+ end
98
+
99
+ require 'fileutils'
100
+ class File
101
+ def File.safe_write(dest, txt=nil, &block)
102
+ tmp = dest + "-" + rand(9999).to_s
103
+ begin
104
+ File.open(tmp, "w") do |file|
105
+ if block
106
+ block.call(file)
107
+ elsif txt
108
+ file.write(txt)
109
+ end
110
+ end
111
+ FileUtils.mv(tmp, dest)
112
+ rescue Exception => e
113
+ FileUtils.remove_entry_secure(tmp)
114
+ raise
115
+ end
116
+ end
117
+ end
118
+
119
+ class Hash
120
+ original_get = self.instance_method(:[])
121
+
122
+ define_method(:[]) do |key, default=nil|
123
+ value = original_get.bind(self).call(key)
124
+ if value || include?(key)
125
+ value
126
+ else
127
+ default
128
+ end
129
+ end
130
+
131
+ def require(key)
132
+ if !include?(key)
133
+ raise "'#{key}' is not defined!"
134
+ end
135
+ self[key]
136
+ end
137
+ end
@@ -0,0 +1,88 @@
1
+ # encoding: UTF-8
2
+
3
+ # Copyright 2012 Mikko Apo
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'monitor'
18
+
19
+ module Ki
20
+ class ServiceRegistry < Hash
21
+ attr_reader :by_parent
22
+
23
+ def initialize
24
+ @monitor = Monitor.new
25
+ @by_parent = {}
26
+ end
27
+
28
+ def register(*args)
29
+ @monitor.synchronize do
30
+ case args.size
31
+ when 1
32
+ args.first.each_pair do |url, clazz|
33
+ register(url, clazz)
34
+ end
35
+ when 2
36
+ url, clazz = args
37
+ self[url]=clazz
38
+ (@by_parent[File.dirname(url)]||=Array.new) << args
39
+ else
40
+ raise "Not supported '#{args.inspect}'"
41
+ end
42
+ end
43
+ self
44
+ end
45
+
46
+ def find(url, value=nil)
47
+ @monitor.synchronize do
48
+ if include?(url)
49
+ self[url]
50
+ elsif @by_parent.include?(url)
51
+ services = @by_parent[url]
52
+ if services
53
+ if value
54
+ services = services.select { |id, service| service.supports?(value) }
55
+ end
56
+ services = ServiceList.new.concat(services)
57
+ end
58
+ services
59
+ end
60
+ end
61
+ end
62
+
63
+ def find!(url, value=nil)
64
+ found = find(url, value)
65
+ if found.nil?
66
+ raise "Could not resolve '#{url}'"
67
+ end
68
+ found
69
+ end
70
+
71
+ def clear
72
+ @monitor.synchronize do
73
+ @by_parent.clear
74
+ super
75
+ end
76
+ end
77
+
78
+ class ServiceList < Array
79
+ def services
80
+ map { |url, service| service }
81
+ end
82
+
83
+ def service_names
84
+ map { |url, service| File.basename(url) }
85
+ end
86
+ end
87
+ end
88
+ end