httparty 0.16.2 → 0.16.3
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of httparty might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.editorconfig +18 -0
- data/.gitignore +1 -0
- data/Changelog.md +11 -0
- data/README.md +1 -1
- data/examples/README.md +22 -11
- data/examples/body_stream.rb +14 -0
- data/examples/microsoft_graph.rb +52 -0
- data/examples/multipart.rb +22 -0
- data/features/steps/mongrel_helper.rb +3 -3
- data/httparty.gemspec +2 -1
- data/lib/httparty.rb +21 -1
- data/lib/httparty/connection_adapter.rb +11 -4
- data/lib/httparty/hash_conversions.rb +6 -2
- data/lib/httparty/logger/apache_formatter.rb +29 -6
- data/lib/httparty/logger/curl_formatter.rb +4 -4
- data/lib/httparty/logger/logger.rb +3 -1
- data/lib/httparty/logger/logstash_formatter.rb +59 -0
- data/lib/httparty/module_inheritable_attributes.rb +3 -3
- data/lib/httparty/net_digest_auth.rb +6 -5
- data/lib/httparty/request.rb +8 -1
- data/lib/httparty/request/body.rb +16 -5
- data/lib/httparty/response.rb +19 -5
- data/lib/httparty/response/headers.rb +2 -2
- data/lib/httparty/utils.rb +11 -0
- data/lib/httparty/version.rb +1 -1
- data/spec/httparty/connection_adapter_spec.rb +7 -3
- data/spec/httparty/cookie_hash_spec.rb +1 -1
- data/spec/httparty/exception_spec.rb +1 -1
- data/spec/httparty/hash_conversions_spec.rb +2 -0
- data/spec/httparty/logger/apache_formatter_spec.rb +1 -2
- data/spec/httparty/logger/curl_formatter_spec.rb +1 -1
- data/spec/httparty/logger/logger_spec.rb +6 -1
- data/spec/httparty/logger/logstash_formatter_spec.rb +44 -0
- data/spec/httparty/net_digest_auth_spec.rb +22 -22
- data/spec/httparty/parser_spec.rb +1 -1
- data/spec/httparty/request/body_spec.rb +57 -8
- data/spec/httparty/request_spec.rb +68 -13
- data/spec/httparty/response_spec.rb +32 -20
- data/spec/httparty/ssl_spec.rb +1 -1
- data/spec/httparty_spec.rb +28 -7
- data/website/css/common.css +1 -1
- metadata +25 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d2efd166a0534991fe03c8f0ae6a72424c9e8c3d
|
4
|
+
data.tar.gz: 41ec81b8fbd9ae18ae51f8b93ce0a36e6b1174c8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 53473b5f6b517e590cf7cd5937dee78c6493226ad6508865e9e57efe844b62218473e7137ea83a902de113221f53e0626f163eea39ac5d1094a65a7253e6a7ff
|
7
|
+
data.tar.gz: 4490aaf02f3bbf8eee7568bf62e37ea0c1d23caef64338b1dd09688efa8fe1b3b9047c3891d6e822f88b61b16ad9aa27618437d307b722d8b52e90f9016db8a9
|
data/.editorconfig
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
; This file is for unifying the coding style for different editors and IDEs.
|
2
|
+
; More information at http://EditorConfig.org
|
3
|
+
|
4
|
+
root = true
|
5
|
+
[*]
|
6
|
+
end_of_line = lf
|
7
|
+
trim_trailing_whitespace = true
|
8
|
+
|
9
|
+
[**.rb]
|
10
|
+
indent_size = 2
|
11
|
+
indent_style = spaces
|
12
|
+
insert_final_newline = true
|
13
|
+
|
14
|
+
[**.xml]
|
15
|
+
trim_trailing_whitespace = false
|
16
|
+
|
17
|
+
[**.html]
|
18
|
+
trim_trailing_whitespace = false
|
data/.gitignore
CHANGED
data/Changelog.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
|
2
|
+
## 0.16.3
|
3
|
+
* [Add Logstash-compatible formatter](https://github.com/jnunemaker/httparty/pull/612)
|
4
|
+
* [Add support for headers specified with symbols](https://github.com/jnunemaker/httparty/pull/622)
|
5
|
+
* [Fix response object marshalling](https://github.com/jnunemaker/httparty/pull/618)
|
6
|
+
* [Add ability to send multipart, without passing file](https://github.com/jnunemaker/httparty/pull/615)
|
7
|
+
* [Fix detection of content_type for multipart payload](https://github.com/jnunemaker/httparty/pull/616)
|
8
|
+
* [Process dynamic headers before making actual request](https://github.com/jnunemaker/httparty/pull/606)
|
9
|
+
* [Fix multipart uploads with ActionDispatch::Http::UploadedFile TempFile by using original_filename](https://github.com/jnunemaker/httparty/pull/598)
|
10
|
+
* [Added support for lock and unlock http requests](https://github.com/jnunemaker/httparty/pull/596)
|
11
|
+
|
1
12
|
## 0.16.2
|
2
13
|
|
3
14
|
* [Support ActionDispatch::Http::UploadedFile again](https://github.com/jnunemaker/httparty/pull/585)
|
data/README.md
CHANGED
@@ -64,7 +64,7 @@ httparty "https://api.stackexchange.com/2.2/questions?site=stackoverflow"
|
|
64
64
|
|
65
65
|
* [Docs](https://github.com/jnunemaker/httparty/tree/master/docs)
|
66
66
|
* https://groups.google.com/forum/#!forum/httparty-gem
|
67
|
-
*
|
67
|
+
* https://www.rubydoc.info/github/jnunemaker/httparty
|
68
68
|
* http://stackoverflow.com/questions/tagged/httparty
|
69
69
|
|
70
70
|
## Contributing
|
data/examples/README.md
CHANGED
@@ -13,22 +13,22 @@
|
|
13
13
|
* Creates a custom parser for XML using crack gem
|
14
14
|
* Uses `get` request
|
15
15
|
|
16
|
-
* [Create HTML Nokogiri parser](nokogiri_html_parser.rb)
|
16
|
+
* [Create HTML Nokogiri parser](nokogiri_html_parser.rb)
|
17
17
|
* Adds Html as a format
|
18
18
|
* passed the body of request to Nokogiri
|
19
|
-
|
19
|
+
|
20
20
|
* [More Custom Parsers](custom_parsers.rb)
|
21
21
|
* Create an additional parser for atom or make it the ONLY parser
|
22
|
-
|
22
|
+
|
23
23
|
* [Basic Auth, Delicious](delicious.rb)
|
24
24
|
* Basic Auth, shows how to merge those into options
|
25
25
|
* Uses `get` requests
|
26
|
-
|
26
|
+
|
27
27
|
* [Passing Headers, User Agent](headers_and_user_agents.rb)
|
28
28
|
* Use the class method of Httparty
|
29
29
|
* Pass the User-Agent in the headers
|
30
30
|
* Uses `get` requests
|
31
|
-
|
31
|
+
|
32
32
|
* [Basic Post Request](basic.rb)
|
33
33
|
* Httparty included into poro class
|
34
34
|
* Uses `post` requests
|
@@ -36,7 +36,7 @@
|
|
36
36
|
* [Access Rubyurl Shortener](rubyurl.rb)
|
37
37
|
* Httparty included into poro class
|
38
38
|
* Uses `post` requests
|
39
|
-
|
39
|
+
|
40
40
|
* [Add a custom log file](logging.rb)
|
41
41
|
* create a log file and have httparty log requests
|
42
42
|
|
@@ -44,23 +44,23 @@
|
|
44
44
|
* Httparty included into poro class
|
45
45
|
* Creates methods for different endpoints
|
46
46
|
* Uses `get` requests
|
47
|
-
|
47
|
+
|
48
48
|
* [Accessing Tripit](tripit_sign_in.rb)
|
49
49
|
* Httparty included into poro class
|
50
50
|
* Example of using `debug_output` to see headers/urls passed
|
51
51
|
* Getting and using Cookies
|
52
52
|
* Uses `get` requests
|
53
|
-
|
53
|
+
|
54
54
|
* [Accessing Twitter](twitter.rb)
|
55
55
|
* Httparty included into poro class
|
56
56
|
* Basic Auth
|
57
|
-
* Loads settings from a config file
|
57
|
+
* Loads settings from a config file
|
58
58
|
* Uses `get` requests
|
59
59
|
* Uses `post` requests
|
60
|
-
|
60
|
+
|
61
61
|
* [Accessing WhoIsMyRep](whoismyrep.rb)
|
62
62
|
* Httparty included into poro class
|
63
|
-
* Uses `get` requests
|
63
|
+
* Uses `get` requests
|
64
64
|
* Two ways to pass params to get, inline on the url or in query hash
|
65
65
|
|
66
66
|
* [Rescue Json Error](rescue_json.rb)
|
@@ -70,3 +70,14 @@
|
|
70
70
|
* Uses `get` requests
|
71
71
|
* Uses `stream_body` mode
|
72
72
|
* Download file without using the memory
|
73
|
+
|
74
|
+
* [Microsoft graph](microsoft_graph.rb)
|
75
|
+
* Basic Auth
|
76
|
+
* Uses `post` requests
|
77
|
+
* Uses multipart
|
78
|
+
|
79
|
+
* [Multipart](multipart.rb)
|
80
|
+
* Multipart data upload _(with and without file)_
|
81
|
+
|
82
|
+
* [Uploading File](body_stream.rb)
|
83
|
+
* Uses `body_stream` to upload file
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# To upload file to a server use :body_stream
|
2
|
+
|
3
|
+
HTTParty.put(
|
4
|
+
'http://localhost:3000/train',
|
5
|
+
body_stream: File.open('sample_configs/config_train_server_md.yml', 'r')
|
6
|
+
)
|
7
|
+
|
8
|
+
|
9
|
+
# Actually, it works with any IO object
|
10
|
+
|
11
|
+
HTTParty.put(
|
12
|
+
'http://localhost:3000/train',
|
13
|
+
body_stream: StringIO.new('foo')
|
14
|
+
)
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
|
3
|
+
class MicrosoftGraph
|
4
|
+
MS_BASE_URL = "https://login.microsoftonline.com".freeze
|
5
|
+
TOKEN_REQUEST_PATH = "oauth2/v2.0/token".freeze
|
6
|
+
|
7
|
+
def initialize(tenant_id)
|
8
|
+
@tenant_id = tenant_id
|
9
|
+
end
|
10
|
+
|
11
|
+
# Make a request to the Microsoft Graph API, for instance https://graph.microsoft.com/v1.0/users
|
12
|
+
def request(url)
|
13
|
+
return false unless (token = bearer_token)
|
14
|
+
|
15
|
+
response = HTTParty.get(
|
16
|
+
url,
|
17
|
+
headers: {
|
18
|
+
Authorization: "Bearer #{token}"
|
19
|
+
}
|
20
|
+
)
|
21
|
+
|
22
|
+
return false unless response.code == 200
|
23
|
+
|
24
|
+
return JSON.parse(response.body)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# A post to the Microsoft Graph to get a bearer token for the specified tenant. In this example
|
30
|
+
# our Rails application has already been given permission to request these tokens by the admin of
|
31
|
+
# the specified tenant_id.
|
32
|
+
#
|
33
|
+
# See here for more information https://developer.microsoft.com/en-us/graph/docs/concepts/auth_v2_service
|
34
|
+
#
|
35
|
+
# This request also makes use of the multipart/form-data post body.
|
36
|
+
def bearer_token
|
37
|
+
response = HTTParty.post(
|
38
|
+
"#{MS_BASE_URL}/#{@tenant_id}/#{TOKEN_REQUEST_PATH}",
|
39
|
+
multipart: true,
|
40
|
+
body: {
|
41
|
+
client_id: Rails.application.credentials[Rails.env.to_sym][:microsoft_client_id],
|
42
|
+
client_secret: Rails.application.credentials[Rails.env.to_sym][:microsoft_client_secret],
|
43
|
+
scope: 'https://graph.microsoft.com/.default',
|
44
|
+
grant_type: 'client_credentials'
|
45
|
+
}
|
46
|
+
)
|
47
|
+
|
48
|
+
return false unless response.code == 200
|
49
|
+
|
50
|
+
JSON.parse(response.body)['access_token']
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# If you are uploading file in params, multipart will used as content-type automatically
|
2
|
+
|
3
|
+
HTTParty.post(
|
4
|
+
'http://localhost:3000/user',
|
5
|
+
body: {
|
6
|
+
name: 'Foo Bar',
|
7
|
+
email: 'example@email.com',
|
8
|
+
avatar: File.open('/full/path/to/avatar.jpg')
|
9
|
+
}
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
# However, you can force it yourself
|
14
|
+
|
15
|
+
HTTParty.post(
|
16
|
+
'http://localhost:3000/user',
|
17
|
+
multipart: true,
|
18
|
+
body: {
|
19
|
+
name: 'Foo Bar',
|
20
|
+
email: 'example@email.com'
|
21
|
+
}
|
22
|
+
)
|
@@ -95,7 +95,7 @@ module DigestAuthenticationUsingMD5Sess
|
|
95
95
|
def self.extended(base)
|
96
96
|
base.custom_headers["WWW-Authenticate"] = %(Digest realm="#{REALM}",qop="#{QOP}",algorithm="MD5-sess",nonce="#{NONCE}",opaque="opaque"')
|
97
97
|
end
|
98
|
-
|
98
|
+
|
99
99
|
def process(request, response)
|
100
100
|
if authorized?(request)
|
101
101
|
super
|
@@ -103,11 +103,11 @@ module DigestAuthenticationUsingMD5Sess
|
|
103
103
|
reply_with(response, 401, "Incorrect. You have 20 seconds to comply.")
|
104
104
|
end
|
105
105
|
end
|
106
|
-
|
106
|
+
|
107
107
|
def md5(str)
|
108
108
|
Digest::MD5.hexdigest(str)
|
109
109
|
end
|
110
|
-
|
110
|
+
|
111
111
|
def authorized?(request)
|
112
112
|
auth = request.params["HTTP_AUTHORIZATION"]
|
113
113
|
params = {}
|
data/httparty.gemspec
CHANGED
@@ -9,13 +9,14 @@ Gem::Specification.new do |s|
|
|
9
9
|
s.licenses = ['MIT']
|
10
10
|
s.authors = ["John Nunemaker", "Sandro Turriate"]
|
11
11
|
s.email = ["nunemaker@gmail.com"]
|
12
|
-
s.homepage = "
|
12
|
+
s.homepage = "https://github.com/jnunemaker/httparty"
|
13
13
|
s.summary = 'Makes http fun! Also, makes consuming restful web services dead easy.'
|
14
14
|
s.description = 'Makes http fun! Also, makes consuming restful web services dead easy.'
|
15
15
|
|
16
16
|
s.required_ruby_version = '>= 2.0.0'
|
17
17
|
|
18
18
|
s.add_dependency 'multi_xml', ">= 0.5.2"
|
19
|
+
s.add_dependency('mime-types', "~> 3.0")
|
19
20
|
|
20
21
|
# If this line is removed, all hard partying will cease.
|
21
22
|
s.post_install_message = "When you HTTParty, you must party hard!"
|
data/lib/httparty.rb
CHANGED
@@ -4,6 +4,7 @@ require 'net/https'
|
|
4
4
|
require 'uri'
|
5
5
|
require 'zlib'
|
6
6
|
require 'multi_xml'
|
7
|
+
require 'mime/types'
|
7
8
|
require 'json'
|
8
9
|
require 'csv'
|
9
10
|
|
@@ -40,6 +41,7 @@ module HTTParty
|
|
40
41
|
# [:+local_port:] Local port to bind to before connecting.
|
41
42
|
# [:+body_stream:] Allow streaming to a REST server to specify a body_stream.
|
42
43
|
# [:+stream_body:] Allow for streaming large files without loading them into memory.
|
44
|
+
# [:+multipart:] Force content-type to be multipart
|
43
45
|
#
|
44
46
|
# There are also another set of options with names corresponding to various class methods. The methods in question are those that let you set a class-wide default, and the options override the defaults on a request-by-request basis. Those options are:
|
45
47
|
# * :+base_uri+: see HTTParty::ClassMethods.base_uri.
|
@@ -546,6 +548,14 @@ module HTTParty
|
|
546
548
|
perform_request Net::HTTP::Mkcol, path, options, &block
|
547
549
|
end
|
548
550
|
|
551
|
+
def lock(path, options = {}, &block)
|
552
|
+
perform_request Net::HTTP::Lock, path, options, &block
|
553
|
+
end
|
554
|
+
|
555
|
+
def unlock(path, options = {}, &block)
|
556
|
+
perform_request Net::HTTP::Unlock, path, options, &block
|
557
|
+
end
|
558
|
+
|
549
559
|
attr_reader :default_options
|
550
560
|
|
551
561
|
private
|
@@ -566,6 +576,14 @@ module HTTParty
|
|
566
576
|
def process_headers(options)
|
567
577
|
if options[:headers] && headers.any?
|
568
578
|
options[:headers] = headers.merge(options[:headers])
|
579
|
+
options[:headers] = Utils.stringify_keys(process_dynamic_headers(options[:headers]))
|
580
|
+
end
|
581
|
+
end
|
582
|
+
|
583
|
+
def process_dynamic_headers(headers)
|
584
|
+
headers.each_with_object({}) do |header, processed_headers|
|
585
|
+
key, value = header
|
586
|
+
processed_headers[key] = value.respond_to?(:call) ? value.call : value
|
569
587
|
end
|
570
588
|
end
|
571
589
|
|
@@ -577,7 +595,8 @@ module HTTParty
|
|
577
595
|
|
578
596
|
def validate_format
|
579
597
|
if format && parser.respond_to?(:supports_format?) && !parser.supports_format?(format)
|
580
|
-
|
598
|
+
supported_format_names = parser.supported_formats.map(&:to_s).sort.join(', ')
|
599
|
+
raise UnsupportedFormat, "'#{format.inspect}' Must be one of: #{supported_format_names}"
|
581
600
|
end
|
582
601
|
end
|
583
602
|
end
|
@@ -635,6 +654,7 @@ module HTTParty
|
|
635
654
|
end
|
636
655
|
|
637
656
|
require 'httparty/hash_conversions'
|
657
|
+
require 'httparty/utils'
|
638
658
|
require 'httparty/exceptions'
|
639
659
|
require 'httparty/parser'
|
640
660
|
require 'httparty/request'
|
@@ -4,7 +4,7 @@ module HTTParty
|
|
4
4
|
# == Custom Connection Factories
|
5
5
|
#
|
6
6
|
# If you like to implement your own connection adapter, subclassing
|
7
|
-
#
|
7
|
+
# HTTParty::ConnectionAdapter will make it easier. Just override
|
8
8
|
# the #connection method. The uri and options attributes will have
|
9
9
|
# all the info you need to construct your http connection. Whatever
|
10
10
|
# you return from your connection method needs to adhere to the
|
@@ -38,12 +38,12 @@ module HTTParty
|
|
38
38
|
# in the #options attribute. It is up to you to interpret them within your
|
39
39
|
# connection adapter. Take a look at the implementation of
|
40
40
|
# HTTParty::ConnectionAdapter#connection for examples of how they are used.
|
41
|
-
# The keys used in options are
|
41
|
+
# The keys used in options are
|
42
42
|
# * :+timeout+: timeout in seconds
|
43
43
|
# * :+open_timeout+: http connection open_timeout in seconds, overrides timeout if set
|
44
44
|
# * :+read_timeout+: http connection read_timeout in seconds, overrides timeout if set
|
45
45
|
# * :+debug_output+: see HTTParty::ClassMethods.debug_output.
|
46
|
-
# * :+cert_store+: contains certificate data. see method 'attach_ssl_certificates'
|
46
|
+
# * :+cert_store+: contains certificate data. see method 'attach_ssl_certificates'
|
47
47
|
# * :+pem+: contains pem client certificate data. see method 'attach_ssl_certificates'
|
48
48
|
# * :+p12+: contains PKCS12 client client certificate data. see method 'attach_ssl_certificates'
|
49
49
|
# * :+verify+: verify the server’s certificate against the ca certificate.
|
@@ -91,7 +91,14 @@ module HTTParty
|
|
91
91
|
host = clean_host(uri.host)
|
92
92
|
port = uri.port || (uri.scheme == 'https' ? 443 : 80)
|
93
93
|
if options.key?(:http_proxyaddr)
|
94
|
-
http = Net::HTTP.new(
|
94
|
+
http = Net::HTTP.new(
|
95
|
+
host,
|
96
|
+
port,
|
97
|
+
options[:http_proxyaddr],
|
98
|
+
options[:http_proxyport],
|
99
|
+
options[:http_proxyuser],
|
100
|
+
options[:http_proxypass]
|
101
|
+
)
|
95
102
|
else
|
96
103
|
http = Net::HTTP.new(host, port)
|
97
104
|
end
|
@@ -39,7 +39,9 @@ module HTTParty
|
|
39
39
|
if value.empty?
|
40
40
|
normalized_keys << ["#{key}[]", '']
|
41
41
|
else
|
42
|
-
normalized_keys = value.to_ary.flat_map
|
42
|
+
normalized_keys = value.to_ary.flat_map do |element|
|
43
|
+
normalize_keys("#{key}[]", element)
|
44
|
+
end
|
43
45
|
end
|
44
46
|
elsif value.respond_to?(:to_hash)
|
45
47
|
stack << [key, value.to_hash]
|
@@ -52,7 +54,9 @@ module HTTParty
|
|
52
54
|
if child_value.respond_to?(:to_hash)
|
53
55
|
stack << ["#{parent}[#{child_key}]", child_value.to_hash]
|
54
56
|
elsif child_value.respond_to?(:to_ary)
|
55
|
-
child_value.to_ary.each
|
57
|
+
child_value.to_ary.each do |v|
|
58
|
+
normalized_keys << normalize_keys("#{parent}[#{child_key}][]", v).flatten
|
59
|
+
end
|
56
60
|
else
|
57
61
|
normalized_keys << normalize_keys("#{parent}[#{child_key}]", child_value).flatten
|
58
62
|
end
|
@@ -3,7 +3,7 @@ module HTTParty
|
|
3
3
|
class ApacheFormatter #:nodoc:
|
4
4
|
TAG_NAME = HTTParty.name
|
5
5
|
|
6
|
-
attr_accessor :level, :logger
|
6
|
+
attr_accessor :level, :logger
|
7
7
|
|
8
8
|
def initialize(logger, level)
|
9
9
|
@logger = logger
|
@@ -11,11 +11,34 @@ module HTTParty
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def format(request, response)
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
14
|
+
@request = request
|
15
|
+
@response = response
|
16
|
+
|
17
|
+
logger.public_send level, message
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :request, :response
|
23
|
+
|
24
|
+
def message
|
25
|
+
"[#{TAG_NAME}] [#{current_time}] #{response.code} \"#{http_method} #{path}\" #{content_length || '-'} "
|
26
|
+
end
|
27
|
+
|
28
|
+
def current_time
|
29
|
+
Time.now.strftime("%Y-%m-%d %H:%M:%S %z")
|
30
|
+
end
|
31
|
+
|
32
|
+
def http_method
|
33
|
+
request.http_method.name.split("::").last.upcase
|
34
|
+
end
|
35
|
+
|
36
|
+
def path
|
37
|
+
request.path.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
def content_length
|
41
|
+
response.respond_to?(:headers) ? response.headers['Content-Length'] : response['Content-Length']
|
19
42
|
end
|
20
43
|
end
|
21
44
|
end
|