goliath 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of goliath might be problematic. Click here for more details.
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.yardopts +2 -0
- data/Gemfile +3 -0
- data/LICENSE +66 -0
- data/README.md +86 -0
- data/Rakefile +18 -0
- data/examples/activerecord/config/srv.rb +7 -0
- data/examples/activerecord/srv.rb +37 -0
- data/examples/async_upload.rb +34 -0
- data/examples/conf_test.rb +27 -0
- data/examples/config/conf_test.rb +12 -0
- data/examples/config/echo.rb +1 -0
- data/examples/config/http_log.rb +7 -0
- data/examples/config/shared.rb +5 -0
- data/examples/custom_server.rb +57 -0
- data/examples/echo.rb +37 -0
- data/examples/gziped.rb +40 -0
- data/examples/hello_world.rb +10 -0
- data/examples/http_log.rb +85 -0
- data/examples/rack_routes.rb +44 -0
- data/examples/stream.rb +37 -0
- data/examples/valid.rb +19 -0
- data/goliath.gemspec +41 -0
- data/lib/goliath.rb +38 -0
- data/lib/goliath/api.rb +165 -0
- data/lib/goliath/application.rb +90 -0
- data/lib/goliath/connection.rb +94 -0
- data/lib/goliath/constants.rb +51 -0
- data/lib/goliath/env.rb +92 -0
- data/lib/goliath/goliath.rb +49 -0
- data/lib/goliath/headers.rb +37 -0
- data/lib/goliath/http_status_codes.rb +44 -0
- data/lib/goliath/plugins/latency.rb +33 -0
- data/lib/goliath/rack/default_mime_type.rb +30 -0
- data/lib/goliath/rack/default_response_format.rb +33 -0
- data/lib/goliath/rack/formatters/html.rb +90 -0
- data/lib/goliath/rack/formatters/json.rb +42 -0
- data/lib/goliath/rack/formatters/xml.rb +90 -0
- data/lib/goliath/rack/heartbeat.rb +23 -0
- data/lib/goliath/rack/jsonp.rb +38 -0
- data/lib/goliath/rack/params.rb +30 -0
- data/lib/goliath/rack/render.rb +66 -0
- data/lib/goliath/rack/tracer.rb +31 -0
- data/lib/goliath/rack/validation/boolean_value.rb +59 -0
- data/lib/goliath/rack/validation/default_params.rb +46 -0
- data/lib/goliath/rack/validation/numeric_range.rb +59 -0
- data/lib/goliath/rack/validation/request_method.rb +33 -0
- data/lib/goliath/rack/validation/required_param.rb +54 -0
- data/lib/goliath/rack/validation/required_value.rb +58 -0
- data/lib/goliath/rack/validation_error.rb +38 -0
- data/lib/goliath/request.rb +199 -0
- data/lib/goliath/response.rb +93 -0
- data/lib/goliath/runner.rb +236 -0
- data/lib/goliath/server.rb +149 -0
- data/lib/goliath/test_helper.rb +118 -0
- data/lib/goliath/version.rb +4 -0
- data/spec/integration/async_request_processing.rb +23 -0
- data/spec/integration/echo_spec.rb +27 -0
- data/spec/integration/keepalive_spec.rb +28 -0
- data/spec/integration/pipelining_spec.rb +43 -0
- data/spec/integration/valid_spec.rb +24 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/unit/connection_spec.rb +59 -0
- data/spec/unit/env_spec.rb +55 -0
- data/spec/unit/headers_spec.rb +53 -0
- data/spec/unit/rack/default_mime_type_spec.rb +34 -0
- data/spec/unit/rack/formatters/json_spec.rb +54 -0
- data/spec/unit/rack/formatters/xml_spec.rb +66 -0
- data/spec/unit/rack/heartbeat_spec.rb +47 -0
- data/spec/unit/rack/params_spec.rb +94 -0
- data/spec/unit/rack/render_spec.rb +87 -0
- data/spec/unit/rack/validation/boolean_value_spec.rb +54 -0
- data/spec/unit/rack/validation/default_params_spec.rb +71 -0
- data/spec/unit/rack/validation/numeric_range_spec.rb +96 -0
- data/spec/unit/rack/validation/request_method_spec.rb +47 -0
- data/spec/unit/rack/validation/required_param_spec.rb +92 -0
- data/spec/unit/rack/validation/required_value_spec.rb +99 -0
- data/spec/unit/rack/validation_error_spec.rb +40 -0
- data/spec/unit/request_spec.rb +59 -0
- data/spec/unit/response_spec.rb +35 -0
- data/spec/unit/runner_spec.rb +129 -0
- data/spec/unit/server_spec.rb +137 -0
- metadata +409 -0
@@ -0,0 +1,85 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Simple example that takes all requests and forwards them to
|
4
|
+
# another API using EM-HTTP-Request. Information about the
|
5
|
+
# request and response is then stored into a Mongo database.
|
6
|
+
#
|
7
|
+
|
8
|
+
$: << "../lib" << "./lib"
|
9
|
+
|
10
|
+
require 'rubygems'
|
11
|
+
require 'goliath'
|
12
|
+
require 'em-mongo'
|
13
|
+
require 'em-http'
|
14
|
+
require 'em-synchrony/em-http'
|
15
|
+
require 'pp'
|
16
|
+
|
17
|
+
class HttpLog < Goliath::API
|
18
|
+
use ::Rack::Reloader, 0 if Goliath.dev?
|
19
|
+
use Goliath::Rack::Params
|
20
|
+
|
21
|
+
def on_headers(env, headers)
|
22
|
+
env.logger.info 'proxying new request: ' + headers.inspect
|
23
|
+
env['client-headers'] = headers
|
24
|
+
end
|
25
|
+
|
26
|
+
def response(env)
|
27
|
+
start_time = Time.now.to_f
|
28
|
+
|
29
|
+
params = {:head => env['client-headers'], :query => env.params}
|
30
|
+
|
31
|
+
req = EM::HttpRequest.new("#{forwarder}#{env[Goliath::Request::REQUEST_PATH]}")
|
32
|
+
resp = case(env[Goliath::Request::REQUEST_METHOD])
|
33
|
+
when 'GET' then req.get(params)
|
34
|
+
when 'POST' then req.post(params.merge(:body => env[Goliath::Request::RACK_INPUT].read))
|
35
|
+
when 'HEAD' then req.head(params)
|
36
|
+
else p "UNKNOWN METHOD #{env[Goliath::Request::REQUEST_METHOD]}"
|
37
|
+
end
|
38
|
+
|
39
|
+
process_time = Time.now.to_f - start_time
|
40
|
+
|
41
|
+
response_headers = {}
|
42
|
+
resp.response_header.each_pair do |k, v|
|
43
|
+
response_headers[to_http_header(k)] = v
|
44
|
+
end
|
45
|
+
|
46
|
+
record(process_time, resp, env['client-headers'], response_headers)
|
47
|
+
|
48
|
+
[resp.response_header.status, response_headers, resp.response]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Need to convert from the CONTENT_TYPE we'll get back from the server
|
52
|
+
# to the normal Content-Type header
|
53
|
+
def to_http_header(k)
|
54
|
+
k.downcase.split('_').collect { |e| e.capitalize }.join('-')
|
55
|
+
end
|
56
|
+
|
57
|
+
# Write the request information into mongo
|
58
|
+
def record(process_time, resp, client_headers, response_headers)
|
59
|
+
e = env
|
60
|
+
EM.next_tick do
|
61
|
+
doc = {
|
62
|
+
request: {
|
63
|
+
http_method: e[Goliath::Request::REQUEST_METHOD],
|
64
|
+
path: e[Goliath::Request::REQUEST_PATH],
|
65
|
+
headers: client_headers,
|
66
|
+
params: e.params
|
67
|
+
},
|
68
|
+
response: {
|
69
|
+
status: resp.response_header.status,
|
70
|
+
length: resp.response.length,
|
71
|
+
headers: response_headers,
|
72
|
+
body: resp.response
|
73
|
+
},
|
74
|
+
process_time: process_time,
|
75
|
+
date: Time.now.to_i
|
76
|
+
}
|
77
|
+
|
78
|
+
if e[Goliath::Request::RACK_INPUT]
|
79
|
+
doc[:request][:body] = e[Goliath::Request::RACK_INPUT].read
|
80
|
+
end
|
81
|
+
|
82
|
+
e.mongo.insert(doc)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:<< '../lib' << 'lib'
|
3
|
+
|
4
|
+
require 'goliath'
|
5
|
+
|
6
|
+
# Example demonstrating how to have an API acting as a router.
|
7
|
+
# RackRoutes defines multiple uris and how to map them accordingly.
|
8
|
+
# Some of these routes are redirected to other Goliath API.
|
9
|
+
#
|
10
|
+
# The reason why only the last API is being used by the Goliath Server
|
11
|
+
# is because its name matches the filename.
|
12
|
+
# All the APIs are available but by default the server will use the one
|
13
|
+
# matching the file name.
|
14
|
+
|
15
|
+
# Our custom Goliath API
|
16
|
+
class HelloWorld < Goliath::API
|
17
|
+
def response(env)
|
18
|
+
[200, {}, "hello world!"]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Bonjour < Goliath::API
|
23
|
+
def response(env)
|
24
|
+
[200, {}, "bonjour!"]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class RackRoutes < Goliath::API
|
29
|
+
map '/version' do
|
30
|
+
run Proc.new { |env| [200, {"Content-Type" => "text/html"}, ["Version 0.1"]] }
|
31
|
+
end
|
32
|
+
|
33
|
+
map "/hello_world" do
|
34
|
+
run HelloWorld.new
|
35
|
+
end
|
36
|
+
|
37
|
+
map "/bonjour" do
|
38
|
+
run Bonjour.new
|
39
|
+
end
|
40
|
+
|
41
|
+
map '/' do
|
42
|
+
run Proc.new { |env| [404, {"Content-Type" => "text/html"}, ["Try /version /hello_world or /bonjour"]] }
|
43
|
+
end
|
44
|
+
end
|
data/examples/stream.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:<< '../lib' << 'lib'
|
3
|
+
|
4
|
+
#
|
5
|
+
# A simple HTTP streaming API which returns a 200 response for any GET request
|
6
|
+
# and then emits numbers 1 through 10 in 1 second intervals, and then closes the
|
7
|
+
# connection.
|
8
|
+
#
|
9
|
+
# A good use case for this pattern would be to provide a stream of updates or a
|
10
|
+
# 'firehose' like API to stream data back to the clients. Simply hook up to your
|
11
|
+
# datasource and then stream the data to your clients via HTTP.
|
12
|
+
#
|
13
|
+
|
14
|
+
require 'goliath'
|
15
|
+
|
16
|
+
class Stream < Goliath::API
|
17
|
+
def on_close(env)
|
18
|
+
env.logger.info "Connection closed."
|
19
|
+
end
|
20
|
+
|
21
|
+
def response(env)
|
22
|
+
i = 0
|
23
|
+
pt = EM.add_periodic_timer(1) do
|
24
|
+
env.stream_send("#{i} ")
|
25
|
+
i += 1
|
26
|
+
end
|
27
|
+
|
28
|
+
EM.add_timer(10) do
|
29
|
+
pt.cancel
|
30
|
+
|
31
|
+
env.stream_send("!! BOOM !!\n")
|
32
|
+
env.stream_close
|
33
|
+
end
|
34
|
+
|
35
|
+
[200, {}, Goliath::Response::STREAMING]
|
36
|
+
end
|
37
|
+
end
|
data/examples/valid.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:<< '../lib' << 'lib'
|
3
|
+
|
4
|
+
require 'goliath'
|
5
|
+
|
6
|
+
class Valid < Goliath::API
|
7
|
+
|
8
|
+
# reload code on every request in dev environment
|
9
|
+
use ::Rack::Reloader, 0 if Goliath.dev?
|
10
|
+
|
11
|
+
use Goliath::Rack::Params
|
12
|
+
use Goliath::Rack::ValidationError
|
13
|
+
|
14
|
+
use Goliath::Rack::Validation::RequiredParam, {:key => 'test'}
|
15
|
+
|
16
|
+
def response(env)
|
17
|
+
[200, {}, 'OK']
|
18
|
+
end
|
19
|
+
end
|
data/goliath.gemspec
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require 'goliath/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'goliath'
|
7
|
+
s.version = Goliath::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ['dan sinclair', 'Ilya Grigorik']
|
10
|
+
s.email = ['dj2@everburning.com', 'ilya@igvita.com']
|
11
|
+
s.homepage = 'http://labs.postrank.com/'
|
12
|
+
s.summary = 'Framework for writing API servers'
|
13
|
+
s.description = s.summary
|
14
|
+
|
15
|
+
s.required_ruby_version = '>=1.9.2'
|
16
|
+
|
17
|
+
s.add_dependency 'eventmachine', '>= 1.0.0.beta.1'
|
18
|
+
s.add_dependency 'em-synchrony', '>= 0.3.0.beta.1'
|
19
|
+
s.add_dependency 'http_parser.rb'
|
20
|
+
s.add_dependency 'log4r'
|
21
|
+
|
22
|
+
s.add_dependency 'rack'
|
23
|
+
s.add_dependency 'rack-contrib'
|
24
|
+
s.add_dependency 'rack-respond_to'
|
25
|
+
s.add_dependency 'async-rack'
|
26
|
+
s.add_dependency 'multi_json'
|
27
|
+
|
28
|
+
s.add_development_dependency 'rspec', '>2.0'
|
29
|
+
s.add_development_dependency 'nokogiri'
|
30
|
+
s.add_development_dependency 'em-http-request', '>= 1.0.0.beta.1'
|
31
|
+
s.add_development_dependency 'em-mongo'
|
32
|
+
s.add_development_dependency 'yajl-ruby'
|
33
|
+
s.add_development_dependency 'rack-rewrite'
|
34
|
+
|
35
|
+
s.add_development_dependency 'yard'
|
36
|
+
s.add_development_dependency 'bluecloth'
|
37
|
+
|
38
|
+
s.files = `git ls-files`.split("\n")
|
39
|
+
s.test_files = `git ls-files -- spec/*`.split("\n")
|
40
|
+
s.require_paths = ['lib']
|
41
|
+
end
|
data/lib/goliath.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'http/parser'
|
3
|
+
require 'async_rack'
|
4
|
+
require 'stringio'
|
5
|
+
|
6
|
+
require 'goliath/version'
|
7
|
+
require 'goliath/goliath'
|
8
|
+
require 'goliath/runner'
|
9
|
+
require 'goliath/server'
|
10
|
+
require 'goliath/constants'
|
11
|
+
require 'goliath/connection'
|
12
|
+
require 'goliath/request'
|
13
|
+
require 'goliath/response'
|
14
|
+
require 'goliath/headers'
|
15
|
+
require 'goliath/http_status_codes'
|
16
|
+
|
17
|
+
require 'goliath/rack/default_response_format'
|
18
|
+
require 'goliath/rack/heartbeat'
|
19
|
+
require 'goliath/rack/params'
|
20
|
+
require 'goliath/rack/render'
|
21
|
+
require 'goliath/rack/default_mime_type'
|
22
|
+
require 'goliath/rack/tracer'
|
23
|
+
require 'goliath/rack/validation_error'
|
24
|
+
require 'goliath/rack/formatters/json'
|
25
|
+
require 'goliath/rack/formatters/html'
|
26
|
+
require 'goliath/rack/formatters/xml'
|
27
|
+
require 'goliath/rack/jsonp'
|
28
|
+
|
29
|
+
require 'goliath/rack/validation/request_method'
|
30
|
+
require 'goliath/rack/validation/required_param'
|
31
|
+
require 'goliath/rack/validation/required_value'
|
32
|
+
require 'goliath/rack/validation/numeric_range'
|
33
|
+
require 'goliath/rack/validation/default_params'
|
34
|
+
require 'goliath/rack/validation/boolean_value'
|
35
|
+
|
36
|
+
require 'goliath/api'
|
37
|
+
|
38
|
+
require 'goliath/application'
|
data/lib/goliath/api.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'goliath/response'
|
2
|
+
require 'goliath/request'
|
3
|
+
|
4
|
+
module Goliath
|
5
|
+
# All Goliath APIs subclass Goliath::API. All subclasses _must_ override the
|
6
|
+
# {#response} method.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# require 'goliath'
|
10
|
+
#
|
11
|
+
# class HelloWorld < Goliath::API
|
12
|
+
# def response(env)
|
13
|
+
# [200, {}, "hello world"]
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
class API
|
18
|
+
class << self
|
19
|
+
# Retrieves the middlewares defined by this API server
|
20
|
+
#
|
21
|
+
# @return [Array] array contains [middleware class, args, block]
|
22
|
+
def middlewares
|
23
|
+
@middlewares ||= [[::Rack::ContentLength, nil, nil],
|
24
|
+
[Goliath::Rack::DefaultResponseFormat, nil, nil]]
|
25
|
+
end
|
26
|
+
|
27
|
+
# Specify a middleware to be used by the API
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
# use Goliath::Rack::Validation::RequiredParam, {:key => 'echo'}
|
31
|
+
#
|
32
|
+
# use ::Rack::Rewrite do
|
33
|
+
# rewrite %r{^(.*?)\??gziped=(.*)$}, lambda { |match, env| "#{match[1]}?echo=#{match[2]}" }
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# @param name [Class] The middleware class to use
|
37
|
+
# @param args Any arguments to pass to the middeware
|
38
|
+
# @param block A block to pass to the middleware
|
39
|
+
def use(name, args = nil, &block)
|
40
|
+
middlewares.push([name, args, block])
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the plugins configured for this API
|
44
|
+
#
|
45
|
+
# @return [Array] array contains [plugin name, args]
|
46
|
+
def plugins
|
47
|
+
@plugins ||= []
|
48
|
+
end
|
49
|
+
|
50
|
+
# Specify a plugin to be used by the API
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# plugin Goliath::Plugin::Latency
|
54
|
+
#
|
55
|
+
# @param name [Class] The plugin class to use
|
56
|
+
# @param args The arguments to the plugin
|
57
|
+
def plugin(name, *args)
|
58
|
+
plugins.push([name, args])
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the router maps configured for the API
|
62
|
+
#
|
63
|
+
# @return [Array] array contains [path, block]
|
64
|
+
def maps
|
65
|
+
@maps ||= []
|
66
|
+
end
|
67
|
+
|
68
|
+
# Specify a router map to be used by the API
|
69
|
+
#
|
70
|
+
# @example
|
71
|
+
# map '/version' do
|
72
|
+
# run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["Version 0.1"]] }
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# @param name [String] The URL path to map
|
76
|
+
# @param block The code to execute
|
77
|
+
def map(name, &block)
|
78
|
+
maps.push([name, block])
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Default stub method to add options into the option parser.
|
83
|
+
#
|
84
|
+
# @example
|
85
|
+
# def options_parser(opts, options)
|
86
|
+
# options[:test] = 0
|
87
|
+
# opts.on('-t', '--test NUM', "The test number") { |val| options[:test] = val.to_i }
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# @param opts [OptionParser] The options parser
|
91
|
+
# @param options [Hash] The hash to insert the parsed options into
|
92
|
+
def options_parser(opts, options)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Accessor for the current env object
|
96
|
+
#
|
97
|
+
# @note This will not work in a streaming server. You must pass around the env object.
|
98
|
+
#
|
99
|
+
# @return [Goliath::Env] The current environment data for the request
|
100
|
+
def env
|
101
|
+
Thread.current[Goliath::Constants::GOLIATH_ENV]
|
102
|
+
end
|
103
|
+
|
104
|
+
# The API will proxy missing calls to the env object if possible.
|
105
|
+
#
|
106
|
+
# The two entries in this example are equivalent as long as you are not
|
107
|
+
# in a streaming server.
|
108
|
+
#
|
109
|
+
# @example
|
110
|
+
# logger.info "Hello"
|
111
|
+
# env.logger.info "Hello"
|
112
|
+
|
113
|
+
def method_missing(name, *args, &blk)
|
114
|
+
name = name.to_s
|
115
|
+
if env.respond_to?(name)
|
116
|
+
env.send(name, *args, &blk)
|
117
|
+
else
|
118
|
+
super(name.to_sym, *args, &blk)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# {#call} is executed automatically by the middleware chain and will setup
|
123
|
+
# the environment for the {#response} method to execute. This includes setting
|
124
|
+
# up a new Fiber, handing any execptions thrown from the API and executing
|
125
|
+
# the appropriate callback method for the API.
|
126
|
+
#
|
127
|
+
# @param env [Goliath::Env] The request environment
|
128
|
+
# @return [Goliath::Connection::AsyncResponse] An async response.
|
129
|
+
def call(env)
|
130
|
+
Fiber.new {
|
131
|
+
begin
|
132
|
+
Thread.current[Goliath::Constants::GOLIATH_ENV] = env
|
133
|
+
status, headers, body = response(env)
|
134
|
+
|
135
|
+
if body == Goliath::Response::STREAMING
|
136
|
+
env[Goliath::Constants::STREAM_START].call(status, headers)
|
137
|
+
else
|
138
|
+
env[Goliath::Constants::ASYNC_CALLBACK].call([status, headers, body])
|
139
|
+
end
|
140
|
+
|
141
|
+
rescue Exception => e
|
142
|
+
env.logger.error(e.message)
|
143
|
+
env.logger.error(e.backtrace.join("\n"))
|
144
|
+
|
145
|
+
env[Goliath::Constants::ASYNC_CALLBACK].call([400, {}, {:error => e.message}])
|
146
|
+
end
|
147
|
+
}.resume
|
148
|
+
|
149
|
+
Goliath::Connection::AsyncResponse
|
150
|
+
end
|
151
|
+
|
152
|
+
# Response is the main implementation method for Goliath APIs. All APIs
|
153
|
+
# should override this method in order to do any actual work.
|
154
|
+
#
|
155
|
+
# The response method will be executed in a new Fiber and wrapped in a
|
156
|
+
# begin rescue block to handle an thrown API errors.
|
157
|
+
#
|
158
|
+
# @param env [Goliath::Env] The request environment
|
159
|
+
# @return [Array] Array contains [Status code, Headers Hash, Body]
|
160
|
+
def response(env)
|
161
|
+
env.logger.error('You need to implement response')
|
162
|
+
[400, {}, {:error => 'No response implemented'}]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|