faraday 0.5.7 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +9 -8
- data/README.md +99 -20
- data/faraday.gemspec +9 -11
- data/lib/faraday.rb +1 -1
- data/lib/faraday/adapter.rb +11 -86
- data/lib/faraday/adapter/action_dispatch.rb +3 -12
- data/lib/faraday/adapter/em_synchrony.rb +9 -24
- data/lib/faraday/adapter/excon.rb +7 -19
- data/lib/faraday/adapter/net_http.rb +37 -35
- data/lib/faraday/adapter/patron.rb +16 -23
- data/lib/faraday/adapter/test.rb +4 -10
- data/lib/faraday/adapter/typhoeus.rb +11 -39
- data/lib/faraday/builder.rb +82 -33
- data/lib/faraday/connection.rb +3 -10
- data/lib/faraday/error.rb +20 -15
- data/lib/faraday/middleware.rb +7 -2
- data/lib/faraday/request.rb +13 -10
- data/lib/faraday/request/json.rb +31 -0
- data/lib/faraday/request/multipart.rb +63 -0
- data/lib/faraday/request/url_encoded.rb +37 -0
- data/lib/faraday/response.rb +72 -30
- data/lib/faraday/response/logger.rb +34 -0
- data/lib/faraday/response/raise_error.rb +16 -0
- data/lib/faraday/upload_io.rb +14 -6
- data/lib/faraday/utils.rb +54 -17
- data/test/adapters/live_test.rb +36 -14
- data/test/adapters/logger_test.rb +1 -1
- data/test/adapters/net_http_test.rb +33 -0
- data/test/connection_test.rb +0 -39
- data/test/env_test.rb +84 -6
- data/test/helper.rb +17 -8
- data/test/live_server.rb +19 -17
- data/test/middleware_stack_test.rb +91 -0
- data/test/request_middleware_test.rb +75 -21
- data/test/response_middleware_test.rb +34 -31
- metadata +21 -17
- data/lib/faraday/adapter/logger.rb +0 -32
- data/lib/faraday/request/active_support_json.rb +0 -21
- data/lib/faraday/request/yajl.rb +0 -18
- data/lib/faraday/response/active_support_json.rb +0 -30
- data/lib/faraday/response/yajl.rb +0 -26
- data/test/adapters/typhoeus_test.rb +0 -31
- data/test/connection_app_test.rb +0 -60
- data/test/form_post_test.rb +0 -58
- data/test/multipart_test.rb +0 -48
data/Gemfile
CHANGED
@@ -4,15 +4,16 @@ source "http://rubygems.org"
|
|
4
4
|
# put test-only gems in this group so their generators
|
5
5
|
# and rake tasks are available in development mode:
|
6
6
|
group :development, :test do
|
7
|
-
gem 'patron', '~> 0.4'
|
7
|
+
gem 'patron', '~> 0.4', :platforms => :ruby
|
8
8
|
gem 'sinatra', '~> 1.1'
|
9
|
-
gem 'typhoeus', '~> 0.2'
|
10
|
-
gem '
|
11
|
-
gem 'em-http-request', '~> 0.3', :require => 'em-http'
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
9
|
+
gem 'typhoeus', '~> 0.2', :platforms => :ruby
|
10
|
+
gem 'excon', '~> 0.5.8'
|
11
|
+
gem 'em-http-request', '~> 0.3', :require => 'em-http', :platforms => :ruby
|
12
|
+
gem 'em-synchrony', '~> 0.2', :require => ['em-synchrony', 'em-synchrony/em-http'], :platforms => :ruby_19
|
13
|
+
gem 'webmock'
|
14
|
+
# ActiveSupport::JSON will be used in ruby 1.8 and Yajl in 1.9; this is to test against both adapters
|
15
|
+
gem 'activesupport', '~> 2.3.8', :require => nil, :platforms => [:ruby_18, :jruby]
|
16
|
+
gem 'yajl-ruby', :require => 'yajl', :platforms => :ruby_19
|
16
17
|
end
|
17
18
|
|
18
19
|
gemspec
|
data/README.md
CHANGED
@@ -7,30 +7,109 @@ This mess is gonna get raw, like sushi. So, haters to the left.
|
|
7
7
|
## Usage
|
8
8
|
|
9
9
|
conn = Faraday.new(:url => 'http://sushi.com') do |builder|
|
10
|
-
builder.use Faraday::Request::
|
11
|
-
builder.use Faraday::
|
12
|
-
builder.use Faraday::
|
13
|
-
builder.use Faraday::Adapter::
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
builder.request :
|
18
|
-
builder.
|
19
|
-
builder.adapter :
|
20
|
-
builder.adapter :em_synchrony # Faraday::Adapter::EMSynchrony
|
21
|
-
builder.response :yajl # Faraday::Response::Yajl
|
10
|
+
builder.use Faraday::Request::UrlEncoded # convert request params as "www-form-urlencoded"
|
11
|
+
builder.use Faraday::Request::JSON # encode request params as json
|
12
|
+
builder.use Faraday::Response::Logger # log the request to STDOUT
|
13
|
+
builder.use Faraday::Adapter::NetHttp # make http requests with Net::HTTP
|
14
|
+
|
15
|
+
# or, use shortcuts:
|
16
|
+
builder.request :url_encoded
|
17
|
+
builder.request :json
|
18
|
+
builder.response :logger
|
19
|
+
builder.adapter :net_http
|
22
20
|
end
|
21
|
+
|
22
|
+
## GET ##
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
24
|
+
response = conn.get '/nigiri/sake.json' # GET http://sushi.com/nigiri/sake.json
|
25
|
+
response.body
|
26
|
+
|
27
|
+
conn.get '/nigiri', 'X-Awesome' => true # custom request header
|
28
|
+
|
29
|
+
conn.get do |req| # GET http://sushi.com/search?page=2&limit=100
|
30
|
+
req.url '/search', :page => 2
|
31
|
+
req.params['limit'] = 100
|
32
|
+
end
|
33
|
+
|
34
|
+
## POST ##
|
35
|
+
|
36
|
+
conn.post '/nigiri', { :name => 'Maguro' } # POST "name=maguro" to http://sushi.com/nigiri
|
37
|
+
|
38
|
+
# post payload as JSON instead of "www-form-urlencoded" encoding:
|
39
|
+
conn.post '/nigiri', payload, 'Content-Type' => 'application/json'
|
40
|
+
|
41
|
+
# a more verbose way:
|
42
|
+
conn.post do |req|
|
43
|
+
req.url '/nigiri'
|
44
|
+
req.headers['Content-Type'] = 'application/json'
|
45
|
+
req.body = { :name => 'Unagi' }
|
46
|
+
end
|
47
|
+
|
48
|
+
If you're ready to roll with just the bare minimum:
|
49
|
+
|
50
|
+
# default stack (net/http), no extra middleware:
|
51
|
+
response = Faraday.get 'http://sushi.com/nigiri/sake.json'
|
52
|
+
|
53
|
+
## Advanced middleware usage
|
54
|
+
|
55
|
+
The order in which middleware is stacked is important. Like with Rack, the first middleware on the list wraps all others, while the last middleware is the innermost one, so that's usually the adapter.
|
56
|
+
|
57
|
+
conn = Faraday.new(:url => 'http://sushi.com') do |builder|
|
58
|
+
# POST/PUT params encoders:
|
59
|
+
builder.request :multipart
|
60
|
+
builder.request :url_encoded
|
61
|
+
builder.request :json
|
62
|
+
|
63
|
+
builder.adapter :net_http
|
64
|
+
end
|
65
|
+
|
66
|
+
This request middleware setup affects POST/PUT requests in the following way:
|
67
|
+
|
68
|
+
1. `Request::Multipart` checks for files in the payload, otherwise leaves everything untouched;
|
69
|
+
2. `Request::UrlEncoded` encodes as "application/x-www-form-urlencoded" if not already encoded or of another type
|
70
|
+
2. `Request::JSON` encodes as "application/json" if not already encoded or of another type
|
71
|
+
|
72
|
+
Because "UrlEncoded" is higher on the stack than JSON encoder, it will get to process the request first. Swapping them means giving the other priority. Specifying the "Content-Type" for the request is explicitly stating which middleware should process it.
|
73
|
+
|
74
|
+
Examples:
|
75
|
+
|
76
|
+
payload = { :name => 'Maguro' }
|
77
|
+
|
78
|
+
# post payload as JSON instead of urlencoded:
|
79
|
+
conn.post '/nigiri', payload, 'Content-Type' => 'application/json'
|
80
|
+
|
81
|
+
# uploading a file:
|
82
|
+
payload = { :profile_pic => Faraday::UploadIO.new('avatar.jpg', 'image/jpeg') }
|
83
|
+
|
84
|
+
# "Multipart" middleware detects files and encodes with "multipart/form-data":
|
85
|
+
conn.put '/profile', payload
|
86
|
+
|
87
|
+
## Writing middleware
|
88
|
+
|
89
|
+
Middleware are classes that respond to `call()`. They wrap the request/response cycle.
|
90
|
+
|
91
|
+
def call(env)
|
92
|
+
# do something with the request
|
93
|
+
|
94
|
+
@app.call(env).on_complete do
|
95
|
+
# do something with the response
|
96
|
+
end
|
30
97
|
end
|
31
98
|
|
32
|
-
|
33
|
-
|
99
|
+
It's important to do all processing of the response only in the `on_complete` block. This enables middleware to work in parallel mode where requests are asynchronous.
|
100
|
+
|
101
|
+
The `env` is a hash with symbol keys that contains info about the request and, later, response. Some keys are:
|
102
|
+
|
103
|
+
# request phase
|
104
|
+
:method - :get, :post, ...
|
105
|
+
:url - URI for the current request; also contains GET parameters
|
106
|
+
:body - POST parameters for :post/:put requests
|
107
|
+
:request_headers
|
108
|
+
|
109
|
+
# response phase
|
110
|
+
:status - HTTP response status code, such as 200
|
111
|
+
:body - the response body
|
112
|
+
:response_headers
|
34
113
|
|
35
114
|
## Testing
|
36
115
|
|
data/faraday.gemspec
CHANGED
@@ -12,8 +12,8 @@ Gem::Specification.new do |s|
|
|
12
12
|
## If your rubyforge_project name is different, then edit it and comment out
|
13
13
|
## the sub! line in the Rakefile
|
14
14
|
s.name = 'faraday'
|
15
|
-
s.version = '0.
|
16
|
-
s.date = '2011-
|
15
|
+
s.version = '0.6.0'
|
16
|
+
s.date = '2011-03-31'
|
17
17
|
s.rubyforge_project = 'faraday'
|
18
18
|
|
19
19
|
## Make sure your summary is short. The description may be as long
|
@@ -52,7 +52,6 @@ Gem::Specification.new do |s|
|
|
52
52
|
lib/faraday/adapter/action_dispatch.rb
|
53
53
|
lib/faraday/adapter/em_synchrony.rb
|
54
54
|
lib/faraday/adapter/excon.rb
|
55
|
-
lib/faraday/adapter/logger.rb
|
56
55
|
lib/faraday/adapter/net_http.rb
|
57
56
|
lib/faraday/adapter/patron.rb
|
58
57
|
lib/faraday/adapter/test.rb
|
@@ -62,24 +61,23 @@ Gem::Specification.new do |s|
|
|
62
61
|
lib/faraday/error.rb
|
63
62
|
lib/faraday/middleware.rb
|
64
63
|
lib/faraday/request.rb
|
65
|
-
lib/faraday/request/
|
66
|
-
lib/faraday/request/
|
64
|
+
lib/faraday/request/json.rb
|
65
|
+
lib/faraday/request/multipart.rb
|
66
|
+
lib/faraday/request/url_encoded.rb
|
67
67
|
lib/faraday/response.rb
|
68
|
-
lib/faraday/response/
|
69
|
-
lib/faraday/response/
|
68
|
+
lib/faraday/response/logger.rb
|
69
|
+
lib/faraday/response/raise_error.rb
|
70
70
|
lib/faraday/upload_io.rb
|
71
71
|
lib/faraday/utils.rb
|
72
72
|
test/adapters/live_test.rb
|
73
73
|
test/adapters/logger_test.rb
|
74
|
+
test/adapters/net_http_test.rb
|
74
75
|
test/adapters/test_middleware_test.rb
|
75
|
-
test/adapters/typhoeus_test.rb
|
76
|
-
test/connection_app_test.rb
|
77
76
|
test/connection_test.rb
|
78
77
|
test/env_test.rb
|
79
|
-
test/form_post_test.rb
|
80
78
|
test/helper.rb
|
81
79
|
test/live_server.rb
|
82
|
-
test/
|
80
|
+
test/middleware_stack_test.rb
|
83
81
|
test/request_middleware_test.rb
|
84
82
|
test/response_middleware_test.rb
|
85
83
|
]
|
data/lib/faraday.rb
CHANGED
data/lib/faraday/adapter.rb
CHANGED
@@ -1,11 +1,9 @@
|
|
1
1
|
module Faraday
|
2
2
|
class Adapter < Middleware
|
3
|
-
|
4
|
-
MULTIPART_TYPE = 'multipart/form-data'.freeze
|
5
|
-
CONTENT_TYPE = 'Content-Type'.freeze
|
6
|
-
DEFAULT_BOUNDARY = "-----------RubyMultipartPost".freeze
|
3
|
+
CONTENT_LENGTH = 'Content-Length'.freeze
|
7
4
|
|
8
5
|
extend AutoloadHelper
|
6
|
+
|
9
7
|
autoload_all 'faraday/adapter',
|
10
8
|
:ActionDispatch => 'action_dispatch',
|
11
9
|
:NetHttp => 'net_http',
|
@@ -13,8 +11,7 @@ module Faraday
|
|
13
11
|
:EMSynchrony => 'em_synchrony',
|
14
12
|
:Patron => 'patron',
|
15
13
|
:Excon => 'excon',
|
16
|
-
:Test => 'test'
|
17
|
-
:Logger => 'logger'
|
14
|
+
:Test => 'test'
|
18
15
|
|
19
16
|
register_lookup_modules \
|
20
17
|
:action_dispatch => :ActionDispatch,
|
@@ -23,91 +20,19 @@ module Faraday
|
|
23
20
|
:typhoeus => :Typhoeus,
|
24
21
|
:patron => :Patron,
|
25
22
|
:em_synchrony => :EMSynchrony,
|
26
|
-
:excon => :Excon
|
27
|
-
:logger => :Logger
|
23
|
+
:excon => :Excon
|
28
24
|
|
29
25
|
def call(env)
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
# act on the request before sending it out.
|
36
|
-
#
|
37
|
-
# env - The current request environment Hash.
|
38
|
-
# body - A Hash of keys/values. Strings and empty values will be
|
39
|
-
# ignored. Default: env[:body]
|
40
|
-
# headers - The Hash of request headers. Default: env[:request_headers]
|
41
|
-
#
|
42
|
-
# Returns nothing. If the body is processed, it is replaced in the
|
43
|
-
# environment for you.
|
44
|
-
def process_body_for_request(env, body = env[:body], headers = env[:request_headers])
|
45
|
-
return if body.nil? || body.empty? || !body.respond_to?(:each_key)
|
46
|
-
if has_multipart?(body)
|
47
|
-
env[:request] ||= {}
|
48
|
-
env[:request][:boundary] ||= DEFAULT_BOUNDARY
|
49
|
-
headers[CONTENT_TYPE] = MULTIPART_TYPE + ";boundary=#{env[:request][:boundary]}"
|
50
|
-
env[:body] = create_multipart(env, body)
|
51
|
-
else
|
52
|
-
type = headers[CONTENT_TYPE]
|
53
|
-
headers[CONTENT_TYPE] = FORM_TYPE if type.nil? || type.empty?
|
54
|
-
parts = []
|
55
|
-
process_to_params(parts, env[:body]) do |key, value|
|
56
|
-
"#{key}=#{escape(value.to_s)}"
|
57
|
-
end
|
58
|
-
env[:body] = parts * "&"
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def has_multipart?(body)
|
63
|
-
body.values.each do |v|
|
64
|
-
if v.respond_to?(:content_type)
|
65
|
-
return true
|
66
|
-
elsif v.respond_to?(:values)
|
67
|
-
return true if has_multipart?(v)
|
68
|
-
end
|
26
|
+
if !env[:body] and Connection::METHODS_WITH_BODIES.include? env[:method]
|
27
|
+
# play nice and indicate we're sending an empty body
|
28
|
+
env[:request_headers][CONTENT_LENGTH] = "0"
|
29
|
+
# Typhoeus hangs on PUT requests if body is nil
|
30
|
+
env[:body] = ''
|
69
31
|
end
|
70
|
-
false
|
71
32
|
end
|
72
33
|
|
73
|
-
def
|
74
|
-
|
75
|
-
parts = []
|
76
|
-
process_to_params(parts, params) do |key, value|
|
77
|
-
Faraday::Parts::Part.new(boundary, key, value)
|
78
|
-
end
|
79
|
-
parts << Faraday::Parts::EpiloguePart.new(boundary)
|
80
|
-
env[:request_headers]['Content-Length'] = parts.inject(0) {|sum,i| sum + i.length }.to_s
|
81
|
-
Faraday::CompositeReadIO.new(*parts.map{|p| p.to_io })
|
82
|
-
end
|
83
|
-
|
84
|
-
def process_to_params(pieces, params, base = nil)
|
85
|
-
params.to_a.each do |key, value|
|
86
|
-
key_str = base ? "#{base}[#{key}]" : key
|
87
|
-
|
88
|
-
block = block_given? ? Proc.new : nil
|
89
|
-
case value
|
90
|
-
when Array
|
91
|
-
values = value.inject([]) { |a,v| a << [nil, v] }
|
92
|
-
process_to_params(pieces, values, key_str, &block)
|
93
|
-
when Hash
|
94
|
-
process_to_params(pieces, value, key_str, &block)
|
95
|
-
else
|
96
|
-
pieces << block.call(key_str, value)
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
# assume that query and fragment are already encoded properly
|
102
|
-
def full_path_for(path, query = nil, fragment = nil)
|
103
|
-
full_path = path.dup
|
104
|
-
if query && !query.empty?
|
105
|
-
full_path << "?#{query}"
|
106
|
-
end
|
107
|
-
if fragment && !fragment.empty?
|
108
|
-
full_path << "##{fragment}"
|
109
|
-
end
|
110
|
-
full_path
|
34
|
+
def response_headers(env)
|
35
|
+
env[:response_headers] ||= Utils::Headers.new
|
111
36
|
end
|
112
37
|
end
|
113
38
|
end
|
@@ -19,21 +19,12 @@ module Faraday
|
|
19
19
|
|
20
20
|
def call(env)
|
21
21
|
super
|
22
|
-
|
23
|
-
@session.__send__(env[:method], full_path, env[:body], env[:request_headers])
|
22
|
+
@session.__send__(env[:method], env[:url].request_uri, env[:body], env[:request_headers])
|
24
23
|
resp = @session.response
|
25
|
-
env.update
|
26
|
-
|
27
|
-
:response_headers => resp.headers,
|
28
|
-
:body => resp.body
|
24
|
+
env.update :status => resp.status, :body => resp.body
|
25
|
+
response_headers(env).update resp.headers
|
29
26
|
@app.call env
|
30
27
|
end
|
31
|
-
|
32
|
-
# TODO: build in support for multipart streaming if action dispatch supports it.
|
33
|
-
def create_multipart(env, params, boundary = nil)
|
34
|
-
stream = super
|
35
|
-
stream.read
|
36
|
-
end
|
37
28
|
end
|
38
29
|
end
|
39
30
|
end
|
@@ -1,33 +1,17 @@
|
|
1
1
|
module Faraday
|
2
2
|
class Adapter
|
3
3
|
class EMSynchrony < Faraday::Adapter
|
4
|
-
|
4
|
+
dependency do
|
5
5
|
require 'em-synchrony/em-http'
|
6
6
|
require 'fiber'
|
7
|
-
rescue LoadError, NameError => e
|
8
|
-
self.load_error = e
|
9
|
-
end
|
10
|
-
|
11
|
-
class Header
|
12
|
-
include Net::HTTPHeader
|
13
|
-
def initialize response
|
14
|
-
@header = {}
|
15
|
-
response.response_header.each do |key, value|
|
16
|
-
case key
|
17
|
-
when "CONTENT_TYPE"; self.content_type = value
|
18
|
-
when "CONTENT_LENGTH"; self.content_length = value
|
19
|
-
else; self[key] = value
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
7
|
end
|
24
8
|
|
25
9
|
def call(env)
|
26
|
-
|
10
|
+
super
|
27
11
|
request = EventMachine::HttpRequest.new(URI::parse(env[:url].to_s))
|
28
12
|
options = {:head => env[:request_headers]}
|
29
13
|
options[:ssl] = env[:ssl] if env[:ssl]
|
30
|
-
|
14
|
+
|
31
15
|
if env[:body]
|
32
16
|
if env[:body].respond_to? :read
|
33
17
|
options[:body] = env[:body].read
|
@@ -41,7 +25,7 @@ module Faraday
|
|
41
25
|
uri = Addressable::URI.parse(proxy[:uri])
|
42
26
|
options[:proxy] = {
|
43
27
|
:host => uri.host,
|
44
|
-
:port => uri.
|
28
|
+
:port => uri.inferred_port
|
45
29
|
}
|
46
30
|
if proxy[:username] && proxy[:password]
|
47
31
|
options[:proxy][:authorization] = [proxy[:username], proxy[:password]]
|
@@ -67,13 +51,14 @@ module Faraday
|
|
67
51
|
client = block.call
|
68
52
|
end
|
69
53
|
|
70
|
-
|
71
|
-
|
72
|
-
|
54
|
+
client.response_header.each do |name, value|
|
55
|
+
response_headers(env)[name.to_sym] = value
|
56
|
+
end
|
57
|
+
env.update :status => client.response_header.status, :body => client.response
|
73
58
|
|
74
59
|
@app.call env
|
75
60
|
rescue Errno::ECONNREFUSED
|
76
|
-
raise Error::ConnectionFailed,
|
61
|
+
raise Error::ConnectionFailed, $!
|
77
62
|
end
|
78
63
|
end
|
79
64
|
end
|
@@ -1,21 +1,15 @@
|
|
1
1
|
module Faraday
|
2
2
|
class Adapter
|
3
3
|
class Excon < Faraday::Adapter
|
4
|
-
|
5
|
-
require 'excon'
|
6
|
-
rescue LoadError, NameError => e
|
7
|
-
self.load_error = e
|
8
|
-
end
|
4
|
+
dependency 'excon'
|
9
5
|
|
10
6
|
def call(env)
|
11
7
|
super
|
12
8
|
|
13
9
|
conn = ::Excon.new(env[:url].to_s)
|
14
10
|
if ssl = (env[:url].scheme == 'https' && env[:ssl])
|
15
|
-
::Excon.ssl_verify_peer = !!ssl
|
16
|
-
|
17
|
-
::Excon.ssl_ca_path = ca_file
|
18
|
-
end
|
11
|
+
::Excon.ssl_verify_peer = !!ssl.fetch(:verify, true)
|
12
|
+
::Excon.ssl_ca_path = ssl[:ca_file] if ssl[:ca_file]
|
19
13
|
end
|
20
14
|
|
21
15
|
resp = conn.request \
|
@@ -23,18 +17,12 @@ module Faraday
|
|
23
17
|
:headers => env[:request_headers],
|
24
18
|
:body => env[:body]
|
25
19
|
|
26
|
-
env.update
|
27
|
-
|
28
|
-
:response_headers => {},
|
29
|
-
:body => resp.body
|
30
|
-
|
31
|
-
resp.headers.each do |key, value|
|
32
|
-
env[:response_headers][key.downcase] = value
|
33
|
-
end
|
20
|
+
env.update :status => resp.status.to_i, :body => resp.body
|
21
|
+
response_headers(env).update resp.headers
|
34
22
|
|
35
23
|
@app.call env
|
36
|
-
rescue ::Excon::Errors::SocketError
|
37
|
-
raise Error::ConnectionFailed
|
24
|
+
rescue ::Excon::Errors::SocketError
|
25
|
+
raise Error::ConnectionFailed, $!
|
38
26
|
end
|
39
27
|
end
|
40
28
|
end
|