gini-api 0.9.2 → 0.9.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 +4 -4
- data/README.md +6 -6
- data/gini-api.gemspec +1 -9
- data/lib/gini-api/client.rb +88 -45
- data/lib/gini-api/oauth.rb +102 -66
- data/lib/gini-api/version.rb +1 -5
- data/spec/gini-api/oauth_spec.rb +0 -2
- data/spec/integration/api_spec.rb +5 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 48a332c434967ccd2d8906da4f64f7a8a27346b3
|
4
|
+
data.tar.gz: a3143decf245e1ef61800716558726431d9601e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 33f11fa2e20aefb5e51e68fa495445c1b5da7eb6f46612ef4ed3dcbc513d071753bd2241b0e8220c9b217cbfbbf141931927a17827c3813c1f359eb54741cee2
|
7
|
+
data.tar.gz: 9819589db65cde5ddb460ac6e75887d1adf976be5bffc58707b5daf413b5999e4234e7d951d8615e02d5a0b07f219b37eba9346b13f47bb23763b9c81d8bf82c
|
data/README.md
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
[](https://travis-ci.org/gini/gini-api-ruby)
|
6
6
|
[](https://codeclimate.com/github/gini/gini-api-ruby)
|
7
7
|
[](https://coveralls.io/r/gini/gini-api-ruby)
|
8
|
-
[](http://badge.fury.io/rb/gini-api)
|
9
9
|
|
10
10
|
## Resources
|
11
11
|
|
@@ -42,10 +42,10 @@ api = Gini::Api::Client.new(
|
|
42
42
|
)
|
43
43
|
|
44
44
|
# auth_code (has been extracted outside of gini-api-ruby)
|
45
|
-
api.login('1234567890')
|
45
|
+
api.login(auth_code: '1234567890')
|
46
46
|
|
47
47
|
# username/password
|
48
|
-
api.login('
|
48
|
+
api.login(username: 'me@example.com', password: 'secret')
|
49
49
|
```
|
50
50
|
|
51
51
|
### Upload
|
@@ -68,7 +68,7 @@ list.total
|
|
68
68
|
# => 15
|
69
69
|
list.documents
|
70
70
|
# => [Gini::Api::Document, Gini::Api::Document, ...]
|
71
|
-
list.each
|
71
|
+
list.each { |doc| puts doc.id }
|
72
72
|
# => 1234567890-abc, 0987654321-cba, ...
|
73
73
|
```
|
74
74
|
|
@@ -97,7 +97,7 @@ search.total
|
|
97
97
|
# => 5
|
98
98
|
search.documents
|
99
99
|
# => [Gini::Api::Document, Gini::Api::Document, ...]
|
100
|
-
search.each
|
100
|
+
search.each { |doc| puts doc.id }
|
101
101
|
# => 1234567890-abc, 0987654321-cba, ...
|
102
102
|
```
|
103
103
|
|
@@ -176,7 +176,7 @@ rescue Gini::Api::OAuthError => e
|
|
176
176
|
end
|
177
177
|
```
|
178
178
|
|
179
|
-
Please keep in mind that the amount of availabe instance variables differs depending on the error situation. For details please refer to the
|
179
|
+
Please keep in mind that the amount of availabe instance variables differs depending on the error situation. For details please refer to the ```Gini::Api::Error``` class.
|
180
180
|
|
181
181
|
## Developers
|
182
182
|
|
data/gini-api.gemspec
CHANGED
@@ -4,17 +4,9 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
|
5
5
|
require 'gini-api/version'
|
6
6
|
|
7
|
-
gem_version = Gini::Api::BASE_VERSION.dup
|
8
|
-
|
9
|
-
if ENV.has_key?('BUILD_NUMBER')
|
10
|
-
gem_version << ".#{ENV['BUILD_NUMBER']}"
|
11
|
-
else
|
12
|
-
gem_version = Gini::Api::VERSION
|
13
|
-
end
|
14
|
-
|
15
7
|
Gem::Specification.new do |spec|
|
16
8
|
spec.name = 'gini-api'
|
17
|
-
spec.version =
|
9
|
+
spec.version = Gini::Api::VERSION
|
18
10
|
spec.authors = ['Daniel Kerwin']
|
19
11
|
spec.email = ['tech@gini.net']
|
20
12
|
spec.description = %q{Ruby client to interact with the Gini API.}
|
data/lib/gini-api/client.rb
CHANGED
@@ -61,14 +61,6 @@ module Gini
|
|
61
61
|
# Sanitize api_uri
|
62
62
|
@api_uri.sub!(/(\/)+$/, '')
|
63
63
|
|
64
|
-
# Create upload connection
|
65
|
-
@upload_connection = Faraday.new(url: @api_uri) do |builder|
|
66
|
-
builder.use(Faraday::Request::Multipart)
|
67
|
-
builder.use(Faraday::Request::UrlEncoded)
|
68
|
-
builder.request(:retry, 3)
|
69
|
-
builder.adapter(Faraday.default_adapter)
|
70
|
-
end
|
71
|
-
|
72
64
|
# Register parser (json+xml) based on API version
|
73
65
|
register_parser
|
74
66
|
|
@@ -135,7 +127,7 @@ module Gini
|
|
135
127
|
}.merge(options)
|
136
128
|
|
137
129
|
timeout(@processing_timeout) do
|
138
|
-
@token.send(verb.to_sym,
|
130
|
+
@token.send(verb.to_sym, resource_to_location(resource).to_s , opts)
|
139
131
|
end
|
140
132
|
rescue OAuth2::Error => e
|
141
133
|
raise Gini::Api::RequestError.new(
|
@@ -160,49 +152,26 @@ module Gini
|
|
160
152
|
# doc = api.upload('/tmp/myfile.pdf') { |d| puts "Progress: #{d.progress}" }
|
161
153
|
#
|
162
154
|
def upload(file, &block)
|
163
|
-
|
164
|
-
|
165
|
-
duration = {}
|
155
|
+
duration = Hash.new(0)
|
166
156
|
|
167
157
|
# Document upload
|
168
|
-
duration[:upload] =
|
169
|
-
@response = @upload_connection.post do |req|
|
170
|
-
req.options[:timeout] = @upload_timeout
|
171
|
-
req.url 'documents/'
|
172
|
-
req.headers['Content-Type'] = 'multipart/form-data'
|
173
|
-
req.headers['Authorization'] = "Bearer #{@token.token}"
|
174
|
-
req.headers.merge!(version_header)
|
175
|
-
req.body = { file: Faraday::UploadIO.new(file, 'application/octet-stream') }
|
176
|
-
end
|
177
|
-
end
|
158
|
+
duration[:upload], response = upload_document(file)
|
178
159
|
|
179
160
|
# Start polling (0.5s) when document has been uploaded successfully
|
180
|
-
if
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
end
|
189
|
-
rescue Timeout::Error => e
|
190
|
-
ex = Gini::Api::ProcessingError.new(e.message)
|
191
|
-
ex.docid = doc.id
|
192
|
-
raise ex
|
193
|
-
end
|
161
|
+
if response.status == 201
|
162
|
+
doc = Gini::Api::Document.new(self, response.headers['location'])
|
163
|
+
duration[:processing] = poll_document(doc, &block)
|
164
|
+
|
165
|
+
duration[:total] = duration.values.inject(:+)
|
166
|
+
doc.duration = duration
|
167
|
+
|
168
|
+
doc
|
194
169
|
else
|
195
|
-
|
196
|
-
"Document upload failed with HTTP code #{
|
197
|
-
|
170
|
+
fail Gini::Api::UploadError.new(
|
171
|
+
"Document upload failed with HTTP code #{response.status}",
|
172
|
+
response
|
198
173
|
)
|
199
174
|
end
|
200
|
-
|
201
|
-
# Combine duration values and update doc object
|
202
|
-
duration[:total] = duration[:upload] + duration[:processing]
|
203
|
-
doc.duration = duration
|
204
|
-
|
205
|
-
doc
|
206
175
|
end
|
207
176
|
|
208
177
|
# Delete document
|
@@ -279,6 +248,80 @@ module Gini
|
|
279
248
|
end
|
280
249
|
Gini::Api::DocumentSet.new(self, response.parsed)
|
281
250
|
end
|
251
|
+
|
252
|
+
private
|
253
|
+
|
254
|
+
# Helper to covert resource to a valid location.
|
255
|
+
#
|
256
|
+
# @param [String] resource URI to be converted
|
257
|
+
#
|
258
|
+
# @return [URI::HTTPS] URI::HTTPS object create from resource
|
259
|
+
#
|
260
|
+
def resource_to_location(resource)
|
261
|
+
parsed_resource = URI.parse(resource)
|
262
|
+
@api_host ||= URI.parse(@api_uri).host
|
263
|
+
|
264
|
+
URI::HTTPS.build(
|
265
|
+
host: @api_host,
|
266
|
+
path: parsed_resource.path,
|
267
|
+
query: parsed_resource.query
|
268
|
+
)
|
269
|
+
end
|
270
|
+
|
271
|
+
# Poll document and duration
|
272
|
+
#
|
273
|
+
# @param [Gini::Api::Document] doc Document instance to poll
|
274
|
+
#
|
275
|
+
# @return [Integer] Processing duration
|
276
|
+
#
|
277
|
+
def poll_document(doc, &block)
|
278
|
+
duration = 0
|
279
|
+
timeout(@processing_timeout) do
|
280
|
+
duration = Benchmark.realtime do
|
281
|
+
doc.poll(&block)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
duration
|
285
|
+
rescue Timeout::Error => e
|
286
|
+
ex = Gini::Api::ProcessingError.new(e.message)
|
287
|
+
ex.docid = doc.id
|
288
|
+
raise ex
|
289
|
+
end
|
290
|
+
|
291
|
+
# Setup API upload connection
|
292
|
+
#
|
293
|
+
# @return [Faraday] Faraday object to use in upload
|
294
|
+
#
|
295
|
+
def upload_connection
|
296
|
+
@upload_connection ||= Faraday.new(url: @api_uri) do |builder|
|
297
|
+
builder.use(Faraday::Request::Multipart)
|
298
|
+
builder.use(Faraday::Request::UrlEncoded)
|
299
|
+
builder.request(:retry, 3)
|
300
|
+
builder.adapter(Faraday.default_adapter)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
# Helper to upload document
|
305
|
+
#
|
306
|
+
# @param [String] file location of document to be uploaded
|
307
|
+
#
|
308
|
+
# @return [Faraday::Response] Response object from upload
|
309
|
+
#
|
310
|
+
def upload_document(file)
|
311
|
+
response = nil
|
312
|
+
duration = Benchmark.realtime do
|
313
|
+
response = upload_connection.post do |req|
|
314
|
+
req.options[:timeout] = @upload_timeout
|
315
|
+
req.url 'documents/'
|
316
|
+
req.headers['Content-Type'] = 'multipart/form-data'
|
317
|
+
req.headers['Authorization'] = "Bearer #{@token.token}"
|
318
|
+
req.headers.merge!(version_header)
|
319
|
+
req.body = { file: Faraday::UploadIO.new(file, 'application/octet-stream') }
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
return duration, response
|
324
|
+
end
|
282
325
|
end
|
283
326
|
end
|
284
327
|
end
|
data/lib/gini-api/oauth.rb
CHANGED
@@ -37,7 +37,15 @@ module Gini
|
|
37
37
|
if opts.key?(:auth_code) && !opts[:auth_code].empty?
|
38
38
|
opts[:auth_code]
|
39
39
|
else
|
40
|
-
|
40
|
+
# Generate CSRF token to verify the response
|
41
|
+
csrf_token = SecureRandom.hex
|
42
|
+
location = login_with_credentials(
|
43
|
+
api,
|
44
|
+
client,
|
45
|
+
csrf_token,
|
46
|
+
opts[:username],
|
47
|
+
opts[:password])
|
48
|
+
extract_auth_code(location, csrf_token)
|
41
49
|
end
|
42
50
|
|
43
51
|
# Exchange code for a real token.
|
@@ -71,102 +79,130 @@ module Gini
|
|
71
79
|
end
|
72
80
|
end
|
73
81
|
|
74
|
-
#
|
82
|
+
# Destroy token
|
75
83
|
#
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
84
|
+
def destroy
|
85
|
+
@token.refresh_token && @token.refresh!
|
86
|
+
response = token.delete("/accessToken/#{@token.token}")
|
87
|
+
unless response.status == 204
|
88
|
+
fail_with_oauth_error(
|
89
|
+
"Failed to destroy token /accessToken/#{@token.token} "\
|
90
|
+
"(code=#{response.status})",
|
91
|
+
response
|
92
|
+
)
|
93
|
+
end
|
94
|
+
rescue OAuth2::Error => e
|
95
|
+
fail_with_oauth_error(
|
96
|
+
"Failed to destroy token (code=#{e.response.status})",
|
97
|
+
e.response
|
98
|
+
)
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
# Helper method to fail with Gini::Api::OAuthError
|
80
104
|
#
|
81
|
-
# @
|
105
|
+
# @param [String] msg Exception message
|
106
|
+
# @param [OAuth2::Response] response Response object
|
82
107
|
#
|
83
|
-
def
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
# Build authentication URI
|
88
|
-
auth_uri = client.auth_code.authorize_url(
|
89
|
-
redirect_uri: api.oauth_redirect,
|
90
|
-
state: csrf_token
|
108
|
+
def fail_with_oauth_error(msg, response = nil)
|
109
|
+
raise Gini::Api::OAuthError.new(
|
110
|
+
msg,
|
111
|
+
response
|
91
112
|
)
|
113
|
+
end
|
92
114
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
"API login failed (code=#{response.status})",
|
103
|
-
response
|
104
|
-
)
|
105
|
-
end
|
106
|
-
rescue OAuth2::Error => e
|
107
|
-
raise Gini::Api::OAuthError.new(
|
108
|
-
"Failed to acquire auth_code (code=#{e.response.status})",
|
109
|
-
e.response
|
110
|
-
)
|
111
|
-
end
|
112
|
-
|
113
|
-
# Parse the location header from the response and fill hash
|
114
|
-
# query_params ({'code' => '123abc', 'state' => 'supersecret'})
|
115
|
-
begin
|
116
|
-
q = URI.parse(response.headers['location']).query
|
117
|
-
query_params = Hash[*q.split(/\=|\&/)]
|
118
|
-
rescue => e
|
119
|
-
raise Gini::Api::OAuthError.new("Failed to parse location header: #{e.message}")
|
120
|
-
end
|
115
|
+
# Extract auth_code from URI
|
116
|
+
#
|
117
|
+
# @param [String] location Location URI containing the auth_code
|
118
|
+
# @param [String] csrf_token CSRF token to verify request
|
119
|
+
#
|
120
|
+
# @return [String] Collected authorization code
|
121
|
+
#
|
122
|
+
def extract_auth_code(location, csrf_token)
|
123
|
+
query_params = parse_location(location)
|
121
124
|
|
122
125
|
unless query_params['state'] == csrf_token
|
123
|
-
|
126
|
+
fail_with_oauth_error(
|
124
127
|
"CSRF token mismatch detected (should=#{csrf_token}, "\
|
125
128
|
"is=#{query_params['state']})"
|
126
129
|
)
|
127
130
|
end
|
128
131
|
|
129
132
|
unless query_params.key?('code') && !query_params['code'].empty?
|
130
|
-
|
131
|
-
"Failed to extract code from location #{
|
133
|
+
fail_with_oauth_error(
|
134
|
+
"Failed to extract code from location #{location}"
|
132
135
|
)
|
133
136
|
end
|
134
137
|
|
135
138
|
query_params['code']
|
136
139
|
end
|
137
140
|
|
138
|
-
#
|
141
|
+
# Parse auth_code and state from URI
|
142
|
+
#
|
143
|
+
# @param [String] location Location URI with auth_code and state
|
144
|
+
#
|
145
|
+
# @return [Hash] Hash with auth_code and state
|
146
|
+
#
|
147
|
+
def parse_location(location)
|
148
|
+
# Parse the location header from the response and return hash
|
149
|
+
# {'code' => '123abc', 'state' => 'supersecret'}
|
150
|
+
q = URI.parse(location).query
|
151
|
+
Hash[*q.split(/\=|\&/)]
|
152
|
+
rescue => e
|
153
|
+
fail_with_oauth_error("Failed to parse location header: #{e.message}")
|
154
|
+
end
|
155
|
+
|
156
|
+
# Login with username/password
|
139
157
|
#
|
140
158
|
# @param [Gini::Api::Client] api API object
|
141
159
|
# @param [OAuth2::Client] client OAuth2 client object
|
142
|
-
# @param [String]
|
160
|
+
# @param [String] csrf_token CSRF token to verify request
|
161
|
+
# @param [String] username API username
|
162
|
+
# @param [String] password API password
|
143
163
|
#
|
144
|
-
# @return [
|
164
|
+
# @return [String] Location header
|
145
165
|
#
|
146
|
-
def
|
147
|
-
|
166
|
+
def login_with_credentials(api, client, csrf_token, username, password)
|
167
|
+
# Build authentication URI
|
168
|
+
auth_uri = client.auth_code.authorize_url(
|
169
|
+
redirect_uri: api.oauth_redirect,
|
170
|
+
state: csrf_token
|
171
|
+
)
|
172
|
+
|
173
|
+
# Accquire auth code
|
174
|
+
response = client.request(
|
175
|
+
:post,
|
176
|
+
auth_uri,
|
177
|
+
body: { username: username, password: password }
|
178
|
+
)
|
179
|
+
unless response.status == 303
|
180
|
+
fail_with_oauth_error(
|
181
|
+
"API login failed (code=#{response.status})",
|
182
|
+
response
|
183
|
+
)
|
184
|
+
end
|
185
|
+
response.headers['location']
|
148
186
|
rescue OAuth2::Error => e
|
149
|
-
|
150
|
-
"Failed to
|
187
|
+
fail_with_oauth_error(
|
188
|
+
"Failed to acquire auth_code (code=#{e.response.status})",
|
151
189
|
e.response
|
152
190
|
)
|
153
191
|
end
|
154
192
|
|
155
|
-
#
|
193
|
+
# Exchange auth_code for a real token
|
156
194
|
#
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
)
|
166
|
-
end
|
195
|
+
# @param [Gini::Api::Client] api API object
|
196
|
+
# @param [OAuth2::Client] client OAuth2 client object
|
197
|
+
# @param [String] auth_code authorization code
|
198
|
+
#
|
199
|
+
# @return [OAuth2::AccessToken] AccessToken object
|
200
|
+
#
|
201
|
+
def exchange_code_for_token(api, client, auth_code)
|
202
|
+
client.auth_code.get_token(auth_code, redirect_uri: api.oauth_redirect)
|
167
203
|
rescue OAuth2::Error => e
|
168
|
-
|
169
|
-
"Failed to
|
204
|
+
fail_with_oauth_error(
|
205
|
+
"Failed to exchange auth_code for token (code=#{e.response.status})",
|
170
206
|
e.response
|
171
207
|
)
|
172
208
|
end
|
data/lib/gini-api/version.rb
CHANGED
data/spec/gini-api/oauth_spec.rb
CHANGED
@@ -28,10 +28,14 @@ describe 'Gini::Api integration test' do
|
|
28
28
|
|
29
29
|
context 'OAuth' do
|
30
30
|
|
31
|
-
|
31
|
+
before do
|
32
32
|
@api.login(username: @user, password: @pass)
|
33
|
+
end
|
34
|
+
|
35
|
+
it '#login sets token' do
|
33
36
|
expect(@api.token.token).to match(/\w+-\w+/)
|
34
37
|
expect(@api.token.expired?).to be_false
|
38
|
+
@api.logout
|
35
39
|
end
|
36
40
|
|
37
41
|
it '#logout destroys token' do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gini-api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Kerwin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-03-
|
11
|
+
date: 2014-03-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oauth2
|