Skip to content

Commit e467eca

Browse files
committed
Add support for async conversions
- Bump Ruby version to latest 2.6 patch - Request path method extraction with async prefix support - Add async result class - Refactor Task param handling - Support passing webhooks (@davidashman) - Add polling method (@davidashman)
1 parent 88ed026 commit e467eca

File tree

8 files changed

+178
-26
lines changed

8 files changed

+178
-26
lines changed

.ruby-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.6.3
1+
2.6.10

lib/convert_api.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require 'convert_api/errors'
66
require 'convert_api/result'
77
require 'convert_api/result_file'
8+
require 'convert_api/async_result'
89
require 'convert_api/upload_io'
910
require 'convert_api/file_param'
1011
require 'convert_api/format_detector'
@@ -27,6 +28,13 @@ def convert(to_format, params, from_format: nil, conversion_timeout: nil)
2728
Task.new(from_format, to_format, params, conversion_timeout: conversion_timeout).run
2829
end
2930

31+
# Poll ConvertAPI for job status
32+
# Raises ClientError with status code 202 if the job is not complete yet
33+
# Raises ClientError with status code 404 if the job is not found
34+
def poll(job_id)
35+
Result.new(client.get("async/job/#{job_id}"))
36+
end
37+
3038
def user
3139
client.get('user')
3240
end

lib/convert_api/async_result.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module ConvertApi
2+
class AsyncResult
3+
attr_reader :response
4+
5+
def initialize(response)
6+
@response = response
7+
end
8+
9+
def job_id
10+
response['JobId']
11+
end
12+
end
13+
end

lib/convert_api/client.rb

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ class Client
2828
'Accept' => 'application/json'
2929
}
3030

31+
# Parameters that are always on the URL, even for POST
32+
POST_URL_PARAMS = [
33+
:WebHook,
34+
:JobId
35+
].freeze
36+
3137
def get(path, params = {}, options = {})
3238
handle_response do
3339
request = Net::HTTP::Get.new(request_uri(path, params), DEFAULT_HEADERS)
@@ -38,7 +44,7 @@ def get(path, params = {}, options = {})
3844

3945
def post(path, params, options = {})
4046
handle_response do
41-
request = Net::HTTP::Post.new(request_uri(path), DEFAULT_HEADERS)
47+
request = Net::HTTP::Post.new(request_uri(path, post_url_params(params)), DEFAULT_HEADERS)
4248
request.form_data = build_form_data(params)
4349

4450
http(options).request(request)
@@ -113,16 +119,22 @@ def build_form_data(params)
113119
data = {}
114120

115121
params.each do |key, value|
116-
if value.is_a?(Array)
117-
value.each_with_index { |v, i| data["#{key}[#{i}]"] = v }
118-
else
119-
data[key] = value
122+
unless POST_URL_PARAMS.include?(key)
123+
if value.is_a?(Array)
124+
value.each_with_index { |v, i| data["#{key}[#{i}]"] = v }
125+
else
126+
data[key] = value
127+
end
120128
end
121129
end
122130

123131
data
124132
end
125133

134+
def post_url_params(params)
135+
params.select { |k, v| POST_URL_PARAMS.include?(k) }
136+
end
137+
126138
def base_uri
127139
config.base_uri
128140
end

lib/convert_api/task.rb

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,45 @@ class Task
33
def initialize(from_format, to_format, params, conversion_timeout: nil)
44
@from_format = from_format
55
@to_format = to_format
6-
@params = params
76
@conversion_timeout = conversion_timeout || config.conversion_timeout
8-
end
9-
10-
def run
11-
params = normalize_params(@params).merge(
7+
@params = normalize_params(params).merge(
128
Timeout: @conversion_timeout,
139
StoreFile: true,
1410
)
11+
@async = @params.delete(:Async)
12+
@converter = detect_converter
13+
end
1514

16-
from_format = @from_format || detect_format(params)
15+
attr_reader :converter
16+
17+
def run
1718
read_timeout = @conversion_timeout + config.conversion_timeout_delta if @conversion_timeout
18-
converter = detect_converter(params)
19-
converter_path = converter ? "/converter/#{converter}" : ''
2019

2120
response = ConvertApi.client.post(
22-
"convert/#{from_format}/to/#{@to_format}#{converter_path}",
23-
params,
21+
request_path,
22+
@params,
2423
read_timeout: read_timeout,
2524
)
2625

26+
return AsyncResult.new(response) if async?
27+
2728
Result.new(response)
2829
end
2930

3031
private
3132

33+
def async?
34+
@async.to_s.downcase == 'true'
35+
end
36+
37+
def request_path
38+
from_format = @from_format || detect_format
39+
converter_path = converter ? "/converter/#{converter}" : ''
40+
async = async? ? 'async/' : ''
41+
42+
"#{async}convert/#{from_format}/to/#{@to_format}#{converter_path}"
43+
end
44+
3245
def normalize_params(params)
3346
result = {}
3447

@@ -62,16 +75,16 @@ def files_batch(values)
6275
files
6376
end
6477

65-
def detect_format(params)
66-
return DEFAULT_URL_FORMAT if params[:Url]
78+
def detect_format
79+
return DEFAULT_URL_FORMAT if @params[:Url]
6780

68-
resource = params[:File] || Array(params[:Files]).first
81+
resource = @params[:File] || Array(@params[:Files]).first
6982

7083
FormatDetector.new(resource, @to_format).run
7184
end
7285

73-
def detect_converter(params)
74-
params.each do |key, value|
86+
def detect_converter
87+
@params.each do |key, value|
7588
return value if key.to_s.downcase == 'converter'
7689
end
7790

spec/convert_api/client_spec.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,36 @@
1010
expect(subject['FileId']).to be_instance_of(String)
1111
end
1212
end
13+
14+
describe '#post' do
15+
let(:file) { 'https://www.w3.org/TR/2003/REC-PNG-20031110/iso_8859-1.txt' }
16+
let(:path) { 'convert/txt/to/pdf/converter/openoffice' }
17+
let(:options) { {} }
18+
let(:mock_response) { OpenStruct.new(code: 200, body: '{}') }
19+
20+
subject{ client.post(path, params, options) }
21+
22+
context 'with normal parameters' do
23+
let(:params) { { File: file } }
24+
let(:uri_with_secret) { "/#{path}?Secret=#{ConvertApi.config.api_secret}" }
25+
26+
it 'makes a post request with no extra URL parameters' do
27+
expect(Net::HTTP::Post).to(receive(:new).with(uri_with_secret, described_class::DEFAULT_HEADERS).and_call_original)
28+
expect_any_instance_of(Net::HTTP).to(receive(:request).and_return(mock_response))
29+
expect(subject).to be_an_instance_of(Hash)
30+
end
31+
end
32+
33+
context 'with parameters that MUST be passed via URL' do
34+
let(:webhook) { 'https://www.convertapi.com/fake-webhook' }
35+
let(:params) { { File: file, WebHook: webhook } }
36+
let(:uri_with_selected_params) { "/#{path}?#{URI.encode_www_form({ WebHook: webhook, Secret: ConvertApi.config.api_secret})}" }
37+
38+
it 'makes a post request that passes the required parameters via URL' do
39+
expect(Net::HTTP::Post).to(receive(:new).with(uri_with_selected_params, described_class::DEFAULT_HEADERS).and_call_original)
40+
expect_any_instance_of(Net::HTTP).to(receive(:request).and_return(mock_response))
41+
expect(subject).to be_an_instance_of(Hash)
42+
end
43+
end
44+
end
1345
end

spec/convert_api/task_spec.rb

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@
88
let(:file) { 'https://www.w3.org/TR/2003/REC-PNG-20031110/iso_8859-1.txt' }
99
let(:result) { double }
1010

11-
it 'executes task and returns result' do
12-
expect(ConvertApi.client).to(
13-
receive(:post).with('convert/txt/to/pdf', instance_of(Hash), instance_of(Hash)).and_return(result)
14-
)
11+
shared_examples 'successful task' do
12+
it 'executes task and returns result' do
13+
expect(ConvertApi.client).to(
14+
receive(:post).with('convert/txt/to/pdf', instance_of(Hash), instance_of(Hash)).and_return(result)
15+
)
1516

16-
expect(subject).to be_instance_of(ConvertApi::Result)
17+
expect(subject).to be_instance_of(ConvertApi::Result)
18+
end
1719
end
1820

21+
it_behaves_like 'successful task'
22+
1923
context 'with converter' do
2024
let(:params) { { File: file, Converter: 'openoffice' } }
2125

@@ -53,5 +57,50 @@
5357

5458
expect(subject).to be_instance_of(ConvertApi::Result)
5559
end
60+
61+
it 'executes task and returns result' do
62+
expect(ConvertApi.client).to(
63+
receive(:post).with('convert/txt/to/pdf', instance_of(Hash), instance_of(Hash)).and_return(result)
64+
)
65+
66+
expect(subject).to be_instance_of(ConvertApi::Result)
67+
end
68+
end
69+
70+
71+
describe 'async' do
72+
shared_examples 'successful async task' do
73+
it 'submits an async task and returns result' do
74+
expect(ConvertApi.client).to(
75+
receive(:post).with('async/convert/txt/to/pdf', instance_of(Hash), instance_of(Hash)).and_return(result)
76+
)
77+
78+
expect(subject).to be_instance_of(ConvertApi::AsyncResult)
79+
end
80+
end
81+
82+
context 'Async: false' do
83+
let(:params) { { Async: false, File: file } }
84+
85+
it_behaves_like 'successful task'
86+
end
87+
88+
context 'Async: "false"' do
89+
let(:params) { { Async: 'false', File: file } }
90+
91+
it_behaves_like 'successful task'
92+
end
93+
94+
context 'Async: true' do
95+
let(:params) { { Async: true, File: file } }
96+
97+
it_behaves_like 'successful async task'
98+
end
99+
100+
context 'Async: "true"' do
101+
let(:params) { { Async: "true", File: file } }
102+
103+
it_behaves_like 'successful async task'
104+
end
56105
end
57106
end

spec/convert_api_spec.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,31 @@
112112
expect { subject }.to raise_error(ConvertApi::FormatError)
113113
end
114114
end
115+
116+
context 'async' do
117+
shared_examples 'successful async conversion' do
118+
it 'returns result' do
119+
expect(subject).to be_instance_of(ConvertApi::AsyncResult)
120+
expect(subject.job_id).to be_a_kind_of(String)
121+
end
122+
end
123+
124+
context 'with web resource' do
125+
let(:from_format) { 'web' }
126+
let(:params) { {Async: true, Url: 'http://convertapi.com' } }
127+
128+
it_behaves_like 'successful async conversion'
129+
end
130+
131+
context 'with multiple files' do
132+
let(:to_format) { 'zip' }
133+
let(:params) { { Async: true, Files: [file1, file2] } }
134+
let(:file1) { 'examples/files/test.pdf' }
135+
let(:file2) { ConvertApi::UploadIO.new('examples/files/test.pdf', 'test2.pdf') }
136+
137+
it_behaves_like 'successful async conversion'
138+
end
139+
end
115140
end
116141

117142
describe '.user' do

0 commit comments

Comments
 (0)