servicestack 0.1.0 → 0.1.2
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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +35 -0
- data/lib/servicestack/json_service_client.rb +115 -7
- data/lib/servicestack/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 422e88b02d244852ff3d8941817d5fc6c22c58c2a7698458b4d2d96df75af2bf
|
|
4
|
+
data.tar.gz: 9b2941734b9154d258de44f156e2e66b600f6042825535b017cf81699bcaf6da
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '09d3557ceda3563802cedbdcf7feddcadb4ccf5251994c00ecdadefa5417fab664429502b7a94379c53e4136f76e2337075d76f07a22cb015c990cbffb59ea7d'
|
|
7
|
+
data.tar.gz: 8abcf83d19d0530c7873a4a6993fc11390305379657f8db395e7a60a07e2faede986dc807e585a68bfd399aeb2b4b6829bbcfa08b42347568ecd304e14a19c43
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.1.2]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `on_authentication_required` callback for re-authenticating a client before
|
|
10
|
+
automatically retrying a Request that returned 401 Unauthorized
|
|
11
|
+
|
|
12
|
+
## [0.1.1]
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `post_file_with_request` and `post_files_with_request` for uploading files with
|
|
17
|
+
a Request DTO as a `multipart/form-data` Request, incl. an `UploadFile` that
|
|
18
|
+
accepts file contents as a String or any IO
|
|
19
|
+
- `post_files_with_request_url` for uploading files to a custom URL
|
|
20
|
+
|
|
5
21
|
## [0.1.0]
|
|
6
22
|
|
|
7
23
|
### Added
|
|
@@ -17,4 +33,6 @@ All notable changes to this project will be documented in this file.
|
|
|
17
33
|
- Built-in ServiceStack DTOs referenced by generated DTOs (`ResponseStatus`,
|
|
18
34
|
`QueryBase`, `QueryResponse`, `Authenticate`, ...)
|
|
19
35
|
|
|
36
|
+
[0.1.2]: https://github.com/ServiceStack/servicestack-ruby/releases/tag/v0.1.2
|
|
37
|
+
[0.1.1]: https://github.com/ServiceStack/servicestack-ruby/releases/tag/v0.1.1
|
|
20
38
|
[0.1.0]: https://github.com/ServiceStack/servicestack-ruby/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -162,6 +162,25 @@ refreshed and the failed Request retried:
|
|
|
162
162
|
client.set_refresh_token(refresh_token)
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
+
### Transparently handle 401 Unauthorized Responses
|
|
166
|
+
|
|
167
|
+
If the Server returns a 401 Unauthorized Response either because the client was
|
|
168
|
+
unauthenticated or its Bearer Token or API Key had expired, use the
|
|
169
|
+
`on_authentication_required` callback to re-configure the client before the
|
|
170
|
+
original Request is automatically retried:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
client.on_authentication_required = lambda { |c|
|
|
174
|
+
c.authenticate(user_name, password)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Automatically retries Requests returning 401 Responses
|
|
178
|
+
res = client.send(Secured.new)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
A configured Refresh Token takes precedence over the callback, which is only
|
|
182
|
+
used when no Refresh Token is set or refreshing it failed.
|
|
183
|
+
|
|
165
184
|
### Batched Requests
|
|
166
185
|
|
|
167
186
|
```ruby
|
|
@@ -174,6 +193,22 @@ Or send a Request to a one-way endpoint that ignores its Response:
|
|
|
174
193
|
client.publish(Hello.new(name: 'World'))
|
|
175
194
|
```
|
|
176
195
|
|
|
196
|
+
### Uploading Files
|
|
197
|
+
|
|
198
|
+
Use `post_file_with_request` to upload a file with an API Request, whose contents
|
|
199
|
+
can be a String or any IO:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
res = File.open('photo.png', 'rb') do |file|
|
|
203
|
+
client.post_file_with_request(UploadPhoto.new(album: 'Holiday'),
|
|
204
|
+
ServiceStack::UploadFile.new(field_name: 'file', file_name: 'photo.png',
|
|
205
|
+
content_type: 'image/png', stream: file))
|
|
206
|
+
end
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The Request DTO's populated properties are sent as form fields alongside the file.
|
|
210
|
+
To upload multiple files use `post_files_with_request`.
|
|
211
|
+
|
|
177
212
|
### Custom URLs
|
|
178
213
|
|
|
179
214
|
```ruby
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'net/http'
|
|
5
|
+
require 'securerandom'
|
|
5
6
|
require 'uri'
|
|
6
7
|
require_relative 'dto'
|
|
7
8
|
require_relative 'types'
|
|
@@ -19,6 +20,29 @@ module ServiceStack
|
|
|
19
20
|
HEAD = 'HEAD'
|
|
20
21
|
end
|
|
21
22
|
|
|
23
|
+
# A file uploaded in a multipart/form-data Request.
|
|
24
|
+
#
|
|
25
|
+
# Its contents can be supplied as a String or any IO that responds to `read`,
|
|
26
|
+
# e.g. a File or StringIO.
|
|
27
|
+
class UploadFile
|
|
28
|
+
attr_accessor :field_name, :file_name, :content_type, :stream
|
|
29
|
+
|
|
30
|
+
def initialize(field_name: 'file', file_name: nil, content_type: nil, stream: nil)
|
|
31
|
+
@field_name = field_name
|
|
32
|
+
@file_name = file_name
|
|
33
|
+
@content_type = content_type
|
|
34
|
+
@stream = stream
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The file contents to upload.
|
|
38
|
+
def contents
|
|
39
|
+
return @stream unless @stream.respond_to?(:read)
|
|
40
|
+
|
|
41
|
+
@stream.binmode if @stream.respond_to?(:binmode)
|
|
42
|
+
@stream.read
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
22
46
|
# Either the typed Response of a successful API Request or the structured
|
|
23
47
|
# ResponseStatus error of a failed one, returned by `api`.
|
|
24
48
|
class ApiResult
|
|
@@ -47,7 +71,7 @@ module ServiceStack
|
|
|
47
71
|
attr_accessor :base_url, :reply_base_url, :oneway_base_url, :headers,
|
|
48
72
|
:bearer_token, :refresh_token, :refresh_token_uri,
|
|
49
73
|
:user_name, :password, :request_filter, :response_filter,
|
|
50
|
-
:timeout, :cookies
|
|
74
|
+
:timeout, :cookies, :on_authentication_required
|
|
51
75
|
|
|
52
76
|
class << self
|
|
53
77
|
# Filters applied to every Request and Response of all clients.
|
|
@@ -203,6 +227,42 @@ module ServiceStack
|
|
|
203
227
|
execute(method, to_absolute_url(path), body, args: args)
|
|
204
228
|
end
|
|
205
229
|
|
|
230
|
+
# ── File Uploads ──
|
|
231
|
+
|
|
232
|
+
# Uploads a file with a Request DTO as a multipart/form-data Request,
|
|
233
|
+
# returning its typed Response, e.g:
|
|
234
|
+
#
|
|
235
|
+
# File.open('photo.png', 'rb') do |file|
|
|
236
|
+
# client.post_file_with_request(UploadPhoto.new(album: 'Holiday'),
|
|
237
|
+
# ServiceStack::UploadFile.new(field_name: 'file', file_name: 'photo.png',
|
|
238
|
+
# content_type: 'image/png', stream: file))
|
|
239
|
+
# end
|
|
240
|
+
def post_file_with_request(request, file, args: nil)
|
|
241
|
+
post_files_with_request(request, [file], args: args)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Uploads multiple files with a Request DTO as a multipart/form-data Request.
|
|
245
|
+
def post_files_with_request(request, files, args: nil)
|
|
246
|
+
url = create_url_from_dto(HttpMethods::POST, request)
|
|
247
|
+
post_files_with_request_url(url, request, files, response_as: resolve_response_type(request), args: args)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Uploads files with a Request DTO to a custom relative path or absolute URL.
|
|
251
|
+
def post_files_with_request_url(path, request, files, response_as: nil, args: nil)
|
|
252
|
+
boundary = "----ServiceStackFormBoundary#{SecureRandom.hex(12)}"
|
|
253
|
+
body = multipart_body(boundary, request, files)
|
|
254
|
+
|
|
255
|
+
json = execute(HttpMethods::POST, to_absolute_url(path), body, args: args,
|
|
256
|
+
content_type: "multipart/form-data; boundary=#{boundary}")
|
|
257
|
+
return nil if response_as.nil?
|
|
258
|
+
return json if response_as == String
|
|
259
|
+
|
|
260
|
+
parsed = json.to_s.strip.empty? ? {} : JSON.parse(json)
|
|
261
|
+
return parsed unless response_as.respond_to?(:from_hash)
|
|
262
|
+
|
|
263
|
+
response_as.from_hash(parsed)
|
|
264
|
+
end
|
|
265
|
+
|
|
206
266
|
# Converts a relative path into an absolute URL of this client.
|
|
207
267
|
def to_absolute_url(path_or_url)
|
|
208
268
|
return path_or_url if path_or_url.to_s.start_with?('http://', 'https://')
|
|
@@ -232,11 +292,11 @@ module ServiceStack
|
|
|
232
292
|
response_type.from_hash(parsed)
|
|
233
293
|
end
|
|
234
294
|
|
|
235
|
-
def execute(method, url, body, args: nil, retry_on_auth_failure: true)
|
|
295
|
+
def execute(method, url, body, args: nil, retry_on_auth_failure: true, content_type: nil)
|
|
236
296
|
url = append_query_string(url, args) if args && !args.empty?
|
|
237
297
|
|
|
238
298
|
uri = URI.parse(url)
|
|
239
|
-
request = new_http_request(method, uri, body)
|
|
299
|
+
request = new_http_request(method, uri, body, content_type: content_type)
|
|
240
300
|
|
|
241
301
|
@request_filter&.call(request)
|
|
242
302
|
self.class.global_request_filter&.call(request)
|
|
@@ -249,8 +309,8 @@ module ServiceStack
|
|
|
249
309
|
capture_cookies(response)
|
|
250
310
|
|
|
251
311
|
status_code = response.code.to_i
|
|
252
|
-
if status_code == 401 && retry_on_auth_failure &&
|
|
253
|
-
return execute(method, url, body, retry_on_auth_failure: false)
|
|
312
|
+
if status_code == 401 && retry_on_auth_failure && handle_authentication_required
|
|
313
|
+
return execute(method, url, body, retry_on_auth_failure: false, content_type: content_type)
|
|
254
314
|
end
|
|
255
315
|
|
|
256
316
|
# Redirects aren't followed, e.g. Services that redirect to a HTML sign in
|
|
@@ -264,7 +324,7 @@ module ServiceStack
|
|
|
264
324
|
raise WebServiceException.new(e.message, inner_exception: e)
|
|
265
325
|
end
|
|
266
326
|
|
|
267
|
-
def new_http_request(method, uri, body)
|
|
327
|
+
def new_http_request(method, uri, body, content_type: nil)
|
|
268
328
|
request_class = case method.to_s.upcase
|
|
269
329
|
when HttpMethods::GET then Net::HTTP::Get
|
|
270
330
|
when HttpMethods::POST then Net::HTTP::Post
|
|
@@ -288,7 +348,7 @@ module ServiceStack
|
|
|
288
348
|
request['Cookie'] = @cookies.map { |k, v| "#{k}=#{v}" }.join('; ') unless @cookies.empty?
|
|
289
349
|
|
|
290
350
|
if body && has_request_body?(method)
|
|
291
|
-
request['Content-Type']
|
|
351
|
+
request['Content-Type'] = content_type || request['Content-Type'] || MIME_TYPE_JSON
|
|
292
352
|
request.body = body.is_a?(String) ? body : JSON.generate(to_hash(body))
|
|
293
353
|
end
|
|
294
354
|
|
|
@@ -315,6 +375,26 @@ module ServiceStack
|
|
|
315
375
|
end
|
|
316
376
|
end
|
|
317
377
|
|
|
378
|
+
# Re-authenticates the client after a Request returned 401 Unauthorized,
|
|
379
|
+
# returning whether the Request should be retried.
|
|
380
|
+
#
|
|
381
|
+
# Uses the Refresh Token when configured, otherwise the
|
|
382
|
+
# `on_authentication_required` callback.
|
|
383
|
+
def handle_authentication_required
|
|
384
|
+
return true if refresh_access_token
|
|
385
|
+
return false if @on_authentication_required.nil?
|
|
386
|
+
|
|
387
|
+
if @on_authentication_required.arity.zero?
|
|
388
|
+
@on_authentication_required.call
|
|
389
|
+
else
|
|
390
|
+
@on_authentication_required.call(self)
|
|
391
|
+
end
|
|
392
|
+
true
|
|
393
|
+
rescue StandardError
|
|
394
|
+
# Re-authenticating failed, return the original 401 Response
|
|
395
|
+
false
|
|
396
|
+
end
|
|
397
|
+
|
|
318
398
|
def refresh_access_token
|
|
319
399
|
return false if @refresh_token.to_s.empty?
|
|
320
400
|
|
|
@@ -402,6 +482,34 @@ module ServiceStack
|
|
|
402
482
|
dto
|
|
403
483
|
end
|
|
404
484
|
|
|
485
|
+
# Builds the multipart/form-data body of a file upload Request, sending the
|
|
486
|
+
# populated Request DTO properties as form fields
|
|
487
|
+
def multipart_body(boundary, request, files)
|
|
488
|
+
body = +''
|
|
489
|
+
|
|
490
|
+
to_hash(request).each do |name, value|
|
|
491
|
+
next if value.nil?
|
|
492
|
+
|
|
493
|
+
body << "--#{boundary}\r\n"
|
|
494
|
+
body << "Content-Disposition: form-data; name=\"#{name}\"\r\n\r\n"
|
|
495
|
+
body << "#{qs_value(value)}\r\n"
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
files.each do |file|
|
|
499
|
+
field_name = file.field_name.to_s.empty? ? 'file' : file.field_name
|
|
500
|
+
file_name = file.file_name.to_s.empty? ? 'file' : file.file_name
|
|
501
|
+
|
|
502
|
+
body << "--#{boundary}\r\n"
|
|
503
|
+
body << "Content-Disposition: form-data; name=\"#{field_name}\"; filename=\"#{file_name}\"\r\n"
|
|
504
|
+
body << "Content-Type: #{file.content_type || 'application/octet-stream'}\r\n\r\n"
|
|
505
|
+
body << file.contents.to_s.dup.force_encoding(Encoding::BINARY)
|
|
506
|
+
body << "\r\n"
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
body << "--#{boundary}--\r\n"
|
|
510
|
+
body.force_encoding(Encoding::BINARY)
|
|
511
|
+
end
|
|
512
|
+
|
|
405
513
|
def has_request_body?(method)
|
|
406
514
|
!%w[GET DELETE HEAD OPTIONS].include?(method.to_s.upcase)
|
|
407
515
|
end
|
data/lib/servicestack/version.rb
CHANGED