motion-http 0.2.0 → 1.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a32a0271f1f711f5a11522c48894a06ea9e8924615d38bae2a235572f15d2135
4
- data.tar.gz: 590d6d30eb2889d97f47ed0e072313eb93dd38eb3e6485f0f894e526869f6a79
3
+ metadata.gz: 6fdd9853db4edb1720e95dac2ab15074cfe361e21a8c80b940854d22a8c1fe38
4
+ data.tar.gz: eb9b3042d28b232921fd1adda2ece8e17fd4397a63bf6a7bdeaba0fcd045929a
5
5
  SHA512:
6
- metadata.gz: a01adbafd00b594929e7b7f86de05e55ff60e1040db5e736247dcb5dca8f01d21804c54ae5ecca7f365a6181dfb030794acfb667a93694bf0a0da0cd4a17f0d4
7
- data.tar.gz: 684d0b718b0d84c7e4222cc16c97f39b4bf875e43b752c7d3f72d9e6540b9cc2e938a690a4542678eb5bff4d89f6409197750a4c608df0a48c9e7a327794901f
6
+ metadata.gz: 1c151143736e9dc4ca7e9b4ace845ffec8a6deb1316fcfd083c0ec9ce1577d3f32b41059f27fe228d3161076a3b63bda26f514c2c4f0573d464f9a43088da9f8
7
+ data.tar.gz: 7f5525e0c1f1f7155feda2ef235ad8a124a34da785d180aa211d5c04e2dc8d5b355eea3abb912fd7af8a192f132bb06a38fcf66b9302ff1da9236ec51324b66b
data/README.md CHANGED
@@ -6,9 +6,9 @@ Supported platforms:
6
6
  - iOS, macOS, tvOS, watchOS
7
7
  - Android
8
8
 
9
- On Android, this gem depends on the super popular [OkHttp](http://square.github.io/okhttp/) networking library.
9
+ It makes use of the officially supported networking libraries provided by Apple and Google. The goal of this gem is to provide you with a stable alternative to using these libraries directly, using a syntax that is much easier to use.
10
10
 
11
- Please note that this library is still a work in progress. Please report bugs and suggestions for improvement!
11
+ Please report any bugs or suggestions for improvement!
12
12
 
13
13
  ## Installation
14
14
 
@@ -19,27 +19,31 @@ Add this line to your application's Gemfile:
19
19
  And then execute:
20
20
 
21
21
  $ bundle
22
- $ rake gradle:install # for Android apps
22
+ $ rake gradle:install # Android only
23
23
 
24
24
  ### iOS Specific Configuration
25
25
 
26
- If you will be making insecure HTTP requests (not HTTPS), you will need to explicitly allow insecure HTTP requests by adding this line to your app's configuration in your Rakefile:
26
+ If you will be making insecure requests (not using HTTPS), you will need to explicitly allow insecure HTTP requests by adding this line to your app's configuration (in your `Rakefile`). You might want to do this if you are trying to access localhost in development.
27
27
 
28
- app.info_plist['NSAppTransportSecurity'] = { 'NSAllowsArbitraryLoads' => true }
28
+ ```ruby
29
+ app.development do
30
+ app.info_plist['NSAppTransportSecurity'] = { 'NSAllowsArbitraryLoads' => true }
31
+ end
32
+ ```
29
33
 
30
34
  ## Usage
31
35
 
32
- Using `motion-http` is quick and easy. You can use the simple approach for making one-off requests, or the advanced approach of creating a reusable API client for further customization.
36
+ Using `motion-http` is quick and easy. You can either make one-off requests or create a reusable API client for further customization.
33
37
 
34
- ### Simple Usage
38
+ ### Basic Usage
35
39
 
36
40
  The basic syntax for a request looks like this:
37
41
  ```ruby
38
- HTTP.method(url, params, options) do |response|
39
- # this block will be called asynchronously
42
+ HTTP.method(url, options) do |response|
43
+ # this will be called asynchronously
40
44
  end
41
45
  ```
42
- Where `method` can be `get`, `post`, `put`, `patch`, or `delete`.
46
+ Where `method` can be either `get`, `post`, `put`, `patch`, `delete`, `head`, `options`, or `trace`.
43
47
 
44
48
  For example, to make a simple `GET` request:
45
49
  ```ruby
@@ -52,23 +56,27 @@ HTTP.get("http://www.example.com") do |response|
52
56
  end
53
57
  ```
54
58
 
55
- You can specify query params as the second argument:
59
+ If you need to specify query params:
56
60
  ```ruby
57
- HTTP.get("http://www.example.com/search", term: "my search term") do |response|
61
+ HTTP.get("http://www.example.com/search", params: { term: "my search term" }) do |response|
58
62
  # ...
59
63
  end
60
64
  ```
61
65
 
62
- The response object contains the status code, headers, and body from the response as well:
66
+ The response object contains the status code, headers, body, and shortcut methods for checking the response status:
63
67
  ```ruby
64
68
  HTTP.get("http://example.com") do |response|
65
- puts response.status_code
69
+ puts response.status_code.to_s
66
70
  puts response.headers.inspect
67
71
  puts response.body
72
+ response.success? # 2xx status
73
+ response.redirect? # 3xx status
74
+ response.client_error? # 4xx status
75
+ response.server_error? # 5xx status
68
76
  end
69
77
  ```
70
78
 
71
- JSON responses will automatically be parsed when requesting the `response.object`:
79
+ If the response body has a JSON content type it will automatically be parsed when requesting the `response.object`:
72
80
  ```ruby
73
81
  HTTP.get("http://api.example.com/people.json") do |response|
74
82
  if response.success?
@@ -81,17 +89,38 @@ HTTP.get("http://api.example.com/people.json") do |response|
81
89
  end
82
90
  ```
83
91
 
84
- The third argument is a hash of options. Currently the only option supported at this time is `follow_redirects` which defaults to true:
92
+ Use the `follow_redirects` option to specify whether or not to follow redirects. The default is `true`:
85
93
  ```ruby
86
- HTTP.get("http://example.com/redirect", nil, follow_redirects: false) do |response|
94
+ HTTP.get("http://example.com/redirect", follow_redirects: false) do |response|
87
95
  # ...
88
96
  end
89
97
  ```
90
98
 
91
- To make a simple `POST` request, the value passed as the second argument will be encoded as the request body:
99
+ #### POST Requests
100
+
101
+ When making a `POST` request, specifying the request body is easy:
92
102
  ```ruby
93
- json = { widget: { name: "Foobar" } }
94
- HTTP.post("http://www.example.com/widgets", json) do |response|
103
+ HTTP.post("http://www.example.com/endpoint", body: raw_request_body) do |response|
104
+ # ...
105
+ end
106
+ ```
107
+
108
+ Specify the `:form` option and it will automatically be encoded as `application/x-www-form-urlencoded` request body:
109
+ ```ruby
110
+ HTTP.post("http://www.example.com/login", form: { user: 'andrew', password: 'secret'}) do |response|
111
+ if response.success?
112
+ puts "Authenticated!"
113
+ elsif response.client_error?
114
+ puts "Bad username or password"
115
+ else
116
+ puts "Oops! Something went wrong."
117
+ end
118
+ end
119
+ ```
120
+
121
+ Likewise, to send a JSON encoded request body, use the `:json` option:
122
+ ```ruby
123
+ HTTP.post("http://www.example.com/widgets", json: { widget: { name: "Foobar" } }) do |response|
95
124
  if response.success?
96
125
  puts "Widget created!"
97
126
  elsif response.status_code == 422
@@ -102,11 +131,24 @@ HTTP.post("http://www.example.com/widgets", json) do |response|
102
131
  end
103
132
  ```
104
133
 
105
- `PUT`, `PATCH`, and `DELETE` requests work the same way:
134
+ To specify request specific headers, use the `:headers` option. This overrides any previously set headers. In this example, we override the default JSON content type:
106
135
  ```ruby
107
- HTTP.put(url, params) { ... }
108
- HTTP.patch(url, params) { ... }
109
- HTTP.delete(url, params) { ... }
136
+ HTTP.post("http://www.example.com/widgets",
137
+ headers: { 'Content-Type' => 'application/vnd.api+json' },
138
+ json: { widget: { name: "Foobar" } }
139
+ ) do |response|
140
+ # ...
141
+ end
142
+ ```
143
+
144
+ All other HTTP method requests work the same way:
145
+ ```ruby
146
+ HTTP.put(url, options) { ... }
147
+ HTTP.patch(url, options) { ... }
148
+ HTTP.delete(url, options) { ... }
149
+ HTTP.head(url, options) { ... }
150
+ HTTP.options(url, options) { ... }
151
+ HTTP.trace(url, options) { ... }
110
152
  ```
111
153
 
112
154
  ### Advanced Usage
@@ -117,6 +159,7 @@ A common use case is to create a reusable HTTP client that uses a common base UR
117
159
  client = HTTP::Client.new("http://api.example.com")
118
160
  # Set or replace a single header:
119
161
  client.header "X-API-TOKEN", "abc123xyz"
162
+ client.header["X-API-TOKEN"] = "abc123xyz"
120
163
 
121
164
  # To set or replace multiple headers:
122
165
  client.headers "X-API-TOKEN" => "abc123xyz",
@@ -125,7 +168,7 @@ client.headers "X-API-TOKEN" => "abc123xyz",
125
168
  # Note that it is valid for some headers to appear multiple times (Accept, Vary, etc).
126
169
  # To append multiple headers of the same key:
127
170
  client.add_header "Accept", "application/json"
128
- client.add_header "Accept", "application/vnd.api+json"
171
+ client.headers.add "Accept", "application/json"
129
172
  ```
130
173
 
131
174
  Then you can make your requests relative to the base URL that you specified when creating your client.
@@ -135,6 +178,41 @@ client.get("/people") do |response|
135
178
  end
136
179
  ```
137
180
 
181
+ ### Basic Auth / Token Auth
182
+
183
+ To make Basic Auth requests, either set the credentials before the request, or set it on your client:
184
+
185
+ ```ruby
186
+ HTTP.basic_auth('username', 'password').get('https://example.com/protected')
187
+ # or
188
+ client.basic_auth('username', 'password')
189
+ client.get('/protected')
190
+ ```
191
+
192
+ The `auth` method is another shortcut for setting any value of the Authorization header:
193
+
194
+ ```ruby
195
+ HTTP.auth("Token token=#{my_token}")
196
+ # or
197
+ client.auth("Token token=#{my_token}")
198
+ # same as
199
+ client.headers['Authorization'] = "Token token=#{my_token}"
200
+ ```
201
+
202
+ ### Logging
203
+
204
+ Logging is disabled by default. To enable logging:
205
+
206
+ ```
207
+ HTTP.logger.enable!
208
+ ```
209
+
210
+ To disable it again:
211
+
212
+ ```
213
+ HTTP.logger.disable!
214
+ ```
215
+
138
216
  ## Contributing
139
217
 
140
218
  1. Fork it
@@ -143,12 +221,37 @@ end
143
221
  4. Push to the branch (`git push origin my-new-feature`)
144
222
  5. Create new Pull Request
145
223
 
146
- ## MIT License
224
+ ## License
147
225
 
148
- Copyright 2018 Andrew Havens
226
+ Copyright 2018-2019 Andrew Havens
149
227
 
150
228
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
151
229
 
152
230
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
153
231
 
154
232
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
233
+
234
+ Parts of the source are under the following license:
235
+
236
+ Copyright (c) 2015-2016, HipByte (info@hipbyte.com) and contributors.
237
+ All rights reserved.
238
+
239
+ Redistribution and use in source and binary forms, with or without
240
+ modification, are permitted provided that the following conditions are met:
241
+
242
+ 1. Redistributions of source code must retain the above copyright notice, this
243
+ list of conditions and the following disclaimer.
244
+ 2. Redistributions in binary form must reproduce the above copyright notice,
245
+ this list of conditions and the following disclaimer in the documentation
246
+ and/or other materials provided with the distribution.
247
+
248
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
249
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
250
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
251
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
252
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
253
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
254
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
255
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
256
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
257
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -1,61 +1,13 @@
1
1
  class Motion
2
2
  class HTTP
3
3
  class Adapter
4
- JSONMediaType = Okhttp3::MediaType.parse("application/json; charset=utf-8")
5
-
6
- def self.client
7
- @client ||= Okhttp3::OkHttpClient.new
8
- end
9
-
10
4
  def self.perform(request, &callback)
11
- http_method = request.http_method
12
- url = request.url
13
- headers = request.headers
14
- params = request.params
15
-
16
- request = OkHttp3::Request::Builder.new
17
- request.url(url) # TODO: encode GET params and append to URL prior to calling this method
18
- headers.each do |key, value|
19
- if value.is_a? Array
20
- value.each {|val| request.addHeader(key, val) }
21
- else
22
- request.header(key, value)
23
- end
24
- end
25
- if http_method != :get
26
- puts "would have set body for #{http_method.to_s.upcase} #{url}"
27
- # body = OkHttp3::RequestBody.create(JSONMediaType, params) # TODO: allow other content types
28
- # request.method(http_method.to_s, body)
29
- end
30
- client.newCall(request.build).enqueue(OkhttpCallback.new(request, callback))
5
+ volley_request = VolleyRequest.create(request, callback)
6
+ queue.add(volley_request)
31
7
  end
32
8
 
33
- class OkhttpCallback
34
- def initialize(request, callback)
35
- @request = request
36
- @callback = callback
37
- end
38
-
39
- def onFailure(call, e)
40
- puts "Error: #{e.getMessage}"
41
- @callback.call(Response.new(@request, nil, Headers.new, e.getMessage))
42
- end
43
-
44
- def onResponse(call, response)
45
- @callback.call(parse_response(response))
46
- end
47
-
48
- def parse_response(response)
49
- headers = Headers.new
50
- i = 0
51
- while i < response.headers.size
52
- key = response.headers.name(i)
53
- value = response.headers.value(i)
54
- headers.add(key, value)
55
- i += 1
56
- end
57
- Response.new(@request, response.code, headers, response.body.string)
58
- end
9
+ def self.queue
10
+ @queue ||= Com::Android::Volley::Toolbox::Volley.newRequestQueue(Motion::HTTP.application_context)
59
11
  end
60
12
  end
61
13
  end
@@ -0,0 +1,36 @@
1
+ # Copyright (c) 2015-2016, HipByte (info@hipbyte.com) and contributors.
2
+ # All rights reserved.
3
+
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+
7
+ # 1. Redistributions of source code must retain the above copyright notice, this
8
+ # list of conditions and the following disclaimer.
9
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ # this list of conditions and the following disclaimer in the documentation
11
+ # and/or other materials provided with the distribution.
12
+
13
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
+ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
+
24
+ # Copied from https://github.com/HipByte/Flow/blob/44283b31a63bc826d2c068557b6357dc1195680b/flow/base64/android/base64.rb
25
+ class Base64
26
+ def self.encode(string)
27
+ bytes = Java::Lang::String.new(string).getBytes("UTF-8")
28
+ Android::Util::Base64.encodeToString(bytes, Android::Util::Base64::NO_WRAP)
29
+ end
30
+
31
+ def self.decode(string)
32
+ java_string = Java::Lang::String.new(string)
33
+ bytes = Android::Util::Base64.decode(java_string, Android::Util::Base64::NO_WRAP)
34
+ Java::Lang::String.new(bytes, "UTF-8")
35
+ end
36
+ end
data/lib/android/json.rb CHANGED
@@ -1,6 +1,29 @@
1
+ # Copyright (c) 2015-2016, HipByte (info@hipbyte.com) and contributors.
2
+ # All rights reserved.
3
+
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+
7
+ # 1. Redistributions of source code must retain the above copyright notice, this
8
+ # list of conditions and the following disclaimer.
9
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ # this list of conditions and the following disclaimer in the documentation
11
+ # and/or other materials provided with the distribution.
12
+
13
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
+ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
+
1
24
  # NOTE: Copied from https://github.com/HipByte/Flow/blob/master/flow/json/android/json.rb
2
25
  class JSON
3
- def self.load(str)
26
+ def self.parse(str)
4
27
  tok = Org::JSON::JSONTokener.new(str)
5
28
  obj = tok.nextValue
6
29
  if obj == nil
@@ -0,0 +1,5 @@
1
+ class ParamsEncoder
2
+ def self.encode(arg)
3
+ Java::Net::URLEncoder.encode(arg, 'UTF-8')
4
+ end
5
+ end
@@ -0,0 +1,52 @@
1
+ class VolleyRequest < Com::Android::Volley::Request
2
+ attr_accessor :original_request, :callback
3
+
4
+ METHOD_CODES = {
5
+ get: 0,
6
+ post: 1,
7
+ put: 2,
8
+ delete: 3,
9
+ head: 4,
10
+ options: 5,
11
+ trace: 6,
12
+ patch: 7,
13
+ }
14
+
15
+ def self.create(request, callback)
16
+ volley_request = new(METHOD_CODES[request.http_method], request.url, nil)
17
+ volley_request.original_request = request
18
+ volley_request.headers = request.headers.to_hash
19
+ volley_request.body = request.body
20
+ volley_request.callback = callback
21
+ volley_request
22
+ end
23
+
24
+ def parseNetworkResponse(networkResponse)
25
+ response = build_response(networkResponse)
26
+ Com::Android::Volley::Response.success(response, Com::Android::Volley::Toolbox::HttpHeaderParser.parseCacheHeaders(networkResponse))
27
+ end
28
+
29
+ def deliverResponse(response)
30
+ Motion::HTTP.logger.log_response(response)
31
+ callback.call(response) if callback
32
+ end
33
+
34
+ def deliverError(error)
35
+ if error.networkResponse
36
+ response = build_response(error.networkResponse)
37
+ deliverResponse(response)
38
+ else
39
+ Motion::HTTP.logger.error("Error while requesting #{original_request.url}: #{error.getMessage}")
40
+ error.getStackTrace.each do |line|
41
+ puts line.toString
42
+ end
43
+ response = Motion::HTTP::Response.new(original_request, nil, nil, error.getMessage)
44
+ callback.call(response) if callback
45
+ end
46
+ end
47
+
48
+ def build_response(networkResponse)
49
+ body = parse_body_from_response(networkResponse)
50
+ Motion::HTTP::Response.new(original_request, networkResponse.statusCode, Motion::HTTP::Headers.new(networkResponse.headers), body)
51
+ end
52
+ end
data/lib/cocoa/adapter.rb CHANGED
@@ -16,15 +16,14 @@ class Motion
16
16
  ns_url_request = build_ns_url_request
17
17
  task = @session.dataTaskWithRequest(ns_url_request, completionHandler: -> (data, response, error) {
18
18
  if error
19
- NSLog("Error: %@", error) # TODO: use configurable logging
20
- error_message = error.localizedDescription
21
- error_message += error.userInfo[NSLocalizedDescriptionKey] if error.userInfo[NSLocalizedDescriptionKey]
22
- response = Response.new(@request, response.statusCode, Headers.new(response.allHeaderFields), error_message)
19
+ error_message = "#{error.localizedDescription} #{error.userInfo['NSLocalizedDescriptionKey']}"
20
+ Motion::HTTP.logger.error("Error while requesting #{@request.url}: #{error_message}")
21
+ response = Response.new(@request, response&.statusCode, Headers.new(response&.allHeaderFields), error_message)
23
22
  else
24
23
  response = Response.new(@request, response.statusCode, Headers.new(response.allHeaderFields), data.to_s)
24
+ Motion::HTTP.logger.log_response(response)
25
25
  end
26
- Motion::HTTP.logger.log_response(response)
27
- callback.call(response)
26
+ callback.call(response) if callback
28
27
  })
29
28
  task.resume
30
29
  end
@@ -32,11 +31,23 @@ class Motion
32
31
  def build_ns_url_request
33
32
  ns_url_request = NSMutableURLRequest.alloc.initWithURL(NSURL.URLWithString(@request.url))
34
33
  ns_url_request.HTTPMethod = @request.http_method.to_s.upcase
35
- if @request.params
36
- # TODO: json serialization
37
- ns_url_request.setValue('application/x-www-form-urlencoded', forHTTPHeaderField: 'Content-Type')
38
- ns_url_request.HTTPBody = FormDataSerializer.serialize(@request.params).dataUsingEncoding(NSUTF8StringEncoding)
34
+ @request.headers.each do |key, value|
35
+ if value.is_a? Array
36
+ value.each {|v2| ns_url_request.addValue(v2, forHTTPHeaderField: key) }
37
+ else
38
+ ns_url_request.setValue(value, forHTTPHeaderField: key)
39
+ end
39
40
  end
41
+
42
+ if @request.body
43
+ if @request.body.is_a?(NSData)
44
+ body_data = @request.body
45
+ else
46
+ body_data = NSString.alloc.initWithString(@request.body).dataUsingEncoding(NSUTF8StringEncoding)
47
+ end
48
+ ns_url_request.HTTPBody = body_data
49
+ end
50
+
40
51
  # TODO: add other headers
41
52
  ns_url_request
42
53
  end
@@ -0,0 +1,35 @@
1
+ # Copyright (c) 2015-2016, HipByte (info@hipbyte.com) and contributors.
2
+ # All rights reserved.
3
+
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+
7
+ # 1. Redistributions of source code must retain the above copyright notice, this
8
+ # list of conditions and the following disclaimer.
9
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ # this list of conditions and the following disclaimer in the documentation
11
+ # and/or other materials provided with the distribution.
12
+
13
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
+ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
+
24
+ # Copied from https://github.com/HipByte/Flow/blob/44283b31a63bc826d2c068557b6357dc1195680b/flow/base64/cocoa/base64.rb
25
+ class Base64
26
+ def self.encode(string)
27
+ data = string.dataUsingEncoding(NSUTF8StringEncoding)
28
+ data.base64EncodedStringWithOptions(0)
29
+ end
30
+
31
+ def self.decode(string)
32
+ data = NSData.alloc.initWithBase64EncodedString(string, options: 0)
33
+ NSString.alloc.initWithData(data, encoding: NSUTF8StringEncoding)
34
+ end
35
+ end
data/lib/cocoa/json.rb CHANGED
@@ -1,3 +1,27 @@
1
+ # Copyright (c) 2015-2016, HipByte (info@hipbyte.com) and contributors.
2
+ # All rights reserved.
3
+
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+
7
+ # 1. Redistributions of source code must retain the above copyright notice, this
8
+ # list of conditions and the following disclaimer.
9
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ # this list of conditions and the following disclaimer in the documentation
11
+ # and/or other materials provided with the distribution.
12
+
13
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
+ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
+
24
+ # Copied from https://github.com/HipByte/Flow/blob/44283b31a63bc826d2c068557b6357dc1195680b/flow/json/cocoa/json.rb
1
25
  class JSON
2
26
  def self.parse(json_string)
3
27
  error_ptr = Pointer.new(:id)
@@ -0,0 +1,8 @@
1
+ class ParamsEncoder
2
+ # TODO: check if iOS implements anything more performant/reliable that we should be using.
3
+ def self.encode(arg)
4
+ arg.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/) do |m|
5
+ '%' + m.unpack('H2' * m.bytesize).join('%').upcase
6
+ end.tr(' ', '+')
7
+ end
8
+ end
@@ -3,9 +3,10 @@ class Motion
3
3
  class Client
4
4
  attr_reader :base_url
5
5
 
6
- def initialize(base_url = nil)
6
+ def initialize(base_url = nil, options = nil)
7
7
  @base_url = base_url || ''
8
- @headers = Headers.new
8
+ options ||= {}
9
+ @headers = Headers.new(options.delete(:headers))
9
10
  end
10
11
 
11
12
  def header(key, value)
@@ -25,31 +26,57 @@ class Motion
25
26
  @headers
26
27
  end
27
28
 
28
- # FIXME: doesn't work on Android for some reason
29
- # [:get, :post, :put, :patch, :delete].each do |method|
30
- # define_method "#{method}", do |path, params = nil, options = nil, &callback|
31
- # Request.new(method, base_url + path, headers, params, options).perform(&callback)
32
- # end
33
- # end
29
+ def basic_auth(username, password)
30
+ header_value = 'Basic ' + Base64.encode("#{username}:#{password}")
31
+ auth(header_value)
32
+ self
33
+ end
34
+
35
+ def auth(header_value)
36
+ @headers.set 'Authorization', header_value
37
+ self
38
+ end
39
+
40
+ def get(path, options = nil, &callback)
41
+ request(:get, path, options, &callback)
42
+ end
43
+
44
+ def post(path, options = nil, &callback)
45
+ request(:post, path, options, &callback)
46
+ end
47
+
48
+ def put(path, options = nil, &callback)
49
+ request(:put, path, options, &callback)
50
+ end
34
51
 
35
- def get(path, params = nil, options = nil, &callback)
36
- Request.new(:get, base_url + path, headers, params, options).perform(&callback)
52
+ def patch(path, options = nil, &callback)
53
+ request(:patch, path, options, &callback)
37
54
  end
38
55
 
39
- def post(path, params = nil, options = nil, &callback)
40
- Request.new(:post, base_url + path, headers, params, options).perform(&callback)
56
+ def delete(path, options = nil, &callback)
57
+ request(:delete, path, options, &callback)
41
58
  end
42
59
 
43
- def put(path, params = nil, options = nil, &callback)
44
- Request.new(:put, base_url + path, headers, params, options).perform(&callback)
60
+ def head(path, options = nil, &callback)
61
+ request(:head, path, options, &callback)
45
62
  end
46
63
 
47
- def patch(path, params = nil, options = nil, &callback)
48
- Request.new(:patch, base_url + path, headers, params, options).perform(&callback)
64
+ def options(path, options = nil, &callback)
65
+ request(:options, path, options, &callback)
49
66
  end
50
67
 
51
- def delete(path, params = nil, options = nil, &callback)
52
- Request.new(:delete, base_url + path, headers, params, options).perform(&callback)
68
+ def trace(path, options = nil, &callback)
69
+ request(:trace, path, options, &callback)
70
+ end
71
+
72
+ def request(http_method, path, options = nil, &callback)
73
+ options ||= {}
74
+ headers_dup = headers.dup
75
+ if options[:headers]
76
+ options.delete(:headers).each {|key, value| headers_dup.set(key, value) }
77
+ end
78
+ options[:headers] = headers_dup
79
+ Request.new(http_method, base_url + path, options).perform(&callback)
53
80
  end
54
81
  end
55
82
  end
@@ -2,12 +2,27 @@ class Motion
2
2
  class HTTP
3
3
  class Headers
4
4
  def initialize(headers = {})
5
- @headers = headers
5
+ @headers = {}
6
+ if headers
7
+ headers.each {|key, value| set(key, value) }
8
+ end
9
+ end
10
+
11
+ def get(key)
12
+ @headers[key.downcase]
13
+ end
14
+ # alias :[] :get # FIXME: doesn't work in Android
15
+ def [](key)
16
+ get(key)
6
17
  end
7
18
 
8
19
  def set(key, value)
9
20
  @headers[key.downcase] = value
10
21
  end
22
+ # alias :[]= :set # FIXME: doesn't work in Android
23
+ def []=(key, value)
24
+ set(key, value)
25
+ end
11
26
 
12
27
  def add(key, value)
13
28
  key = key.downcase
@@ -17,13 +32,22 @@ class Motion
17
32
  end
18
33
  @headers[key] << value
19
34
  end
35
+ # alias :<< :add # FIXME: doesn't work in Android
36
+ def <<(key, value)
37
+ add(key, value)
38
+ end
20
39
 
21
40
  def each(&block)
22
41
  @headers.each(&block)
23
42
  end
24
43
 
25
- def [](key)
26
- @headers[key.downcase]
44
+ def to_hash
45
+ @headers # TODO: flatten array values
46
+ end
47
+
48
+ # FIXME: Android doesn't support dup (Java exception raised: java.lang.CloneNotSupportedException: Class com.yourcompany.motion_http.Headers doesn't implement Cloneable)
49
+ def dup
50
+ Headers.new(@headers)
27
51
  end
28
52
  end
29
53
  end
@@ -1,37 +1,56 @@
1
1
  class Motion
2
2
  class HTTP
3
3
  class Logger
4
- def log(message)
5
- puts message # TODO: add option to enable/disable logging
4
+ attr_reader :enabled
5
+
6
+ def initialize
7
+ @enabled = false # logging is disabled by default
6
8
  end
7
9
 
8
- def log_request(request)
9
- log "Request:\n#{request.http_method.to_s.upcase} #{request.url}"
10
+ def enable!
11
+ @enabled = true
12
+ end
10
13
 
11
- if request.headers
12
- request.headers.each do |k,v|
13
- log "#{k}: #{v.inspect}"
14
- end
15
- end
14
+ def disable!
15
+ @enabled = false
16
+ end
17
+
18
+ def _logger
19
+ @_logger ||= Motion::Lager.new
20
+ end
21
+
22
+ def debug(message, color = :gray)
23
+ _logger.debug(message, color) if enabled
24
+ end
25
+
26
+ def log(message, color = :white)
27
+ _logger.log(message, color) if enabled
28
+ end
29
+
30
+ def error(message, color = :red)
31
+ _logger.error(message, color) # always log even if logging is disabled
32
+ end
16
33
 
17
- if request.params
18
- # log serialized_params
19
- request.params.each do |k,v|
20
- log "\t#{k}=#{v.inspect}"
21
- end
34
+ def log_request(request)
35
+ debug "\nRequest:\n#{request.http_method.to_s.upcase} #{request.url}"
36
+ request.headers.each do |k,v|
37
+ debug "#{k}: #{v}"
22
38
  end
23
- log "\n"
39
+ debug(request.body) if request.body
24
40
  end
25
41
 
26
42
  def log_response(response)
27
- log "Response:"
28
- log "URL: #{response.original_request.url}"
29
- log "Status: #{response.status_code}"
30
- response.headers.each do |key, value|
31
- log "#{key}: #{value}"
43
+ debug "\nResponse:"
44
+ if response.original_request
45
+ debug "URL: #{response.original_request.url}"
32
46
  end
33
- log "\n#{response.body}"
47
+ debug "Status: #{response.status_code}"
48
+ response.headers.each do |k,v|
49
+ debug "#{k}: #{v}"
50
+ end
51
+ debug("\n#{response.body}")
34
52
  end
53
+
35
54
  end
36
55
  end
37
56
  end
@@ -1,14 +1,55 @@
1
1
  class Motion
2
2
  class HTTP
3
3
  class Request
4
- attr_reader :http_method, :url, :headers, :params, :options
4
+ attr_reader :http_method, :url, :headers, :body, :options
5
5
 
6
- def initialize(http_method, url, headers = nil, params = nil, options = nil)
6
+ def initialize(http_method, url, options = nil)
7
7
  @http_method = http_method
8
8
  @url = url
9
- @headers = headers || Headers.new
10
- @params = params
11
- @options = options
9
+ @options = options ||= {}
10
+ @headers = @options.delete(:headers) || Headers.new
11
+ @body = @options.delete(:body)
12
+
13
+ if @options[:params]
14
+ @params = @options.delete(:params)
15
+ flatten_params!
16
+ encode_params!
17
+ @url = "#{url}?#{@params.map{|k,v|"#{k}=#{v}"}.join('&')}"
18
+
19
+ elsif @options[:form]
20
+ @headers['Content-Type'] ||= 'application/x-www-form-urlencoded'
21
+ @params = @options.delete(:form)
22
+ flatten_params!
23
+ encode_params!
24
+ @body = @params.map{|k,v|"#{k}=#{v}"}.join('&')
25
+
26
+ elsif @options[:json]
27
+ @headers['Content-Type'] ||= 'application/json; charset=utf-8'
28
+ @body = @options.delete(:json).to_json
29
+ end
30
+ end
31
+
32
+ def flatten_params!
33
+ new_params = {}
34
+ @params.each do |k,v|
35
+ if v.is_a? Hash
36
+ v.each do |nested_k, nested_v|
37
+ new_params["#{k}[#{nested_k}]"] = nested_v
38
+ end
39
+ else
40
+ new_params[k] = v
41
+ end
42
+ end
43
+ @params = new_params
44
+ flatten_params! if @params.any? {|k,v| v.is_a? Hash }
45
+ end
46
+
47
+ def encode_params!
48
+ new_params = {}
49
+ @params.each do |k,v|
50
+ new_params[ParamsEncoder.encode(k)] = ParamsEncoder.encode(v)
51
+ end
52
+ @params = new_params
12
53
  end
13
54
 
14
55
  def perform(&callback)
@@ -11,8 +11,19 @@ class Motion
11
11
  end
12
12
 
13
13
  def success?
14
- return false unless status_code
15
- status_code >= 200 && status_code < 300
14
+ status_code && (200..299) === status_code
15
+ end
16
+
17
+ def redirect?
18
+ status_code && (300..399) === status_code
19
+ end
20
+
21
+ def client_error?
22
+ status_code && (400..499) === status_code
23
+ end
24
+
25
+ def server_error?
26
+ status_code && (500..599) === status_code
16
27
  end
17
28
 
18
29
  def object
data/lib/common/http.rb CHANGED
@@ -1,39 +1,54 @@
1
1
  class Motion
2
2
  class HTTP
3
3
  class << self
4
+ attr_accessor :application_context # Android
5
+
4
6
  def logger
5
7
  @logger ||= Logger.new
6
8
  end
7
9
 
8
- def client
9
- @client ||= Client.new
10
+ def client(*args)
11
+ Client.new(*args)
12
+ end
13
+
14
+ def basic_auth(username, password)
15
+ client.basic_auth(username, password)
16
+ end
17
+
18
+ def auth(header_value)
19
+ client.auth(header_value)
10
20
  end
11
21
 
12
- # FIXME: doesn't work on Android
13
- # [:get, :post, :put, :patch, :delete].each do |method|
14
- # define_method "#{method}", do |url, params = nil, options = nil, &callback|
15
- # client.send(method, url, params, options, &callback)
16
- # end
17
- # end
22
+ def get(url, options = nil, &callback)
23
+ client.get(url, options, &callback)
24
+ end
25
+
26
+ def post(url, options = nil, &callback)
27
+ client.post(url, options, &callback)
28
+ end
29
+
30
+ def put(url, options = nil, &callback)
31
+ client.put(url, options, &callback)
32
+ end
18
33
 
19
- def get(url, params = nil, options = nil, &callback)
20
- client.get(url, params, options, &callback)
34
+ def patch(url, options = nil, &callback)
35
+ client.patch(url, options, &callback)
21
36
  end
22
37
 
23
- def post(url, params = nil, options = nil, &callback)
24
- client.post(url, params, options, &callback)
38
+ def delete(url, options = nil, &callback)
39
+ client.delete(url, options, &callback)
25
40
  end
26
41
 
27
- def put(url, params = nil, options = nil, &callback)
28
- client.put(url, params, options, &callback)
42
+ def head(url, options = nil, &callback)
43
+ client.head(url, options, &callback)
29
44
  end
30
45
 
31
- def patch(url, params = nil, options = nil, &callback)
32
- client.patch(url, params, options, &callback)
46
+ def options(url, options = nil, &callback)
47
+ client.options(url, options, &callback)
33
48
  end
34
49
 
35
- def delete(url, params = nil, options = nil, &callback)
36
- client.delete(url, params, options, &callback)
50
+ def trace(url, options = nil, &callback)
51
+ client.trace(url, options, &callback)
37
52
  end
38
53
  end
39
54
  end
data/lib/motion-http.rb CHANGED
@@ -4,6 +4,8 @@ unless defined?(Motion::Project::Config)
4
4
  raise "This gem is only intended to be used in a RubyMotion project."
5
5
  end
6
6
 
7
+ require 'motion-lager'
8
+
7
9
  lib_dir_path = File.dirname(File.expand_path(__FILE__))
8
10
  Motion::Project::App.setup do |app|
9
11
  app.files.unshift(*Dir.glob(File.join(lib_dir_path, "common/**/*.rb")))
@@ -12,9 +14,8 @@ Motion::Project::App.setup do |app|
12
14
  when :android
13
15
  require "motion-gradle"
14
16
  app.files.unshift(*Dir.glob(File.join(lib_dir_path, "android/**/*.rb")))
15
- app.gradle do
16
- dependency "com.squareup.okhttp3:okhttp:3.9.0"
17
- end
17
+ app.permissions << :internet
18
+ app.gradle { dependency 'com.android.volley:volley:1.1.1' }
18
19
  when :ios, :tvos, :osx, :watchos, :'ios-extension'
19
20
  app.files.unshift(*Dir.glob(File.join(lib_dir_path, "cocoa/**/*.rb")))
20
21
  else
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: motion-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Havens
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-15 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2021-10-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: motion-lager
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  description: A cross-platform HTTP client for RubyMotion that's quick and easy to
14
28
  use.
15
29
  email:
@@ -20,10 +34,14 @@ extra_rdoc_files: []
20
34
  files:
21
35
  - README.md
22
36
  - lib/android/adapter.rb
37
+ - lib/android/base64.rb
23
38
  - lib/android/json.rb
39
+ - lib/android/params_encoder.rb
40
+ - lib/android/volley_request.rb
24
41
  - lib/cocoa/adapter.rb
25
- - lib/cocoa/form_data_serializer.rb
42
+ - lib/cocoa/base64.rb
26
43
  - lib/cocoa/json.rb
44
+ - lib/cocoa/params_encoder.rb
27
45
  - lib/common/http.rb
28
46
  - lib/common/http/client.rb
29
47
  - lib/common/http/headers.rb
@@ -31,11 +49,11 @@ files:
31
49
  - lib/common/http/request.rb
32
50
  - lib/common/http/response.rb
33
51
  - lib/motion-http.rb
34
- homepage: https://github.com/andrewhavens/motion-http
52
+ homepage: https://github.com/rubymotion-community/motion-http
35
53
  licenses:
36
54
  - MIT
37
55
  metadata: {}
38
- post_install_message:
56
+ post_install_message:
39
57
  rdoc_options: []
40
58
  require_paths:
41
59
  - lib
@@ -50,9 +68,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
50
68
  - !ruby/object:Gem::Version
51
69
  version: '0'
52
70
  requirements: []
53
- rubyforge_project:
54
- rubygems_version: 2.7.6
55
- signing_key:
71
+ rubygems_version: 3.2.15
72
+ signing_key:
56
73
  specification_version: 4
57
74
  summary: A cross-platform HTTP client for RubyMotion that's quick and easy to use.
58
75
  test_files: []
@@ -1,29 +0,0 @@
1
- class FormDataSerializer
2
- def self.serialize(params)
3
- flattened_params = {}
4
- params.each do |k, v|
5
- add_param(flattened_params, k, v)
6
- end
7
- serialized_params = []
8
- flattened_params.each do |k, v|
9
- serialized_params << "#{k}=#{serialize_value(v)}"
10
- end
11
- serialized_params.join('&')
12
- end
13
-
14
- def self.add_param(hash, k, v)
15
- if v.is_a? Hash
16
- v.each do |sub_k, sub_v|
17
- add_param(hash, "#{k}[#{sub_k}]", sub_v)
18
- end
19
- else
20
- hash[k] = v
21
- end
22
- end
23
-
24
- def self.serialize_value(v)
25
- allowed_characters = NSCharacterSet.URLQueryAllowedCharacterSet.mutableCopy
26
- allowed_characters.removeCharactersInString(":#[]@!$&'()*+,;=")
27
- NSString.alloc.initWithString(v.to_s).stringByAddingPercentEncodingWithAllowedCharacters(allowed_characters)
28
- end
29
- end